Skip to content

fix(globals): #649 i64.const global initializers reach the emitted image — decoder captures both words, startup materializes the R9 table#652

Merged
avrabe merged 1 commit into
mainfrom
fix/649-i64-global-init
Jul 8, 2026
Merged

fix(globals): #649 i64.const global initializers reach the emitted image — decoder captures both words, startup materializes the R9 table#652
avrabe merged 1 commit into
mainfrom
fix/649-i64-global-init

Conversation

@avrabe

@avrabe avrabe commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #649. Lineage: #645 (i64 global get/set pair lowering + width-aware slot layout), #643, #648/#369 (GI-FPU-001 float loud-skip lane).

Mechanism

WasmGlobal.init_i32 only decoded a leading i32.const; an i64.const init decoded to None and every consumer's unwrap_or(0) silently ZEROED the slot. #645 made the accesses width-correct but a nonzero i64 init still never reached the output — worse, the default self-contained image never materialized a globals table at all (R9 was never initialized; reads hit whatever R9=0 pointed at).

Fix

Decoder (synth-core/wasm_decoder.rs): init_i32: Option<i32>init: Option<GlobalInit> with I32(i32)/I64(i64) — a rename, so the compiler finds every consumer (no wildcard survives). f32/f64 inits deliberately stay None: float global access is the GI-FPU-001 loud-skip lane (#648) and capturing bit patterns here must not quietly unskip it.

Consumers audited (all of them, forced by the rename):

consumer handling
identify_stack_pointer_global (#237 SP promotion) matches GlobalInit::I32 only — an SP is an i32 address; i64 inits are data, never promoted
--native-pointer-abi __synth_globals slots (NativeGlobalsLayout, all data/bss write sites in build_relocatable_elf) stays 4-byte i32 slots; wide globals already REFUSED up front (#643 bail = the decline-loudly arm), so only I32 inits can land; byte-identical for i32-global modules vs origin/main
self-contained Cortex-M image (build_multi_func_cortex_m_elf + single-function build_cortex_m_elf) previously NO materialization anywhere: now generate_minimal_startup receives the table words (#643 summed-width layout, i64 = two words lo-first), sets R9 = 0x2000_0000 + linmem_size, and stores every captured init word (via the R12 scratch — reserved, and no user code has run yet) before BLX; RAM sizing + the NoBits phdr/section cover the table; tables past the STR.W #imm12 range decline loudly
plain --relocatable (host-owned R9 table) unchanged contract — the host provides the table (the #643 oracle pins it)
RV32 no global lowering: every global access already loud-skips (pinned by the #643 oracle)

No globals ⇒ the historical 28-byte startup blob and the whole ELF stay byte-identical (cmp vs an origin/main build: identical; frozen anchors 10/10).

Red → green

scripts/repro/i64_global_init_649_differential.py + i64_global_init_649.wat — the DEFAULT cortex-m4f image (the exact #649 verification shape), fully artifact-derived: unicorn executes the image's real reset path up to the call scaffold (the harness fabricates nothing), then runs the stateful export sequence against one wasmtime instance. Symbols from the ELF symtab, never disasm text.

Gates

  • Frozen anchors 10/10; global-free module ELF byte-identical to origin/main; --native-pointer-abi i32 output byte-identical, wide-global refusal still loud
  • thumb-2: i64 global.set/global.get truncate to 32 bits (high word silently dropped) #643 differential oracle: PASS (both ARM paths + RV32 loud-skip contract)
  • cargo test --workspace green (new: decoder i64-init capture test, startup-materializer byte test, table-layout test)
  • cargo fmt --check + cargo clippy --workspace --all-targets -- -D warnings clean

🤖 Generated with Claude Code

…age — decoder captures both words, startup materializes the R9 table

The decoder's `init_i32` captured only a leading `i32.const`; an i64
global's init decoded to `None` and every consumer's `unwrap_or(0)`
silently ZEROED the slot. #645 fixed the get/set PAIR lowering and the
width-aware slot layout, but a nonzero `(global (mut i64) (i64.const X))`
read before any set still returned garbage — the init existed nowhere in
the output.

Decoder: `WasmGlobal.init_i32` is RENAMED to `init: Option<GlobalInit>`
(I32/I64) so no consumer survives by wildcard. f32/f64 inits stay `None`
on purpose — float global access is the GI-FPU-001 (#369/#648) loud-skip
lane and capturing bits here must not quietly unskip it.

Consumers audited (all of them, post-rename):
- `identify_stack_pointer_global`: matches `GlobalInit::I32` only (an SP
  is an i32 address; i64 inits are data, never promoted).
- `--native-pointer-abi` `__synth_globals` region: stays 4-byte i32
  slots; wide globals are already REFUSED up front (#643 bail — the
  decline-loudly arm), so only I32 inits can land. Byte-identical for
  i32-global modules (verified against origin/main).
- Self-contained Cortex-M image (the #649 repro shape): globals were
  never materialized AT ALL — R9 was never even initialized. Now
  `generate_minimal_startup` gets the table words (#643 summed-width
  layout, i64 = two words lo-first), points R9 at
  `0x2000_0000 + linmem_size`, and stores every word before calling the
  user function; RAM sizing + the NoBits region cover the table. Both
  the all-exports and single-function builders. Tables past the STR.W
  #imm12 range decline loudly. No globals => the historical 28-byte
  startup blob and the whole ELF stay BYTE-IDENTICAL (verified).

Red -> green: scripts/repro/i64_global_init_649_differential.py runs the
default cortex-m4f image's REAL reset path in unicorn (artifact-derived,
nothing fabricated), then the stateful export sequence vs one wasmtime
instance. origin/main: FAIL (4 divergences — init reads return vector-
table garbage). This branch: PASS (init reads, i32-after-i64 offset
canary, #645 set-then-get pin, control).

Gates: frozen anchors 10/10, #643 oracle PASS, cargo test --workspace
green, fmt + clippy --workspace -D warnings clean.

Fixes #649. Refs #645, #643, #648, #369.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.17486% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-cli/src/main.rs 95.42% 7 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe avrabe merged commit df93bb1 into main Jul 8, 2026
32 checks passed
@avrabe avrabe deleted the fix/649-i64-global-init branch July 8, 2026 16:10
avrabe added a commit that referenced this pull request Jul 8, 2026
…sound mask bounds (#648/#652/#653/#654) (#657)

Pin sweep 0.33.1 -> 0.34.0 + CHANGELOG.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

non-i32 global initializers silently zeroed — i64.const/f32.const/f64.const inits drop to 0 (init_i32 only decodes i32.const)

1 participant