ADR 0009 — C7: context effects, and C state embedded by value
- Status: Accepted (project owner, 2026-10-24, choosing "embed by value" for kernel locks and objects over out-of-line C state). The rest is the agent's design under that choice, following
cosmoos/kernel-memory-and-lifetimes.md§4–§7; the owner may revise it. - Scope: C7 (
cosmoos/c7-report.md). It amends ADR 0008 point 4 (imported types only behind pointers).
Context
C7 wraps CosmoOS's locks, kernel objects and quiescence (audit in cosmoos/c7-report.md). Three facts from the code decide the design:
- The kernel's synchronisation objects are C state that lives inside the data it protects.
spinlock_tis 24 bytes (kernel/include/kernel/spinlock.h:31-37).struct mutexembeds a spinlock, a wait queue and list links (mutex.h:20-35). Astruct kobjectis embedded in its owner and found again withcontainer_of. - Once initialised, some of this state points into itself.
mutex_initrunslist_init, which setshead->next = head(kernel/scheduler/mutex.c:21-22,list.h:24-28). Moving such a value bitwise corrupts it. - Atomic versus sleepable is a runtime property:
might_sleep()(lockdep.h:239) panics on sleeping with preemption disabled. Nothing checks it statically, and nothing checks at all that a finalkobject_put, whose release may block, is not made under a spinlock (object.c:74-81has nomight_sleep).
Decision
1. Context effects (C7a)
- Two effects:
sleep(may block) andalloc(may allocate). A function's effects are those of everything it calls and every value it drops, inferred over the whole package. Built-in operations declare theirs (heap allocation isalloc; hosted I/O issleep). @effects(sleep, alloc)(any subset;@effects()for none) declares them: on anexternfunction it is what Nova assumes, and on a Nova function it is an upper bound that the inferred set must fit. An extern function without@effectsis assumed to have both, the most conservative choice. A call through a C function pointer has both.@context(atomic)(nosleep) and@context(irq)(nosleep, noalloc; KQ-1's Nova default) on a function forbid effects in its body. On a struct,@context(atomic)makes every point where a value of it is live (maybe initialised and not yet dropped) atomic: holding a spin-lock guard is an atomic context, with no annotation at the use.- Effects are checked in every profile. They create obligations only where a context or an
@effectsbound is declared, so hosted programs without them are unaffected. - The checker proves only that Nova code respects the declared effects of what it calls. CosmoOS's runtime checks (
might_sleep, lockdep) stay the backstop. Nova generates nomight_sleep()calls: the debug half of the macro callslockdep_might_sleep, which modules cannot link to (it is not exported), and every sleeping primitive Nova calls has its own.
2. C state embedded by value (C7b)
- A Nova struct may hold an imported C type by value, and so may an imported struct (an embedded member). Such a type, and every type that contains one by value, is C-laid-out. Like an imported type, it has no Nova layout. Generated C defines it with the real C member types, so the C compiler lays it out, and it gets no layout assertions. The interpreter faults on it.
- C-laid-out values are immovable. They are made in place and never moved:
- the only way to make one is a struct literal given directly to
Box.createIn(or nested in such a literal); - imported fields are left out of the literal and zero-filled (C's
{0}), which is the only imported value Nova can make; - the contents of such a box are never moved out;
- a C-laid-out type cannot be a local, parameter, result, array element, enum payload, optional or type argument (a reference to one can be).
staticitems come later (§4).
- the only way to make one is a struct literal given directly to
@placedmethods (fn m(borrow mut this)) run C's initialisers at the final address. AfterBox.createInplaces the value, the compiler calls the@placedmethods of its fields' types, innermost first in field order, then the type's own. They run before anything else can borrow the value, so code never sees uninitialised C state. A lock's@placedmethod callsspinlock_initwith its name.Ptr.containerOf(unsafe) recovers a struct from a pointer to one of its fields, using C'soffsetof. Kernel callbacks need it, since they receive the embeddedstruct kobject *orstruct quiesce_head *. (Implementation note, C7b: it moved to C7d, whose release and quiescence callbacks are generated by the compiler and are its only users so far.)
3. CosmoOS bindings (C7c–C7e, integrations/cosmoos/c7/)
SpinLock<T>,IrqSpinLock<T>andMutex<T>hold their C lock and their data.lock()returns a guard that borrows the lock and givesborrow mut Taccess. Spin guards are@context(atomic).Mutex.lockissleep. Dropping a guard unlocks. Data is reachable only through a guard.KRef<T>, a counted reference to a Nova-defined kernel object (it embedskobject). Its drop iskobject_put, with thesleepeffect unless the type is@nonblockingRelease. A final put under a spin guard is then a compile error, the hole of fact 3.Quiesce.synchronize()(sleep).Quiesce.deferDrop(box)usescall_quiescewith an embeddedquiesce_head. It isunsafein modules, because module unload does not wait for pending callbacks (module.c:627-631; KQ-3). Its contract is that the module stays loaded until the callback has run.- Not in C7:
QuiesceGuardread sections in modules (quiesce_read_unlockinlines the unexportedpreempt_enable,quiesce.h; KQ-2; spin guards are read sections already),PerCpu<T>(CosmoOS exports no general per-CPU allocation), and statics (§4).
4. Deferred
static items (locks and caches with a lifetime of the module), and with them ObjectCache<T> (C5). Lock-order checking stays lockdep's job.
Consequences
- ADR 0008 point 4 now reads: imported types appear by value only in C-laid-out types, which are immovable and made in place. Everywhere else they appear behind pointers and references.
type-system.mdgains §32 (effects) and §33 (embedded C state).profiles.md§1 changes the effects row to checked everywhere.kernel-memory-and-lifetimes.md§5–§6 point to them.- The proposals of
kernel-memory-and-lifetimes.mdthat C7 leaves out stay proposals:PerCpu, statics,Unlinked<T>,@scopedOnly, and the irq-state ordering of nestedIrqSpinGuards beyondusing's LIFO.