From d87390128fae3cf6ce9cdc27a82e9bd6cd95189c Mon Sep 17 00:00:00 2001 From: "Shota Kudo (chaploud)" Date: Thu, 16 Jul 2026 13:10:41 +0900 Subject: [PATCH] ADR-0204 close: D-521 refutation record + campaign retrospective + v2.2.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lesson 2026-07-16-outlining-once-called-handlers-size-neutral: fn-ptr table stage A left emit.compile at 707 KB and grew the binary +28.8 KB (reverted) — a giant symbol from once-called inlined handlers is aggregation, not duplication; out-lining is size-neutral. Predictive question = "how many call sites share this code?", not symbol size. - debt: D-521 DISCHARGED (size premise measured-refuted); D-522 reworded to its stage-2 residual (demand-driven, ~200 KB). - ADR-0204: Status -> Implemented; D2 amended with the refutation. - handover: campaign section closed. - CHANGELOG + build.zig.zon: v2.2.1 (D-522 stage 1 = CLI -21% ReleaseSafe, no API/behaviour/JIT-output change). --- .dev/debt.yaml | 39 ++++------------ .dev/decisions/0204_binary_size_campaign.md | 16 ++++++- .dev/handover.md | 43 +++++++++--------- ...ining-once-called-handlers-size-neutral.md | 44 +++++++++++++++++++ .dev/lessons/INDEX.md | 1 + CHANGELOG.md | 24 ++++++++++ build.zig.zon | 2 +- 7 files changed, 116 insertions(+), 53 deletions(-) create mode 100644 .dev/lessons/2026-07-16-outlining-once-called-handlers-size-neutral.md diff --git a/.dev/debt.yaml b/.dev/debt.yaml index d8026fd08..e83fd6ec2 100644 --- a/.dev/debt.yaml +++ b/.dev/debt.yaml @@ -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" @@ -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" diff --git a/.dev/decisions/0204_binary_size_campaign.md b/.dev/decisions/0204_binary_size_campaign.md index 360c5b719..5c0b604fc 100644 --- a/.dev/decisions/0204_binary_size_campaign.md +++ b/.dev/decisions/0204_binary_size_campaign.md @@ -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); @@ -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 diff --git a/.dev/handover.md b/.dev/handover.md index 829a310c4..183ac90ff 100644 --- a/.dev/handover.md +++ b/.dev/handover.md @@ -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/` 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) @@ -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). diff --git a/.dev/lessons/2026-07-16-outlining-once-called-handlers-size-neutral.md b/.dev/lessons/2026-07-16-outlining-once-called-handlers-size-neutral.md new file mode 100644 index 000000000..b7d277848 --- /dev/null +++ b/.dev/lessons/2026-07-16-outlining-once-called-handlers-size-neutral.md @@ -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). diff --git a/.dev/lessons/INDEX.md b/.dev/lessons/INDEX.md index 666c428d2..e58da3f79 100644 --- a/.dev/lessons/INDEX.md +++ b/.dev/lessons/INDEX.md @@ -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/ 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//` 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 ` 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` | diff --git a/CHANGELOG.md b/CHANGELOG.md index c07093271..74562158a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,30 @@ SemVer compatibility guarantees start at the first stable `v2.0.0` tag. _No changes yet._ +## [2.2.1] - 2026-07-16 + +Binary-size campaign (ADR-0204, PRs #144-#146), triggered by downstream +embedder measurement (cljw): the zwasm binary shrank ~21% (ReleaseSafe +arm64 CLI 5,282,584 → 4,173,736 B) with no API, behaviour, or JIT-output +change. + +### Changed + +- **JIT host-callback FP thunks share their bridge bodies** (D-522 + stage 1): the `f32`/`f64`-arg host-call thunks previously monomorphized + the full marshalling body per (arg-kinds × result × slot) — + ~300 B × 3,840 instantiations. The bodies now live in 60 shared + `noinline` bridges; each per-slot thunk is a ~23 B tail-forwarder. + `api.jit_host_bridge` code: 1,311 KB → 232 KB (−82%). Thunk C-ABI + signatures, trap semantics, and the embedder-facing API are unchanged. + +### Internal + +- Binary-size baseline + per-stage rows recorded in + `bench/results/size_history.yaml`; campaign record in ADR-0204 + (including the measured refutation of the "table-driven dispatch + shrinks the emitter" hypothesis — D-521 discharged). + ## [2.2.0] - 2026-07-09 AOT-full-fidelity campaign (ADR-0203, PRs #136-#142): `.cwasm` is now a diff --git a/build.zig.zon b/build.zig.zon index e3d5675b1..e2ae61a09 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .zwasm, - .version = "2.2.0", + .version = "2.2.1", .fingerprint = 0xfdac0191bf453d15, .minimum_zig_version = "0.16.0", .dependencies = .{