Skip to main content

synth_core/
dwarf_line.rs

1//! VCR-DBG-001 step 3 — compose the source-line table (the join half).
2//!
3//! The DWARF Tier-1 bridge maps an ARM text offset back to a source `file:line`
4//! through three established facts:
5//!   1. each ARM instruction carries `source_line` = the wasm OP INDEX
6//!      (`ArmInstruction.source_line`);
7//!   2. step 1 (`FunctionOps.op_offsets`) maps op-index → the wasm code BYTE
8//!      OFFSET (module-relative);
9//!   3. step 2 parses the input wasm's `.debug_line` → (code-section-relative
10//!      address → `file:line`) rows.
11//!
12//! This module is the join for the wasm half — **op-index → source line** —
13//! which step 4 (emit) composes with the ARM layout (ARM-text-offset → op-index
14//! is just `source_line`). It is pure plain-data (no gimli, no backend): the
15//! caller parses the rows and supplies them, so the module is Bazel-clean and
16//! unwired (frozen-safe) until the emitter consumes it.
17//!
18//! The crux it encodes (validated on `scripts/repro/dwarf_coherent.wasm`,
19//! VCR-DBG-001 step-3 fixture): `op_offsets` are MODULE-relative while DWARF
20//! addresses are CODE-section-relative, and they differ by a single constant —
21//! the code section's payload start. So normalization is one subtraction:
22//! `dwarf_addr = op_offset - code_base`.
23
24/// One `.debug_line` row: a code-section-relative address and its source line.
25/// `file` is an opaque caller-supplied id (e.g. an index into the line
26/// program's file table) so this stays gimli-free.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct LineRow {
29    /// Code-section-relative address (the DWARF-for-wasm address space).
30    pub addr: u32,
31    pub line: u32,
32    pub file: u32,
33}
34
35/// A resolved source location for a wasm op.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct SourceLoc {
38    pub line: u32,
39    pub file: u32,
40}
41
42/// Map each wasm op (by its module-relative `op_offsets` byte offset) to a
43/// source location, by normalizing into the code-section-relative DWARF address
44/// space (`op_offset - code_base`) and taking the covering line-table row (the
45/// last row whose address is ≤ the op's address — standard line-table lookup).
46///
47/// Returns one entry per `op_offsets` element (parallel to a function's ops).
48/// `None` where the op precedes `code_base` (shouldn't happen for real code) or
49/// no row covers it (an op before the first line-table address).
50pub fn op_offsets_to_source(
51    op_offsets: &[u32],
52    code_base: u32,
53    rows: &[LineRow],
54) -> Vec<Option<SourceLoc>> {
55    let mut sorted: Vec<LineRow> = rows.to_vec();
56    sorted.sort_by_key(|r| r.addr);
57    op_offsets
58        .iter()
59        .map(|&off| {
60            let a = off.checked_sub(code_base)?;
61            // Largest row addr ≤ a (the line in effect at address a).
62            sorted
63                .iter()
64                .rev()
65                .find(|r| r.addr <= a)
66                .map(|r| SourceLoc {
67                    line: r.line,
68                    file: r.file,
69                })
70        })
71        .collect()
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn covering_row_lookup() {
80        // code_base 100; rows at code-rel 0→line10, 8→line11, 20→line12.
81        let rows = [
82            LineRow {
83                addr: 0,
84                line: 10,
85                file: 1,
86            },
87            LineRow {
88                addr: 8,
89                line: 11,
90                file: 1,
91            },
92            LineRow {
93                addr: 20,
94                line: 12,
95                file: 1,
96            },
97        ];
98        // ops at module 100 (→0), 104 (→4), 108 (→8), 130 (→30).
99        let got = op_offsets_to_source(&[100, 104, 108, 130], 100, &rows);
100        assert_eq!(got[0].map(|s| s.line), Some(10)); // addr 0  → row 0
101        assert_eq!(got[1].map(|s| s.line), Some(10)); // addr 4  → still row 0
102        assert_eq!(got[2].map(|s| s.line), Some(11)); // addr 8  → row 8
103        assert_eq!(got[3].map(|s| s.line), Some(12)); // addr 30 → row 20 (last ≤)
104    }
105
106    #[test]
107    fn op_before_first_row_is_none() {
108        let rows = [LineRow {
109            addr: 8,
110            line: 11,
111            file: 1,
112        }];
113        // op at module 100 → code-rel 0, before the first row (addr 8).
114        let got = op_offsets_to_source(&[100], 100, &rows);
115        assert_eq!(got[0], None);
116    }
117
118    #[test]
119    fn op_before_code_base_is_none() {
120        let rows = [LineRow {
121            addr: 0,
122            line: 1,
123            file: 1,
124        }];
125        let got = op_offsets_to_source(&[50], 100, &rows);
126        assert_eq!(got[0], None);
127    }
128}