ADR 0027 — M14: a static Mutex, and ObjectCache<T>
- Status: Accepted (project owner, 2026-10-29), both questions as recommended (§5). The wording is the agent's.
- Scope: the two parts of ADR 0025's v1 API (§3.3) that §7 left open: a
Mutex<T>in astaticitem, andObjectCache<T>, a typed slab cache. Both need a language addition. CosmoOS's roadmap 2.0 needs both for SA2 (object caches) and SA3 (locks and waits in statics).
1. Context
1.1 A static mutex
- The C.
struct mutex(CosmoOSkernel/include/kernel/mutex.h:20) holds a spinlock, an owner, astruct waitqueue, a name, a lockdep class,pi_active, astruct list_node pi_linkandhandoff.mutex_init(kernel/scheduler/mutex.c:13) makespi_linkand the wait queue's two lists point to themselves (list_init,waitqueue_init). - CosmoOS's own static initialisers for such state take the object's own name:
LIST_HEAD_INIT(name)is{ &(name), &(name) }andWAITQUEUE_INIT(name)uses(name).waiters(list.h:21,wait.h:58). There is noMUTEX_INIT. - Nova's statics (type-system §39.4) build C state only through a C constant macro whose arguments are static initialisers. None of those can name the static being defined, so a self-pointing list cannot be written. Static spin locks work because
SPINLOCK_INIThas no self-pointers (ADR 0025 §7). - SA3 needs more of the same: wait queues, completions and list heads in statics all point to themselves.
1.2 An object cache
- The C.
kmem_cache_create(name, object_size, align)allocates and returns a cache, or NULL;kmem_cache_alloc(cache, flags)andkmem_cache_free(cache, obj)(kernel/include/kernel/kmalloc.h:47-53). CosmoOS creates its caches at start-up into globals (kernel-services/network/mbuf.c:26-27,kmalloc.c:43). - What Nova lacks:
- Moving a value into raw memory.
p.write(v)andp.read()are defined only forCopytypes (§27.3, N0282). A cache's objects are generally notCopy(they own boxes, or have adrop), so a cache box cannot be filled or emptied. - A type's size and alignment as a value.
kmem_cache_createneeds them. The compiler knows them (TargetInfo), andBoxpasses them to an allocator as aLayout, but Nova code cannot ask for one.
- Moving a value into raw memory.
- Why not
Box<T, A>: a box's allocator is stateless (C5, §30), and a cache is state. A stateful allocator would make every box carry its allocator: two pointers instead of one, through placement, niche optionals and drop glue.
2. Options
2.1 Static mutex
A. here: a constant initialiser may take the address it initialises | B. Lazy: a StaticMutex<T> that runs mutex_init at its first lock | C. None: mutexes made at start-up | |
|---|---|---|---|
| Language | one new static-initialiser form | none | none |
| CosmoOS | a MUTEX_INIT_AT(p, name) macro in mutex.h | none | none |
| Run-time cost | none | an acquire load per lock, and a spinlock for the first | an allocation and a pointer per lock |
| Also serves | wait queues, completions, list heads (SA3) | mutexes only | — |
2.2 Object cache
A. Moves through raw pointers, Layout.of<T>(), and a library cache | B. Stateful Box allocators | C. Wait | |
|---|---|---|---|
| Language | lift Copy from read/write (unsafe); one built-in | Box<T, A> carries A's state | — |
| Size of a cache box | two pointers (object, cache) | two pointers for every box with a stateful allocator | — |
| Reach | any unsafe container code (rings, intrusive lists) | caches and other stateful allocators | — |
3. Decision
3.1 here (option A for the mutex)
hereis a static initialiser (§39.4) valid only as an argument of a C constant macro whose result initialises a place. Its value is that place's address, and its type is*mut Tfor the place's typeT. Innova@constant public fn constant(name: *CChar, data: T) -> Mutex<T> => Mutex { raw: MUTEX_INIT_AT(here, name), data: data };hereis the address of the fieldrawin whichever static the constructor builds. Through nested@constantconstructors the place is the full path (&nvS_TABLE.f_lock.f_raw).- C. It lowers to an address constant (C11 §6.6p9:
&,.,->and*may form one, and no object is read), so the macro may build self-pointers from it:LIST_HEAD_INIT((p)->wq.waiters)expands to{ &((p)->wq.waiters), &((p)->wq.waiters) }. - Rules.
hereelsewhere is N0338. A@constantfunction that uses it is compile-time only (§39.4, as for C state), so it is never called at run time, where there is no fixed place. The interpreter cannot evaluate C constant macros already (§39.5), so nothing changes there. - lib/kernel.
Mutex<T>.constant(name, data)with CosmoOS's new macro:cA CosmoOS self-test compares a static mutex made by it with one made by/* A mutex in static storage, as mutex_init leaves one; p is its own address. */ #define MUTEX_INIT_AT(p, n) { .lock = SPINLOCK_INIT(n), .owner = NULL, \ .wq = { .lock = SPINLOCK_INIT(n), .waiters = LIST_HEAD_INIT((p)->wq.waiters), \ .callbacks = LIST_HEAD_INIT((p)->wq.callbacks) }, \ .name = (n), .class = 0, .pi_active = 0, .pi_link = LIST_HEAD_INIT((p)->pi_link), .handoff = NULL }mutex_init, member by member, and locks it under lockdep. A change tostruct mutexmust keep the two equal, as forSPINLOCK_INIT.
3.2 Moves through raw pointers, Layout.of<T>(), ObjectCache<T> (option A for the cache)
p.write(v)andp.read()for anyT(§27.3; unsafe, as now):p.write(v)movesvtop, without reading or destroying what was there, forp: *mut T. ACopyvis copied, as now. For a type with a@placed init(§33.3) the store is followed byinitatp, asBox.createdoes.p.read()moves theTatpout. The memory then holds no value: the unsafe code must not read it again, drop it or let anything else do so before writing it (U-rules,unsafe.md). ForCopyT, as now. For a type with C state (§33),readis N0282: C state never moves.
Layout.of<T>(), a@constantbuilt-in:T's size and alignment fromTargetInfo(in C,sizeof/_Alignofof its C type). It is a static initialiser too.- lib/kernel (ADR 0025's API, §3.3 there):
ObjectCache<T: Send>,@threadSafe:constant()(empty, for a static),setUp(name) -> Result<void, AllocError>(once, at start-up:kmem_cache_create(name, Layout.of<T>().size, Layout.of<T>().align), stored atomically), andalloc(value) -> Result<CacheBox<T>, AllocError>(kmem_cache_alloc, thenp.write(value); on failure the value is dropped, asBox.createdoes).allocbeforesetUpisErr(OutOfMemory).CacheBox<T>,@noncopy, two pointers:get,getMut, and adropthat reads the value out, drops it and returns the memory (kmem_cache_free). It isSendwhenTis.- A cache in a box, not a static, would let its boxes outlive it. So v1 has static caches only, the form CosmoOS uses, and a cache is never destroyed.
3.3 What changes elsewhere
- Type-system §27.3 (
read/write), §30.1 (Layout.of<T>()besideLayout), §39.4 (here, andLayout.ofas a static initialiser),unsafe.md(the moved-out state) and ownership.md's U-rules. - Diagnostics: no new codes.
heremisused is N0338, andreadof C state is N0282. - Tests: corpus check-fail for
hereoutside a macro argument, a run-time call of a function using it, andreadof C state; run-pass forread/writeof a type with a drop (each dropped once, under ASan and the interpreter);k3.pyfor a staticMutexand anObjectCache; CosmoOS self-testsnova-kcoreextended (in a later pin bump).
4. Consequences
- Static mutexes, and later static wait queues and completions, cost nothing at run time and are made as CosmoOS's own C makes them, with lockdep's classes from their names.
readandwriteof non-Copyvalues are new ways for unsafe code to duplicate or leak ownership. They are unsafe, and each use needs its/// Safety:argument (U2). The safety-audit pack (safety-audit.md) gains them for K6.ObjectCache<T>objects are notBoxes: code that takes aBox<T, A>does not take aCacheBox<T>.- Not decided here: destroying a cache (needs boxes scoped to their cache), stateful
Boxallocators, and per-CPU object caches.
6. Implementation (2026-10-29)
- Moves through raw pointers (type-system §27.3,
unsafe.mdU2):p.read()andp.write(v)take anyT; the volatile forms still copy. The NIR move checker lets a value leave*pfor a rawp(NirPlace.IsRawDeref), the drop elaborator does not drop the old contents of*p, and a write of a type with@placedmethods is followed byPtrPlace, emitted as itsnvPL_helper (the in-place checker accepts the literal there, as forBox.create).p.read()of a Nova struct holding C state is N0282. layoutOf<T>()(§30.1), notLayout.of<T>(): Nova has no type arguments on a member call, so it is a function, liketruncate<T>. Folded fromTargetInfoto aLayoutliteral; for a C-laid-outT, C'ssizeof/_Alignof.here(§39.4–§39.5): typed by the macro parameter it is given to, checked against the macro's result type, emitted as the address of the place being initialised. lib/kernel'sMutex<T>.constantuses CosmoOS'sMUTEX_INIT_AT, whichintegrations/cosmoos/k3/mutex_init_at.hsupplies tok3.pyuntil CosmoOS'smutex.hdefines it.ObjectCache<T>andCacheBox<T>(lib/kernel/cache.nova) as §3.2 says, with two differences found in the implementation:Tholds no C state in v1: a cache box moves its value out to drop it, and C state never moves. A drop in place would lift this.CacheBox<T>is notSend, like aKernelHeapbox: raw pointers are notSend(§38.1), and a@threadSafedeclaration would claimSharefor anyT.
- Tests:
- run-pass
raw-moves.novaandlayout-of.nova, every C configuration and the interpreter; - check-fail
raw-moves-c-state.nova,layout-of.novaandstatic-here.nova; the changedraw-pointer-types.nova, whose non-Copyread/writeare now valid; - emit-c
static-here.nova:MUTEX_INIT_AT((&nvS_TABLE.f_raw), …); k3.py(k3/statics.nova: a staticMutexand anObjectCache), both architectures, one file and split.
- run-pass
- Found in a boot (2026-10-29, novac 0.3.0, CosmoOS branch
novac-0.3.0):MUTEX_INIT_ATmatchesmutex_initmember by member, butObjectCache.setUpfailed forTaligned below 16: CosmoOS'skmem_cache_createrefuses an alignment belowKMALLOC_MIN_ALIGN(slab.c,cache_setup), and asks for that minimum with 0. lib/kernel now passes 0 for alignments up to 16;nova-kcorethen passes.k3.pyonly compiles, so it could not see this. - Not yet run (superseded by the above): a static mutex compared with
mutex_init's, and the cache, in a booted kernel. That is CosmoOS's side, once a novac release carries this:MUTEX_INIT_ATinmutex.handnova-kcoreextended.
5. Questions for the owner
- A static mutex through
hereand a CosmoOSMUTEX_INIT_ATmacro (A)? (Recommended; B needs no language change but costs a check per lock and covers mutexes only.) - Lift
Copyfrom unsafep.read()/p.write(v)and addLayout.of<T>(), for a lib/kernelObjectCache<T>with static caches only (A)? (Recommended.)