From 262c22476cfee67c8b579b6aec24782bb419fedf Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sun, 21 Jun 2026 13:26:13 +0200 Subject: [PATCH] =?UTF-8?q?test(dwarf):=20step=202=20=E2=80=94=20read=20in?= =?UTF-8?q?put=20wasm=20.debug=5Fline=20into=20(offset=E2=86=92line)=20row?= =?UTF-8?q?s=20(VCR-DBG-001,=20#394,=20#242)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DWARF Tier-1 feature loop, step 2 (READ side). Frozen-safe spike proving synth can parse the input wasm's DWARF line table — the wasm-byte-offset → file:line half that composes with step 1's op_offsets (#399) for machine-addr → source. crates/synth-core/tests/dwarf_line_read_spike.rs: extracts the .debug_* custom sections (wasmparser, already a dep) and parses .debug_line (gimli) into (wasm-byte-offset → file:line) rows on the in-tree DWARF-carrying fixture k_mutex_unlock.dissolved.wasm — e.g. wasm+0x0094 → panicking.rs:80. FROZEN-SAFE: gimli is a DEV-dependency only — synth's Bazel manual-spec crate_universe globs src/ (never tests/), so no MODULE.bazel pin, and the production synth binary does not pull gimli. Zero codegen change; control_step verified unchanged (113 instrs / 354 bytes). The production gimli dep + MODULE.bazel pin land with the EMITTER (step 4), as a gated feature-loop release (v0.12.0), not an idle tick. Roadmap VCR-DBG-001 PROGRESS updated: step 1 merged, step 2 read side proven; next is step 3 (compose op_offsets × line-rows). Trace: skip Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 18 ++ artifacts/verified-codegen-roadmap.yaml | 12 ++ crates/synth-core/Cargo.toml | 6 + .../synth-core/tests/dwarf_line_read_spike.rs | 164 ++++++++++++++++++ 4 files changed, 200 insertions(+) create mode 100644 crates/synth-core/tests/dwarf_line_read_spike.rs diff --git a/Cargo.lock b/Cargo.lock index d6de8b0f..a89d63ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -582,6 +582,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "fastrand" version = "2.3.0" @@ -733,6 +739,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + [[package]] name = "glob" version = "0.3.3" @@ -2091,6 +2108,7 @@ name = "synth-core" version = "0.11.50" dependencies = [ "anyhow", + "gimli", "serde", "serde_json", "sha2 0.11.0", diff --git a/artifacts/verified-codegen-roadmap.yaml b/artifacts/verified-codegen-roadmap.yaml index 84d947ba..ebfbcf37 100644 --- a/artifacts/verified-codegen-roadmap.yaml +++ b/artifacts/verified-codegen-roadmap.yaml @@ -1182,6 +1182,18 @@ artifacts: ARM address resolves to the correct source line; AND assert `.text` byte-identical to the no-DWARF build (the load-bearing safety property). Then cut v0.12.0. + PROGRESS: + - step (1) MERGED #399 (decoder `op_offsets` byte-offset side-table). + - step (2) READ side proven in-tree 2026-06-21 (frozen-safe spike, + crates/synth-core/tests/dwarf_line_read_spike.rs): gimli DEV-dep + extracts the `.debug_*` custom sections (wasmparser) and parses + `.debug_line` into (wasm-byte-offset → file:line) rows on the + DWARF-carrying fixture k_mutex_unlock.dissolved.wasm — e.g. + wasm+0x0094 → panicking.rs:80. DEV-dep ⇒ no production gimli dep, no + MODULE.bazel pin (Bazel globs src/ only), zero codegen change, frozen + fixtures bit-identical. The PRODUCTION gimli dep + MODULE.bazel pin + land with the EMITTER (step 4), as a feature-loop release step — not + an idle-tick increment. NEXT: step (3) compose op_offsets × line-rows. status: approved tags: [toolchain, dwarf, debuggability, elf, meld-coordination, feature-loop, release-v0.12.0, synth-394] links: diff --git a/crates/synth-core/Cargo.toml b/crates/synth-core/Cargo.toml index c729b14f..d26ebcb2 100644 --- a/crates/synth-core/Cargo.toml +++ b/crates/synth-core/Cargo.toml @@ -20,3 +20,9 @@ wasmparser.workspace = true [dev-dependencies] wat.workspace = true +# VCR-DBG-001 Tier-1 step 2 (#394, #242) — gimli parses the input wasm's +# `.debug_line` in the read-side spike ONLY. DEV-dep ⇒ frozen-safe: synth's Bazel +# manual-spec crate_universe globs `src/` (never `tests/`), so this needs no +# MODULE.bazel pin, and the production `synth` binary does not pull gimli. The +# production gimli dep + MODULE.bazel pin land with the DWARF EMITTER (step 4). +gimli = "0.31" diff --git a/crates/synth-core/tests/dwarf_line_read_spike.rs b/crates/synth-core/tests/dwarf_line_read_spike.rs new file mode 100644 index 00000000..9151c893 --- /dev/null +++ b/crates/synth-core/tests/dwarf_line_read_spike.rs @@ -0,0 +1,164 @@ +//! VCR-DBG-001 Tier-1, STEP 2 (#394, #242) — READ the input wasm's DWARF. +//! +//! The DWARF feature-loop plan (artifacts/verified-codegen-roadmap.yaml, +//! VCR-DBG-001) bridges synth's source-to-object provenance in numbered steps: +//! +//! (1) decoder records per-op wasm code BYTE OFFSET → MERGED (#399): the +//! `op_offsets: Vec` side-table on `FunctionOps`, giving +//! wasm-op-index → wasm-byte-offset. +//! (2) READ the input wasm DWARF (THIS spike): extract the `.debug_*` custom +//! sections and parse `.debug_line` with gimli → (wasm-byte-offset → +//! file:line) rows. +//! (3) COMPOSE (1)+(2): ARM-text-offset → wasm-op-index → wasm-byte-offset → +//! file:line. +//! (4) EMIT the ARM/RV32 `.debug_line` as non-ALLOC ELF sections. +//! +//! This test proves step (2)'s READ capability in-tree, FROZEN-SAFE: gimli is a +//! DEV-dependency (no production dep, no MODULE.bazel pin — Bazel globs `src/` +//! only), the spike changes ZERO codegen, and every frozen fixture stays +//! bit-identical by construction. The production gimli dep + the emitter land +//! later (step 4), gated as a feature-loop release (v0.12.0). +//! +//! Real Rust/clang→wasm modules ship DWARF in custom sections; the in-tree +//! fixture `k_mutex_unlock.dissolved.wasm` carries `.debug_line`, `.debug_info`, +//! `.debug_abbrev`, `.debug_str`, and `.debug_ranges`. For a wasm module the +//! `.debug_line` row ADDRESS is the wasm code byte offset — exactly what step +//! (1)'s `op_offsets` produces, so steps (1) and (2) compose. + +use std::collections::HashMap; + +use gimli::{Dwarf, EndianSlice, LittleEndian, SectionId}; +use wasmparser::{Parser, Payload}; + +/// Pull every `.debug_*` custom section out of a wasm module (the raw material +/// gimli consumes). wasmparser is already a production dep; this is the +/// extraction half of step (2). +fn extract_debug_sections(wasm: &[u8]) -> HashMap> { + let mut out = HashMap::new(); + for payload in Parser::new(0).parse_all(wasm) { + if let Ok(Payload::CustomSection(c)) = payload + && c.name().starts_with(".debug_") + { + out.insert(c.name().to_string(), c.data().to_vec()); + } + } + out +} + +fn fixture(rel: &str) -> std::path::PathBuf { + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join(rel) +} + +/// A resolved `.debug_line` row: the wasm code byte offset and its source line. +#[derive(Debug)] +struct LineRow { + wasm_byte_offset: u64, + line: u64, + file: Option, +} + +/// Parse `.debug_line` from the extracted sections → (wasm-byte-offset → line) +/// rows. This is the parse half of step (2). +fn read_line_rows(sections: &HashMap>) -> Result, gimli::Error> { + // gimli borrows section bytes; the closure hands each SectionId its bytes + // (or an empty slice for sections this wasm doesn't carry). + let empty: &[u8] = &[]; + let load = |id: SectionId| -> Result, gimli::Error> { + let data = sections.get(id.name()).map_or(empty, |v| v.as_slice()); + Ok(EndianSlice::new(data, LittleEndian)) + }; + let dwarf = Dwarf::load(load)?; + + let mut rows = Vec::new(); + let mut units = dwarf.units(); + while let Some(header) = units.next()? { + let unit = dwarf.unit(header)?; + let Some(program) = unit.line_program.clone() else { + continue; + }; + let header = program.header().clone(); + let mut state = program.rows(); + while let Some((_, row)) = state.next_row()? { + if row.end_sequence() { + continue; + } + // Resolve the file name where the encoding makes it cheap (best + // effort — DWARF<5 vs DWARF5 index conventions differ; the address + // and line are the load-bearing facts for the wasm-offset bridge). + let file = row.file(&header).and_then(|fe| { + dwarf + .attr_string(&unit, fe.path_name()) + .ok() + .and_then(|s| s.to_string().ok().map(|s| s.to_string())) + }); + rows.push(LineRow { + wasm_byte_offset: row.address(), + line: row.line().map(|l| l.get()).unwrap_or(0), + file, + }); + } + } + Ok(rows) +} + +#[test] +fn reads_wasm_debug_line_into_offset_line_rows_dbg001_step2() { + let wasm = std::fs::read(fixture( + "scripts/repro/synth-331/k_mutex_unlock.dissolved.wasm", + )) + .expect("DWARF-carrying fixture in-tree"); + let wasm_size = wasm.len() as u64; + + // (a) extraction — the fixture carries .debug_line + the tables it needs. + let sections = extract_debug_sections(&wasm); + assert!( + sections.contains_key(".debug_line"), + "fixture must carry .debug_line; found sections: {:?}", + sections.keys().collect::>() + ); + assert!( + !sections[".debug_line"].is_empty(), + ".debug_line must be non-empty" + ); + + // (b) parse — gimli yields (wasm-byte-offset → line) rows. + let rows = read_line_rows(§ions).expect("gimli parses the fixture's .debug_line"); + assert!( + !rows.is_empty(), + ".debug_line should decode to ≥1 source-line row" + ); + + // Soundness of the bridge: every row's address is a wasm code byte offset + // (in range), and at least one row carries a real (non-zero) source line — + // the wasm-offset → source mapping step (3) will compose against op_offsets. + for r in &rows { + assert!( + r.wasm_byte_offset < wasm_size, + "row offset {} out of wasm bounds {}", + r.wasm_byte_offset, + wasm_size + ); + } + let with_line = rows.iter().filter(|r| r.line > 0).count(); + assert!( + with_line > 0, + "expected ≥1 row with a non-zero source line; got {} rows", + rows.len() + ); + + eprintln!( + "[dbg001-step2] {} .debug_line rows from synth-331/k_mutex_unlock.dissolved.wasm \ + ({with_line} with a source line). sample:", + rows.len(), + ); + for r in rows.iter().filter(|r| r.line > 0).take(5) { + eprintln!( + " wasm+0x{:04x} -> {}:{}", + r.wasm_byte_offset, + r.file.as_deref().unwrap_or(""), + r.line + ); + } +}