Skip to main content

submilli_engine/codegen/
dwarf.rs

1use gimli::write::{
2    Address, AttributeValue, EndianVec, FileId, LineProgram, LineString, Sections, Unit,
3};
4use gimli::{Encoding, Format, LineEncoding, LittleEndian, constants};
5use std::borrow::Cow;
6
7use crate::compiler_error::{CompilerFailure, CompilerStage};
8use crate::source::{SourceError, package_frame_path};
9use crate::{LineIndex, Sources, Span};
10
11pub struct DebugSources<'a> {
12    pub file: crate::FileId,
13    pub filename: &'a str,
14    pub index: &'a LineIndex,
15    pub package: Option<PackageSources<'a>>,
16}
17
18/// A package's modules and its name, which qualifies their debug paths
19/// ([`package_frame_path`]).
20#[derive(Clone, Copy)]
21pub struct PackageSources<'a> {
22    pub sources: &'a Sources,
23    pub name: &'a str,
24}
25
26impl DebugSources<'_> {
27    pub fn location(&self, span: Span) -> Result<(Cow<'_, str>, u32, u32), CompilerFailure> {
28        self.checked_location(span)
29            .map_err(|error| error.into_compiler_failure(CompilerStage::Codegen))
30    }
31
32    fn checked_location(&self, span: Span) -> Result<(Cow<'_, str>, u32, u32), SourceError> {
33        Span::new(span.file, span.start, span.end)?;
34        if let Some(path) = span.file.reserved_path() {
35            return Ok((Cow::Borrowed(path), 0, 0));
36        }
37        if let Some(package) = self.package {
38            let source = package
39                .sources
40                .get(span.file)
41                .ok_or(SourceError::UnknownFile { file: span.file })?;
42            source.span_text(span)?;
43            let (line, col) = source.line_index().line_col(span.start)?;
44            let path = package_frame_path(package.name, source.path.as_str());
45            return Ok((Cow::Owned(path), line, col));
46        }
47        span.text(self.index.source(), self.file)?;
48        let (line, col) = self.index.line_col(span.start)?;
49        Ok((Cow::Borrowed(self.filename), line, col))
50    }
51}
52
53fn dwarf_failure(error: impl std::fmt::Display) -> CompilerFailure {
54    CompilerFailure::Internal {
55        stage: CompilerStage::Codegen,
56        span: None,
57        message: format!("invalid debug metadata: {error}"),
58    }
59}
60
61pub struct FuncDebugInfo {
62    pub name: String,
63    pub low_pc: u64,
64    pub body_len: u64,
65    pub decl_span: Span,
66    // `low_pc` and these addresses are Code-section-content-relative (buffer-relative
67    // while held by `DebugFunctions`); the builder rebases rows to `low_pc`.
68    pub lines: Vec<(u64, Span)>,
69}
70
71/// The source-backed functions written to a Code section, in order. A backtrace
72/// renders only frames whose function has a subprogram here; a body written
73/// with plain `CodeSection::function` (adapters, vtable helpers) has none.
74///
75/// Positions are recorded relative to the section's buffer because its leading
76/// function count is not encoded until the section is finished;
77/// [`Self::into_debug_info`] shifts them past it.
78#[derive(Default)]
79pub struct DebugFunctions(Vec<FuncDebugInfo>);
80
81impl DebugFunctions {
82    /// Writes `function` to `code` and records it under `name`, with control
83    /// characters escaped (see [`escape_control_chars`]). `lines` are
84    /// offsets into the body, as [`FunctionEmitter::build_with_lines`] returns them.
85    ///
86    /// Code before the first row (a prologue, or an implicit constructor's call
87    /// to its parent) is mapped to `decl_span`. Without that row the engine,
88    /// which looks lines up by address alone, would report the last row of the
89    /// function written before this one.
90    ///
91    /// [`FunctionEmitter::build_with_lines`]: super::function_emitter::FunctionEmitter::build_with_lines
92    pub fn write(
93        &mut self,
94        code: &mut wasm_encoder::CodeSection,
95        (function, mut lines): (wasm_encoder::Function, Vec<(u64, Span)>),
96        name: String,
97        decl_span: Span,
98    ) -> Result<(), CompilerFailure> {
99        let body_len = function.byte_len() as u64;
100        code.function(&function);
101        let low_pc = (code.byte_len() as u64)
102            .checked_sub(body_len)
103            .ok_or_else(|| dwarf_failure("a function body is longer than its code section"))?;
104        if lines.first().is_none_or(|(offset, _)| *offset > 0) {
105            lines.insert(0, (0, decl_span));
106        }
107        let lines = lines
108            .into_iter()
109            .map(|(offset, span)| {
110                low_pc
111                    .checked_add(offset)
112                    .map(|address| (address, span))
113                    .ok_or_else(|| dwarf_failure("line address overflows the code section"))
114            })
115            .collect::<Result<_, _>>()?;
116        self.0.push(FuncDebugInfo {
117            name: escape_control_chars(name),
118            low_pc,
119            body_len,
120            decl_span,
121            lines,
122        });
123        Ok(())
124    }
125
126    /// The recorded functions with addresses relative to the Code section's
127    /// content, whose first `count_len` bytes encode its function count.
128    pub fn into_debug_info(self, count_len: u64) -> Result<Vec<FuncDebugInfo>, CompilerFailure> {
129        let shift = |address: u64| {
130            address
131                .checked_add(count_len)
132                .ok_or_else(|| dwarf_failure("function address overflows the code section"))
133        };
134        self.0
135            .into_iter()
136            .map(|function| {
137                Ok(FuncDebugInfo {
138                    low_pc: shift(function.low_pc)?,
139                    lines: function
140                        .lines
141                        .into_iter()
142                        .map(|(address, span)| Ok((shift(address)?, span)))
143                        .collect::<Result<_, CompilerFailure>>()?,
144                    ..function
145                })
146            })
147            .collect()
148    }
149}
150
151pub fn build_dwarf(
152    funcs: &[FuncDebugInfo],
153    code_size: u64,
154    filename: &str,
155    sources: &DebugSources<'_>,
156) -> Result<Vec<(&'static str, Vec<u8>)>, CompilerFailure> {
157    validate_path(filename)?;
158    validate_addresses(funcs, code_size)?;
159    let unit_path = sources.package.map_or(Cow::Borrowed(filename), |package| {
160        Cow::Owned(package_frame_path(package.name, filename))
161    });
162    let encoding = Encoding {
163        // wasm32; must update with every Address if moving to wasm64.
164        address_size: 4,
165        format: Format::Dwarf32,
166        version: 4,
167    };
168
169    let mut line_program = LineProgram::new(
170        encoding,
171        LineEncoding::default(),
172        // Working directory left empty — the language has no file-system
173        // story yet, and the embedder threads in only the filename.
174        LineString::String(Vec::new()),
175        None,
176        LineString::String(unit_path.as_bytes().to_vec()),
177        None,
178    );
179    let mut files = std::collections::BTreeMap::<String, FileId>::new();
180    for function in funcs {
181        for span in
182            std::iter::once(function.decl_span).chain(function.lines.iter().map(|(_, span)| *span))
183        {
184            let (path, _, _) = sources.location(span)?;
185            validate_path(&path)?;
186            files.entry(path.to_string()).or_insert_with(|| {
187                line_program.add_file(
188                    LineString::String(path.as_bytes().to_vec()),
189                    line_program.default_directory(),
190                    None,
191                )
192            });
193        }
194    }
195
196    for f in funcs {
197        line_program.row().address_offset = 0;
198        line_program.set_address(Address::Constant(f.low_pc));
199        for (abs_addr, span) in &f.lines {
200            let (path, line, col) = sources.location(*span)?;
201            line_program.row().address_offset = abs_addr
202                .checked_sub(f.low_pc)
203                .filter(|offset| *offset <= f.body_len)
204                .ok_or_else(|| dwarf_failure("line address is outside its function"))?;
205            line_program.row().file = *files
206                .get(path.as_ref())
207                .ok_or_else(|| dwarf_failure("source file was not registered"))?;
208            line_program.row().line = u64::from(line);
209            line_program.row().column = u64::from(col);
210            line_program.row().is_statement = true;
211            line_program.generate_row();
212        }
213        line_program.end_sequence(f.body_len);
214    }
215
216    let mut dwarf = gimli::write::Dwarf::new();
217    let unit_id = dwarf.units.add(Unit::new(encoding, line_program));
218    let unit = dwarf.units.get_mut(unit_id);
219
220    let cu_root = unit.root();
221    let cu_name = dwarf.strings.add(unit_path.as_bytes());
222    let cu_dir = dwarf.strings.add("");
223    {
224        let cu = unit.get_mut(cu_root);
225        cu.set(constants::DW_AT_name, AttributeValue::StringRef(cu_name));
226        cu.set(constants::DW_AT_comp_dir, AttributeValue::StringRef(cu_dir));
227        // No DWARF code for TypeScript; C99 is the closest recognised substitute.
228        cu.set(
229            constants::DW_AT_language,
230            AttributeValue::Language(constants::DW_LANG_C99),
231        );
232        cu.set(
233            constants::DW_AT_low_pc,
234            AttributeValue::Address(Address::Constant(0)),
235        );
236        cu.set(constants::DW_AT_high_pc, AttributeValue::Udata(code_size));
237    }
238
239    for f in funcs {
240        let id = unit.add(cu_root, constants::DW_TAG_subprogram);
241        let name_id = dwarf.strings.add(f.name.as_str());
242        let entry = unit.get_mut(id);
243        entry.set(constants::DW_AT_name, AttributeValue::StringRef(name_id));
244        entry.set(
245            constants::DW_AT_low_pc,
246            AttributeValue::Address(Address::Constant(f.low_pc)),
247        );
248        // DWARF v4 allows `DW_AT_high_pc` to be a constant offset from
249        // `DW_AT_low_pc` rather than an absolute address — keeps the
250        // attribute width small (no relocation).
251        entry.set(constants::DW_AT_high_pc, AttributeValue::Udata(f.body_len));
252        let (path, line, _) = sources.location(f.decl_span)?;
253        let file = *files
254            .get(path.as_ref())
255            .ok_or_else(|| dwarf_failure("declaration file was not registered"))?;
256        entry.set(
257            constants::DW_AT_decl_file,
258            AttributeValue::FileIndex(Some(file)),
259        );
260        entry.set(
261            constants::DW_AT_decl_line,
262            AttributeValue::Udata(u64::from(line)),
263        );
264    }
265
266    let mut sections = Sections::new(EndianVec::new(LittleEndian));
267    dwarf.write(&mut sections).map_err(dwarf_failure)?;
268
269    let mut out: Vec<(&'static str, Vec<u8>)> = Vec::new();
270    sections
271        .for_each(|id, data| -> std::result::Result<(), gimli::write::Error> {
272            let bytes = data.slice();
273            if !bytes.is_empty() {
274                out.push((id.name(), bytes.to_vec()));
275            }
276            Ok(())
277        })
278        .map_err(dwarf_failure)?;
279    Ok(out)
280}
281
282/// A method's name is its key, which a string literal can fill with any code
283/// unit. DWARF strings end at a NUL, and a line break would split the one-line
284/// frame header, so control characters are written as Rust-style escapes.
285fn escape_control_chars(name: String) -> String {
286    if !name.chars().any(char::is_control) {
287        return name;
288    }
289    name.chars()
290        .map(|c| {
291            if c.is_control() {
292                c.escape_default().to_string()
293            } else {
294                c.to_string()
295            }
296        })
297        .collect()
298}
299
300fn validate_path(path: &str) -> Result<(), CompilerFailure> {
301    if path.is_empty() || path.contains('\0') {
302        return Err(dwarf_failure(
303            "source path is empty or contains a null byte",
304        ));
305    }
306    Ok(())
307}
308
309fn validate_addresses(funcs: &[FuncDebugInfo], code_size: u64) -> Result<(), CompilerFailure> {
310    if code_size > u64::from(u32::MAX) {
311        return Err(dwarf_failure(
312            "code section exceeds the wasm32 address range",
313        ));
314    }
315    for function in funcs {
316        if function.name.contains('\0') {
317            return Err(dwarf_failure("function name contains a null byte"));
318        }
319        let end = function
320            .low_pc
321            .checked_add(function.body_len)
322            .filter(|end| *end <= code_size)
323            .ok_or_else(|| dwarf_failure("function is outside the code section"))?;
324        let mut previous = function.low_pc;
325        for (address, _) in &function.lines {
326            if *address < previous || *address > end {
327                return Err(dwarf_failure(
328                    "line addresses must be ordered within their function",
329                ));
330            }
331            previous = *address;
332        }
333    }
334    Ok(())
335}
336
337#[cfg(test)]
338mod tests {
339    use super::{DebugSources, FuncDebugInfo};
340    fn build_dwarf(
341        funcs: &[FuncDebugInfo],
342        size: u64,
343        filename: &str,
344        index: &crate::LineIndex,
345    ) -> Vec<(&'static str, Vec<u8>)> {
346        super::build_dwarf(
347            funcs,
348            size,
349            filename,
350            &DebugSources {
351                file: crate::FileId(0),
352                filename,
353                index,
354                package: None,
355            },
356        )
357        .unwrap()
358    }
359    use crate::LineIndex;
360
361    #[test]
362    fn rejects_invalid_paths_and_address_sequences_before_writing() {
363        let index = LineIndex::new("x").unwrap();
364        for filename in ["", "bad\0.ts"] {
365            let sources = DebugSources {
366                file: crate::FileId(0),
367                filename,
368                index: &index,
369                package: None,
370            };
371            assert!(super::build_dwarf(&[], 0, filename, &sources).is_err());
372        }
373        let sources = DebugSources {
374            file: crate::FileId(0),
375            filename: "test.ts",
376            index: &index,
377            package: None,
378        };
379        for addresses in [vec![3], vec![9], vec![6, 5]] {
380            let function = FuncDebugInfo {
381                name: "main".into(),
382                low_pc: 4,
383                body_len: 4,
384                decl_span: crate::Span::at(crate::FileId(0)),
385                lines: addresses
386                    .into_iter()
387                    .map(|address| (address, crate::Span::at(crate::FileId(0))))
388                    .collect(),
389            };
390            assert!(super::build_dwarf(&[function], 8, "test.ts", &sources).is_err());
391        }
392        assert!(super::build_dwarf(&[], u64::MAX, "test.ts", &sources).is_err());
393        let function = FuncDebugInfo {
394            name: "bad\0name".into(),
395            low_pc: 0,
396            body_len: 1,
397            decl_span: crate::Span::at(crate::FileId(0)),
398            lines: vec![],
399        };
400        assert!(super::build_dwarf(&[function], 1, "test.ts", &sources).is_err());
401    }
402
403    #[test]
404    fn package_debug_rows_and_declarations_use_their_own_source() {
405        let mut registry = crate::Sources::new();
406        let root = registry.add("index.ts", "root").unwrap();
407        let dependency = registry.add("util.ts", "// é\n\nfunction helper").unwrap();
408        let source = registry.get(root).unwrap();
409        let sources = DebugSources {
410            file: root,
411            filename: "index.ts",
412            index: source.line_index(),
413            package: Some(super::PackageSources {
414                sources: &registry,
415                name: "@acme/util",
416            }),
417        };
418        let span = crate::Span::new(dependency, 7, 15).unwrap();
419        let sections = super::build_dwarf(
420            &[FuncDebugInfo {
421                name: "helper".into(),
422                low_pc: 4,
423                body_len: 2,
424                decl_span: span,
425                lines: vec![(4, span)],
426            }],
427            6,
428            "index.ts",
429            &sources,
430        )
431        .unwrap();
432        let dwarf = gimli::Dwarf::load(|id| -> Result<_, gimli::Error> {
433            let bytes = sections
434                .iter()
435                .find(|(name, _)| *name == id.name())
436                .map_or(&[][..], |(_, bytes)| bytes.as_slice());
437            Ok(gimli::EndianSlice::new(bytes, gimli::LittleEndian))
438        })
439        .unwrap();
440        let unit = dwarf.unit(dwarf.units().next().unwrap().unwrap()).unwrap();
441        let program = unit.line_program.clone().unwrap();
442        let mut rows = program.rows();
443        let (header, row) = rows.next_row().unwrap().unwrap();
444        assert_eq!(row.line().unwrap().get(), 3);
445        assert_eq!(
446            dwarf
447                .attr_string(&unit, row.file(header).unwrap().path_name())
448                .unwrap()
449                .slice(),
450            b"@acme/util/util.ts"
451        );
452        let mut entries = unit.entries();
453        entries.next_dfs().unwrap().unwrap();
454        let entry = entries.next_dfs().unwrap().unwrap();
455        assert_eq!(
456            entry
457                .attr_value(gimli::DW_AT_decl_line)
458                .unwrap()
459                .udata_value(),
460            Some(3)
461        );
462        let gimli::AttributeValue::FileIndex(file) =
463            entry.attr_value(gimli::DW_AT_decl_file).unwrap()
464        else {
465            panic!("file index");
466        };
467        let declaration = unit
468            .line_program
469            .as_ref()
470            .unwrap()
471            .header()
472            .file(file)
473            .unwrap();
474        assert_eq!(
475            dwarf
476                .attr_string(&unit, declaration.path_name())
477                .unwrap()
478                .slice(),
479            b"@acme/util/util.ts"
480        );
481    }
482
483    #[test]
484    fn empty_function_list_still_emits_compile_unit() {
485        let li = LineIndex::new("").unwrap();
486        let sections = build_dwarf(&[], /*code_size=*/ 0, "script.subm", &li);
487        let names: Vec<&str> = sections.iter().map(|(n, _)| *n).collect();
488        assert!(names.contains(&".debug_info"), "got {names:?}");
489        assert!(names.contains(&".debug_abbrev"), "got {names:?}");
490        assert!(names.contains(&".debug_str"), "got {names:?}");
491    }
492
493    #[test]
494    fn single_function_strings_present() {
495        let li = LineIndex::new("function main(): void { }").unwrap();
496        let sections = build_dwarf(
497            &[FuncDebugInfo {
498                name: "main".to_string(),
499                low_pc: 4,
500                body_len: 2,
501                decl_span: crate::Span::at(crate::FileId(0)),
502                lines: Vec::new(),
503            }],
504            10,
505            "script.subm",
506            &li,
507        );
508        let strs = sections
509            .iter()
510            .find(|(n, _)| *n == ".debug_str")
511            .expect(".debug_str");
512        assert!(
513            strs.1.windows(4).any(|w| w == b"main"),
514            "expected `main` in .debug_str, got {:?}",
515            strs.1,
516        );
517    }
518
519    #[test]
520    fn line_program_emitted_when_lines_present() {
521        let src = "function main(): void { }";
522        let li = LineIndex::new(src).unwrap();
523        let sections = build_dwarf(
524            &[FuncDebugInfo {
525                name: "main".to_string(),
526                low_pc: 4,
527                body_len: 2,
528                decl_span: crate::Span::at(crate::FileId(0)),
529                lines: vec![(4, crate::Span::new(crate::FileId(0), 0, 8).unwrap())],
530            }],
531            6,
532            "script.subm",
533            &li,
534        );
535        assert!(
536            sections.iter().any(|(n, _)| *n == ".debug_line"),
537            ".debug_line should be emitted when lines are present",
538        );
539    }
540}