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_ROOT lives 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 until PIT_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 .val inside the branch that uses it.
  • Never let an allocating call be an argument beside a .val read — 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 PitGCRef before 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.