ADR 0009 — C7: context effects, and C state embedded by value
Inherited. ADR 0009 was accepted before CeeSafe's fork (Decision 0006), when the language had its earlier name. In this record, "the reference" is that language and project and "the reference compiler" its compiler: read them as CeeSafe and
ceesafec. Links to documents CeeSafe did not keep are given as text.
- 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 the reference assumes, and on a CeeSafe 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 the reference 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 CeeSafe code respects the declared effects of what it calls. CosmoOS's runtime checks (
might_sleep, lockdep) stay the backstop. The reference 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 the reference calls has its own.
2. C state embedded by value (C7b)
- A CeeSafe 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 The reference 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 the reference 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 reference-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.