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#[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 pub lines: Vec<(u64, Span)>,
69}
70
71#[derive(Default)]
79pub struct DebugFunctions(Vec<FuncDebugInfo>);
80
81impl DebugFunctions {
82 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 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 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 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 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 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
282fn 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: ®istry,
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(&[], 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}