Working state — a note taken while the work happens, not a specification. The system as it is meant to be is in Architecture.
Work proposal — the programs arc, with its measurement floor and deletions
2026-08-05, RATIFIED same day (all seven §3 rulings answered; recorded inline).
Inputs: plans/programs.md rev 2 (the rulings), plans/measurement-suite.md
(the suite design + prototypes), and four code investigations run today — full notes
preserved in plans/proposal-notes/ (compiler-seams, builder-memory,
deletion-inventory, runtime-sharing-audit; every claim there carries file:line).
Decisions taken in conversation: trendline on crumbpit.org fed from a perf-data
branch, published to Cloudflare; bench baseline on the shared linux pit-ci
runner, counters gate; pit product v1 targets darwin + linux, windows close
behind (no windows CI runner; the docker/wine job already builds and tests
windows). Standing constraint: mach bytecode only, no AOT.
What this arc is, in John’s priority order: (1) fix the little runtime bugs; (2) formalize the idea of a program and remove the unneeded methods of execution and building; (3) stand up the tooling to publish and watch the system’s performance — which is what makes a proper compiler arc possible NEXT time, and lets the trend live on the website. Hot reload matters mainly as the proof of the three creation methods. This arc also separates the compiler and benchmarks its stages; compiler quality itself (better LTO, smarter passes) is deliberately OUT — it is the next arc, run against the trend data this one creates.
A program is not a new concept — this arc formalizes an existing one. The
./pit that make produces is already a program: the “pit dev environment”
program, built by forge with a cart fused in. ./pit compile mcode <file> runs the
mcode tool through that program’s shell today; ./mcode <args> is the same thing
with a different start declaration — mcode as the entry actor instead of the dev
shell. pit product is therefore a formalization wrapping the two steps that
already exist (pit cement presses the cart, pit forge embeds it) into one
command with the start declaration and closure discovery on top. The system state
this builds on, stated once: mcode → a fused cart with one mach pool and an entry
actor; and the shop can additionally lower mach per executable or per module.
Nothing else. Multi-cart mounting is verified ABSENT (the only runtime cart
reader is cart_boot.c’s one root lane; no $mount endowment exists; a cement
cart’s sole consumer is forge) — bundles are the distribution idea, and this arc
pins that: any resurrection of runtime cart mounting is a refusal, not a feature.
0. What the investigations settled
The compiler splits into three products. Front end and streamline are already
disjoint inside pit-compiler; the linker consumes five compiler entries and drives
streamline as a library (unit/link are peer stages in passflags.cm by
construction); the lowerer imports neither. Total decoupling cost: ~35 lines
(compiler→mach test-helper edge) + ~450 lines (codec/validators into a
dependency-free module), no logic change. And the “pit file that turns mcode into
bytecode” already ships — shoplib/mach_pool_emit.cm writes every mach byte
(pmp1 magic included); mach_pool.c’s charter is “C reads a pool and never writes
one”. Old P5 shrinks to almost nothing. The one genuine data gap: pit.mcode.unit@3
has no exports declaration, so the linker re-derives export shape by escape
analysis — a field would retire that machinery and serve reload (R4) and
stone-at-return (R5) too.
The 128 MB builder heaps are representation waste, not a leak. Four
representations of one unit coexist at peak (canonical rows ~8.4 MB + interned CP1
stream encoded for all functions up front ~7–12 MB + sites table ~11.7 MB +
token stream held to the end ~6 MB + a ~21 MB canonical-encode blob) ≈ 41 MB live;
the GC policy reserves round_up_pow2(2.5×live) → 128 MB; builders are one-shot
(one derivation, one actor); 77 × 128 MB = 9.9 GB ≈ the measured 9.16 GB. Closed.
Defects beside it: ACTOR_MEMORY_LIMIT (1 GiB) is advisory — nothing anywhere
refuses to grow; and the physical block lags the logical shrink by one GC, so a
quieted actor strands its pre-shrink block (observed: 256 MB block over 120 MB
live). Profiling is better than documented: pit profile exists (CPU sampling,
allocation sites, a compile preset, follows new builders) and every GC already logs
27 fields including allocation sites to observe.jsonl.
The deletion bill is real: ~1,300–1,500 LOC deletable, ~1,100 LOC of C becomes
per-product optional. Biggest single item: the static link lane (~550–600 LOC +
~210 test LOC) has zero surviving consumers once the press flips to pool lane
and the shop moves to pools-per-unit/executable — the dev realize lane is the static
lane. The linker’s program-side census (~330 LOC) dies iff R5 is a
compiler-emitted stone op dominating the module return. boot.qop/shop.qop are
already mach-free, so R8 is mostly true at the artifact level already.
The runtime execution path is clean — the pooling model is not a sharing
problem. Per image call: one memory_order_acquire load of pin_owner, no slow
path; every shared-write candidate on the execution lane is unreachable or closed at
pool-open; pool granularity changes mapping count and pin traffic, never the
execution story. Interned text: the belief is correct — the ct pool has exactly
7 producers, all boot/registration; runtime text lives on actor heaps; pit_key_*
reads take no lock. Both GCs are 100% thread-local. What the audit did find:
the send path takes two process-global locks per message (actors_mutex +
engine.lock — the next image_lock shape), pin/unpin is O(n)×4 per pool per actor
start plus a redundant blake2 re-hash per actor, nan32’s opaque_lock is a global
mutex per Pit_GetOpaque, and three real races (unsynchronized pit_class_id_alloc
++ — security-adjacent, opaque records authenticate by class id; g_log_actor_id
free-while-read; pit_tick_drain_head push; twin global_timer_id counters).
1. The lanes
Sizes: S < a day · M = days · L = a week-plus. Everything in lanes A–C can start today; D–F are the arc proper and sequence behind rulings and each other.
Lane A — Measurement + CI (independent; the floor everything is judged against)
| # | Item | Size | Notes |
|---|---|---|---|
| A1 | Harden the bench suite into pit.bench.*@1 producers (fleet block, per-unit compile rows, GC row asserts gc_count > 0) | M | prototypes exist: benchmarks/bench_suite.ce, start_latency.ce |
| A2 | Scheduler counters: enqueue/dequeue/wakeup totals + queue-depth integral in engine, surfaced via $inspect | S | ~40 LOC C + ~15 endowment; makes actor-switching measurable; also the measure-first prerequisite for C3 |
| A3 | Memory observability: actor death record (peak heap; a builder’s peak is currently lost, ~30 LOC C); byte figures into the two telemetry schemas (~40 LOC); pit profile run -- <cmd> wrapper; switch reports to heap_alloc_size (heap_size under-reports ≤2×) | S–M | pit profile already does the hard parts |
| A4 | CI bench job on pit-ci (linux): counters gate via perf/bench_gate.py; one JSONL line appended to a perf-data branch per push; artifact retained. Include per-stage compiler timings for one representative file (tokenize/parse/fold/mcode/streamline/link/lower rows) — the baseline the next compiler arc runs against | M | model: ir_census.py; publish shape: the existing publish-nightly fan-in |
| A5 | Publish to Cloudflare: CI builds the Hugo site (docs + the trend page reading perf-data) and deploys to Cloudflare static hosting via wrangler, John’s CF credentials as gitea CI secrets. All off dev — master merge later flips the branch; dev.crumbpit.org is the floated host for the interim | M | needs an interactive session with John: activate the runner on the server, install the CF token as a secret — Claude never handles the credential itself |
| A6 | CI hygiene: auto-pickup of the pit-ci image tag (drop hand-pinned .gitea/ci-image.env), wire make budget into the gate. Windows-runner ruling RESOLVED: none — the docker/wine job is the windows lane | S–M | budget exists and is honest; check-all never runs it (Makefile:618). 2026-08-06: budget is now the SHOP budget only — the compiler half was retired, so this item’s scope shrank to shop_budget.py |
Lane B — Compiler correctness + memory (independent start; two items reseed)
| # | Item | Size | Notes |
|---|---|---|---|
| B1 | Exact-decimal mcode literals. Tokenizer keeps the spelling (number row already has text); fold in exact decimal; refuse-to-fold what no supported rep represents. Kills 1e300 → null-in-artifact | M | hard gate for F3 — today shipped mach is the only rep-exact path to a narrow-rep target |
| B2 | Module-main emission: stone op before return (R5) + exports declaration on pit.mcode.unit@3. One landing — same emission site, three customers (linker finalization, R4’s reload walk, census deletion) | M | census detects frozenness via a dominating stone op — this exact mechanism is what makes F2 worth ~330 LOC |
| B3 | Builder live-set diet: drop the token stream after parse; stop holding rows + interned stream + sites simultaneously; stream the canonical encode. Target ≤ 15–20 MB live per builder | M–L | the “are we doing something stupid” answer; verified by A3’s death records |
| B4 | GC: fix the one-GC physical-shrink lag; make the memory limit enforce (per-actor and/or buddy.cap) with a named refusal | M | turns “OS kills the daemon” into an honest error |
| B5 | Replace the fleet width clamp with a byte budget | S | end state = John’s “unbounded by count, memory is the constraint” — but with the constraint real. Sequenced strictly after B3+B4 |
B1 and B2 both change unit artifacts: land adjacent, one reseed batch.
Lane C — Runtime soundness (small, immediate; from the sharing audit)
| # | Item | Size | Notes |
|---|---|---|---|
| C1 | pit_class_id_alloc → atomic | S | ~20 LOC; duplicate ids defeat opaque-record auth |
| C2 | g_log_actor_id lifetime; pit_tick_drain_head push; unify global_timer_id | S | three known races |
| C3 | Send-path lock split (start: own mutex for timer_heap) | M | CLOSED, not implemented — measured, see proposal-notes/send-path-measurement.md. The two process-global send-path locks are 13.5 % of engine-thread wall time and the proposed timer_heap split prices at 0.4 %. What the profile names is target->msg_mutex at 63 % — a per-actor lock, a different defect, its own lane |
| C4 | Pin/unpin O(1); hoist placement dedup ahead of the per-actor blake2 + STONE re-hash | M | DONE — pin/unpin scans replaced by a view-pointer index; the dedup probe runs before any hashing. Warm actor spawn 4.62 ms → 2.93 ms (−37 %); 195 of 418 window adoptions on a fresh daemon now skip hashing ($inspect.snapshot()’s runtime_image_adopt_known) |
| C5 | Fix the five comments that overclaim sharing | S | truth-in-comments; the audit lists them |
Lane D — Products (the arc core)
| # | Item | Size | Notes |
|---|---|---|---|
| D1 | The start declaration (R1/R2): boot entry carries {root, resident/one-shot, shop?}; C reads it; ensure_shop_dirs conditional; PIT_APP/sys_is_app_bundle die (PIT_INPROCESS survives as forge-internal); daemon C pulled by cake only when daemon actors are in the closure | L | the seam; nothing ships until it exists |
| D2 | pit product <locator> [--include …] (R3/R6): closure via $start-site static analysis + include list; one-pool whole-program press, stripping on, max opt; press moves out of the shop actor (into the builder worker — retires the +292 KB); wraps cement+forge as one command. Targets: darwin + linux first, windows immediately after on the proven L6 cross lane | L | acceptance: mcode compiles a file on a bare linux container; windows binary boots under wine in the existing CI job |
| D3 | R9 general: per-product feature stripping (hooks, trace, daemon) — C fold + endowment-shim removal as one lever; absent feature ⇒ named realization refusal | M–L | $hook’s PIT_HOOK_ARMED fold is the model; ~1,100 LOC of daemon C is the first customer |
Lane E — Reload + debugger (after B2 and D2)
| # | Item | Size | Notes |
|---|---|---|---|
| E1 | Hot reload (R4/R7): shop default = pool per mcode unit; swap = walk the export record, repoint function bytecode pointers; add/remove member ⇒ warn/error; no frame migration | L | B2’s exports field is the walk’s ground truth |
| E2 | Single-stepping on top of suspend_actor (park-at-next-word, resume-one) | M | the only missing debugger primitive; freeze/frames/locals/source-resolve already work (shop_tools/debug.ce) |
| E3 | Symbol sidecar + remote attach: build emits binary + origin sidecar keyed by pool content hash (identity stays under R8); pdb fetches by hash | M | ship-the-symbols, not new invention |
Lane F — Deletions (rolling; strictly sequenced)
| # | What | LOC | Gated by |
|---|---|---|---|
| F1 | Now: dead modules (verify_ir.cm, analyze.cm), PIT_POOL_PAYLOAD block (38 lines), allow_compile/allow_static/allow_mach/allow_dylib, restart_frame doc rewrite (both places), dev/small pass-profile collapse (DEV_LINK ≡ SHIP already; dev becomes a granularity, not a pass set) | ~150 + docs | nothing (profile collapse reseeds — batch with B1/B2’s reseed) |
| F2 | Linker program-side census + one pinned test | ~330 | B2 landed |
| F3 | mach sharing + pit.catalog.variants@1 + --payload mach arms | ~300 | B1 landed + R8 exceptions ratified (§3) |
| F4 | The static link lane (+ its tests) | ~550–600 + ~210 | D2 (press on pool lane) and E1 (shop on pools) — last, it is today’s only lane |
2. Sequencing
now: A1 A2 A3 A6 · B1 B2 (one reseed batch, + F1 riding it) · C1 C2 C5
then: A4 A5 · B3 · C4 · D1 · C3 (once A2 numbers exist)
then: B4 → B5 · D2 → D3 · F2 (after B2)
after D2: E1 E2 E3 · F3 (after B1 + ruling)
last: F4
Three forced orderings, so they don’t get lost: B1 before F3 (rep-exactness), B2 before F2 (the stone-op mechanism), D2+E1 before F4 (the static lane is load-bearing until both flips). Everything else is preference.
3. Rulings — RATIFIED 2026-08-05 (John, in conversation)
- R8 exception keep-list — ratified as listed (
boot/targets/linux/*,mach_pool_fixture.h,PIT_INPROCESSas forge-internal). - The clamp — keep-count-clamp-until-B5-byte-budget accepted.
- R5 mechanism — ratified: compiler-emitted
stoneop for what a module returns. F2’s census deletion is live once B2 lands. - B1 shape — recommended shape ratified: keep-spelling + exact-decimal fold + refuse-to-fold-unrepresentable.
- Profile collapse — ratified. A “profile” is henceforth your selection of flags in a Makefile — pool granularity + stripping + pass switches on the product/link plan; the named dev/small pass-sets die (F1).
- Windows — no CI runner; but programs must work on windows — the system and CI already do (docker/wine); D2 carries windows as a near-v1 target.
- L2b stash — SUPERSEDED. It was rescued to branch
l2b-pgo-emitter-stash(tip5984b11a1); John then killed the L2b PGO drain outright on 2026-08-07 and the branch is deleted. PGO may return later. Seeplans/carried.md, “L2b (the PGO drain) — KILLED BY RULING 2026-08-07”.
3b. Gate discipline for this arc (ruled: do not go overboard)
This arc sharpens what exists; rigorous per-change testing would slow it more than it protects it. The discipline:
- Per landing: build + the targeted tests for what changed. No full
make check-all, no seed-fixpoint proof, no CLI E2E battery per change. The 3×-seed / 2×-CLI habit from the finalization arc is explicitly OFF here. - Reseeds are batched — B1+B2+F1 ride one reseed; later artifact-changing landings batch the same way.
- The full battery runs once, at arc end: the three suites + cross builds on the important targets — 64-bit macos, linux, windows — with emscripten as a bonus lane, and a nan32-on-64-bit run for the low-memory sanity check.
- CI keeps its existing per-push gates (they are already paid for); the point is not to add per-change ceremony locally.
4. Confirmed clean — stop worrying about these
- Actors executing from shared pools do not contend. One acquire-load per image call, no slow path, no shared writes on the execution lane. Granularity (per-module / per-executable / many-serving) is a size-and-reload lever only.
- Runtime-created text is not shared. Actor heaps hold it; the ct pool is boot/registration-only (7 producers); key lookups take no lock.
- Both GCs are entirely thread-local — the cleanest subsystem in the runtime.
- Compile artifacts don’t duplicate work across the fleet: 0 redundant keyed compiles inside a press; the three-press repetition and the boot-lane identity flip were the waste, and both are fixed or ruled.
- Multi-cart mounting does not exist.
cart_boot.cmounts one root lane, no$mountendowment is in the tree, and a cement cart’s only consumer is forge. Pinned here so it stays gone — bundles are the distribution idea.
5. Plan retirement
When this proposal is ratified: plans/programs.md (rulings) + this file +
plans/measurement-suite.md + plans/proposal-notes/ are the live set;
board.md, one-binary.md, compiler-perf.md, derivations.md, todo.md,
edicts.md, night-2026-08-04/ move to plans/archive/ as evidence.
method.md and rejected-findings.md stay. Carried-forward items not in the lanes
above (hpack miscompile, E3 scratch-image check, platform rollout, the dangling
jump_false defect, make local lacking daemon-fresh, the foreign-daemon-socket
trap) get one line each in a carried.md so nothing silently drops.
Executed 2026-08-07 (arc/constraint-cleanup): the moves are done, every live
pointer is repointed, and todo.md was triaged item by item before archiving — the
survivors, the deaths with their reasons, and one item the inventory wrongly called
dead are all recorded in plans/carried.md under “Carried out of plans/todo.md at
its retirement”. plans/carried.md joins the live set (it is the ledger those items
land in). The L2b PGO emitter branch was deleted, not preserved: John killed the
drain on 2026-08-07. The compiler-quality arc that follows this one starts from
compiler-vision.md’s surviving opportunity map plus lane A’s per-stage baseline;
plans/memory-arc.md holds the queued memory/residency arc.
Source: plans/work-proposal.md