Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 9 additions & 30 deletions .dev/debt.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,6 @@
# Refresh on every /continue resume — .claude/skills/continue/RESUME.md Step 0.5.

entries:
- id: "D-521"
layer: "code"
status: "note"
description: |-
Binary-size campaign lever #2 (ADR-0204 D2): complete the table-driven
arm64 dispatch and DELETE the legacy switch. `engine.codegen.arm64.emit
.compile` is a single 707 KB __text symbol. Root cause sharpened
2026-07-16: `dispatch_collector.dispatch()` is an `inline for` +
`@call(.auto)` over the 409 registered arm64 op modules — every
registered handler body is INLINED into the compile() instantiation
(and dispatch is a linear O(n) compare chain in the JIT-compile hot
path), on top of the ~161 legacy switch arms inlining their op_*.zig
helpers. Stage A = convert dispatch()/dispatchX86_64Ctx() to a
comptime-built fn-pointer table indexed by @intFromEnum(op) (O(1)
dispatch, kills the inline duplication; handlers need a uniform
canonical signature per axis — wrap where inferred error sets
differ). Stage B = migrate the remaining ~161 switch ops to per-op
files and delete the switch. Acceptance: emitted JIT bytes
IDENTICAL — test-aot-diff 63/63 + fuzz-diff lanes + 3-OS test-all.
first_raised: "2026-07-16"
last_reviewed: "2026-07-16"
refs: |-
ADR-0204 D2; from_cljw_05 request 1; dispatch_collector ADR-0074
front: binary-size
- id: "D-522"
layer: "api"
status: "note"
Expand All @@ -41,12 +17,15 @@ entries:
**Stage 1 DONE 2026-07-16** (shared noinline fpBridge1/fpBridge2
bodies; thunks forward-only): jit_host_bridge 1,311 KB -> 232 KB
(-82%), whole CLI 5,282,584 -> 4,173,736 B (-21%), no ABI change.
Stage 2 (collapse the slot axis by conveying K at runtime — call
site materializes the slot index before BLR) is RE-SCORED by the
stage-1 measurement: remaining recoverable ~200 KB (209 KB of
per-slot thunks -> ~15 KB shared), against a JIT call-sequence
change on both arches + AOT format touch. Demand-driven — re-score
after D-521 lands; do not grind speculatively (measure-first).
Campaign CLOSED with stage 1 (v2.2.1; ADR-0204 Implemented).
RESIDUAL = stage 2 only: collapse the x64 slot axis by conveying K
at runtime (call site materializes the slot index before BLR) —
remaining recoverable ~200 KB (209 KB of per-slot thunks -> ~15 KB
shared), against a JIT call-sequence change on both arches + AOT
format touch. Demand-driven; trigger = a real embedder squeezed on
size after v2.2.1, or the next size campaign. Do not grind
speculatively (measure-first; cf. the D-521 refutation lesson
2026-07-16-outlining-once-called-handlers-size-neutral.md).
Constraints unchanged: no api break, no per-call regression beyond
noise (ADR-0181 bench series).
first_raised: "2026-07-16"
Expand Down
16 changes: 15 additions & 1 deletion .dev/decisions/0204_binary_size_campaign.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# ADR-0204: Binary-size campaign — host-bridge thunk collapse + table-driven emitter completion

- **Status**: Accepted (kickoff; stages land as separate PRs)
- **Status**: Implemented (D1 stage 1 shipped in #145: jit_host_bridge
−82%, CLI −21% ReleaseSafe / −8% ReleaseFast; D1 stage 2 re-scored
~200 KB → demand-driven in D-522; D2 size premise refuted by stage-A
measurement → D-521 discharged, see 2026-07-16 amendment below; D3 not
pursued. Released as v2.2.1.)
- **Date**: 2026-07-16
- **Front**: binary-size (D-521 / D-522; dogfooding request `from_cljw_05`)
- **Findings base**: symbol-attribution measurement 2026-07-16 (below);
Expand Down Expand Up @@ -72,6 +76,16 @@ t1fp = 3×5×64 = 960, t0..t4 = 5×5×64 = 1,600. Two independent findings:

### D2 — Complete the table-driven arm64 dispatch; delete the legacy switch (D-521, lever #2: ~500 KB potential)

> **Amended 2026-07-16 — size premise REFUTED by stage-A measurement.**
> Converting `dispatch()` to a comptime fn-pointer table left
> `emit.compile` at 707 KB and grew the binary +28.8 KB (reverted). The
> 707 KB symbol is *aggregation* of once-called inlined handlers, not
> duplication — out-lining is size-neutral. See lesson
> `2026-07-16-outlining-once-called-handlers-size-neutral.md`. D-521 is
> discharged; the per-op-file migration of the remaining ~161 switch arms
> stays on the ADR-0074 maintainability trajectory, NOT as a size lever.
> The original text below is retained for the record.

`engine.codegen.arm64.emit.compile` (src/engine/codegen/arm64/emit.zig,
~2,100-line fn) is a ~161-arm switch over `ZirOp` whose arms mostly
delegate to already-extracted `op_*.zig` handlers. The table-driven
Expand Down
43 changes: 22 additions & 21 deletions .dev/handover.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,27 @@ PIC codegen D-516 fix; release.yml auto-built Release + 5 assets; release was
USER-DIRECTED 2026-07-09 per ADR-0156). v1 frozen at `v1.11.1`. Dev model: cut
a `develop/<slug>` branch from `main` → PR → CI `ci-required` 3-OS gate must be
green to merge. **Release stays user-only (ADR-0156)** — never autonomously tag /
publish / cut over. Active campaign: binary-size (ADR-0204, below); no cron
self-re-arm.

## Active front — binary-size campaign (ADR-0204, kickoff 2026-07-16)

Trigger = dogfooding mailbox `from_cljw_05` (cljw measures zwasm at 44% of its
shipped code; user authorized an independent zwasm-side campaign). Measured
baseline (ReleaseSafe arm64 CLI 5,282,584 B @71cccba76; ReleaseFast base
DOUBLED since 2026-06-12: 1,972,696 → 3,884,856 B):
- **Lever #1 = D-522**: `api.jit_host_bridge` comptime thunk cross-product =
1,311 KB / 5,453 syms (t2fp×2,880 + t1fp×960 + t0..4). Characterize the
instantiation axes first, then collapse to descriptor-driven trampolines.
ABI/api unchanged; bench-gated.
- **Lever #2 = D-521**: `engine.codegen.arm64.emit.compile` = one 707 KB
symbol (~161-arm ZirOp switch). Finish dispatch_collector (ADR-0074)
registration, delete the fall-through switch; x86_64 twin same treatment.
Acceptance: emitted JIT bytes IDENTICAL (test-aot-diff + fuzz-diff).
- Re-run `record_binary_size.sh` per stage-merge (rows into the same PR).
- cljw reply posted (`to_cljw_05.md`): thunk cross-product ≫ api-surface
gating (ADR-0073 already covers the latter); x86_64 emitter already exists.
publish / cut over (v2.2.1 tag was user-granted 2026-07-16 in-session). No
active campaign; no cron self-re-arm.

## Binary-size campaign — CLOSED 2026-07-16 (ADR-0204 Implemented, v2.2.1)

Trigger = dogfooding mailbox `from_cljw_05` (cljw measures zwasm at 44% of
its shipped code). Kickoff #144 (measured attribution re-prioritized the
cljw asks) · stage 1 #145 (D-522: shared noinline fpBridge1/2 bodies —
jit_host_bridge 1,311→232 KB, **CLI −21%** ReleaseSafe, −8% ReleaseFast;
DA-critique 20/20) · close #146 (refutation record + v2.2.1). Key outcomes:
- **D-522 stage 1 SHIPPED**; stage 2 (slot-axis, ~200 KB) re-scored →
demand-driven note in debt.yaml.
- **D-521 DISCHARGED — size premise refuted by measurement**: fn-ptr-table
stage A left `emit.compile` at 707 KB, +28.8 KB binary → reverted. The
giant symbol is once-called-handler AGGREGATION, not duplication. Lesson
`2026-07-16-outlining-once-called-handlers-size-neutral.md` (predictive
question = "how many call sites share this code?", not symbol size).
- ReleaseFast `base` had DOUBLED unnoticed since 2026-06-12 (1.97→3.88 MB;
series cadence = phase-boundary only); now 3.56 MB. cljw replies
`to_cljw_05/06.md` in the mailbox (x86_64 emitter already exists +
4.0 MB budget-line revisit suggestion).

## AOT-full-fidelity campaign — CLOSED 2026-07-09 (ADR-0203 Implemented)

Expand Down Expand Up @@ -105,7 +106,7 @@ Report = `.dev/meta_audits/2026-07-06-senior-runtime-gap-analysis.md`.
dogfooded into cljw (pins zwasm by git tag-hash). Runners ReleaseSafe.
- **EH**: cross-instance JIT EH both arches. Interp+JIT EH corpus green. Realworld 56
fixtures interp 56/0; JIT diff-gated.
- **Debt**: 68 entries — **0 `now`-class** (D-505 DONE: 3 arm64-SIMD bitmask sites
- **Debt**: 69 entries — **0 `now`-class** (D-505 DONE: 3 arm64-SIMD bitmask sites
spill-aware, bitselect/fma SPILL-EXEMPT, spill_aware promoted to CI+gate_commit;
follow-on D-506 = FP spill stage-2, note-class). 完成形 plateau (all dims
confirmed, surface audits clean, interp+JIT fuzz 0-crash, v1-JIT parity D-265 closed).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Out-lining once-called inlined handlers is size-NEUTRAL (a giant symbol ≠ duplication)

- **Date**: 2026-07-16
- **Area**: engine/codegen dispatch; binary size (ADR-0204 campaign)
- **Trigger**: D-521 stage A — converting `dispatch_collector.dispatch()`
from `inline for` + `@call(.auto)` to a comptime fn-pointer table,
expecting `engine.codegen.arm64.emit.compile` (one 707 KB `__text`
symbol) to shrink by the out-lined handler share.

## Observation

Measured (ReleaseSafe arm64 CLI, on top of D-522 stage 1):

- `emit.compile`: 707,492 B → 707,540 B — **unchanged**.
- Whole binary: 4,173,736 → 4,202,536 B — **+28.8 KB** (+213 handler
symbols; part of the delta is symtab/strtab, not code).
- Change reverted.

## Why (re-derivable)

A function inlined into its **single** call site is not duplicated —
the code exists exactly once, merely aggregated under the caller's
symbol. Out-lining it moves the same bytes behind a `call` and ADDS
call overhead + a symbol-table row. Per-symbol size attribution
(`nm` next-addr − addr) makes such a caller look like a monolith to
attack, but symbol size ≠ recoverable size. Contrast D-522 stage 1
(same campaign, same day): there the bodies were monomorphized into
**64 copies each** (per-slot comptime K) — genuine duplication — and
sharing them recovered −1.08 MB. **The question that predicts the
outcome is "how many call sites / instantiations share this code?",
not "how big is the symbol?"**

Same family as the D-507 scalar-elision retrospective ("biggest
lever" refuted by measurement): size/perf hypotheses from symbol
attribution or peer-project claims (here from_cljw_05's "table-driven
encoder typically shrinks 50–80%") must be probed with the cheapest
reversible experiment before scheduling a migration campaign.

## Residual

The per-op-file migration of the remaining ~161 legacy switch arms
(ADR-0074 trajectory) and O(1) table dispatch remain *maintainability/
compile-speed* options — pursue them on their own merits if ever
measured to matter, never as a size lever (D-521 discharged).
1 change: 1 addition & 0 deletions .dev/lessons/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

| Date | Slug | Keywords | One-line | |
|------------|----------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 2026-07-16 | outlining-once-called-handlers-size-neutral | binary size, symbol size not recoverable size, giant emit.compile 707KB aggregation not duplication, inline for dispatch fn-pointer table stage A refuted +28.8KB reverted, once-called inline = no duplication, D-522 per-slot 64-copies genuine duplication -1.08MB, how many call sites share this code, probe cheapest reversible experiment before migration campaign, from_cljw_05 50-80% claim refuted, D-521 discharged | D-521 stage A (fn-ptr table dispatch replacing inline-for) measured: `emit.compile` UNCHANGED at 707 KB, binary +28.8 KB → reverted. A once-called inlined handler is aggregated, not duplicated — out-lining is size-neutral minus overhead. Contrast D-522 stage 1 (−1.08 MB): per-slot comptime K monomorphized each body ×64 = genuine duplication. Predictive question = "how many call sites/instantiations share this code?", not "how big is the symbol?". Probe before scheduling migration campaigns. | |
| 2026-07-09 | failure-path-tests-certified-the-defect | ADR-0203 D5 cache silent-miss, failure-path testing, unit test asserted defective behavior, DA critique reproduced corrupt-entry brick + ZWASM_DEBUG fail + interp override, happy-path bias, tests mirror implementation not invariant, enumerate failure modes red-first, self-heal degrade-not-fail | Stage-5 `--cache` shipped the happy path complete while the D5 invariant "cache can never make run fail" was implemented only for read errors; the unit test ASSERTED the defect (corrupt entry returned as hit) and a same-file comment contradicted the module doc. DA critique reproduced 4 failing-run shapes. Lesson: enumerate an invariant's failure modes and write one red test per mode BEFORE implementing — implementer-written tests otherwise mirror the implementation, not the invariant. Fixed 4a17ceda7 (header-gate + self-heal, refusal=bypass, interp bypass). | |
| 2026-07-09 | stale-cache-binary-false-differential-finding | stale .zig-cache/o/<hash> exe path, per-edit new hash dir old binary survives, fault-injection artifact mimics real finding, MEM-MISMATCH offset 0x3 +1 signature, re-resolve via zig build step, disambiguate by running candidates on committed known-green corpora, clean-rebuild green before trusting post-injection findings | Running a differential runner by a remembered `.zig-cache/o/<hash>/` path after source edits executed a STALE (fault-injected) build — its lone campaign "finding" was the injection artifact. Each edit compiles to a NEW hash dir; old ones survive. Rules: re-resolve the exe via `zig build <step>` after any edit (or verify the candidate against the committed known-green corpora first); end every fault-injection experiment with a clean rebuild + green run; a finding mirroring a recent deliberate perturbation is presumed contamination until a fresh build reproduces it. | |
| 2026-07-01 | listen-test-runner-endofstream-guest-stdout | failed command --listen=-, phantom test failure exit 0, Zig 0.16 test runner EndOfStream panic after all pass, WASI guest stdout real fd 1 corrupts protocol, builtin.is_test guard fd.zig signal.zig | `zig build test` shows a phantom `failed command: … --listen=-` even when all tests pass — a WASI test wrote guest stdout to real fd 1 (the --listen protocol channel); guard real std-fd writes with `!builtin.is_test` |
Expand Down
Loading