fix(dce): comptime-guard GC/subtyping JIT helpers so sub-v3 builds DCE feature.gc#150
Merged
Merged
Conversation
…lds DCE feature.gc `check_build_dce` FAILed on `main` (all six `-Dwasm=v1_0` / `-Dwasm=v2_0` rows): `instruction.wasm_3_0.ref_test_ops.gcRefMatchesNonNullCore` was present in every non-GC binary. Root cause: ADR-0203 D1 (D-516 position-independence) holds the GC / subtyping JIT helpers as `JitRuntime` fn-pointer FIELDS whose DEFAULT is the real helper (so a reloaded `.cwasm` resolves the address in the running process — no setup wiring). A field default `= jitGcRefTest` takes the helper's ADDRESS unconditionally, and `JitRuntime` is instantiated in every build, so the whole GC cohort (`jitGcAlloc` / `jitGcArray*` / `jitGcRefTest` / `jitGcRefCast` / `jitCallIndirectResolve`) — and its `feature/gc/*` reach — stayed live even in a v1_0 build. Only the one symbol whose namespace path contains `wasm_3_0` tripped the `nm`-grep gate, but the entire cohort leaked. The emit sites read the helper via `@offsetOf` (an offset, not an address) and are comptime-fenced behind the wasm_3_0 op dispatch, so they DCE fine — the struct field default is the only always-live address-take. Fix: guard each helper body with `if (comptime !wasm_v3_plus) @Panic(...)` as the first statement. In a sub-v3 build the comptime-false remainder is un-analysed, so the default points at a bare `@panic` stub with no GC references and the GC code DCEs. v3 builds are unaffected (guard is `if (comptime false)` → real body runs; ADR-0203 D1 intact). A sub-v3 build can never emit a GC / subtyping op (the validator rejects them), so the stub is unreachable — `@panic` is the correct default (mirrors `defaultReifyExnref`). Verified: v1_0 + v2_0 forbidden-symbol scan CLEAN on both arm64-native and x86_64-linux cross (the CI leg's arch); v1_0:p1 `.text` also drops 2957749 → 2614800 B as the leaked GC code is reclaimed. `zig build test` + lint green. The regression shipped 2026-07-09 (ADR-0203) but surfaced only now: `check_build_dce` runs in the push-to-main extended leg and the intervening main pushes were doc-only (heavy legs skipped) or concurrency-cancelled, so the gate never completed until #149. Lesson: 2026-07-18-fnptr-field-default-defeats-build-option-dce.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
mainCI is red: the push-to-maincheck_build_dceextended leg FAILs onthe Linux (x86_64) gate — all six
-Dwasm=v1_0/-Dwasm=v2_0rows reportinstruction.wasm_3_0.ref_test_ops.gcRefMatchesNonNullCorepresent in anon-GC binary (run 29565595088).
Root cause
ADR-0203 D1 (D-516, position-independence) holds the GC / subtyping JIT
helpers as
JitRuntimefn-pointer fields whose default is the realhelper (so a reloaded
.cwasmresolves the address in the running process —no setup wiring). A field default
= jitGcRefTesttakes the helper'saddress unconditionally, and
JitRuntimeis instantiated in every build,so the whole GC cohort (
jitGcAlloc/jitGcArray*/jitGcRefTest/jitGcRefCast/jitCallIndirectResolve) — and itsfeature/gc/*reach —stayed live even in a v1_0 build. Only the one symbol whose namespace path
contains
wasm_3_0tripped the gate'snm-grep, but the entire cohortleaked (this is the "同様なもの" audit — see the size drop below).
The emit sites read the helper via
@offsetOf(an offset, not an address)and are comptime-fenced behind the wasm_3_0 op dispatch, so they DCE fine.
The struct field default is the only always-live address-take.
The regression shipped 2026-07-09 (ADR-0203) but surfaced only now:
check_build_dceruns only in the push-to-main extended leg, and everyintervening main push (#143–#148) was doc-only (heavy legs auto-skipped) or
concurrency-cancelled — so the gate never actually completed until #149.
Fix
Guard each helper body with
if (comptime !wasm_v3_plus) @panic(...)as thefirst statement. In a sub-v3 build the comptime-false remainder is
un-analysed, so the default points at a bare
@panicstub with no GCreferences → the GC code DCEs. v3 builds are unaffected (guard is
if (comptime false)→ real body runs; ADR-0203 D1 intact). A sub-v3build can never emit a GC / subtyping op (the validator rejects them), so the
stub is unreachable —
@panicis the correct default (mirrorsdefaultReifyExnref).Single file:
src/engine/codegen/shared/jit_abi.zig(+ lesson).Verification
check_build_dce --gate(all 9wasm×wasicombos × arm64-native[check_build_dce] OK, every rowcleanon both arches.nmscan CLEAN on x86_64-linux (the failingCI leg's arch) and arm64-native.
.text2,957,749 → 2,614,800 B (−343 KB).zig build test+zig build lintgreen.gate_commit.shgreen.Notes
inadvertently broke; it is not a deviation.
push-to-main extended, so a codegen regression can hide across a run of
doc-only / cancelled pushes. Running
check_build_dceonce per PR (alreadyflagged as a future refinement in the handover) would have caught this at
PR time.