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// ---------------------------------------------------------------------------
75// VCR-DBG-001 step 4 — PRODUCTION read + emit (the `--debug-line` feature).
76//
77// `read_input_dwarf_line` ports the read-side spike
78// (`tests/dwarf_line_read_spike.rs` + `dwarf_compose_step3.rs::code_base`):
79// pull the `.debug_*` custom sections out of the input wasm, parse `.debug_line`
80// with gimli, and also report the code-section payload start (`code_base`) the
81// compose normalizes against. `emit_debug_sections` ports the emit-side spike
82// (`tests/dwarf_emit_roundtrip_step4.rs::emit_dwarf`): take an address-ordered
83// (arm_addr → source line) table and produce a FULL debugger-readable DWARF unit
84// (`.debug_info`/`.debug_abbrev`/`.debug_str`/`.debug_line`) via gimli::write,
85// the CU's DW_AT_stmt_list pointing at the line table. Both are gated behind
86// `--debug-line`; when the input carries no DWARF, `read_input_dwarf_line`
87// returns empty rows (graceful no-op) and the emit is skipped, so the default
88// object stays bit-identical.
89
90use std::collections::HashMap;
91
92use gimli::{Dwarf, EndianSlice, LittleEndian, SectionId};
93use wasmparser::{Parser, Payload};
94
95/// Result of reading the input wasm's DWARF line table: the parsed rows plus the
96/// code-section payload start (`code_base`) the op-offset compose subtracts.
97#[derive(Debug, Default, Clone)]
98pub struct InputDwarfLine {
99 /// Code-section-relative `.debug_line` rows (`addr` is a wasm code byte
100 /// offset; for the synth bridge that equals the DWARF address space).
101 pub rows: Vec<LineRow>,
102 /// Module-relative byte offset of the code section payload start. Empty wasm
103 /// or a wasm with no code section reports 0.
104 pub code_base: u32,
105 /// GLOBAL source-file table `(directory, file name)`. `LineRow.file` indexes
106 /// into this — NOT the input's per-unit DWARF file index. The input wasm has
107 /// several compilation units each with their own file table (colliding
108 /// per-unit indices), so the reader resolves every row's file to a real
109 /// `(dir, name)` string and interns it here, giving one flat table the emit
110 /// reproduces 1:1. Empty when no row resolves to a file (emit then falls back
111 /// to its single-file default).
112 pub files: Vec<(String, String)>,
113}
114
115/// Read the input wasm's `.debug_line` into code-section-relative
116/// `(addr → line)` rows and report `code_base`. Returns an empty table (rows
117/// empty, the feature a no-op) when the input carries no `.debug_*` sections or
118/// no parseable line program — never an error for a DWARF-free module.
119pub fn read_input_dwarf_line(wasm: &[u8]) -> InputDwarfLine {
120 // (a) extract every `.debug_*` custom section + find the code payload start.
121 let mut sections: HashMap<String, Vec<u8>> = HashMap::new();
122 let mut code_base = 0u32;
123 for payload in Parser::new(0).parse_all(wasm) {
124 match payload {
125 Ok(Payload::CustomSection(c)) if c.name().starts_with(".debug_") => {
126 sections.insert(c.name().to_string(), c.data().to_vec());
127 }
128 Ok(Payload::CodeSectionStart { range, .. }) => {
129 code_base = range.start as u32;
130 }
131 _ => {}
132 }
133 }
134 if !sections.contains_key(".debug_line") {
135 return InputDwarfLine {
136 rows: Vec::new(),
137 code_base,
138 files: Vec::new(),
139 };
140 }
141
142 // (b) parse `.debug_line` with gimli. A malformed line program degrades to
143 // an empty table (the feature no-ops) rather than failing the compile.
144 let (rows, files) = parse_debug_line_rows(§ions).unwrap_or_default();
145 InputDwarfLine {
146 rows,
147 code_base,
148 files,
149 }
150}
151
152/// gimli read of `.debug_line` → `(rows, global file table)`. The input carries
153/// several compilation units, each with its own file table, so a row's per-unit
154/// `file_index` is ambiguous once rows are flattened. To keep `LineRow.file`
155/// meaningful across units, each row's file index is resolved (via that unit's
156/// own header + `dwarf.attr_string`) to a real `(directory, name)` string and
157/// interned into one flat GLOBAL table; `LineRow.file` becomes that global index.
158/// `header.file(idx)` / `header.directory(idx)` handle the DWARF version's
159/// indexing (v4 is 1-based; v5 is 0-based) so both are read correctly.
160type LineTable = (Vec<LineRow>, Vec<(String, String)>);
161
162fn parse_debug_line_rows(sections: &HashMap<String, Vec<u8>>) -> Result<LineTable, gimli::Error> {
163 let empty: &[u8] = &[];
164 let load = |id: SectionId| -> Result<EndianSlice<'_, LittleEndian>, gimli::Error> {
165 let data = sections.get(id.name()).map_or(empty, |v| v.as_slice());
166 Ok(EndianSlice::new(data, LittleEndian))
167 };
168 let dwarf = Dwarf::load(load)?;
169
170 let mut rows = Vec::new();
171 let mut files: Vec<(String, String)> = Vec::new();
172 let mut units = dwarf.units();
173 while let Some(header) = units.next()? {
174 let unit = dwarf.unit(header)?;
175 let Some(program) = unit.line_program.clone() else {
176 continue;
177 };
178 // The header borrows the program; clone it so we can consume `program`
179 // in `rows()` while still resolving file indices afterwards.
180 let line_header = program.header().clone();
181 let mut state = program.rows();
182 while let Some((_, row)) = state.next_row()? {
183 if row.end_sequence() {
184 continue;
185 }
186 // Resolve this row's per-unit file index to a real (dir, name) and
187 // intern it into the flat global table (dedup); store the global idx.
188 let file = resolve_file(&dwarf, &unit, &line_header, row.file_index())
189 .map(|entry| intern_file(&mut files, entry))
190 .unwrap_or(0);
191 rows.push(LineRow {
192 addr: row.address() as u32,
193 line: row.line().map(|l| l.get() as u32).unwrap_or(0),
194 file,
195 });
196 }
197 }
198 Ok((rows, files))
199}
200
201/// Resolve a line-program `file_index` to `(directory, file name)` strings using
202/// the owning unit's header. `header.file` / `header.directory` apply the correct
203/// per-version indexing; `attr_string` resolves both inline (`.debug_line`) and
204/// `.debug_line_str`/`.debug_str` forms. `None` when the index has no file entry
205/// (e.g. DWARF v4 index 0).
206fn resolve_file(
207 dwarf: &Dwarf<EndianSlice<'_, LittleEndian>>,
208 unit: &gimli::Unit<EndianSlice<'_, LittleEndian>>,
209 header: &gimli::LineProgramHeader<EndianSlice<'_, LittleEndian>>,
210 file_index: u64,
211) -> Option<(String, String)> {
212 let file = header.file(file_index)?;
213 let name = dwarf
214 .attr_string(unit, file.path_name())
215 .ok()?
216 .to_string_lossy()
217 .into_owned();
218 let dir = match header.directory(file.directory_index()) {
219 Some(av) => dwarf
220 .attr_string(unit, av)
221 .ok()
222 .map(|s| s.to_string_lossy().into_owned())
223 .unwrap_or_default(),
224 None => String::new(),
225 };
226 Some((dir, name))
227}
228
229/// Intern a `(dir, name)` into the global file table, returning its index
230/// (existing if already present). Keeps the table small (the fixture interns a
231/// single `panicking.rs`).
232fn intern_file(files: &mut Vec<(String, String)>, entry: (String, String)) -> u32 {
233 if let Some(i) = files.iter().position(|f| *f == entry) {
234 return i as u32;
235 }
236 files.push(entry);
237 (files.len() - 1) as u32
238}
239
240/// One function to describe with a `DW_TAG_subprogram` child DIE (#394, Tier-1)
241/// so a debugger backtrace shows the FUNCTION NAME, not a bare address. Carries
242/// the function's name and its object-relative `[low_pc, high_pc)` `.text` range
243/// (byte offsets against the `.text` base). The emitter relocates `low_pc`
244/// against the SAME `.text` symbol as the CU (addend = `low_pc`) and writes
245/// `high_pc - low_pc` as the offset form of `DW_AT_high_pc` (no relocation).
246#[derive(Debug, Clone)]
247pub struct SubprogramInfo {
248 /// The function name shown in a debugger frame. The caller composes it
249 /// with priority `name`-section name > export name > synthetic `func_N`
250 /// (#394 Tier-1.x), so internal functions carry their real developer-facing
251 /// name (e.g. `core::panicking::panic_fmt::h...`) whenever the input wasm
252 /// has a `name` custom section.
253 pub name: String,
254 /// Object-relative `.text` byte offset of the function's first instruction.
255 pub low_pc: u64,
256 /// Object-relative `.text` byte offset one past the function's last byte.
257 pub high_pc: u64,
258}
259
260/// Emit an address-ordered `(arm_addr, line)` table as a FULL minimal DWARF unit
261/// (gimli::write) and return EVERY non-empty `.debug_*` section it produces —
262/// `.debug_info`, `.debug_abbrev`, `.debug_str`, `.debug_line` (and
263/// `.debug_line_str`/`.debug_ranges` etc. when non-empty). The caller composes
264/// the table (one address-sorted, de-duped sequence covering every function);
265/// this produces the section bytes for non-ALLOC ELF `PROGBITS` sections.
266/// Returns an empty `Vec` for an empty table (nothing to map ⇒ no sections ⇒
267/// output stays byte-identical).
268///
269/// Crucially this emits a real root `DW_TAG_compile_unit` DIE with `DW_AT_name`,
270/// `DW_AT_low_pc`/`DW_AT_high_pc` spanning the emitted text, and the line program
271/// attached — so the CU's `DW_AT_stmt_list` points at `.debug_line`. That makes
272/// the line table reachable via the NORMAL debugger walk (`.debug_info` → CU →
273/// `DW_AT_stmt_list` → line program), not just a standalone `.debug_line` parse.
274///
275/// Ports `tests/dwarf_emit_roundtrip_step4.rs::emit_dwarf` (which emits the same
276/// full unit and round-trips through `Dwarf::units()`).
277pub fn emit_debug_sections(
278 table: &[(u64, u32, u32)],
279 text_sym: usize,
280 files: &[(String, String)],
281 subprograms: &[SubprogramInfo],
282) -> Vec<EmittedDwarfSection> {
283 use gimli::write::{Address, AttributeValue, DwarfUnit, LineProgram, LineString, Sections};
284
285 if table.is_empty() {
286 return Vec::new();
287 }
288
289 let encoding = gimli::Encoding {
290 format: gimli::Format::Dwarf32,
291 version: 4,
292 address_size: 4,
293 };
294 let mut dwarf = DwarfUnit::new(encoding);
295
296 // The line program's extent: one past the last mapped address (the
297 // `end_sequence` terminator below).
298 let high_pc = table.iter().map(|&(a, _, _)| a).max().unwrap_or(0) + 1;
299
300 // #564: the CU DIE's `DW_AT_high_pc` must cover the CODE extent, not merely
301 // the line-table extent. The child `DW_TAG_subprogram` DIEs (#557) carry
302 // true code extents, and a function's code routinely extends past its last
303 // line-mapped address (the mapped op itself is ≥2 bytes, plus any unmapped
304 // epilogue/literal-pool tail) — a CU high_pc derived from the line table
305 // then fails to CONTAIN its own children (`llvm-dwarfdump --verify`
306 // post-link: "DIE address ranges are not contained in its parent's
307 // ranges"), and a debugger walking CUs by PC range misses the tail
308 // function. Cover max(subprogram high_pc), i.e. the `.text` extent the
309 // caller composed; fall back to the line-table extent when no subprograms
310 // were passed (nothing to contain).
311 let cu_high_pc = subprograms
312 .iter()
313 .map(|sp| sp.high_pc)
314 .fold(high_pc, u64::max);
315
316 // Reproduce the input's source-file table so a debugger resolves each stop to
317 // the REAL file (e.g. `panicking.rs`), not the fabricated `synth.wasm`. The
318 // comp-dir/file passed to `LineProgram::new` become the unit's primary file;
319 // use the FIRST real interned file for it (never `synth.wasm`) so even the
320 // primary is a genuine name. Then `add_file` each interned file and map its
321 // global index → the returned `FileId`. When the input carried no resolvable
322 // file table (`files` empty) keep the historical single-file default so a
323 // DWARF-less-but-lined input still emits something rather than panicking.
324 //
325 // gimli 0.33's `LineProgram::new` args are
326 // `(working_dir, working_dir_info, source_file, source_file_info)`.
327 let (primary_dir, primary_name) = files
328 .first()
329 .map(|(d, n)| (d.clone().into_bytes(), n.clone().into_bytes()))
330 .unwrap_or_else(|| (b"/synth".to_vec(), b"synth.wasm".to_vec()));
331 let mut program = LineProgram::new(
332 encoding,
333 gimli::LineEncoding::default(),
334 LineString::String(primary_dir),
335 None,
336 LineString::String(primary_name.clone()),
337 None,
338 );
339 // Global file index → emitted `FileId`. `file_ids[0]` always exists (either
340 // the first real file or the single fallback), so an out-of-range row index
341 // clamps to it rather than panicking.
342 let mut file_ids = Vec::with_capacity(files.len().max(1));
343 if files.is_empty() {
344 let dir = program.default_directory();
345 file_ids.push(program.add_file(LineString::String(b"synth.wasm".to_vec()), dir, None));
346 } else {
347 for (dir, name) in files {
348 let dir_id = program.add_directory(LineString::String(dir.clone().into_bytes()));
349 file_ids.push(program.add_file(
350 LineString::String(name.clone().into_bytes()),
351 dir_id,
352 None,
353 ));
354 }
355 }
356
357 // The sequence base is `.text + 0` as a RELOCATABLE address (one
358 // `DW_LNE_set_address` against the `.text` symbol, addend 0); each row's
359 // `address_offset` stays a text-relative DELTA, so only this single site
360 // needs a relocation per section. Addend 0 ⇒ the in-place bytes are
361 // byte-identical to the previous `Address::Constant(0)` form.
362 let text_base = Address::Symbol {
363 symbol: text_sym,
364 addend: 0,
365 };
366 program.begin_sequence(Some(text_base));
367 for &(addr, line, file) in table {
368 let row = program.row();
369 row.address_offset = addr;
370 row.file = *file_ids.get(file as usize).unwrap_or(&file_ids[0]);
371 row.line = line as u64;
372 program.generate_row();
373 }
374 program.end_sequence(high_pc);
375 dwarf.unit.line_program = program;
376
377 // Populate the root DW_TAG_compile_unit DIE: a name, the text span, and (via
378 // gimli auto-wiring the attached line_program) DW_AT_stmt_list → .debug_line.
379 // Name the CU after the primary real source file (fallback `synth.wasm`).
380 {
381 let cu_name = files
382 .first()
383 .map(|(_, n)| n.clone())
384 .unwrap_or_else(|| "synth.wasm".to_string());
385 let name_id = dwarf.strings.add(cu_name);
386 let root = dwarf.unit.root();
387 let root_die = dwarf.unit.get_mut(root);
388 root_die.set(gimli::DW_AT_name, AttributeValue::StringRef(name_id));
389 root_die.set(gimli::DW_AT_low_pc, AttributeValue::Address(text_base));
390 root_die.set(gimli::DW_AT_high_pc, AttributeValue::Udata(cu_high_pc));
391 }
392
393 // #394 Tier-1: attach one DW_TAG_subprogram child DIE per function so a
394 // debugger backtrace shows the FUNCTION NAME, not a bare address.
395 // - DW_AT_name = the export/function name.
396 // - DW_AT_low_pc = the function's `.text` address, relocated against the
397 // SAME `__synth_text_base` symbol as the CU low_pc but with addend =
398 // the function's object-relative offset, so it shifts correctly when a
399 // linker places `.text` (each is an extra `.rel.debug_info` record).
400 // - DW_AT_high_pc = the offset (size) form `high_pc - low_pc` — a plain
401 // constant, so it needs no relocation (matches the CU's high_pc form).
402 // No parameters/locals/frame-base yet — that is Tier-2, gated on VCR-RA.
403 {
404 let root = dwarf.unit.root();
405 for sp in subprograms {
406 let name_id = dwarf.strings.add(sp.name.clone());
407 let die_id = dwarf.unit.add(root, gimli::DW_TAG_subprogram);
408 let die = dwarf.unit.get_mut(die_id);
409 die.set(gimli::DW_AT_name, AttributeValue::StringRef(name_id));
410 die.set(
411 gimli::DW_AT_low_pc,
412 AttributeValue::Address(Address::Symbol {
413 symbol: text_sym,
414 addend: sp.low_pc as i64,
415 }),
416 );
417 die.set(
418 gimli::DW_AT_high_pc,
419 AttributeValue::Udata(sp.high_pc.saturating_sub(sp.low_pc)),
420 );
421 }
422 }
423
424 let seed = RelocWriter {
425 inner: gimli::write::EndianVec::new(LittleEndian),
426 relocs: Vec::new(),
427 };
428 let mut sections = Sections::new(seed);
429 if dwarf.write(&mut sections).is_err() {
430 return Vec::new();
431 }
432
433 let mut out: Vec<EmittedDwarfSection> = Vec::new();
434 let _ = sections.for_each(|id, w: &RelocWriter| -> Result<(), ()> {
435 let bytes = w.inner.slice();
436 if !bytes.is_empty()
437 && let Some(name) = section_name(id)
438 {
439 let text_relocs = w
440 .relocs
441 .iter()
442 .map(|&(offset, _addend, size)| DwarfTextReloc {
443 offset: offset as u32,
444 size,
445 })
446 .collect();
447 out.push(EmittedDwarfSection {
448 name,
449 bytes: bytes.to_vec(),
450 text_relocs,
451 });
452 }
453 Ok(())
454 });
455 out
456}
457
458/// A relocation a `.debug_*` section needs against the `.text` symbol so a host
459/// linker fixes up the embedded `.text` address when `.text` is placed. REL
460/// form: the in-place bytes already hold the addend (always `0` for our
461/// text-base references), so only the site (`offset`) and `size` travel here.
462#[derive(Debug, Clone, Copy, PartialEq, Eq)]
463pub struct DwarfTextReloc {
464 /// Byte offset within the section where the relocated address word sits.
465 pub offset: u32,
466 /// Size of the relocated value (always 4 for DWARF32 addresses).
467 pub size: u8,
468}
469
470/// One emitted `.debug_*` section: its ELF name, bytes, and the `.text`-symbol
471/// relocations it needs (empty for address-free sections like `.debug_str`).
472#[derive(Debug, Clone)]
473pub struct EmittedDwarfSection {
474 /// `'static` ELF section name (e.g. `.debug_line`).
475 pub name: &'static str,
476 /// Section payload bytes.
477 pub bytes: Vec<u8>,
478 /// `.text`-symbol relocations within this section (REL, in-place addend 0).
479 pub text_relocs: Vec<DwarfTextReloc>,
480}
481
482/// A gimli `write::Writer` that delegates to an inner `EndianVec` but records
483/// every `Address::Symbol` write as a relocation. ONLY `write_address` is
484/// overridden — `write_offset` (gimli's internal section-to-section references,
485/// e.g. `.debug_info` → `.debug_str`/`.debug_abbrev` and `DW_AT_stmt_list` →
486/// `.debug_line`) keeps the default, so those stay CONCRETE intra-file offsets
487/// and need no section symbols. The relocations captured are the `.text`
488/// references: the line program's `DW_LNE_set_address`, the CU's `DW_AT_low_pc`,
489/// and one per `DW_TAG_subprogram` `DW_AT_low_pc` (#394). `Clone` so
490/// `Sections::new` can seed each section writer.
491#[derive(Clone)]
492struct RelocWriter {
493 inner: gimli::write::EndianVec<LittleEndian>,
494 /// (offset within section, addend, size) for each `Address::Symbol` write.
495 relocs: Vec<(usize, i64, u8)>,
496}
497
498impl gimli::write::Writer for RelocWriter {
499 type Endian = LittleEndian;
500
501 fn endian(&self) -> Self::Endian {
502 self.inner.endian()
503 }
504
505 fn len(&self) -> usize {
506 self.inner.len()
507 }
508
509 fn write(&mut self, bytes: &[u8]) -> gimli::write::Result<()> {
510 self.inner.write(bytes)
511 }
512
513 fn write_at(&mut self, offset: usize, bytes: &[u8]) -> gimli::write::Result<()> {
514 self.inner.write_at(offset, bytes)
515 }
516
517 fn write_address(
518 &mut self,
519 address: gimli::write::Address,
520 size: u8,
521 ) -> gimli::write::Result<()> {
522 use gimli::write::Address;
523 match address {
524 Address::Constant(val) => self.inner.write_udata(val, size),
525 Address::Symbol { symbol: _, addend } => {
526 // REL: record the site and write the addend in place (0 ⇒ the
527 // bytes match the old `Address::Constant(0)` exactly).
528 let offset = self.inner.len();
529 self.relocs.push((offset, addend, size));
530 self.inner.write_udata(addend as u64, size)
531 }
532 }
533 }
534}
535
536/// `'static` ELF section name for the `.debug_*` sections the emitter can
537/// produce. Returns `None` for any section id we do not wire (none are expected
538/// for this minimal unit, but the match keeps the names `'static`).
539fn section_name(id: SectionId) -> Option<&'static str> {
540 Some(match id {
541 SectionId::DebugInfo => ".debug_info",
542 SectionId::DebugAbbrev => ".debug_abbrev",
543 SectionId::DebugStr => ".debug_str",
544 SectionId::DebugLine => ".debug_line",
545 SectionId::DebugLineStr => ".debug_line_str",
546 SectionId::DebugRanges => ".debug_ranges",
547 SectionId::DebugRngLists => ".debug_rnglists",
548 SectionId::DebugStrOffsets => ".debug_str_offsets",
549 SectionId::DebugAddr => ".debug_addr",
550 _ => return None,
551 })
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn covering_row_lookup() {
560 // code_base 100; rows at code-rel 0→line10, 8→line11, 20→line12.
561 let rows = [
562 LineRow {
563 addr: 0,
564 line: 10,
565 file: 1,
566 },
567 LineRow {
568 addr: 8,
569 line: 11,
570 file: 1,
571 },
572 LineRow {
573 addr: 20,
574 line: 12,
575 file: 1,
576 },
577 ];
578 // ops at module 100 (→0), 104 (→4), 108 (→8), 130 (→30).
579 let got = op_offsets_to_source(&[100, 104, 108, 130], 100, &rows);
580 assert_eq!(got[0].map(|s| s.line), Some(10)); // addr 0 → row 0
581 assert_eq!(got[1].map(|s| s.line), Some(10)); // addr 4 → still row 0
582 assert_eq!(got[2].map(|s| s.line), Some(11)); // addr 8 → row 8
583 assert_eq!(got[3].map(|s| s.line), Some(12)); // addr 30 → row 20 (last ≤)
584 }
585
586 #[test]
587 fn op_before_first_row_is_none() {
588 let rows = [LineRow {
589 addr: 8,
590 line: 11,
591 file: 1,
592 }];
593 // op at module 100 → code-rel 0, before the first row (addr 8).
594 let got = op_offsets_to_source(&[100], 100, &rows);
595 assert_eq!(got[0], None);
596 }
597
598 #[test]
599 fn op_before_code_base_is_none() {
600 let rows = [LineRow {
601 addr: 0,
602 line: 1,
603 file: 1,
604 }];
605 let got = op_offsets_to_source(&[50], 100, &rows);
606 assert_eq!(got[0], None);
607 }
608}