Summary
There is no way to compare-and-swap a word of foreign memory on Clasp. Every route signals
MP:NOT-ATOMIC. SBCL and AllegroCL both offer one, so portable lock-free code that keeps its state
in mmaped or malloced memory — shared-memory rings, cross-process counters, allocator
metadata — has no Clasp implementation.
This is an enhancement request rather than a bug report, and it looks like unfinished work rather
than a deliberate exclusion: MP:CAS's own docstring says the place set "is planned to be
expanded", and, as detailed below, most of the compiler machinery already exists and is currently
unreachable.
Reproduction
(let* ((fd (clasp-ffi:%foreign-alloc 64))
(addr (clasp-ffi:%foreign-data-address fd)))
(clasp-ffi:%mem-set-uint32 (+ addr 8) 5)
(format t "~&plain access works: ~a~%" (clasp-ffi:%mem-ref-uint32 (+ addr 8)))
(mp:cas (clasp-ffi:%mem-ref-uint32 (+ addr 8)) 5 9))
plain access works: 5
NOT-ATOMIC: Don't know how to atomically access the place (%MEM-REF-UINT32 ...)
The same failure occurs for the generic accessor (mp:cas (clasp-ffi:%mem-ref fd :uint32 8) 5 9),
for mp:get-atomic-expansion on either form, and identically through CFFI's mem-ref. Plain
foreign reads and writes work fine; only the atomic route is missing.
The obvious workaround does not exist either: specialised arrays have no CAS, since only svref
(element type T) is a CAS-able place — src/lisp/kernel/cleavir/primop.lisp:672-674 asserts
(and (eql etype 't) (eql rank 1)) — so moving a shared counter into a
(simple-array (unsigned-byte 32) (*)) does not help.
Where it would go
The place registry is src/lisp/kernel/lsp/atomics.lisp: a place is CAS-able iff
(core:get-sysprop <accessor> 'atomic-expander) is non-nil, populated by define-atomic-expander
forms at :307-371, five more in src/lisp/kernel/clos/atomics.lisp, and one per structure
accessor at src/lisp/kernel/lsp/defstruct.lisp:362. Anything else reaches atomics.lisp:23 and
signals MP:NOT-ATOMIC (src/lisp/kernel/clos/conditions.lisp:748-753) — exactly the message
above. Note there are two copies to keep in sync: the kernel's and the cross-compiler's
src/cross-clasp/mp-atomics.lisp:324-441.
The machinery mostly exists already, and some of it is dead code
This is the part that made me think the request is small rather than large:
cmp:irc-cmpxchg (src/lisp/kernel/cleavir/cmpir.lisp:713-725) already emits LLVM cmpxchg,
takes a raw pointer, an old value, a new value and a memory order, and is not Lisp-object
specific.
cc-blir:cas (src/lisp/kernel/cleavir/blir.lisp:14-15) is a BIR instruction that CASes a raw
address with a settable order, is fully translated to LLVM at
src/lisp/kernel/cleavir/translate.lisp:1473-1479 — and nothing in the tree produces it.
grep -rn 'cc-blir:cas' src/ returns only the class definition and its translator.
- Its sibling
cc-blir:memref2 already computes a byte-offset address from a pointer, which is the
address arithmetic a foreign accessor needs.
cmp:irc-atomicrmw, with a complete operation table, likewise has zero callers.
One design point worth settling before any patch
clasp-ffi:%mem-ref dispatches on a runtime type keyword, while a CAS needs the C type at
compile time to choose a width. And MP:CAS's contract is EQ-matching on a Lisp object
(atomics.lisp:151-155), whereas the hardware compares raw 32- or 64-bit words — the FFI accessors
box their results (src/core/fli.cc:864-867 returns mk_fixnum_uint32(v)).
So the cheapest correct increment is probably not an MP:CAS place at all, but a typed
function — something like (core:cas-mem-uint32 address old new) returning the prior word or a
boolean — with MP:CAS support layered on top later for the constant-type case. That also sidesteps
the boxing question entirely. I would be glad to be told the preferred shape.
Relationship to #672
#672 is adjacent but does not cover this:
every place it enumerates is a Lisp place — svref, car, cdr, structure accessors,
symbol-value, symbol-plist, slot-value, instance access, arrays. Foreign memory, FFI,
mem-ref and pointers appear nowhere in its body or its comments. I am filing separately rather
than commenting there so the FFI case is not lost inside a five-year-old umbrella.
Minor, related: the MP:FENCE docstring is inaccurate
Not worth its own issue, but it is in the same area. src/core/mpPackage.cc:656 promises that
ORDER is "the same as accepted by ATOMIC, except that :relaxed does not make sense for fences
and will be rejected". :relaxed is rejected on no path: gen-fence's error
(src/lisp/kernel/cmp/codegen-special-form.lisp:43-45) is only reachable once a transform has
fired, and src/lisp/kernel/cleavir/bir-to-bmir.lisp:374-378 defines transforms for exactly four
orders — :sequentially-consistent, :acquire-release, :acquire, :release. :relaxed, and any
typo, falls through to the out-of-line function, which does (void)order and emits seq_cst.
To be fair to the design: the inlining path genuinely works — with cmp:*compile-native* bound to
T I measured dmb ishld for :acquire against dmb ish for the other three — and every fallback
is stronger than requested, never weaker, so this is a diagnostics and performance matter, not a
memory-model one. Only the docstring's claim is wrong.
Environment
clasp-boehmprecise-3.0.1-112-gc7faba5ec-non-cst, macOS arm64, Boehm precise GC
- Verified against
upstream/main @ 205f83feca442b694b9d1cb02ca4a573d37cc845
- For comparison: SBCL
(sb-ext:cas (sb-sys:sap-ref-32 sap off) old new) and AllegroCL
(excl:atomic-conditional-setf (sys:memref-int addr off 0 :unsigned-long) new old) each compile
to a single LOCK CMPXCHG
Summary
There is no way to compare-and-swap a word of foreign memory on Clasp. Every route signals
MP:NOT-ATOMIC. SBCL and AllegroCL both offer one, so portable lock-free code that keeps its statein
mmaped ormalloced memory — shared-memory rings, cross-process counters, allocatormetadata — has no Clasp implementation.
This is an enhancement request rather than a bug report, and it looks like unfinished work rather
than a deliberate exclusion:
MP:CAS's own docstring says the place set "is planned to beexpanded", and, as detailed below, most of the compiler machinery already exists and is currently
unreachable.
Reproduction
The same failure occurs for the generic accessor
(mp:cas (clasp-ffi:%mem-ref fd :uint32 8) 5 9),for
mp:get-atomic-expansionon either form, and identically through CFFI'smem-ref. Plainforeign reads and writes work fine; only the atomic route is missing.
The obvious workaround does not exist either: specialised arrays have no CAS, since only
svref(element type
T) is a CAS-able place —src/lisp/kernel/cleavir/primop.lisp:672-674asserts(and (eql etype 't) (eql rank 1))— so moving a shared counter into a(simple-array (unsigned-byte 32) (*))does not help.Where it would go
The place registry is
src/lisp/kernel/lsp/atomics.lisp: a place is CAS-able iff(core:get-sysprop <accessor> 'atomic-expander)is non-nil, populated bydefine-atomic-expanderforms at
:307-371, five more insrc/lisp/kernel/clos/atomics.lisp, and one per structureaccessor at
src/lisp/kernel/lsp/defstruct.lisp:362. Anything else reachesatomics.lisp:23andsignals
MP:NOT-ATOMIC(src/lisp/kernel/clos/conditions.lisp:748-753) — exactly the messageabove. Note there are two copies to keep in sync: the kernel's and the cross-compiler's
src/cross-clasp/mp-atomics.lisp:324-441.The machinery mostly exists already, and some of it is dead code
This is the part that made me think the request is small rather than large:
cmp:irc-cmpxchg(src/lisp/kernel/cleavir/cmpir.lisp:713-725) already emits LLVMcmpxchg,takes a raw pointer, an old value, a new value and a memory order, and is not Lisp-object
specific.
cc-blir:cas(src/lisp/kernel/cleavir/blir.lisp:14-15) is a BIR instruction that CASes a rawaddress with a settable order, is fully translated to LLVM at
src/lisp/kernel/cleavir/translate.lisp:1473-1479— and nothing in the tree produces it.grep -rn 'cc-blir:cas' src/returns only the class definition and its translator.cc-blir:memref2already computes a byte-offset address from a pointer, which is theaddress arithmetic a foreign accessor needs.
cmp:irc-atomicrmw, with a complete operation table, likewise has zero callers.One design point worth settling before any patch
clasp-ffi:%mem-refdispatches on a runtime type keyword, while a CAS needs the C type atcompile time to choose a width. And
MP:CAS's contract isEQ-matching on a Lisp object(
atomics.lisp:151-155), whereas the hardware compares raw 32- or 64-bit words — the FFI accessorsbox their results (
src/core/fli.cc:864-867returnsmk_fixnum_uint32(v)).So the cheapest correct increment is probably not an
MP:CASplace at all, but a typedfunction — something like
(core:cas-mem-uint32 address old new)returning the prior word or aboolean — with
MP:CASsupport layered on top later for the constant-type case. That also sidestepsthe boxing question entirely. I would be glad to be told the preferred shape.
Relationship to #672
#672 is adjacent but does not cover this:
every place it enumerates is a Lisp place —
svref,car,cdr, structure accessors,symbol-value,symbol-plist,slot-value, instance access, arrays. Foreign memory, FFI,mem-refand pointers appear nowhere in its body or its comments. I am filing separately ratherthan commenting there so the FFI case is not lost inside a five-year-old umbrella.
Minor, related: the
MP:FENCEdocstring is inaccurateNot worth its own issue, but it is in the same area.
src/core/mpPackage.cc:656promises thatORDERis "the same as accepted byATOMIC, except that:relaxeddoes not make sense for fencesand will be rejected".
:relaxedis rejected on no path:gen-fence's error(
src/lisp/kernel/cmp/codegen-special-form.lisp:43-45) is only reachable once a transform hasfired, and
src/lisp/kernel/cleavir/bir-to-bmir.lisp:374-378defines transforms for exactly fourorders —
:sequentially-consistent,:acquire-release,:acquire,:release.:relaxed, and anytypo, falls through to the out-of-line function, which does
(void)orderand emitsseq_cst.To be fair to the design: the inlining path genuinely works — with
cmp:*compile-native*bound toTI measureddmb ishldfor:acquireagainstdmb ishfor the other three — and every fallbackis stronger than requested, never weaker, so this is a diagnostics and performance matter, not a
memory-model one. Only the docstring's claim is wrong.
Environment
clasp-boehmprecise-3.0.1-112-gc7faba5ec-non-cst, macOS arm64, Boehm precise GCupstream/main@205f83feca442b694b9d1cb02ca4a573d37cc845(sb-ext:cas (sb-sys:sap-ref-32 sap off) old new)and AllegroCL(excl:atomic-conditional-setf (sys:memref-int addr off 0 :unsigned-long) new old)each compileto a single
LOCK CMPXCHG