Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.
Roadmap — from the green base to what the docs describe
docs/ says how the system should work. This says what is between here and there, in
dependency order, and why each step unblocks the next. plans/worklist.md is the row-level
ledger. Rulings arrive in conversation and land here or in plans/edicts.md; see
plans/method.md. References to review-queue Dn below point at
plans/archive/review-queue-2026-07-25.md, kept for reasoning only.
What 9466791a was actually about, stated properly because the framing matters: the
target-final artifact chain and the image runtime. Content-addressed objects with
derivation keys separated from content hashes (mcode@3, executable manifest, target
realization, cart@2, bundle@2), and code that is mapped rather than parsed. Splitting
engine_lite is a consequence, not the point.
THE PREMISE ARC — ruled by John 2026-07-27, the successor sequence
“No earnest compiler rework until the basic premise is done” — engine_lite shrunk because actor construction is uniform and pulled into modules; actors execute bytecode from mapped-in stone pools; and all the deletions that make the transition real. Until then the mcode/mach rework stands on shaky ground. (The in-flight slot-limit agent still delivers its diagnostic + rework PROPOSAL — the proposal waits here for its ruling; hpack keeps waiting; model-fit items that ARE the premise — the import op — land inside it.)
The sequence, in dependency order (each row = roughly one agent landing; the spine is serial because every row touches the start path):
pit.executable@1dies — the consumer conversion (cement’s ~15 reads + EXECRECS, cart.cm, cart.ce, bundle.ce, development_shop, shop_realize, the start door, six tests); target payloads move into the realization. DONE — see the landing record inplans/next-arc.md. (Row 1 also read “[queued next]” and row 830 read “future work” long after it landed; corrected 2026-07-29.)- D1 — realization@1 +
$image— the realization as a distinct stored artifact;$imagebuilt (open/pin/bind/entry, boot holds it, C mechanics / Pit policy); the start path follows realization rows through$imagefor pool-backed executables. - D2 — the import lane — the
importop emitted (model-fit compiler work, in scope), IMPORT rows bound through the realization’s actor-owned result arrays, the realize lane groups units into pools (minimal link plan first: one pool per executable). - D3 — the executable-as-array — 4a (the endowment-module resolution lane) and
4b (THE ARRAY) are DONE: the plan format is one ordered ENTRIES array plus the
NAMES each entry binds, three kinds and no more (UNIT / NATIVE / VALUE). The
separate
bindingstable and its three fulfilment CLASSES are gone,providesfolded into NAMES, and neither the C walker nor cart.cm holds a list of engine-owned names — see the 4b record inplans/next-arc.md. What REMAINS of item 4: the start walk still lives in C (moving it to Pit is phase E’s boot/clerk split), and engine_lite still holds its residual floor. - The fleet runs from pools — the dev mount lowers mcode windows through the
fragment+linker lane instead of pmac+materialize; the seeded fleet executes from
mapped stone. 5a is IN and the switch is OFF (
PIT_POOL_PAYLOAD=1): the lowering, the linking, the C resident entrance and the start walk’s pool arm are all built, one pool per unit;tests/image_lanes.ceis the new gate and it caught a real defect (the closure-frame union arm — five reads took the wrong arm, so an image function’s lexical link read the realization-state array and the whole fleet failed to start). 5b is what remains: with the lever on, a started actor diesnot a functionin the C start walk while the same pools run correctly when entered from Pit — see the landing record inplans/next-arc.md. One pool per EXECUTABLE (the minimal link plan proper) waits behind 5b, because it needs the realization’simages/instancesrows and a plan section naming (image, entry). - The coupled deletions — NOT one landing; 5-6. See
plans/stone-6-7-map.md, which supersedes this row. Two corrections that matter: (a) there is no C single-unit pool assembler —pmp1is written only by Pit — andmach_materialize_cpoolcannot die until one exists, because it dies withpit_load_mach_code, which also serves thepmcdlane that is every window ofroot.cart. That prerequisite (6.0) is the true head of the chain and was unrecorded. (b)pmacis not a “transitional reader” but the live payload form — 113 windows inboot.cart, 125 call sites, and a permanent cross-target producer.ct_lockreaching zero also needspit_rt_ct_allocforPitCode/PitFunctionto go, which this row omits and the memory-model section states correctly — the two passages disagreed. - E — minimal C boot — also 5-6 landings; see
plans/stone-6-7-map.md.start_plan.cis 803 lines (this row said 870,next-arc.mdhas said 743 and 757 — 5a grew it). The honest target is ~150 lines for the cart-mount + header + enter path and ~240-260 counting runtime creation and the boot wota, excludinghost.c. The crossing constraint recorded innext-arc.mdis half wrong: moveengine_claimsderivation intobuild_engine_recordand C never reads ENTRIES, after which 4b’s section-kind recipe applies directly. The boot actor starts the clerk and is the root;shop_actor’s exclusion from the pool lane resolves byroot.cartshrinking from 23 windows to 2, NOT by changing M1’s rule.
NOT gating the premise (follow or interleave freely): bundle@2, the cart@2/pcr2 producer, os decomposition + the internal/ guard, sync-file deletion, std::sysinfo, F’s residue (budgets actually read; the pit_profile.h chain; provider-set discovery), per-object git fetch, the debugger ACTOR (its floor is in flight; the actor itself is wanted tooling and can land any time after $inspect merges).
Status — 2026-07-25
Phase A is COMPLETE. M1 has landed. Code executes from a read-only mapping, and a record’s shape is a row in that mapping — the same C type, not a copy.
| phase | state |
|---|---|
| A — the image layer | DONE. A1a/A1b/A1c, A2, A3a/A3b all landed |
| B — retire the ABI crossing (M1) | root cart done (d39c19da). The seeded fleet in boot.qop is still mach, so a stamp bump is not yet free for the dev loop — see M5 below |
| C — the artifact chain | object store built; the vocabulary (mcode@3, manifest, realization, mach_pool_emit, bundle@2) is not |
| C2 — origin map + the debugger half | unbuilt. $pgo’s real form waits here too |
| D — the executable-as-array | the import opcodes are in — MACH_LOAD_MODULE_RESULT (144) and MACH_LOAD_BOUND_MODULE (145), fresh slots at the tail, handlers only. No emitter produces them, so no artifact contains them and PIT_MACH_ABI still reads mach-v2+fuse1: the stamp is owed when the emitter starts emitting, not when the handler learns to read. The array itself is next |
| E — minimal C boot + the boot/clerk split | after D. Both rewrite how an actor comes into existence — sequence, do not parallelise |
| F — the remaining target levers | independent |
Also landed this session, outside the phases: $shop dissolved into $clerk/$inspect/
$runtime with a token-forgery hole closed and proved closed; $mount deleted from the
design; check-arms built and wired into check-all at 14/14; the immediate-ASCII lane made
unreachable (steps 0-3 of 6).
Green at every commit: make check, make check-arms (14 arms), make check-cli,
make lint, ./pit fuzz. Two gates are RED at baseline and are not ours: make check-web
(a shop_actor stack overflow at depth 1024 under node) and make budget (stale recorded
baselines). make check does NOT run meson test — check-arms does.
What to do next, in order
— DONE (code_cacheec15c99f). Bounded structurally rather than evicted; see the memory-model section below for what a hit actually buys and why eviction is impossible.Finish the immediate-ASCII deletion— DONE (24e6fbdd). One text representation now;PIT_TAG_EMPTY_TEXTis the renamed sentinel and no bit pattern moved.- Phase D —
the two import opcodesdone; the executable-as-array is what remains.
The resolution arc — landing order (ruled 2026-07-26, edicts “The resolution model”)
HARD STOP after landing 8 — John, 2026-07-26. When all eight land, do not proceed into phases C, C2, D, E, or F, and do not start the parallel-safe items (check-web, the profile levers, the debug-contracts arm). John wants to discuss the remaining phases and confirm his own understanding of them before any of that work begins. The arc ends with a conversation, not a dispatch.
Each lands green and alone; agents touching pit-shop/ are serialized. The two-pass rewrite
of use('core/…') sites (first to runtime::…, then to final homes) is accepted — two cheap
mechanical passes beat one circular landing.
Docs— DONE (1add2adb). Thirteen pages state the target.— DONE (::and the chains2d0dabc8). 546 sites rewritten, the/-alias interpretation deleted, the lists live in the recipe and fold intorecipe_fingerprint. A transitionalprograms: ["core/internal"]list carries the CLI until 3 deletes it.The two-phase crossing recipe, now proven and canonical for the landings below: the committed seed carries a compiled resolver/fleet that predates the change, so a rewrite the fleet must compile cannot land in one pass. The crossing: revert sources, add one marked transitional ring, reseed; delete the scaffold, re-apply the rewrite, reseed again. The scaffold exists for exactly one reseed and is absent from the final tree — a crossing tool, not a compatibility arm. Landings 3 (cemented CLI locators), 6 (every symbol renames), and 7 should expect to need it. The stale untracked
boot/boot.cartwedge pairs with it: move the cart aside, boot fromboot.qop, re-cement.— DONE (shop_tools726b573e). 33 tools moved,bin: ["shop_tools"]live,programslist deleted, the script discriminator built (it lived in three places; now one predicate). The CLI’s starting package is still the terminal actor’s own (core), not the cwd’s — deliberate: orthogonal to the bin path, and changing start attribution has its own blast radius.resolve_locator’s newpackage_name_for_rootis the primitive a cwd-package CLI needs when that is wanted.— DONE (std_endowmentsa086077f). Five providers moved, rows deleted, the use-block live and narrated,endowments_wholedeclared. The symbol follows the KEY, not the path — so no crossing fired, and the manifest delta was file-column-only. One meson gate read a file path (PIT_HAVE_HOOK) and would have silently compiled the hook out; fixed. Pre-existing red found:tests/shop_resolve.ceworkspace-member step fails at baseline, only underpit test all— chipped for a separate session.pitlib made real— DONE. Six wrapper modules deleted,native/gone,blob.cout ofruntime.c,internal/time.candrandom.cmin. pitlib is walked whole byMEMBER_MODULE_PACKAGESwith keys taken package-relative, so its fivenativesrows and the hardcodedblobstatic-lead row are all discovery now. The manifest delta is file-column-only across all 12 targets by multiset (the one exception isblob, which changes LINE KIND fromstatic|leadtoextension|while keeping key and symbol).initbecamePIT_USE_BOOT(<fn>)in the C file, scanned besidePIT_USE_PROBE— the namePIT_USE_INITwas already taken by the hand-written-entry macro. No crossing fired: nothing but a stale realization cache + a stale dylib stood in the way, andmake nukecleared both. Two consequences worth knowing: a member package’s flat key needs a third symbol probe inadd_native_provider(the wrapper’s old job, and landing 6 deletes it), andblobis now claims-trimmable like any other module rather than an always-static row.core dies, in two halves (fleet home RULED by John 2026-07-26):
6a — the moves.DONE (1efcc05f). Fleet in pit-shop, support in shoplib (five fleet-only modules in pit-shop instead, with importer evidence), engine its own package,internal/runtime.cmdeleted as a subset ofconfig.cm. No crossing: the seed producer reads the engine by file path and the old daemon boots from compiled content inboot.qop. Census:runtime::remains only for os/sysinfo (landing 7) + four flat-key stragglers 6b retires. OPEN for John with 6b: movingterminal.cemoved the CLI’s starting package topit-shop, so pit-shop’s alias table now partly encodes the CLI’s reach — decide whether that reach belongs in the recipe’s shop-scopealiasesinstead.6b — the symbol flag day.DONE (0b2c1edb). The authority isshoplib::symbols(cake cannot import pit-shop, so it moved rather than being shared); manifest field 4 is the FULL symbol as data and no consumer builds one; every compiled-in module ispit_<package>_<path>_use;dependencies→aliases,name/membersdead; the workspace root is theruntimepackage (.pit/runtime,config.runtime_path,--runtime) and$core_pathwas DELETED rather than renamed — it only restated the ungatedshoplib/shop_pathsmodule. Three lists became one walk plus one build list (LINKED_C_PACKAGES, kept build-side deliberately — linkage is cake’s question, not the clerk’s), the eightpit-*protocol directories dropped their prefix (John’s ruling: the aliases were the natives-table disease in miniature), the 14 k==v aliases died, andbingainedcake. John confirmed: root packageruntime, actor keyedstd/actor. One correction proven against the failure: bare single-segment imports (use('fetch')) genuinely needed the k==v aliases — fixed by qualifying the import, not restoring them. The crossing fired on exactly one thing —runtime::internal/oshas no source file, so only the OLDstatic_target_hit(hardcoded to packagecore) could answer it. Two scaffolds bridged one reseed: a"dependencies": {"runtime": "core"}row in the nine packages that writeruntime::(the old reader readsdependencies, the new one readsaliases, so the new fleet never saw it), and 52 hand-addedstatic|lead|<old-symbol>|<new-symbol>rows inboot/manifest/darwin.manifest. Both gone after the second reseed;make manifestdropped the rows. Two live bugs the rename exposed:engine_lite.cmloaded the couple module by the hardcoded keyinternal/couple, and a stale key silently disarmed$couple’s death pact — a loader cart then never exited. Andcart.cm’s claims closure stripped acore/prefix, which had been making the CART3 spec disagree with pitlib’s keys all along.Every location honest — the separable half DONE (
d30abaca):std/actor.creal (John’s catch closed),wgpua package, theaudio_testarm proven FALSE and deleted (the file needed none of the backends the arm claimed),lane/leaseasPIT_USE_LANE/PIT_USE_LEASEmacros in 22 C files with the manifest fact-identical by multiset (which caught tvos’suikit_window.chaving silently lost itsmainlane).nativesis down to its last two rows:osandsysinfo.The rest is blocked by findings, not effort, and merges into the post-stop agenda:
- Two resolver rules make
platform/posixunusable as a package name (multi-segment = canonical remote locator; the fallback parse takes the first segment — the exact text-guessing the package-identity invariant forbids). Reachable design: flatplatform-*packages + locator-valued module arms; a 15-dir restructure + crossing. - Discovery cannot replace the catalogs until the subsets are ruled: darwin, linux
and ios each take a different subset of
platform/posix’s files — the catalog rows are a per-target capability policy in disguise. Walking posix whole would newly grant iOS$posix_spawn. Needs the package splits (apple, …) and per-target endowment lists; the recipe’s lists are shop-wide today. - The six console
osproviders are runtime C with hand-coded symbols because meson injects-DPIT_USE_NAMEonly for extension rows — moving them to extension rows is the prerequisite for any arm, and deletes the six branches as a consequence. - Windows compiles
platform/posix/os.c(#ifdef _WIN32) — the directory name is already dishonest for one target. pit-qbe/vendor→source/+ theLINKED_C_PACKAGESderivation: mapped, not started —is_native_file/is_support_sourceinpit-shop/package.cm:41-47are the seam; the vendored tree carriesmain.c/tools//test/that must go for “the folder is the unit” to hold; deriving the list needs the closure fed to cake, which cannot import pit-shop.internal/now holds 5 legitimate runtime fixtures +sysinfo.c, whose 7 cross-package importers are exactly what landing 8’s guard would refuse.
- Two resolver rules make
The
internal/guard: RULED (b) — held until os/sysinfo move. John, 2026-07-26: “we have the mechanism to turn the guard on, so we can just do it when it’s ready.” The::boundary makes the refusal statable andendowment_package_refusalis the exact template (~20 lines, fires on the candidate’s package); the turn-on rides with the platform restructure post-stop. No carve-outs, no exemptions — the rule arrives whole.
THE ARC IS CLOSED AND CLOSED OUT (05d91a9f): the file providers are package-private,
watch_client is pit-shop’s, libgit is its own package, the docs are the diffable contract
(22 pages, 14 contradictions fixed), and make budget is a REAL GATE again — re-baselined
after the compiler campaign, with the one anomaly (record_lookup alloc_count +89.8%) flagged
rather than baked in. Five gates green: check, check-arms 14/14, check-cli, lint, budget.
THE ARC IS CLOSED. Seven of eight landed; the eighth is a ruled deferral, not an omission.
The post-stop conversation has begun — its agenda: the os decomposition ($environment,
$clock, entropy, $dlfcn-as-actor, sync-file deletion, $platform), the flat platform-*
restructure with nested-package-root resolution, the endowment-package splits by take-set
(posix/apple/macos/ios) with per-target recipe lists, pit-qbe/vendor→source/ +
LINKED_C_PACKAGES derivation, then the guard — and after all that, phases C, C2, D, E, F.
Docs landed (1add2adb). Of the ambiguities the docs pass surfaced, five are answered from
the record: core does not survive as a package (ruled; $core_path dies with it or is
renamed in landing 6); a bare internal/… path never continues down the chain — always the
caller’s own, or a miss (anything else lets the chain defeat the privacy rule); pit test →
shop_tools::test with the ordinary .ce probe is the intended bin shape; the refusal of
<pkg>::internal/x is a resolution-time rule on requests, so a cemented canonical identity
in an artifact is a name, not a violation; the committed build manifests are rendered outputs
of the one authority, not hand-maintained mapping tables, and stay for cold bootstrap.
Two need John: (1) the endowment lane/lease facts have no home in the three-field
manifest — likely answer is a macro in the C file, the PIT_USE_PROBE precedent, but it is a
design call; (2) endowments_whole — the earlier ruling wrote it as a second list, the docs
pass demoted it to a per-entry mark to keep the four-list count; either works, one must be
picked.
Proposed, pending John — the script-fallback discriminator: an argument that is
locator-shaped (identifiers, /, ::, dots, dashes — no whitespace, parens, operators, or
quotes) is resolution-only, and a miss reports the full chain; only non-locator-shaped text
falls through to the script path; -e forces script always. Decidable from the text alone;
pit sp gets “not found”, never a parse error.
The queue above is empty. What is actually next, in dependency order:
- The executable-as-array (phase D proper). The opcodes and the doc page are in; this is
the work that deletes
start_plan.cand dissolves both surviving engine-value tables. mcode@3and the artifact vocabulary (phase C) — independent of D, andmach_pool_emitis blocked on it.- The
nativestable (edict §5) — not blocked on anything. 24 of its 33 rows across both manifests are pure restatement of a path; the other 9 carry two facts that belong in package membership. Core is ruled: blob, fit, time, random, json, math.
Not next, and now known why: the key intern table. It is blocked behind constants living in the mapped stone pool — see the memory-model section.
Phase A — the image layer — COMPLETE
Kept for the reasoning, which the later phases still lean on. Every increment below landed; see the Status block above.
A1. Record shapes. John’s formulation, which is simpler than 9466791a’s and is what
we build: a shape is just another stone object in the image. The pool holds a list of
text, a list of shapes, a list of bytecode, and numeric constants — all stone. A record
holds a pointer to a shape if it has one; the stone shape points at stone keys, so building
a shaped record is a pointer store and reading one is an index. There is no “dynamic
shape” kind — losing a shape is losing the pointer. Add or delete a key and the record
stops being shaped and becomes an ordinary keyed record.
That deletes a distinction rather than porting one. 9466791a has two kinds, IMAGE and
DYNAMIC, and its own comment marks DYNAMIC as “legacy decoded-Mach/native descriptors
… transition-only until those execution lanes are gone.” Under John’s model those lanes
are exactly what the image layer removes, so the kind tag has nothing left to discriminate
and the kind field itself can go.
One thing genuinely blocks that and must move first. DYNAMIC also carries a mutable
atomic_uint observation_token for development PGO — a lazily assigned process-local
token. An immutable image shape cannot hold it, which is the real reason the two-kind split
exists at all. So PGO’s per-shape token moves out of the shape and into the profile
side, keyed by shape identity, where a mutable development-only observation belongs anyway.
Do that before collapsing the kinds; it is small, and it is the load-bearing prerequisite.
Sequencing, since 94 call sites index the struct directly:
- A1a — accessors over the existing layout, convert all 94 sites, struct untouched. Mechanical, zero semantic change.
- A1b —
move the PGO observation token off the shapeDONE (f0022268), by deleting it. The field was namedreservedin this tree, notobservation_token. A shape’s own address is its identity — shapes are runtime-owned and immortal, and the PIC path already compared raw shape pointers. That removed the field, a global counter, and an O(n) walk ofshape_cacheunder a lock; the walk was itself a dependency on the runtime shape table A1c deletes.PitRecordShapenow has no mutable field at all, which is the precondition A1c needed. - A1c — shapes become stone objects in the pool; records point at them; the
kindfield and the runtime shape table both go. See review-queue D5 for why the table could not go earlier. - Watch
record_capacityand the nan32 arm, where a record’s capacity is written from its shape count, and the_Static_assert(sizeof(PitRecord) == 24).
A2. Image function kind. A PIT_FUNC_KIND_IMAGE arm on PitFunction, plus the GC visit
arms. Genuinely dormant: no value of that kind can be constructed until A3.
A3. The VM cursor. MachExecCode, a by-value tagged union, replaces
PitCodeRegister * in the dispatch loop — 126 call sites where code->cpool[b] becomes an
accessor.
- A3a — register-only union, convert every site. No encoding change, no ABI bump.
Run
make budget: this is the hot loop, and a dispatch regression hides here. - A3b — add the image arm,
Pit_NewImageFunction, and restore the three execution tests deferred from increment 1.
After A3b a pool executes from a read-only mapping — the thing that lets code run from
ROM — with PIT_MACH_ABI still reading mach-v2+fuse1 and boot untouched.
This also unblocks the one thing increment 3 withheld: provider_entry’s construction step
currently refuses, with the original call quoted at the site. Restoring it is deleting the
refusal.
Phase B — retire the ABI crossing (M1)
Measured and proven; see worklist §M. The cart’s engine section carries mcode rather than a finalized pool, and C serializes it at mount in 7.5 ms. The same dev cart then boots on a v2 or v3 binary, and an ABI bump stops being an event.
Do this before the two import opcodes, not after. Every hour spent crossing the ABI without it is an hour spent re-deriving why four agents could not.
Standing constraint: the engine stays one translation unit. C can do 1 mcode → 1 blob;
N mcode → 1 pool is Pit (mach_pool_emit.cm). A multi-unit engine could not be rebuilt by
C, and porting that emitter into the audit surface is what minimal-C forbids. So the
engine_lite split must remove code, never use() it.
Phase C — the artifact chain (the object store is already built)
Correcting my own framing: the object store exists and works.
pit-shop/shop_store.cm already implements the nix-shaped model the docs describe —
objects/blake2/<hash> where the name is the blake2 of the bytes, and
catalog/<bare(key)> mapping names onto hashes, with the comment stating the split
outright: “the catalog key is a compiler derivation identity, not the content hash.” Both
directories are live in .pit/ today. Derivation keys naming computations, content hashes
naming bytes: built.
What is missing is not the store, it is what goes in it. The artifact vocabulary:
pit.mcode.unit@3plus the derivation catalog entries for it — ~893 lines ofcompiler.cm. Today’s unit is@2with a separate cache wrapper.- executable manifest and target realization as distinct stored artifacts, replacing
the
pit.executable@1mixed record that fuses logical identity with one way of running it. mach_emit.lower_resultand its companions — ~250 lines the pool emitter reads.mach_pool_emit.cm— N units → one pool. Blocked on mcode@3: it rejects@2at the door. Before touching it, make its failure findable — theoperands must be numberspanic that defeated four agents is compiler-emitted, one shared block per function, and the optimizer inlines up to 20 calls, so the frame names whichever function the arithmetic was inlined into. Compile with inlining off, or write a Pit-level test with one hand-written@3record.bundle@2— a store snapshot rather than a bag of executables.
cart_v2.cm is already a complete Pit codec, reader and writer, so carts are written in
Pit today. Only cold-boot reading is C, and that C is deleted in phase E.
Cart formats are a Pit-only concern
The only genuine C dependency on a cart is where the engine and the boot entry are. So if every cart version keeps the fixed header stable — magic, profile/ABI stamp, engine offset, boot offset — then the same runtime boots v2, v3, and v4 alike, and a new cart format needs no C change at all. New boot code is loaded from the cart like everything else; the format’s evolution stays entirely in Pit.
That makes the fixed header the versioning boundary, not just the boot ABI, and it is a better reason for phase E than “less C” on its own.
Phase C2 — debug and profile translation
I omitted this and it belongs in the spine. Both halves scale the same way, and that scaling is the whole design problem.
What exists today. mach_compile_mcode_bin already returns a pit.mach.debug sidecar
alongside the blob — {pc -> line, col} per function, plus the filename
(pit-shop/shop_build.cm:656). So the 1 mcode → 1 mach case is solved: bytecode maps
to a source position directly, because there is exactly one unit’s worth of provenance to
record.
What does not exist. The moment several units share one pool, pc -> line is
meaningless — a pc identifies a location in a pool, and which unit it came from is no
longer implied. That needs the DWARF-shaped artifact: a sidecar that takes a pc anywhere in
a pool and yields the mcode unit hash plus its function and site ID, from which source
follows because mcode already carries its spans (it is JSON with positions in it). The docs
call this the pool origin map (docs/spec/artifact-formats.md), and the implementation
does not exist here — pit-shop/mach_origin.cm is 1,259 lines in 9466791a and absent on
this branch.
Why it is not optional. Three things read it:
$runtime— a stopped frame becomes a source line only through this. Without it, merging units into one pool silently costs you the debugger, which is exactly the trade-off the link plan is supposed to let you choose.- PGO — instrumented images associate counters with sites in the final image, and the
map normalizes those observations back to stable mcode function and site IDs so a profile
survives recompilation.
pit-compiler/pgo.cmalready speaks in site IDs; it has nothing to resolve them against once units merge. - hot reload — matching functions across a rebuild uses the
{mcode unit hash, function ID}pair, which is what the map keys on.
$runtime is not on this branch, and its absence makes a doc claim false
CORRECTION — I asserted the raw C capability was complete. It is not. Nine of those ten
source/pit_internal.h:617-628 entries are declarations with no definition anywhere in the
repo: pit_debugger_stack_depth, backtrace_fns, build_backtrace, closure_variables,
local_variables, set_closure_variable, fn_info, fn_bytecode, val_address. A
full-repo grep finds only the header lines. Only pit_debug_info has a body
(source/runtime.c:10068), and it fills in a name plus "<native C>" for C functions —
nothing for bytecode functions. internal/debug/debug.c, the supposed consumer, is seven
stubs returning 0/{}/[] with zero callers.
What actually exists for introspection, and it is a different and better-shaped set:
$vm — already a real, correctly gated endowment — gives memory() (VM-wide and per-actor),
value_footprint(), and dumpmem(), a full object-by-object walk of another actor’s heap.
use('actor') gives snapshot() (every actor: state, queue length, 60+ memory/GC/turn
fields), heap_profile(id), vm_stats(), op_counts(), set_trace(), profile_set(),
shutdown(). Pit_GetStack gives own-actor frames with file and line but no locals.
So the split is: memory and heap-walking are done and work. The debugger half does not
exist — no named locals, no closures, no mailbox contents, no live bytecode, no
breakpoints, no suspend/resume, no hot reload. The call/return trace_hook is wired
throughout source/mach.c but nothing can install one. Any plan sized from my earlier
sentence is wrong: consolidating what exists is days, making
docs/architecture/debugging.md true is a new subsystem.
What is missing is the consolidation into one gated endowment. That is 9466791a’s
internal/runtime_raw.c (685 lines) plus a five-line runtime_adapter.cm wrapping the raw
provider into the public value — neither on this branch. Today the surface is reachable as
an ordinary C module (internal/debug/debug.c), which means nothing gates it: no claim,
no policy decision, nothing to deny.
So docs/architecture/debugging.md currently asserts something untrue —
“A development build has it; a shipped artifact does not, so its absence can be checked rather than trusted.”
An importable module hands full actor introspection to whatever imports it. The
structural-absence property only exists once it is an endowment, because that is what makes
it deniable and makes omitting it a build fact rather than a hope. $runtime is therefore
not a convenience on this list — it is what makes an existing documented security claim
true.
It pairs with the origin map and the two want doing together. The C functions give you
frames, locals and bytecode; the origin map turns a bytecode position into
{mcode unit, function, site} and thence a source line. Neither is a debug session alone.
Seen from this side, the ordering constraint below is sharper: merge units into one pool
without the map and $runtime still returns frames, they just stop resolving to source.
Do not assume the newer surface wins. 9466791a deleted parts of this while adding
runtime_raw.c — Pit_GetStack, pit_trace_hook, pit_capture_disrupt_stack all go in
that commit. Some of what the green base has is more complete. Diff the two surfaces
deliberately before porting.
The dependency this creates: the origin map is a prerequisite for merged pools being
usable, not just constructible. So it lands with mach_pool_emit, not after it —
otherwise the first merged pool is a debugging dead end.
Phase D — the executable, and the engine that runs it
The design is settled (see the conversation record and docs/ — it is not yet written into
docs/ as a page, which it should be).
An executable is an ordered array of entries plus the names they bind. Each entry produces a value into a slot, three ways and no more:
{value: <any>} a constant — 4, "debug", or a runtime-injected actor token
{unit: image, entry} run this bytecode
{native: key} load this compiled-in provider
An endowment is a module result bound to a $name instead of a def name. Same
instantiation, same result array, same slots. So:
delete the three engine-value name tables— partly done, and the premise was wrong. They were not three lists of the same names.plan_engine_values(C) existed for one purpose: to compare its ids against the idscement.cm ENGINE_VALUESbaked into the plan’s binding rows. Nothing else read an id — the walker already resolved a BIND_ENGINE row by name out of the engine’s record. So both id halves are deleted (the C table and its cross-check,ENGINE_VALUES’ numbers, and word 3 of the binding row, now reserved and zero).The other two are not duplicates and do not merge.
cement.cmkeeps a membership set — is this claimed name the engine’s? — which is a build-time question asked before any engine has run, and it is load-bearing: an engine value is legitimately null ($shopin an actor with no shop,$overlingin the root), so a name the engine owns must not fall through to a raw-provider lookup that then refuses.engine_lite.cm’sengine_value()is the run-time half: what the name is. One fact, two sides of the build/run line, and a shared module cannot bridge them because the engine core is loaded as one raw blob and canuse()nothing.Both sides dissolve under the array model rather than merging under this one. When an endowment is an ordinary entry in the executable’s array, “is this name the engine’s?” stops being a question anyone asks.
$delay,$clock,$couple,$contact,log,sendbecome ordinary modules over raw primitives, exactly asstd/filesits over$posix_filestart_plan.c(803 lines) dies — the walk moves to Pit, where “the runner is dumb” is expressibleengine_lite drops ~1,870 lines of standard library, which no actor pays for unless something claimed it. Unused means absent, and the claim is the liveness analysis — no stripping pass needed
Slot resolution is what makes shared modules work: a module’s bytecode names only things inside its own image, relatively. Cross-image references are binding rows the realization fills in. That is why this phase needs A3.
Phase E — minimal C boot
- Cold boot shrinks to: map the cart, read a fixed header (magic, profile/ABI stamp,
engine offset, boot offset),
memcmpthe stamp, load the engine, create actor 0, enter. ~100-150 lines. cart_v2_boot.cnever gets written here andstart_plan.cgoes with Phase D. That is 1,717 lines of C that the plan simply declines.- The fixed header becomes the entire C↔pit boot ABI.
- A
bootactor starts the clerk, asks it for the entry, starts that, and is the root. The clerk mounts — the cart, its own.pitstore, and whatever else its mount modules reach. There is no$mountendowment; the gating is underneath, in what those modules claim. Same code in a dev build and a game build; only the cart’s entry and the clerk’s modules differ.
Phase F — the remaining target levers
The audit found one structural spine: source/pit_profile.h is an eight-arm per-console
#if/#elif chain where each arm sets name, word width, object ABI, rep, region sizes and
image cap indivisibly. Endian is now a lever; three axes are still missing:
| Axis | Why |
|---|---|
| memory budget | every arm exists partly to set three numbers; no way to say “2 MiB dynamic” without being the N64 |
| thread-primitive provider | distinct from the scheduler axis: PIT_SINGLE_THREAD says “one drain”, not “which mutex links”. Six unrelated targets share one #if for four documented reasons |
| out-of-process daemon power | whether the host may fork a daemon, symlink a socket, honour PIT_INPROCESS. Currently !defined(_WIN32) |
Plus: profile name as a build-supplied string rather than a chain output, and the
provider set discovered from linked providers rather than declared by hand in
internal/policy.ce.
Target-varying constants use the existing module-arm mechanism. package.json already
swaps modules per target (file_provider → file_darwin/file_playdate/…). A constant
like the seed path is a module with per-target arms returning it — not a branch, not a
startup fact, not a C edit. That is the generic answer and it retires review-queue D10.
Delete the natives table — a package’s files name themselves
John’s ruling, and it is larger than the row-deletion it started as. edicts.md §5 has
the rule; this is the work.
The root package.json carries a natives array of 26 {"file": …, "key": …} rows, with
more in pitlib, std and cart. It should not exist. A module’s name is <package>/<file>
and an endowment’s name is its filename in a package the shop draws endowments from — so a
row saying {"file": "internal/vm.c", "key": "endowment/vm"} is a hand-maintained
restatement of something the filesystem already says. $vm’s name is a table entry today.
CORRECTION — I called this blocked on “what core’s public surface is”. It is not, and John said so. Once a package’s files name themselves, moving files around is trivial and does not touch the underlying system. The arithmetic, computed rather than estimated:
| root table | pitlib | |
|---|---|---|
| rows | 28 | 5 |
pure restatement (key == file − ".c") | 19 | 5 |
| carries one fact | 9 | 0 |
So 24 of 33 rows say nothing a directory listing does not. pitlib’s table dies outright
under one rule: native/<path>.c provides <path>.
The 9 remaining rows carry exactly two facts between them, and neither is a decision about core:
- 5 endowment rows —
internal/vm.c→endowment/vm. The fact is this file provides an endowment, and its home is the package: put it where the shop draws endowments from and the$nameis the filename (edict §5). - 4 per-target arms — the fact is this file does not exist on that target, and its home
is likewise the package: a target has
$memfsbecausememfs.cis in that platform’s package and nowhere else. Selection needs no arms.
RULED: core is blob, fit, time, random, json, and the math files
John, 2026-07-26. What that implies, because the repo has almost arrived there already:
pitlib/is that set — and as of landing 5 it literally is:blob.c,fit.c,json.c,math/{radians,degrees,cycles}.c,time.cmoverinternal/time.c,random.cm.native/is gone; the outlierstd/random.cmmoved in.- The adapter is still visible in the C. pitlib’s json init symbol is
pit_core_json_init— the symbol sayscorewhile the package sayspitlib. That mismatch is the table’s whole reason for existing, leaked into a function name. Landing 5 deliberately did NOT touch it; landing 6 is the symbol flag day. internal/is not core and never was. nota, wota, qop, deflate, crypto, kim, eddsa, mach, shop_paths, libgit2, qopfs, sysinfo, sysfacts, bootgate, wildstar belong to the shop, the compiler and the platform. The root table made 28 unrelated files look like one namespace, which is what made “what is core?” seem like the hard question. It is not: every one of those files needs a package, and that is an easier question asked file by file.
Free and immediate: — done.
79 lines of stubs exposed as the importable modules internal/debug/debug.c and internal/debug/runtime.cdebug and debug/runtime, zero
callers, deleted with their two natives rows. The built static-extension table now has no
debug entry, so the surface is structurally absent rather than merely unused.
That exposed one piece of dead C: Pit_SetMemoryLimit (source/runtime.c:1919,
declared pit_internal.h:143) had exactly one caller, the deleted stub. Reported rather than
removed, per edict 2. If a memory limit wants setting later it belongs on $runtime with the
other fact-setters, not as a bare C entry point. Pit_SetMaxStackSize beside it is unaffected
— three live callers.
CLOSED: the development shop no longer launders the endowments it holds
Done, and the fix went the whole way. development_shop.ce:1224-1245 is now a comment
where the seven handlers were. The tools hold the endowments themselves — pit ps, stop,
trace, heap, profile and the shell each claim what they use, through
internal/actor_find for the $inspect half. A build that denies $inspect now denies
pit ps, which is where the refusal honestly belongs. The shop keeps $inspect for
actor_token() alone: naming the actors it started itself, which is shop work.
The reasoning, kept because it generalizes: a relay cannot be a boundary. Forwarding a power on request is the same as granting it, minus the record that you did.
What it was, for the record — seven handlers, each one line of shop code standing in front of an endowment the caller could hold directly:
| message | relays |
|---|---|
resolve_actor | $inspect.ref({id}) — turns an arbitrary id string into a live send target |
actors | $inspect.snapshot() |
runtime_stats | $inspect.count() |
trace_actor, system_trace | $hook.set_trace / set_system_trace / get_system_trace |
heap_profile, profile_set, profile_clear | $vm.* |
resolve_actor was the sharp one: hold a shop reference, send {type:"actors"} to enumerate
every id, then resolve_actor on any of them — a live send target, with no $inspect held at
any point. That reopened by message the token-forgery hole this arc had just closed in
use('actor'). docs/architecture/security.md’s account — that $inspect is how you name an
actor you were not handed — is now true again.
My brief caused half of it. I told the agent “actors stays where it is” meaning do not
add a second enumerator to $runtime, which was right, but it left the relay in place — and
the relay was the actual hole. The agent flagged it rather than acting on it, which was
correct.
CLOSED for now: the boot.qop storage question
Measured: boot.qop is 457 MB of pack across 278 versions; boot/content/objects/** —
literally the same bytes, unpacked — is 9.3 MB across 487 versions. 49× the cost for 75%
more versions, because the content store only adds changed objects and they delta, while every
reseed rewrites the archive as one opaque blob.
John’s ruling: we are not acting on this, and the boot chain will NOT mount the store directly. Keep committing what is needed to boot. Revisit near the end of the rebuild, after many more phases, when a different and better storage-and-boot shape may be apparent. Do not re-raise it before then.
Decomposing os — the six surfaces, and what is ruled
platform/posix/os.c exports 28 functions and is reached through a per-target arm that is
false on six consoles, each of which then re-adds its own os through a copy-pasted
if (tc.system == …) branch in cake/plan.cm. That chain of six is the design failure written
down: one module every platform had to provide, with nobody able to say what it should contain.
The groups, measured from pit_os_funcs[]:
| group | functions | disposition |
|---|---|---|
| dynamic linking | dylib_open, dylib_preload, dylib_symbol, dylib_has_symbol, native_module_load | $dlfcn on posix; Windows its own. OPEN: probably an actor |
| sync file I/O | the 10 bootstrap_* | OPEN: may not be needed at all |
| process/env | getenv, exepath, exit_code, boot_refuse, open_url | $environment for vars; the rest wants $platform |
| entropy | random | per-platform endowment, $BCryptGenRandom and friends |
| clock | now | per-platform endowment under a uniform $clock |
| misc | print, stack, enable_benchmark_native_load | OPEN: possibly delete |
Three of them are not platform surfaces at all. load_internal, internal_exists and
internals are lookups into the linked-in static extension table — the runtime’s own module
registry wearing an OS costume. That is why os had to exist on consoles with no OS to speak
of. They go to the shop.
RULED: $clock is a uniform shape over a per-platform endowment
Getting the time is not uniform enough to be one C surface — on an Amiga it is asynchronous.
So $clock is an endowment with a uniform shape: give it a callback, it returns eventually
with the time. How that is implemented varies per platform, and $clock itself uses the
platform-level endowment underneath. Eventually the shop blocks actors from every other
time-related surface, leaving $clock the only door. The raw now stays available to the time
module.
RULED: $environment for environment variables
A clear endowment on its own. What is left of the process/env group — exit_code,
boot_refuse, exepath, open_url — is a grab bag, and the shape it wants is a consistent
$platform available everywhere, answering questions about the machine you are on: core
count and the like, the equivalent of reading /proc on linux or plan9.
OPEN: dynamic linking probably wants to be an actor
The idea: if a source is a C file and the module is not in the static binary, then a dylib must be opened, and that dylib should be in the content hash. Making this an actor splits it out altogether so the logic is not present at all on consoles, rather than present and unused. John: “I’m not 100% sure how this would work.”
OPEN: is the sync file surface needed at all?
The reasoning for deleting it: bootstrapping is now the boot actor’s job. Pit starts by opening the boot cart and starting the well-known boot actor location; after that, everything can be async.
Counting against, and this is a migration cost rather than a refutation: the 10 bootstrap_*
functions have ~40 callers, led by pit-shop/shop_store.cm (22), internal/bundle.ce (15)
and pit-shop/shop_source.cm (13) — the object store, bundles, and source resolution. Nothing
there is structurally sync; the shop is an actor and can await. So the question is not whether
it is possible but whether the rewrite is worth doing now.
One caller is different in kind and should be checked before anything else:
internal/engine_lite.cm:51 holds os_bootstrap_stat_fn. The engine core is loaded as one raw
blob and can use() nothing, so whatever it needs cannot come from a module.
OPEN: does print survive?
Playdate has no print — it writes out through a log wire, and on platforms that support it we
can print more robustly by writing to sockets. The shape if it does survive: endowments
governed by C headers, $stdio providing $stdio.print. But the real question is whether it
is needed at all now.
Traps that have each cost someone a session
— FIXED (make manifestdepends onalldbb85c88).manifest,seedandseed-shipnow depend onpit-present, which builds only when there is no binary at all. Verified against the failure: with a native module deleted,make manifestwent from exit 2 to exit 0.The ABI crossing is NOT mechanically fixable, and my earlier description of it was wrong. The blocker is not
seed: all— it is thatsource/mach.c:2960readsconst char *abi = PIT_MACH_ABI;, a compile-time constant, so a blob carries whichever runtime emitted it. Fixing the dependency order makes things worse: measured with the stamp bumped, the originalmake seedexits 2 with an honest mismatch, while the order-fixed version exits 0 — the producer ran on the old runtime — and the next./pit psdies. A quiet success leaving an unbootable tree.So
seednow carriesSEED_ABI_ASSERT, failing at the point of cause and namingPIT_CROSSING_ACCEPT_ABI, and running before the seed stamp is written so a failed crossing cannot record success. An ABI move is a crossing, not something a reseed performs.Removing a name the cart is cemented against needs a two-phase landing — and this one is inherent, not a bug awaiting M1. My earlier guess that M1 would dissolve it was wrong: a cemented cart is by definition a compiled snapshot of a closure, so removing a name it embeds always needs either a generation carrying both or a boot path that does not use the snapshot. M1 changes what the cart carries, not that fact.
Note also that since
b54b4d90boot/boot.cartis untracked, so “the committed cart” now meansboot/root.cart+boot.qop.— FIXED (build_ps1_fleet/is a TRACKED build directory37e861e6). 8 files, including a generatedcore_static_extensions.cthat still registered thedebugmodules after they were deleted, so anyone grepping for a deleted native found it there and concluded the deletion was incomplete. Untracked, and.gitignorenow carries the general patternsbuild_*/andbuild-*/rather than an enumeration — the enumeration is what missed this dir in the first place.
Known open, not on the critical path
make check-webis RED at baseline —shop_actorstack overflow at depth 1024 under node. Confirmed pre-existing by stashing and re-running.make check-allcannot be green until this is fixed, and it has nothing to do with this arc.PIT_MACH_DEBUG_CONTRACTSis compiled by NO gate arm. It is gated on!NDEBUG && !__OPTIMIZE__(source/mach.c:5230) and every check-arms arm configures-Dbuildtype=release, i.e.-O3. Somach_contract_disrupt,mach_fact_disruptand their ~13 call sites are invisible to the entire suite. This is the same shape as thepit_text_kim.cescape — a compile-time-selected body that the default gate never compiles — and it is exactly the class check-arms was built to close. Wants a debug arm. (A3a covered it by hand:mach.cat-O0both arms clean, plus a full-O3runtime with-DPIT_MACH_DEBUG_CONTRACTS=1running the real suites — vm 1086/0, language 474/0, fuzz 718/0, no contract fired.)make check-armsexists now (13 arms, ~10s warm) and three arms are RED at baseline. Not wired intocheck-alluntil they are green:numrep-fixed32andfixed32+kim8+compact— build failure, and a real guard drift.meson.build:657decides whether to buildmach_image_lifetime_testwithif numrep != 'nan32', but the C file guards itself onPIT_OBJECT_ABI_NAN32, whichpit_profile.h:305also defines for fixed32. So fixed32 builds the target and trips its own#error. The proof it is drift and not design:meson.build:220writes the same condition correctly asnumrep != 'nan32' and numrep != 'fixed32'. Fix by making the meson condition ask about the ABI rather than re-listing rep names.force-gc—text_bodyandgc_policyfail, stably.text_body_test.c:178is the argument-evaluation-order hazard CLAUDE.md documents:Pit_ConcatStringGrow(ctx, t_ref.val, Pit_NewString(ctx, "xyzzy123")). UnderFORCE_GC_AT_MALLOCthe inner allocation always collects, so a register copy oft_ref.valread first goes stale. Check the runtime’s ownPit_ConcatStringGrowcall sites for the same shape, not just the test. Thegc_policyfailures (soft cap 8192 vs 16384, strike streak 0 vs 1) look like assertions that only hold at normal collection cadence — probably the test’s contract, not a bug.
make check-profilesis red at baseline too, for the same fixed32 build failure — itsREPSincludes fixed32 and it compiles the same dir. It is not incheck-all, so nobody was seeing it.profile=in the capacity harness prints onlyPIT_PROFILE_NAME. utf32 and kim8 builds both reportprofile=host-nan64-v1; the string that actually keys artifacts folds every axis. Anyone readingprofile=as the cache key would conclude two different arms share one.OPEN, needs John: the SHAPE/SHAPE_KEY row layouts disagree with the docs. Not a defect — a design call, and it shapes how a record reaches its shape in A1c.
docs/spec/artifact-formats.mdsource/pit_internal.h:332SHAPE u32 first_keyu32 kind(holds"IMG1")u64 reservedi64 key_rows_relativeSHAPE_KEY u32 text_value; u32 sloti64 text_relativeThe— deleted. It heldkindfield"IMG1"and was theIMAGE/DYNAMICdistinction under another name. Word 0 is nowuint32_t reserved, validated as zero, following the idiomPitMachPoolFunctionRowandPitMachPoolValueRowalready use, with a negative test the oldkindcheck never had. The other three fields keep their exact wire offsets and the row stays 24 bytes. Safe to require zero because there is no pmp1 producer in the tree — every pool is a hand-built C fixture.RULED: displacement. A shape row keeps
key_rows_relativeand a shape-key row keepstext_relative, because a record holds a bare pointer into the mapped SHAPES section and a displacement is followable from the field’s own address while an index needs the pool base carried alongside. That also puts shapes in the same world as STONE, which already uses slot-relative displacements — consistent with A1c’s “a shape is just another stone object.”docs/spec/artifact-formats.mddocuments these as indices and is wrong; the doc is corrected as part of A1c.Six defects in the ported pool reader— fixed (a6891ce0), along with two holes found beside them: a STONE reference did not have to name an object start (a well-formed text forged inside another object’s packed body was addressable), and one message covered two distinct failures. The reader was never dormant —image_provider.c:337,676callPit_MachPoolOpen; the earlier “unreachable today” note here was wrong.Still open in the reader, reported and not changed: STONE’s declared alignment is never required to be ≥
PIT_OBJECT_ALIGN(harmless on x86/ARM64, not on strict-alignment targets);entry.flags/fn.flags/fn.abi_flags/value.flagsare decoded and never validated whileimport.flagsmust be zero;mach_pool_shape_key_hashtakes akey_rowit discards.arch_for_named_targetis now unreferenced in-tree.The psp/dc/n64 nan64 cross-check lane stamps a nan32 profile name; now a two-line fix.
— done, and the first half was already mostly written.docs/should gain the executable-array design and the engine-single-unit constraintdocs/shop/endowments.mdhas the runner’s two forms anddocs/architecture/actors-and-memory.mdhas the runner walking the array into result slots; what was missing was the spec-level statement of the shape, nowdocs/spec/executables.md“A realization is an ordered array” — the three entry kinds, the endowment equivalence, and why slot resolution is what lets images be shared. The engine constraint isdocs/architecture/boot.md“The engine is one unit”, stated as what it implies: a split must remove code, neveruse()it.
Answered — work lifted from the retired review queue
John ruled on these directly; they are work items now, not open questions.
Endowment packages, taken on demand or whole — the settled shape
This supersedes the “assumed endowments” sketch below, which named the right need with the
wrong mechanism. John’s ruling, now written into docs/shop/endowments.md:
A target’s endowments are chosen by naming packages, not files. windows_endowments,
macos_endowments + posix_endowments, lang-endowments for the portable ones. A recipe
lists them; to know what a build can grant you read those directories.
The dev-versus-shipped question turns out to be one mechanism, not two, and the hinge is
whether the shop can still compile. A development machine answers “can this be granted?”
with “can this be built?” — a claim for $iocp builds a provider on demand. A shipped
runtime with no compiler cannot, so anything it might ever grant must already be linked.
Hence a second disposition on the same list:
endowments: ["lang-endowments", "platform-web"] on demand — the default
endowments_whole: ["platform-web", "debug-endowments"] every top-level file, claimed or not
That is also the whole answer to $hook/$pgo with nothing claiming them: a development
recipe takes their package whole. No optional claims, no per-name list.
Two consequences to hold onto:
- Whole-linking changes what absence proves. Policy rather than absence is then the enforcement. Legitimate for a playground; it must not be described as small-by-construction.
- Capability levers must stay out of the profile string.
$hook/$pgo/$inspectand the native-build power change no artifact byte; stamping them would fork the realization cache and stop dev-built carts mounting on shipped runtimes. Now explicit indocs/architecture/target-profiles.md.
Which layer branches: package below, module arm above
Ruled. A package boundary where the platforms share nothing — raw endowments, where an
arm would only restate which package a file is in. A module arm where they share an
interface — std/file, sdl/window, whose whole purpose is the shared part, and whose
manifest should stay the one place every implementation is listed.
So the modules arms survive; the natives table and the per-target arms inside it do
not.
A build can assume an endowment, and there are no optional claims
The question was how a component offers introspection as one service among many when the
build may not provide it. The answer is not an optional claim — it is that a build can
include a provider nothing claims. A development runtime says “I can provide $pgo and
$hook” and links them; a sealed cart links exactly what its closure claims.
That keeps two rules intact that would otherwise fight: nothing unclaimed is built stays
true of a shipped artifact, and a claim stays a hard requirement rather than a maybe. The
build decides what is available; the claim decides what an actor gets. A component may keep
requiring $inspect and simply fail on a runtime built without it, which is the honest
refusal rather than a silently degraded shop.
Needs: a way to name assumed endowments in a build, and the recipe or cake plan is where it
belongs, beside the endowments: permission list.
$pgo and $hook cost nothing when absent
The counters and the call/return hook should exist only when their endowment does. Today
the hook is compiled into every build (seven sites in source/mach.c plus GC) and nothing
can install one, and the PGO site table is likewise unconditional. Both should compile out
with the endowment, which is what makes the cost table in
docs/architecture/debugging.md true rather than aspirational.
This also settles pool-reader defect 6: the declared-but-unused PGO row struct is part of the same cleanup.
make check-arms — test every axis
make check compiles one point in a multi-axis profile space, and that has already let a
real breakage through. A configure-and-build sweep across text, numrep, gc, and the
value word is wanted, and John asked for a full test of the axes rather than only a
build. Cheap, and it retires the class of bug that got through.
Fix all six pool-reader defects
Recorded when the reader was landed as a faithful port. All six are agreed sane fixes:
the misleading duplicate-key message, the off-by-one arity bound against 8-bit register
fields, disruption_pc == 0 doubling as the no-handler sentinel, the quadratic validation
paid at open, the zero-length section satisfying the presence check, and the unused PGO row
struct.
Another target-name pass
The shop-side derivations deleted earlier were the right move, and recent work has made that class of hardcoded target handling more obsolete still. Worth a second sweep — the earlier audit’s category (a) rows plus whatever the endowment-by-filename work has since made dead.
Full green everywhere, once the rework settles
check-web and make budget are red at baseline. Not chased now. The bar that matters
first is that the VM runs and logs come out — vm_suite must pass — and a full green suite
across every arm comes after the rework rather than during it.
The macOS .app sniff may dissolve on its own
Kept behind a sys_os predicate because a CLI test depends on app_mode and nothing sets
PIT_APP. Likely disappears with the platform rework, when it becomes an ordinary startup
fact published by the platform package. Recheck rather than fix.
The memory model, and the four places the runtime diverges from it
John’s target, stated 2026-07-25. There are three memory regions and no others:
- The runtime stone pool — a fixed array, seeded before actor 0 exists, never grown.
Indices 0-127 are the ASCII characters; the rest are the constant keys C itself needs
(
PIT_KEY_*).s[i]becomes: read the codepoint, and if it is under 128 returntable[cp]. No hash, no probe, no lock, no allocation. - Mach image pools — stone, many of them, brought along by whatever modules reference them. Just constants for bytecode. Text is deduped within an image, never across.
- Actor heaps — everything else.
Nothing is interned across actors. Text from two modules’ pools may be different objects for the same content, and that is expected. Comparison is by content — and cheaply, because stone text caches its hash in its header, so two copies from different pools carry the same hash. Hash first, then length, then content. Care: antestone text uses that field for the letter count, so the fast path must be gated on stone rather than assumed.
The divergences, all verified
rt->ct_*— the key intern table. BLOCKED, and the blocker is structural. Process-wide, growing, dedup-by-content, underct_lock, and it returns another actor’s object on a hit.pit_key_new/pit_key_new_lenreach it at runtime, soobj[computed_text] = vin two actors yields one pointer — observable by identity or by probe-hit timing. This is the timing channel the text-interning deletion was for.Two agents hit the same wall from opposite directions, independently, and they are right.
mach.cmarks every cacheable loaded unitshared(mark_code_tree_shared), and both collectors then skip it —pit_gc_copy.c:884andpit_gc_compact.c:866each open withif (code->shared) return;. So a shared code register’scpool[]andname, filled bymach_materialize_cpoolthroughpit_key_new, are not actor-GC-scanned and must be process-immortal. The intern table is not incidental to that; it is what gives a cross-actor-shared code object’s constants an owner.pit_new_cfunc_internal(runtime.c:2343) has the identical shape — a process-wide cfunc cache holding apit_key_newresult in a ct-arenaPitFunction.So moving keys to actor heaps before cpool constants live in the mapped stone pool is a use-after-move, read by other actors, silently. The ordering this forces: the image layer must not merely exist, it must be what carries constants — then a shared cpool is read-only by construction and needs no owner at all. Delete the intern table after that, never before.
rt->code_cache— bounded, and answered. The open question was what it is still for with mcode in the cart. Measured: a hit saves mcode lowering, and nothing else matters — 85-90% of all miss time ismach_lower_mcode_windowon the root cart’s 18 windows, ~11.5 ms each, and every actor that starts loads the engine core. Deleting it costs 2.6x on every warm CLI command (15.1 -> 38.9 ms) and +51% on cold boot (185 -> 280 ms), and raises residency, because the cache is the runtime’s only owner of a loaded code tree — a load that does not enter it is never freed at all. So it stays. What was unbounded was one call site:mach_load, the compile-then-run intrinsic. Its blobs are new every time — 201 adds, 4.1 MB, 0 hits, over a session of both suites and the fuzzer — and they no longer enter. Entries over three fuzz runs: 97 -> 302 -> 502 before, 83 -> 86 after. The bound is structural, not a policy: only a blob read off a durable medium (a boot image, a start plan’s CODE window) is cached, so the set is the modules the node has started and every entry is code it may run again. The linear walk is fine at that size (63 probes average, 2.9 us a hit) and needs no index. Eviction is still impossible and that is a real finding. Freeing an entry frees thePitCodeRegisterwhilecode->code_obj— one ct-arenaOBJ_CODEshared by every actor — still points at it, and actor heaps still hold that value. There is no refcount, no back-reference, and no actor-death hook, so nothing can know when a code tree is unreachable. LRU or a size cap would be a use-after-free. Bounding what enters is the only bound available until code has an owner. Two consequences to carry forward: (1)shared = 1makes the GC skip a cached tree’s cpool, which is only sound becausepit_key_newreturns ct-arena text — the intern-table deletion above and this cache are one decision, and moving keys to actor heaps without moving cpool constants into the image pool is a use-after-move in shared code; (2) code loaded outside the cache leaks permanently (nothing callspit_free_code_registerexceptPit_FreeCodeCache), which wants per-actor ownership of dynamically loaded code — its own work item.pit_rt_ct_alloc/ct_lock— the constant arena. Its tenants are the intern table’s texts and the shapes below, both of which are leaving. When the plan is completect_lockshould have no callers left, which makes it a good completeness check: if something still takes it, something still grows that should not.Who still takes it, and why, as of the immediate-ASCII deletion:
rt_intern_text_to_valuein both text bodies (blocked above);pit_rt_ct_allocfor sharedPitCode/PitFunction, immortal by design;pit_record_shape_newfor rows that outlive every actor heap;scheduler.candinternal/vm.cfor read-only accounting snapshots. Two closed with the deletion:pit_key_text_objectno longer interns on any rep, and the generation-time probe inmach_fuse_probe_generatedstopped feeding the table.pit_record_shape_newat runtime (mach.c:2693) — NOT per-record. It resolves a portable shape-spec cpool entry into a descriptor once per loaded code object, which is the pre-image path; a mapped image carries real SHAPES rows needing no materialization. It dies with the last parsed mach blob rather than as its own fix. (Its other caller,qbe_helpers.c:757, is native — ignored per edict 7.)
Shapes follow the same rule as text: inferred from code, deduped in a mach image.
{x: 5, y: 3} gets a shape from the pool; rec.z = 3 drops the shape and becomes an
ordinary hashed record. Nothing makes a shape on the fly. That may be revisited later.
Five other runtime-wide locks exist and are unaudited against this model: cfunc_lock,
image_lock, opaque_lock, lock, and code_lock above.
Pool size: the expansion is honest except on the failure path
Measured 2026-07-25 against the actual artifacts, accounting for 100.00% of every byte across 95 pools (4452 KB) and decoding 928,093 instruction words with zero unknown opcodes.
Corrections to earlier claims in this file, both mine. The expansion is 2.72×, not 4× —
the earlier source figure counted a different file set (1676 KB across the 95 modules, not
1568 KB). And pmac is not the sectioned pmp1 format: it is mach v2, a recursive
function-record tree with a per-function inline cpool. pmp1’s ENTRIES/FUNCTIONS/STONE
sections have no referent in boot/content.
Every suspected culprit was dead: no duplicate pools (95 pools ↔ 95 distinct locators,
1:1), no debug info in the pool (the writer forces lines_present = 0), no unused
constants (13 of 47,011, 110 bytes), no padding, no alignment slack. Inter-pool dedup
already works. Type-guard tests are 97,386 instructions (10.5%) — the price of a checked
dynamic language, not waste. streamline removes 23.1% of the IR it is given.
The one real finding: every guard failure is an inlined 11-instruction log.panic
pit-compiler/mcode.cm:487 emit_log_panic expands to
context ; loadk "log" ; load ; loadk "panic" ; loadk <msg> ; newarray ; push ; stone ; call ; callargs ; disrupt.
- 21,921
DISRUPTsites; 21,920 (100.0%) are this block. - 241,724 words = 944 KB = 21.2% of every pool.
- 13 distinct messages, led by
'cannot read property of function'(8,747 sites). - 78% of all 3,429 functions contain at least one.
- Plus 245 KB of guard-vocab cpool strings, re-stored per function.
internal/down.ceis 3 lines of source → 67 instructions, 22 of them two panic blocks.
Combined saving ≈ 1098 KB, 24.7% of all pools; expansion 2.72× → 2.05×.
John’s ruling on the fix — compiler arc, not now
A shared tail, in bytecode, not a new opcode. The block ends in DISRUPT and never
returns, so it is a one-way branch with nothing to jump back to. A MACH_TRAP <msg_id>
opcode would put the context lookup and the log.panic call inside the VM — C doing what
bytecode does, against edict 2. Rejected for that reason.
Likely shape: loadk <msg> ; jmp <shared_tail> — 2 words instead of 11, one tail per
function, message varying at the site. Check first whether jump targets are function-local;
if so a per-pool tail is not available and per-function sharing only wins where a function
repeats a message.
But log-stripping is the load-bearing feature and comes first. John wants a compiler
option to strip ALL logs from compiled source. With logs stripped the eleven instructions
become bare DISRUPT — one word, no new opcode, no shared tail. So the shared tail is an
optional nicety for the dev loop, where 944 KB matters far less than in a cart.
Settle when that arc starts: is the message part of the log or part of the disruption? If
disruption { } can observe “cannot read property of function”, stripping the log must not
take the message with it — the message would need to live on the disrupt rather than as a
log.panic argument. That is the difference between stripping being safe and stripping
silently degrading error reporting.
Shapes are not being used
MACH_NEWRECORD_SHAPED fires 25 times in 928,093 instructions. Dense-record lowering is
effectively dead; every record literal takes the generic NEWRECORD + per-field guarded
STORE path. So none of A1a/A1b/A1c is on the hot path yet — that work made the
representation correct, which was its purpose. Making the compiler emit shaped records is a
separate arc, after the memory model is safe and coherent. John: “make it safe and coherent
first; then figure out in a separate arc how to make it fast.”
Source: plans/archive/roadmap.md