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}