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 shipsshoplib/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)

#ItemSizeNotes
A1Harden the bench suite into pit.bench.*@1 producers (fleet block, per-unit compile rows, GC row asserts gc_count > 0)Mprototypes exist: benchmarks/bench_suite.ce, start_latency.ce
A2Scheduler counters: enqueue/dequeue/wakeup totals + queue-depth integral in engine, surfaced via $inspectS~40 LOC C + ~15 endowment; makes actor-switching measurable; also the measure-first prerequisite for C3
A3Memory 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–Mpit profile already does the hard parts
A4CI 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 againstMmodel: ir_census.py; publish shape: the existing publish-nightly fan-in
A5Publish 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 interimMneeds an interactive session with John: activate the runner on the server, install the CF token as a secret — Claude never handles the credential itself
A6CI 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 laneS–Mbudget 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)

#ItemSizeNotes
B1Exact-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-artifactMhard gate for F3 — today shipped mach is the only rep-exact path to a narrow-rep target
B2Module-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)Mcensus detects frozenness via a dominating stone op — this exact mechanism is what makes F2 worth ~330 LOC
B3Builder 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 builderM–Lthe “are we doing something stupid” answer; verified by A3’s death records
B4GC: fix the one-GC physical-shrink lag; make the memory limit enforce (per-actor and/or buddy.cap) with a named refusalMturns “OS kills the daemon” into an honest error
B5Replace the fleet width clamp with a byte budgetSend 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)

#ItemSizeNotes
C1pit_class_id_alloc → atomicS~20 LOC; duplicate ids defeat opaque-record auth
C2g_log_actor_id lifetime; pit_tick_drain_head push; unify global_timer_idSthree known races
C3Send-path lock split (start: own mutex for timer_heap)MCLOSED, 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
C4Pin/unpin O(1); hoist placement dedup ahead of the per-actor blake2 + STONE re-hashMDONE — 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)
C5Fix the five comments that overclaim sharingStruth-in-comments; the audit lists them

Lane D — Products (the arc core)

#ItemSizeNotes
D1The 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 closureLthe seam; nothing ships until it exists
D2pit 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 laneLacceptance: mcode compiles a file on a bare linux container; windows binary boots under wine in the existing CI job
D3R9 general: per-product feature stripping (hooks, trace, daemon) — C fold + endowment-shim removal as one lever; absent feature ⇒ named realization refusalM–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)

#ItemSizeNotes
E1Hot 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 migrationLB2’s exports field is the walk’s ground truth
E2Single-stepping on top of suspend_actor (park-at-next-word, resume-one)Mthe only missing debugger primitive; freeze/frames/locals/source-resolve already work (shop_tools/debug.ce)
E3Symbol sidecar + remote attach: build emits binary + origin sidecar keyed by pool content hash (identity stays under R8); pdb fetches by hashMship-the-symbols, not new invention

Lane F — Deletions (rolling; strictly sequenced)

#WhatLOCGated by
F1Now: 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_LINKSHIP already; dev becomes a granularity, not a pass set)~150 + docsnothing (profile collapse reseeds — batch with B1/B2’s reseed)
F2Linker program-side census + one pinned test~330B2 landed
F3mach sharing + pit.catalog.variants@1 + --payload mach arms~300B1 landed + R8 exceptions ratified (§3)
F4The static link lane (+ its tests)~550–600 + ~210D2 (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)

  1. R8 exception keep-list — ratified as listed (boot/targets/linux/*, mach_pool_fixture.h, PIT_INPROCESS as forge-internal).
  2. The clamp — keep-count-clamp-until-B5-byte-budget accepted.
  3. R5 mechanism — ratified: compiler-emitted stone op for what a module returns. F2’s census deletion is live once B2 lands.
  4. B1 shape — recommended shape ratified: keep-spelling + exact-decimal fold + refuse-to-fold-unrepresentable.
  5. 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).
  6. 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.
  7. L2b stash — SUPERSEDED. It was rescued to branch l2b-pgo-emitter-stash (tip 5984b11a1); John then killed the L2b PGO drain outright on 2026-08-07 and the branch is deleted. PGO may return later. See plans/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.c mounts one root lane, no $mount endowment 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