Skip to content

feat(#798): RV32 active data segments SHIP — .wasm_data records + linker placement + startup copy, read-back hard gate - #802

Merged
avrabe merged 7 commits into
mainfrom
fix/48-798-rv32-data
Jul 17, 2026
Merged

feat(#798): RV32 active data segments SHIP — .wasm_data records + linker placement + startup copy, read-back hard gate#802
avrabe merged 7 commits into
mainfrom
fix/48-798-rv32-data

Conversation

@avrabe

@avrabe avrabe commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Closes #798. v0.48 Wave-1 Lane 3.

What

The RV32 single-base scheme (s11 = __linear_memory_base, zeroed RAM) shipped a .text-only object: active data segments were silently dropped — every nonzero initializer byte read 0x00 at runtime (found by the VCR-VER-003 phase-2 probe #777; v0.47/#797 held a loud warning). This PR ships them — the real fix, not the decline:

Red → green

  • De-vacuated control_step_riscv_differential.py: the old harness copied wasmtime's instantiated memory into unicorn, masking the drop (a vacuous gate). It now initializes linmem only from the shipped .wasm_data records. On the pre-fix binary (ac79fb3): loud FAIL, and the semantic red is proven — zeroed-linmem execution returns 0x00000000 for (3000,50,40,0) where wasmtime returns 0x00210a55. Post-fix: 5/5 vectors match incl. gale's reference 0x00210a55.
  • New full-boot oracle scripts/repro/rv32_data_798_boot_differential.py: synth compile → riscv-runtime → clang(riscv32) + ld.lld → unicorn boots fw.elf from _reset — the REAL generated startup copy loop initializes linmem; overlapping segments serve the later-wins image (aabb1122) and get() equals wasmtime (0x2211bbaa). PASS locally.
  • Red-first unit gates: a reversed (first-wins) record pack fails validate_served_image at the classic 0.43.0: #746 fix relocates but MISCOMPILES the wide-static copy path — silent wrong bytes (was: skip) #757 byte; declaration order passes. Malformed blobs parse to None (loud).

Refreeze ritual

All 12 *_riscv_differential.py scripts re-run PASS on the new bytes. Frozen RV32 .text is unchanged (control_step .text sha identical pre/post; data-free objects byte-identical) — frozen_fixtures_rv32_text_is_bit_identical_oracle_001 green with no re-pin needed; the compile-success freeze that held the v0.47 warning is moot because the compile stays green by shipping, and its differential now actually reads the segment (de-vacuated per #797's finding).

Gates

  • cargo test --workspace green (126 suites), cargo fmt --check, clippy -D warnings clean
  • scripts/claim_check.py: 21/21 — claims/roadmap updated to the shipped state + new count-eq pin holding the RV32 served image to the emitted blob read-back
  • e2e tests: records-verbatim, both-records-in-order (overlap), no-section (data-free), instantiation-trap refusal

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L

avrabe and others added 7 commits July 17, 2026 07:41
…it from the SHIPPED .wasm_data records, not wasmtime's memory

The old harness copied wasmtime's instantiated memory image into unicorn,
masking the #798 silent initializer drop (the object ships .text only): the
1200-byte decision table read 0x00 at runtime while the differential stayed
green. Now linear memory is initialized ONLY from the object's .wasm_data
active-segment records (the bytes the generated startup copy loop serves),
and a record-less object fails loudly up front.

RED on the pre-fix binary (ac79fb3):
  - object ships NO .wasm_data records -> loud FAIL, and
  - semantic divergence proven: zeroed-linmem execution of the shipped .text
    returns 0x00000000 for (3000,50,40,0) where wasmtime returns 0x00210a55
    (the differential READS the segment — non-vacuous).

GREEN arrives with the .wasm_data shipping commit (#798).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…e read-back in synth_core::static_data_addr

The RV32 .wasm_data shipping format: repeated [u32 off][u32 len][bytes][pad4]
in declaration order (later-wins preserved by the startup's record-order copy).
served_image_from_records() reconstructs the dense image the emitted blob
actually serves (read-back, never a recompute — mirror-pinning excluded), for
the same validate_served_image gate the #758 ROM image uses.

Red-first unit gate: a REVERSED (first-wins) pack of the #757 3-overlap shape
fails validate_served_image at the classic 0x100008 byte; the declaration-order
pack passes. Plus round-trip, sparse far-offset (8+len flash bytes, no dense
image), and loud-reject on malformed blobs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…ent + startup record-copy loop

- RiscVElfBuilder::build_with_data: non-empty record blobs become a
  .wasm_data PROGBITS (SHF_ALLOC, 4-aligned) section between .text and
  .symtab; empty blobs are BYTE-IDENTICAL to the pre-#798 layout (unit-gated).
- Generated linker.ld: flash-resident .wasm_data output section with
  __wasm_data_records_start/end, placed before _data_load is pinned (no
  overlap with the .data load image). Old scripts + new startup fail the
  link LOUDLY (undefined symbol) instead of silently dropping.
- Generated startup.c: reset-path loop walks the records and byte-copies
  each segment to __linear_memory_base + off before main — record order =
  declaration order, so WASM later-wins overlap semantics hold.
- Backend::compile_module (library path) ships the records too and
  hard-fails on a served/runtime mismatch via the read-back gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…ning becomes the VCR-VER-003 read-back hard gate

The -b riscv arm now packs all_data_segments into .wasm_data records (via
pack_segment_records), reads the emitted blob BACK (served_image_from_records)
and hard-errors on any served/runtime disagreement (validate_served_image,
mixed-split style) — plus the #758-parity instantiation-trap bail when a
segment extends past the declared linear memory. The 'ships NO initializer
image' warning is gone: the initializers ship.

Red->green on the de-vacuated control_step differential: pre-fix object FAILS
(no .wasm_data, zeroed-linmem result 0x00000000 vs 0x00210a55); post-fix all
5 vectors match wasmtime incl. gale's reference 0x00210a55.

Frozen-fixture impact: NONE on .text (control_step .text sha unchanged);
data-free RV32 objects are whole-file byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…full-boot differential through the REAL startup copy loop

- vcr_ver_003_addr_777.rs RV32 class rewritten for the shipped state: nonzero
  data compiles green with NO drop-warning and the .wasm_data record bytes
  verbatim; overlapping segments ship both records in declaration order;
  data-free modules ship no section; a segment past the declared memory is
  refused ('instantiation would trap', #758 parity).
- scripts/repro/rv32_data_798_boot_differential.py: synth compile ->
  riscv-runtime -> clang(riscv32)+ld.lld -> unicorn boots fw.elf FROM _reset,
  so the GENERATED startup's record-copy loop initializes linmem; overlapping
  segments must serve the later-wins image and get() must equal wasmtime.
  Verified locally: linmem aabb1122, result 0x2211bbaa == wasmtime. Missing
  toolchain = loud FAIL, never a skip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
…+ CHANGELOG

- VCR-VER-003 roadmap item (7): warning-era text replaced with the shipped
  scheme (records format, linker placement, startup copy, read-back hard
  gate, de-vacuated differential + full-boot oracle); RV32 shipping removed
  from the BOUNDED follow-ups; pass-criteria updated.
- claims.yaml: comment (c) updated to the shipped state; new count-eq pin
  holds the RV32 served image to the EMITTED blob read back
  (served_image_from_records == 1 in main.rs). 21/21 claims hold.
- CLAUDE.md Track C + CHANGELOG [Unreleased] entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJK5LZZEkV5smCY1jKn18L
@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.03419% with 42 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/synth-backend-riscv/src/backend.rs 0.00% 28 Missing ⚠️
crates/synth-cli/src/main.rs 62.96% 10 Missing ⚠️
crates/synth-backend-riscv/src/elf_builder.rs 98.41% 2 Missing ⚠️
crates/synth-core/src/static_data_addr.rs 98.24% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@avrabe
avrabe merged commit 172ef7a into main Jul 17, 2026
42 checks passed
@avrabe
avrabe deleted the fix/48-798-rv32-data branch July 17, 2026 06:53
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.

RV32: active data-segment initializers are silently dropped — ship them (linker-script placement + startup copy) and hard-decline until then

1 participant