Guide for Implementors
Guide for Implementors
Principles says what the system is. This page says how to work on it — the practices that keep the codebase the shape the principles describe. It is written for anyone changing the runtime, the shop, or the compiler.
Prefer pit over C
C is the floor, not the house. The floor’s job is what pit structurally cannot do for itself: boot an actor into existence, map bytes, hold a mutex, call the OS. Everything above that — formats, policies, tools, services, the compiler itself — is pit, because pit code is inspectable, per-actor isolated, hot-editable, and portable by construction. When a feature could be either, it is pit. When C seems necessary, the first question is which small shim would let the rest be pit.
The sharpest form of this rule is one writer per format: Pit emits, C reads. Every artifact format has exactly one producer, written in pit; C readers check a fixed header by comparison and hand everything else to pit to decode. A second writer — especially a C one — is how formats fork.
Named refusals over silent omission
Silent omission is the defect class this project kills on sight. A lookup that
returns nothing, a payload that cannot be found, a section that does not fit — the
wrong response is to skip it and produce a well-formed artifact with a hole in it,
because every gate passes and the failure surfaces far away as a boot that never
finishes. The right response is a refusal that names the thing and the reason:
"seed: module X of Y has no runnable payload". Refusals are cheap; archaeology
is not. When a refusal fires in practice, it has done its job — read it, do not
soften it.
The same standard applies to gates and tests. A gate rules on evidence (an exit code and the summary line, never a sliding window over output); a red prints what it saw so it diagnoses itself; a deadline that no longer fits is measured and reclassified, never padded to green. A deleted subsystem leaves an observable behind — a counter asserted to zero, with a liveness probe proving the zero is watched — so its return is caught by a test, not a review.
Docs first
The documentation is the single source of truth, in positive voice: it describes
what the system does. Work flows from the docs — a change that alters what an
artifact is or how a boundary behaves lands in the docs (usually
the spec) before the implementation, and the
implementation follows it exactly. When code and a doc disagree, one of them is
wrong on purpose: either the doc is stale (fix the doc, positive voice, no
“no longer”) or the code has drifted (fix the code). What the docs deliberately do
not carry is status: unfinished work, open questions, and journals live in
plans/, which is temporary by definition.
Guard comments bind within a world
A “do not relax this assertion” comment exists to stop drive-by softening — it is not a veto over a deliberate rework. When a landing executes a decision that inverts the old world, the decision is the authority: the assertion inverts, the new comment cites the decision it enforces, and that becomes the new “do not relax”. At the end of a large rework, sweep the surviving guard comments and re-anchor each to the current decision it protects, so no fossil argues against the next migration.
Lifetimes are stated, never assumed
The runtime reclaims and kills; it never restarts or supervises. An actor that is
idle past its ar timer is reaped — that is the contract, and it is a feature.
The consequence for implementors: a permanent service states its own lifetime
with $unneeded(fn, -1), because quiet-between-requests is a service’s normal
state, not evidence it is unneeded. A listener, a logger, a courier that omits the
statement will be reaped in its first quiet minute, and the failure will look like
a healthy process that cannot be reached. Conversely, ephemeral actors state
nothing and are collected on schedule. Replies resolve by an answer or by a
death — never by a clock.
The C safety rules
The runtime uses a per-actor copying garbage collector: any allocation can
move every unrooted value. These rules are mandatory for all C touching
PitValues; the full reference with worked patterns is
C modules.
Rooting. A function making two or more allocating calls
(Pit_New*, Pit_SetProperty*, pit_new_blob*) uses
PIT_FRAME / PIT_ROOT / PIT_RETURN, and every value held across an
allocating call is rooted. Three placement laws, each learned from a real
crash:
PIT_ROOTlives at function scope only. Never inside a loop (the same stack address pushes twice and corrupts the root chain) and never inside a nested block (the ref stays linked untilPIT_RETURN, so block exit leaves the chain threaded through a dead stack slot — in release builds the reused slot can cycle the chain and a later collection spins forever). Declare at function scope; assign.valinside the branch that uses it.- Never let an allocating call be an argument beside a
.valread — C’s argument evaluation order can read the object pointer before the allocation moves it. Bind the allocation to a local first. - A value lifted out of a scanned container is unrooted. Push it into a
PitGCRefbefore any allocation (push first — the push clobbers.val— assign after, read back from.val).
Locking. An observer never blocks on a turn mutex it does not own: a thread that holds its own actor’s mutex and blocks on another’s can deadlock against a second observer doing the same. Observers trylock and report what refcount-safe scalars they can; only owners block. On single-threaded targets a failed trylock is final — nothing else can release the lock, so retrying is never correct.
Resources. A C resource that outlives one call lives in a PITCLASS record
as an opaque pointer, owned by one actor, finalized with the record, tombstoned
before explicit destruction, and never transferred between actors.
Verify the premise, not the suite
A green suite proves the suite; it does not prove your diagnosis. Before fixing, reproduce; before deleting, grep for the callers; before trusting a brief or a comment, check it against the tree — briefs are wrong often enough that checking them is where the value is. When a defect only manifests in a release build, escalate the instrumentation instead of iterating on inference: a debugger sample tells you where, an ASan build on the unoptimized target tells you what, by line. And when a number looks wrong, suspect the measurement first — a wall time that is impossibly short is a wedge, not a speedup.