From 8ac9d9d6a128272604b88cce7dc3775d73b50591 Mon Sep 17 00:00:00 2001 From: Luca Bolognese Date: Wed, 5 Aug 2026 10:24:27 +0200 Subject: [PATCH] Add debug line tables across backends Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 01c2b714-3f71-44ed-bd75-5ed992b8e4a1 --- Cargo.lock | 1 + Cargo.toml | 2 + README.md | 10 + docs/guide.md | 7 + docs/spec/oscan-spec.md | 1 + docs/technical-details.md | 25 ++ src/backend/cranelift_debug.rs | 397 +++++++++++++++++ src/backend/ctx.rs | 20 +- src/backend/func.rs | 39 +- src/backend/link/inprocess.rs | 51 ++- src/backend/link/mod.rs | 53 ++- src/backend/lir.rs | 15 + src/backend/lir_cranelift.rs | 249 ++++++++++- src/backend/llvm/emit.rs | 771 ++++++++++++++++++++++++++++++++- src/backend/llvm/mod.rs | 6 +- src/backend/mod.rs | 8 +- src/backend/target.rs | 28 +- src/codegen.rs | 68 ++- src/debuginfo.rs | 186 ++++++++ src/ir.rs | 17 + src/main.rs | 243 +++++++++-- tests/cli_help.rs | 100 +++++ 22 files changed, 2209 insertions(+), 88 deletions(-) create mode 100644 src/backend/cranelift_debug.rs create mode 100644 src/debuginfo.rs diff --git a/Cargo.lock b/Cargo.lock index 34b9a73..83f9a99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -429,6 +429,7 @@ dependencies = [ "cranelift-frontend", "cranelift-module", "cranelift-object", + "gimli", "object", "serde_json", "sha2", diff --git a/Cargo.toml b/Cargo.toml index be1be90..60bb508 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ backend-cranelift = [ "dep:cranelift-frontend", "dep:cranelift-module", "dep:cranelift-object", + "dep:gimli", ] backend-llvm = [] inprocess-lld = [] @@ -38,6 +39,7 @@ cranelift-codegen = { version = "0.133.1", features = [ cranelift-frontend = { version = "0.133.1", optional = true } cranelift-module = { version = "0.133.1", optional = true } cranelift-object = { version = "0.133.1", optional = true } +gimli = { version = "0.33.0", features = ["write"], optional = true } object = "0.39.1" serde_json = "1.0" sha2 = "0.10" diff --git a/README.md b/README.md index 2de31b4..21927e3 100644 --- a/README.md +++ b/README.md @@ -184,11 +184,21 @@ small `wc`-style command-line program step by step. | `oscan app.osc --backend llvm` | Select LLVM explicitly | | `oscan app.osc --backend cranelift` | Select Cranelift explicitly | | `oscan app.osc --backend c` | Select the C backend explicitly | +| `oscan app.osc --debuginfo line-tables` | Keep Oscan source locations for debugging | | `oscan --help` | Show every option | A release package contains one backend. If you request another one, Oscan tells you which package to install. +Debug information is opt-in: `--debuginfo none` is the default, while +`--debuginfo line-tables` enables source breakpoints, stepping, stack +symbolization, and imported-file locations without changing the selected +optimization level. Oscan does not generate backend-independent local-variable +or type descriptions at this level, although a C toolchain may include +additional records. See +[compiler technical details](docs/technical-details.md#debug-information) for +backend and debugger-format notes. + ## Examples Run any example with `oscan --run`. diff --git a/docs/guide.md b/docs/guide.md index 791debe..a61ff2f 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -1048,6 +1048,13 @@ C is selected implicitly by `--emit-c`, `-o file.c`, and `--target riscv64|wasi`. LLVM IR output selects LLVM. For compatibility, an explicit `--native-target` without `--backend` selects Cranelift. +All three backends accept `--debuginfo line-tables`. This keeps Oscan +source-file and line mappings (including imported files), disables final +stripping, and preserves stack-walking support without changing the backend's +optimization level. The default is `--debuginfo none`. Line-table mode does not +ask Oscan's native emitters to describe local variables or source types; a C +toolchain may include additional records. + LLVM provider lookup order: 1. `OSCAN_LLVM_LIB` — absolute shared-library path diff --git a/docs/spec/oscan-spec.md b/docs/spec/oscan-spec.md index e75a596..021e88c 100644 --- a/docs/spec/oscan-spec.md +++ b/docs/spec/oscan-spec.md @@ -2045,6 +2045,7 @@ oscan [OPTIONS] | `--emit-llvm-ir` | Emit textual LLVM IR to stdout (alias: `--emit-llvm`). Requires the LLVM backend and stops before object emission and linking. | | `--libc` | Use hosted libc mode instead of freestanding mode. Enables access to standard C library functions. | | `--backend ` | Explicitly select `llvm` (direct IR plus in-process LLVM object code), `cranelift` (direct Cranelift AOT object code), or `c` (portability/reference/source backend), limited to the backends the build contains. `native` is a deprecated alias for `cranelift`. Without this option, a released single-backend package uses its own backend; a multi-backend build prefers LLVM on supported Windows/Linux hosts when a compatible provider is available, then Cranelift, then C. | +| `--debuginfo ` | Debug information level: `none` (default) or `line-tables`. Line tables preserve Oscan file/line mappings for breakpoints, stepping, stack traces, and symbolization without changing optimization. Oscan does not generate or guarantee local-variable and type descriptions at this level; a C toolchain may include additional records. | | `--native-target ` | Object target for `--backend llvm` and `--backend cranelift`. Supported: `host` (default), `linux-x86_64`, `windows-x86_64`, `linux-aarch64`, `linux-riscv64`. Provider target capabilities are checked before LLVM emission. | | `--target ` | Cross-compile for target architecture (C backend only). Supported: `riscv64`, `wasi`. Without this flag, compiles for the host platform. | | `--extra-c ` | Extra C source file to compile and link (repeatable). Requires C compiler. | diff --git a/docs/technical-details.md b/docs/technical-details.md index 4770de2..2af59fb 100644 --- a/docs/technical-details.md +++ b/docs/technical-details.md @@ -47,6 +47,31 @@ Some output requests imply a backend: - `--libc`, `--extra-c`, and `--extra-cflags` do not by themselves select C, though they may require a C toolchain during the final build. +## Debug information + +`--debuginfo none` is the release-compatible default. Opting into +`--debuginfo line-tables` preserves Oscan source locations for source +breakpoints, stepping, stack symbolization, and imported-file mappings. It does +not change optimization. Oscan's native emitters do not describe local +variables or source types at this level; a C toolchain may include additional +records. + +| Backend | Line-table implementation | +|---|---| +| C | The generated translation unit uses `#line` directives that name the original `.osc` files. Clang uses `-gline-tables-only`, GCC uses `-g1`, and MSVC uses embedded `/Z7` records plus linker debug output (which can contain more than line tables). | +| LLVM | Oscan emits `DICompileUnit`, `DIFile`, `DISubprogram`, and `DILocation` metadata directly in its LLVM IR with `LineTablesOnly` emission. | +| Cranelift | Oscan interns Cranelift source locations, converts final machine-code ranges into DWARF line rows, and writes the required DWARF sections into the object. | + +Debug final links retain symbols and line sections instead of passing strip +flags. Debug-mode C and native code also preserve frame pointers; unwind +metadata is retained or generated where the backend supports it. + +On Windows, the C backend can produce a PDB through an MSVC-compatible +toolchain. The LLVM and Cranelift object backends currently emit DWARF in COFF, +not CodeView/PDB, so they require a debugger that understands DWARF-in-COFF. +Native CodeView/PDB generation, optimized locals, and full type information are +separate future tiers. + ## Release packages Release contract schema 2 publishes one archive per platform/backend pair. It diff --git a/src/backend/cranelift_debug.rs b/src/backend/cranelift_debug.rs new file mode 100644 index 0000000..53f8d03 --- /dev/null +++ b/src/backend/cranelift_debug.rs @@ -0,0 +1,397 @@ +use std::collections::HashMap; + +use cranelift_codegen::ir::SourceLoc; +use cranelift_codegen::Context; +use cranelift_module::FuncId; +use cranelift_object::ObjectProduct; +use gimli::write::{ + Address, AttributeValue, DwarfUnit, EndianVec, LineProgram, LineString, RelocateWriter, + Relocation, RelocationTarget, Sections, +}; +use gimli::{constants, Encoding, Format, LittleEndian}; +use object::write::{Relocation as ObjectRelocation, SymbolId}; +use object::{BinaryFormat, RelocationEncoding, RelocationFlags, RelocationKind, SectionKind}; + +use crate::debuginfo::{SourceLocation, SourceMap}; + +#[derive(Clone)] +struct DebugSection { + writer: EndianVec, + relocations: Vec, +} + +impl DebugSection { + fn new() -> Self { + DebugSection { + writer: EndianVec::new(LittleEndian), + relocations: Vec::new(), + } + } +} + +impl RelocateWriter for DebugSection { + type Writer = EndianVec; + + fn writer(&self) -> &Self::Writer { + &self.writer + } + + fn writer_mut(&mut self) -> &mut Self::Writer { + &mut self.writer + } + + fn relocate(&mut self, relocation: Relocation) { + self.relocations.push(relocation); + } +} + +#[derive(Clone)] +struct FunctionSource { + source_name: String, + linkage_name: String, + declaration: SourceLocation, +} + +struct LineRow { + offset: u64, + location: SourceLocation, +} + +struct FunctionDebug { + func_id: FuncId, + source: FunctionSource, + size: u64, + rows: Vec, +} + +pub(super) struct CraneliftDebug { + source_map: SourceMap, + locations: Vec, + location_ids: HashMap, + function_sources: HashMap, + functions: Vec, +} + +impl CraneliftDebug { + pub(super) fn new(source_map: &SourceMap) -> Self { + CraneliftDebug { + source_map: source_map.clone(), + locations: Vec::new(), + location_ids: HashMap::new(), + function_sources: HashMap::new(), + functions: Vec::new(), + } + } + + pub(super) fn source_loc(&mut self, location: Option) -> SourceLoc { + let Some(location) = location else { + return SourceLoc::default(); + }; + if let Some(id) = self.location_ids.get(&location) { + return SourceLoc::new(*id); + } + let id = u32::try_from(self.locations.len()) + .expect("internal error: Cranelift source-location table exceeded u32::MAX entries"); + assert_ne!( + id, + u32::MAX, + "internal error: Cranelift source-location table exhausted its non-sentinel IDs" + ); + self.locations.push(location); + self.location_ids.insert(location, id); + SourceLoc::new(id) + } + + pub(super) fn set_function_source( + &mut self, + func_id: FuncId, + source_name: &str, + linkage_name: &str, + declaration: SourceLocation, + ) { + self.function_sources.insert( + func_id, + FunctionSource { + source_name: source_name.to_string(), + linkage_name: linkage_name.to_string(), + declaration, + }, + ); + } + + pub(super) fn capture_function( + &mut self, + func_id: FuncId, + context: &Context, + ) -> Result<(), String> { + let Some(source) = self.function_sources.remove(&func_id) else { + return Ok(()); + }; + let code = context + .compiled_code() + .ok_or_else(|| "Cranelift did not retain compiled function code".to_string())?; + let size = u64::from(code.buffer.total_size()); + if size == 0 { + return Ok(()); + } + + let mut rows = Vec::new(); + for mapping in code.buffer.get_srclocs_sorted() { + if mapping.loc.is_default() { + continue; + } + let Some(location) = self.locations.get(mapping.loc.bits() as usize).copied() else { + return Err(format!( + "Cranelift returned unknown source location {}", + mapping.loc.bits() + )); + }; + let offset = u64::from(mapping.start); + if offset >= size { + continue; + } + if rows + .last() + .is_some_and(|row: &LineRow| row.location == location) + { + continue; + } + rows.push(LineRow { offset, location }); + } + if rows.first().map_or(true, |row| row.offset != 0) { + rows.insert( + 0, + LineRow { + offset: 0, + location: source.declaration, + }, + ); + } + + self.functions.push(FunctionDebug { + func_id, + source, + size, + rows, + }); + Ok(()) + } + + pub(super) fn write(self, product: &mut ObjectProduct) -> Result<(), String> { + if self.functions.is_empty() { + return Ok(()); + } + + let encoding = Encoding { + format: Format::Dwarf32, + version: 4, + address_size: 8, + }; + let primary_path = debug_path( + self.source_map + .primary_path() + .ok_or_else(|| "source map has no primary source file".to_string())?, + ); + let mut line_program = LineProgram::new( + encoding, + gimli::LineEncoding::default(), + LineString::String(b".".to_vec()), + None, + LineString::String(primary_path.clone()), + None, + ); + let mut files = HashMap::new(); + for (file, path) in self.source_map.files() { + let file_id = line_program.add_file( + LineString::String(debug_path(path)), + line_program.default_directory(), + None, + ); + files.insert(file, file_id); + } + + let function_symbols: Vec = self + .functions + .iter() + .map(|function| product.function_symbol(function.func_id)) + .collect(); + for (symbol, function) in self.functions.iter().enumerate() { + line_program.begin_sequence(Some(Address::Symbol { symbol, addend: 0 })); + for source_row in &function.rows { + let row = line_program.row(); + row.address_offset = source_row.offset; + row.file = files[&source_row.location.file]; + row.line = u64::from(source_row.location.line); + row.column = u64::from(source_row.location.column); + row.is_statement = true; + line_program.generate_row(); + } + line_program.end_sequence(function.size); + } + + let mut dwarf = DwarfUnit::new(encoding); + dwarf.unit.line_program = line_program; + add_compile_unit_attributes(&mut dwarf, &primary_path); + for (symbol, function) in self.functions.iter().enumerate() { + let entry = dwarf + .unit + .add(dwarf.unit.root(), constants::DW_TAG_subprogram); + let name = dwarf + .strings + .add(function.source.source_name.as_bytes().to_vec()); + let linkage_name = dwarf + .strings + .add(function.source.linkage_name.as_bytes().to_vec()); + let die = dwarf.unit.get_mut(entry); + die.set(constants::DW_AT_name, AttributeValue::StringRef(name)); + die.set( + constants::DW_AT_linkage_name, + AttributeValue::StringRef(linkage_name), + ); + die.set( + constants::DW_AT_low_pc, + AttributeValue::Address(Address::Symbol { symbol, addend: 0 }), + ); + die.set( + constants::DW_AT_high_pc, + AttributeValue::Udata(function.size), + ); + die.set( + constants::DW_AT_decl_file, + AttributeValue::FileIndex(Some(files[&function.source.declaration.file])), + ); + die.set( + constants::DW_AT_decl_line, + AttributeValue::Udata(u64::from(function.source.declaration.line)), + ); + die.set( + constants::DW_AT_decl_column, + AttributeValue::Udata(u64::from(function.source.declaration.column)), + ); + die.set(constants::DW_AT_external, AttributeValue::Flag(true)); + } + + let mut sections = Sections::new(DebugSection::new()); + dwarf + .write(&mut sections) + .map_err(|error| format!("failed to encode Cranelift DWARF: {error}"))?; + append_sections(product, §ions, &function_symbols) + } +} + +fn add_compile_unit_attributes(dwarf: &mut DwarfUnit, primary_path: &[u8]) { + let producer = dwarf.strings.add(b"Oscan compiler".to_vec()); + let name = dwarf.strings.add(primary_path.to_vec()); + let comp_dir = dwarf.strings.add(b".".to_vec()); + let root = dwarf.unit.root(); + let entry = dwarf.unit.get_mut(root); + entry.set( + constants::DW_AT_producer, + AttributeValue::StringRef(producer), + ); + entry.set( + constants::DW_AT_language, + AttributeValue::Language(constants::DW_LANG_lo_user), + ); + entry.set(constants::DW_AT_name, AttributeValue::StringRef(name)); + entry.set( + constants::DW_AT_comp_dir, + AttributeValue::StringRef(comp_dir), + ); + entry.set(constants::DW_AT_stmt_list, AttributeValue::LineProgramRef); +} + +fn append_sections( + product: &mut ObjectProduct, + sections: &Sections, + function_symbols: &[SymbolId], +) -> Result<(), String> { + let mut object_sections = Vec::new(); + sections + .for_each(|id, section| { + if !section.writer.slice().is_empty() { + let object_id = product.object.add_section( + Vec::new(), + id.name().as_bytes().to_vec(), + SectionKind::Debug, + ); + product + .object + .set_section_data(object_id, section.writer.slice().to_vec(), 1); + object_sections.push((id, object_id)); + } + Ok::<(), String>(()) + }) + .map_err(|error| error.to_string())?; + + sections + .for_each(|id, section| { + let Some((_, object_id)) = object_sections + .iter() + .find(|(section_id, _)| *section_id == id) + .copied() + else { + return Ok(()); + }; + for relocation in §ion.relocations { + let (symbol, kind) = match relocation.target { + RelocationTarget::Symbol(symbol) => { + let Some(symbol) = function_symbols.get(symbol).copied() else { + return Err(format!( + "DWARF relocation referenced unknown function symbol {symbol}" + )); + }; + (symbol, RelocationKind::Absolute) + } + RelocationTarget::Section(target) => { + let Some((_, target_id)) = object_sections + .iter() + .find(|(section_id, _)| *section_id == target) + .copied() + else { + return Err(format!( + "DWARF relocation referenced missing section {}", + target.name() + )); + }; + let symbol = product.object.section_symbol(target_id); + let kind = if product.object.format() == BinaryFormat::Coff { + RelocationKind::SectionOffset + } else { + RelocationKind::Absolute + }; + (symbol, kind) + } + }; + product + .object + .add_relocation( + object_id, + ObjectRelocation { + offset: relocation.offset as u64, + symbol, + addend: relocation.addend, + flags: RelocationFlags::Generic { + kind, + encoding: RelocationEncoding::Generic, + size: relocation.size * 8, + }, + }, + ) + .map_err(|error| { + format!("failed to add relocation to {}: {error}", id.name()) + })?; + } + Ok(()) + }) + .map_err(|error| error.to_string()) +} + +fn debug_path(path: &std::path::Path) -> Vec { + let path = path.to_string_lossy(); + if cfg!(windows) { + path.replace('\\', "/").into_bytes() + } else { + path.into_owned().into_bytes() + } +} diff --git a/src/backend/ctx.rs b/src/backend/ctx.rs index 19b3b17..31f82e5 100644 --- a/src/backend/ctx.rs +++ b/src/backend/ctx.rs @@ -10,7 +10,9 @@ use std::collections::HashMap; +use crate::debuginfo::{DebugInfo, SourceLocation, SourceMap}; use crate::ir; +use crate::token::Span; use super::extern_shim::NativeExternShim; use super::lir::LFunc; @@ -25,6 +27,8 @@ pub enum ExternDeclKind { pub struct BackendContext<'a> { pub program: &'a ir::Program, pub runtime_mode: RuntimeMode, + debug_info: DebugInfo, + source_map: &'a SourceMap, /// Oscan `fn`/`fn!` name -> declared function (the IR's `main` is /// declared under the C-backend-compatible symbol `oscan_main`). pub functions: HashMap, @@ -38,16 +42,30 @@ pub struct BackendContext<'a> { } impl<'a> BackendContext<'a> { - pub fn new(program: &'a ir::Program, runtime_mode: RuntimeMode) -> Self { + pub fn new( + program: &'a ir::Program, + runtime_mode: RuntimeMode, + debug_info: DebugInfo, + source_map: &'a SourceMap, + ) -> Self { BackendContext { program, runtime_mode, + debug_info, + source_map, functions: HashMap::new(), externs: HashMap::new(), extern_shims: Vec::new(), } } + pub fn source_location(&self, span: Span) -> Option { + self.debug_info + .is_enabled() + .then(|| self.source_map.location(span)) + .flatten() + } + /// The mangled symbol name for a user function definition (`main` /// becomes `oscan_main`, matching `src/codegen.rs`'s C backend so all /// backends' object/asm dumps stay easy to cross-reference). diff --git a/src/backend/func.rs b/src/backend/func.rs index 7c25c72..4af38b2 100644 --- a/src/backend/func.rs +++ b/src/backend/func.rs @@ -58,6 +58,7 @@ use std::collections::HashMap; use crate::ast::{BinOp, UnaryOp}; use crate::collection::{CollectionIntrinsic, CollectionOp}; +use crate::debuginfo::SourceLocation; use crate::error::CompileError; use crate::ir as oir; use crate::token::Span; @@ -123,6 +124,7 @@ pub struct FuncTranslator<'a, 'b> { defers: Vec<&'a oir::Expr>, fn_return_ty: BcType, arena_value: LValue, + current_source: Option, /// Whether the current block already ends in a terminator /// (`jump`/`brif`/`return`). Tracked ourselves rather than via /// [`LirBuilder::is_unreachable`] — that method answers a @@ -251,6 +253,9 @@ fn translate_function( ) -> CResult<()> { let sig = oscan_fn_signature(ctx.program, &f.params, &f.return_type); let symbol = BackendContext::user_fn_symbol(&f.name); + if let Some(location) = ctx.source_location(f.span) { + lir.set_function_source(func_id, &f.name, &symbol, location); + } lir.define_function(func_id, &sig, &mut |b, params| { let arena_value = params[0]; @@ -263,6 +268,7 @@ fn translate_function( defers: Vec::new(), fn_return_ty: f.return_type.clone(), arena_value, + current_source: None, terminated: false, }; @@ -302,6 +308,14 @@ fn translate_function( let body_val = translator.lower_function_body(&f.body)?; if !translator.b.is_unreachable() { + let return_span = f + .body + .tail_expr + .as_deref() + .map(oir::Expr::span) + .unwrap_or(f.span); + let return_location = translator.ctx.source_location(return_span); + translator.set_source_location(return_location); // An implicit (bare tail-expression) `return` must copy an // inline-aggregate result exactly like the explicit // `return expr;` case does (see `Stmt::Return` below) — @@ -328,6 +342,11 @@ fn translate_function( } impl<'a, 'b> FuncTranslator<'a, 'b> { + fn set_source_location(&mut self, location: Option) { + self.current_source = location; + self.b.set_source_location(location); + } + fn fresh_var(&mut self, ty: Option) -> LVar { // Every variable needs *some* declared type even when it will // never be def_var'd/use_var'd (Unit bindings): use I8 as an @@ -407,7 +426,10 @@ impl<'a, 'b> FuncTranslator<'a, 'b> { } fn lower_stmt(&mut self, stmt: &'a oir::Stmt) -> CResult<()> { - match stmt { + let saved_source = self.current_source; + let location = self.ctx.source_location(stmt.span()); + self.set_source_location(location); + let result = (|| match stmt { oir::Stmt::Let(ls) => { let raw = self.lower_expr(&ls.value)?; let value = self.bind_value(&ls.ty, raw, &ls.value)?; @@ -497,15 +519,19 @@ impl<'a, 'b> FuncTranslator<'a, 'b> { self.terminated = true; Ok(()) } - } + })(); + self.set_source_location(saved_source); + result } /// Emit every deferred expression (LIFO) and a `return`, exactly like /// `src/codegen.rs`'s `emit_deferred_before_return`. fn emit_return(&mut self, value: Option) -> CResult<()> { + let return_source = self.current_source; for expr in self.defers.clone().into_iter().rev() { self.lower_expr(expr)?; } + self.set_source_location(return_source); match value { Some(v) => { self.b.ret(Some(v)); @@ -750,6 +776,15 @@ impl<'a, 'b> FuncTranslator<'a, 'b> { /// Core scalar/control-flow/call expression lowering. impl<'a, 'b> FuncTranslator<'a, 'b> { fn lower_expr(&mut self, expr: &'a oir::Expr) -> CResult> { + let saved_source = self.current_source; + let location = self.ctx.source_location(expr.span()); + self.set_source_location(location); + let result = self.lower_expr_inner(expr); + self.set_source_location(saved_source); + result + } + + fn lower_expr_inner(&mut self, expr: &'a oir::Expr) -> CResult> { match expr { oir::Expr::IntLit(v, _) => Ok(Some(self.b.iconst(LType::I32, *v))), oir::Expr::FloatLit(v, _) => Ok(Some(self.b.f64const(*v))), diff --git a/src/backend/link/inprocess.rs b/src/backend/link/inprocess.rs index b06b55d..74199e0 100644 --- a/src/backend/link/inprocess.rs +++ b/src/backend/link/inprocess.rs @@ -3,6 +3,8 @@ use std::ffi::{c_char, CString}; use std::fs; use std::path::{Path, PathBuf}; +use crate::debuginfo::DebugInfo; + use super::capability::freestanding_profile_from_bytes; #[cfg(test)] use super::capability::FreestandingProfile; @@ -153,18 +155,7 @@ pub(super) fn link_executable( } remove_stale_output(&output)?; - let mut args = vec![ - "ld.lld".to_string(), - "-s".to_string(), - "-m".to_string(), - "i386pep".to_string(), - "-Bdynamic".to_string(), - "--gc-sections".to_string(), - "--build-id=none".to_string(), - "-o".to_string(), - output.to_string_lossy().into_owned(), - ]; - args.extend(positional_names); + let args = build_lld_args(&output, positional_names, options.debug_info); invoke_lld(&args, &inputs).inspect_err(|_| { let _ = fs::remove_file(&output); })?; @@ -185,6 +176,28 @@ pub(super) fn link_executable( Ok(()) } +fn build_lld_args( + output: &Path, + positional_names: Vec, + debug_info: DebugInfo, +) -> Vec { + let mut args = vec!["ld.lld".to_string()]; + if !debug_info.is_enabled() { + args.push("-s".to_string()); + } + args.extend([ + "-m".to_string(), + "i386pep".to_string(), + "-Bdynamic".to_string(), + "--gc-sections".to_string(), + "--build-id=none".to_string(), + "-o".to_string(), + output.to_string_lossy().into_owned(), + ]); + args.extend(positional_names); + args +} + fn read_strict_input(path: &Path) -> Result, String> { let bytes = fs::read(path).map_err(|error| format!("failed to read '{}': {error}", path.display()))?; @@ -320,6 +333,20 @@ mod tests { assert!(error.contains("thin archive"), "{error}"); } + #[test] + fn lld_only_strips_non_debug_outputs() { + let output = Path::new("program.exe"); + let positional = vec!["program.o".to_string(), "runtime.a".to_string()]; + let release_args = build_lld_args(output, positional.clone(), DebugInfo::None); + let debug_args = build_lld_args(output, positional, DebugInfo::LineTables); + + assert!(release_args.iter().any(|argument| argument == "-s")); + assert!(!debug_args.iter().any(|argument| argument == "-s")); + assert!(debug_args + .iter() + .any(|argument| argument == "--gc-sections")); + } + #[test] fn all_embedded_assets_have_nonempty_digests() { for asset in assets::ASSETS { diff --git a/src/backend/link/mod.rs b/src/backend/link/mod.rs index 5c09086..f65e5a6 100644 --- a/src/backend/link/mod.rs +++ b/src/backend/link/mod.rs @@ -190,6 +190,7 @@ use super::RuntimeMode; /// Inputs that affect final native linking but not Cranelift object emission. pub struct NativeLinkOptions<'a> { pub runtime_mode: RuntimeMode, + pub debug_info: crate::debuginfo::DebugInfo, pub show_warnings: bool, pub allow_elevated_native_link: bool, pub extra_c_files: &'a [String], @@ -630,7 +631,7 @@ fn build_mingw_plan( .collect(), entry: None, gc_sections: true, - strip: true, + strip: !options.debug_info.is_enabled(), build_id_none: true, pie: false, emulation: Some("i386pep"), @@ -708,7 +709,7 @@ fn build_elf_plan( .collect(), entry: None, gc_sections: true, - strip: true, + strip: !options.debug_info.is_enabled(), build_id_none: true, pie: false, emulation: Some(elf_emulation(target)), @@ -879,7 +880,8 @@ fn build_compiler_driver_plan( .collect(), entry: None, gc_sections: true, - strip: options.runtime_mode == RuntimeMode::Freestanding, + strip: options.runtime_mode == RuntimeMode::Freestanding + && !options.debug_info.is_enabled(), build_id_none: options.runtime_mode == RuntimeMode::Freestanding, pie: false, emulation: None, @@ -1012,6 +1014,7 @@ mod tests { }; let options = NativeLinkOptions { runtime_mode: RuntimeMode::Hosted, + debug_info: crate::debuginfo::DebugInfo::None, show_warnings: false, allow_elevated_native_link: false, extra_c_files: &[], @@ -1073,6 +1076,7 @@ mod tests { fn build_compiler_driver_plan_linux_freestanding_still_gets_static() { let options = NativeLinkOptions { runtime_mode: RuntimeMode::Freestanding, + debug_info: crate::debuginfo::DebugInfo::None, show_warnings: false, allow_elevated_native_link: false, extra_c_files: &[], @@ -1097,6 +1101,10 @@ mod tests { ) .expect("plan construction succeeds with no manifest"); assert!(plan.static_link); + assert!( + plan.strip, + "release-default freestanding links stay stripped" + ); let rendered: Vec = plan .render() .into_iter() @@ -1108,6 +1116,45 @@ mod tests { ); } + #[test] + fn compiler_driver_debug_plan_preserves_symbols() { + let options = NativeLinkOptions { + runtime_mode: RuntimeMode::Freestanding, + debug_info: crate::debuginfo::DebugInfo::LineTables, + show_warnings: false, + allow_elevated_native_link: false, + extra_c_files: &[], + extra_cflags: &[], + extra_objects: &[], + extra_libs: &[], + }; + let linker_driver = driver::LinkerDriver { + cmd: "cc".to_string(), + source: crate::CompilerSource::Host, + linker_family: driver::LinkerFamily::GnuLd, + }; + let plan = build_compiler_driver_plan( + Path::new("/obj/program.o"), + Path::new("/out/hello"), + NativeTarget::LinuxX64, + &options, + Path::new("/archives/libosc_runtime_freestanding.a"), + None, + linker_driver, + archive::ShimSource::ArchiveMember, + ) + .expect("debug plan construction succeeds"); + + assert!( + !plan.strip, + "debug links must retain line tables and symbols" + ); + assert!( + !plan.render().iter().any(|arg| arg == "-s"), + "debug linker argv must not request stripping" + ); + } + // --- is_elf_eligible tests (design §10.6) --- #[test] diff --git a/src/backend/lir.rs b/src/backend/lir.rs index f247922..f011ba7 100644 --- a/src/backend/lir.rs +++ b/src/backend/lir.rs @@ -258,6 +258,17 @@ pub trait LirModule { linkage: LLinkage, ) -> Result; + /// Associate a user function with its source declaration. Generated + /// wrappers and runtime imports deliberately omit this hook. + fn set_function_source( + &mut self, + _func: LFunc, + _source_name: &str, + _linkage_name: &str, + _location: crate::debuginfo::SourceLocation, + ) { + } + /// Define `func`: create its entry block, run `body` against an open /// builder, then finalize and install the result. fn define_function( @@ -286,6 +297,10 @@ pub trait LirBuilder { linkage: LLinkage, ) -> Result; + /// Set the source location inherited by subsequently emitted + /// instructions, or clear it for compiler-generated instructions. + fn set_source_location(&mut self, _location: Option) {} + /// Get-or-create the deduplicated 16-byte `{ const char* data; /// int32_t len; }` header cell for a string literal, plus its /// NUL-terminated backing bytes. Returns the header's handle. diff --git a/src/backend/lir_cranelift.rs b/src/backend/lir_cranelift.rs index 167a90b..ac36930 100644 --- a/src/backend/lir_cranelift.rs +++ b/src/backend/lir_cranelift.rs @@ -12,8 +12,8 @@ use std::collections::HashMap; use cranelift_codegen::ir::condcodes::{FloatCC, IntCC}; use cranelift_codegen::ir::{ - types, AbiParam, Function, InstBuilder, MemFlagsData, Signature, StackSlotData, StackSlotKind, - UserFuncName, Value, + types, AbiParam, Function, InstBuilder, MemFlagsData, Signature, SourceLoc, StackSlotData, + StackSlotKind, UserFuncName, Value, }; use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext, Variable}; use cranelift_module::{DataDescription, DataId, FuncId, Linkage, Module}; @@ -24,6 +24,7 @@ use super::lir::{ LirBody, LirBuilder, LirError, LirModule, }; use super::target::{self, NativeTarget}; +use crate::debuginfo::{DebugInfo, SourceLocation, SourceMap}; /// The flags used for every plain scalar/pointer load and store this /// backend emits. Not `trusted()`: Oscan values can indeed be read at @@ -87,6 +88,7 @@ fn float_cc(cc: FloatCmp) -> FloatCC { /// can never be stored alongside the module it is built into). struct ModuleState { module: Option, + debug: Option, /// Dense `LFunc` -> Cranelift id / return type, plus a symbol cache /// so repeat declarations share one handle. func_ids: Vec, @@ -215,17 +217,25 @@ pub struct CraneliftLir { } impl CraneliftLir { - pub fn new(target: NativeTarget) -> Result { - let isa = target::build_isa(target)?; - let builder = ObjectBuilder::new( + pub fn new( + target: NativeTarget, + debug_info: DebugInfo, + source_map: &SourceMap, + ) -> Result { + let isa = target::build_isa(target, debug_info)?; + let mut builder = ObjectBuilder::new( isa, "oscan_program", cranelift_module::default_libcall_names(), ) .map_err(|e| format!("internal error configuring object writer: {e}"))?; + builder.unwind_info(debug_info.is_enabled()); Ok(CraneliftLir { state: ModuleState { module: Some(ObjectModule::new(builder)), + debug: debug_info + .is_enabled() + .then(|| super::cranelift_debug::CraneliftDebug::new(source_map)), func_ids: Vec::new(), func_rets: Vec::new(), func_handles: HashMap::new(), @@ -248,6 +258,19 @@ impl LirModule for CraneliftLir { self.state.declare_function(symbol, sig, linkage) } + fn set_function_source( + &mut self, + func: LFunc, + source_name: &str, + linkage_name: &str, + location: SourceLocation, + ) { + let func_id = self.state.func_ids[func.index()]; + if let Some(debug) = &mut self.state.debug { + debug.set_function_source(func_id, source_name, linkage_name, location); + } + } + fn define_function( &mut self, func: LFunc, @@ -258,6 +281,9 @@ impl LirModule for CraneliftLir { let cl_sig = cl_signature(self.state.module_ref(), sig); let mut function = Function::with_name_signature(UserFuncName::user(0, func_id.as_u32()), cl_sig); + if self.state.debug.is_some() { + function.params.ensure_base_srcloc(SourceLoc::new(0)); + } let mut fb_ctx = FunctionBuilderContext::new(); { @@ -297,6 +323,11 @@ impl LirModule for CraneliftLir { .module() .define_function(func_id, &mut ctx_obj) .map_err(|e| LirError::Backend(format!("{e}")))?; + if let Some(debug) = &mut self.state.debug { + debug + .capture_function(func_id, &ctx_obj) + .map_err(LirError::Backend)?; + } Ok(()) } @@ -306,7 +337,10 @@ impl LirModule for CraneliftLir { .module .take() .ok_or_else(|| "internal error: Cranelift module already finished".to_string())?; - let product = module.finish(); + let mut product = module.finish(); + if let Some(debug) = self.state.debug.take() { + debug.write(&mut product)?; + } let bytes = product .emit() .map_err(|e| format!("internal error emitting object file: {e}"))?; @@ -357,6 +391,15 @@ impl CraneliftBuilder<'_> { } impl LirBuilder for CraneliftBuilder<'_> { + fn set_source_location(&mut self, location: Option) { + let source_loc = self + .state + .debug + .as_mut() + .map_or_else(SourceLoc::default, |debug| debug.source_loc(location)); + self.builder.set_srcloc(source_loc); + } + fn declare_function( &mut self, symbol: &str, @@ -702,3 +745,197 @@ impl LirBuilder for CraneliftBuilder<'_> { } } } + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use gimli::read::AttributeValue; + use gimli::{DebugLine, DebugLineOffset, LittleEndian}; + use object::{Object, ObjectSection}; + + use super::*; + use crate::debuginfo::SourceMapBuilder; + + fn compile_test_object(debug_info: DebugInfo) -> (Vec, PathBuf, PathBuf) { + let root_path = PathBuf::from("project").join("root.osc"); + let imported_path = PathBuf::from("project").join("lib").join("math.osc"); + let mut source_map = SourceMapBuilder::default(); + let root = source_map.intern_file(root_path.clone()); + let imported = source_map.intern_file(imported_path.clone()); + let source_map = source_map.finish(Vec::new()); + let mut lir = CraneliftLir::new(NativeTarget::LinuxX64, debug_info, &source_map) + .unwrap_or_else(|error| panic!("create Cranelift module: {error}")); + + for (symbol, source_name, file, declaration_line, body_line, value) in [ + ("root_fn", "root_fn", root, 3, 4, 1), + ("imported_fn", "imported_fn", imported, 12, 13, 2), + ] { + let sig = LSig::new(Vec::new(), Some(LType::I32)); + let func = lir + .declare_function(symbol, &sig, LLinkage::Export) + .unwrap_or_else(|error| panic!("declare {symbol}: {error}")); + lir.set_function_source( + func, + source_name, + symbol, + SourceLocation { + file, + line: declaration_line, + column: 1, + }, + ); + lir.define_function(func, &sig, &mut |builder, _params| { + builder.set_source_location(Some(SourceLocation { + file, + line: body_line, + column: 5, + })); + let result = builder.iconst(LType::I32, value); + builder.ret(Some(result)); + Ok(()) + }) + .unwrap_or_else(|_| panic!("define {symbol}")); + } + + let artifact = lir + .finish() + .unwrap_or_else(|error| panic!("finish Cranelift module: {error}")); + let LirArtifact::Object(bytes) = artifact else { + panic!("Cranelift must emit an object") + }; + (bytes, root_path, imported_path) + } + + fn debug_source_rows(bytes: &[u8]) -> Vec<(String, u64)> { + let object = object::File::parse(bytes).expect("parse object"); + let line_data = object + .section_by_name(".debug_line") + .expect("debug line section") + .data() + .expect("debug line data"); + let program = DebugLine::new(line_data, LittleEndian) + .program(DebugLineOffset(0), 8, None, None) + .expect("parse line program"); + let mut rows = program.rows(); + let mut source_rows = Vec::new(); + while let Some((header, row)) = rows.next_row().expect("decode line row") { + if row.end_sequence() { + continue; + } + let file = row.file(header).expect("line row file"); + let AttributeValue::String(path) = file.path_name() else { + panic!("DWARF v4 file path must be an inline string") + }; + let path = path.to_string_lossy().into_owned(); + let line = row.line().expect("line number").get(); + source_rows.push((path, line)); + } + source_rows + } + + #[test] + fn default_object_has_no_debug_or_unwind_sections() { + let (bytes, _, _) = compile_test_object(DebugInfo::None); + let object = object::File::parse(bytes.as_slice()).expect("parse object"); + let sections: Vec<_> = object + .sections() + .filter_map(|section| section.name().ok()) + .collect(); + assert!(!sections.iter().any(|name| name.starts_with(".debug_"))); + assert!(!sections.contains(&".eh_frame")); + } + + #[test] + fn line_tables_emit_parseable_multi_file_dwarf_and_unwind_info() { + let (bytes, root_path, imported_path) = compile_test_object(DebugInfo::LineTables); + let object = object::File::parse(bytes.as_slice()).expect("parse object"); + for section in [ + ".debug_abbrev", + ".debug_info", + ".debug_line", + ".debug_str", + ".eh_frame", + ] { + assert!( + object.section_by_name(section).is_some(), + "missing {section}" + ); + } + + let source_rows = debug_source_rows(&bytes); + + for (path, line) in [(root_path, 4), (imported_path, 13)] { + let expected = path.to_string_lossy().replace('\\', "/"); + assert!( + source_rows.iter().any( + |(actual, actual_line)| actual.ends_with(&expected) && *actual_line == line + ), + "missing {expected}:{line} in {source_rows:?}" + ); + } + } + + #[test] + fn nested_locations_can_restore_an_earlier_source_id() { + let mut source_map = SourceMapBuilder::default(); + let file = source_map.intern_file(PathBuf::from("project").join("nested.osc")); + let source_map = source_map.finish(Vec::new()); + let mut lir = CraneliftLir::new(NativeTarget::LinuxX64, DebugInfo::LineTables, &source_map) + .expect("create Cranelift module"); + + let call_sig = LSig::new(vec![LType::I32], Some(LType::I32)); + let imported = lir + .declare_function("opaque_call", &call_sig, LLinkage::Import) + .expect("declare imported function"); + let func = lir + .declare_function("nested", &call_sig, LLinkage::Export) + .expect("declare test function"); + lir.set_function_source( + func, + "nested", + "nested", + SourceLocation { + file, + line: 3, + column: 1, + }, + ); + lir.define_function(func, &call_sig, &mut |builder, params| { + let parent = SourceLocation { + file, + line: 10, + column: 3, + }; + builder.set_source_location(Some(parent)); + builder.set_source_location(Some(SourceLocation { + file, + line: 20, + column: 7, + })); + let child = builder + .call(imported, &[params[0]]) + .expect("imported function returns a value"); + builder.set_source_location(Some(parent)); + let restored = builder + .call(imported, &[child]) + .expect("imported function returns a value"); + builder.ret(Some(restored)); + Ok(()) + }) + .unwrap_or_else(|_| panic!("define test function")); + + let LirArtifact::Object(bytes) = lir.finish().expect("finish Cranelift module") else { + panic!("Cranelift must emit an object") + }; + let source_rows = debug_source_rows(&bytes); + for line in [10, 20] { + assert!( + source_rows + .iter() + .any(|(_, actual_line)| *actual_line == line), + "missing restored source line {line} in {source_rows:?}" + ); + } + } +} diff --git a/src/backend/llvm/emit.rs b/src/backend/llvm/emit.rs index a32b140..2622aee 100644 --- a/src/backend/llvm/emit.rs +++ b/src/backend/llvm/emit.rs @@ -43,11 +43,13 @@ use std::collections::HashMap; use std::fmt::Write as _; +use std::path::Path; use crate::backend::lir::{ copy_chunks, FloatCmp, IntCmp, LBlock, LData, LFunc, LLinkage, LSig, LType, LValue, LVar, LirArtifact, LirBody, LirBuilder, LirError, LirModule, }; +use crate::debuginfo::{DebugInfo, SourceFileId, SourceLocation, SourceMap}; /// LLVM type text for a LIR type in the *value* world. See module docs /// for why `Ptr` is `i64`. @@ -124,6 +126,8 @@ struct FuncDecl { sig: LSig, linkage: LLinkage, defined: bool, + debug_subprogram: Option, + debug_fallback_location: Option, } enum DataDecl { @@ -134,10 +138,183 @@ enum DataDecl { Import { symbol: String }, } +struct DebugMetadata { + source_filename: String, + files: HashMap, + primary_file: u32, + compile_unit: u32, + subroutine_type: u32, + dwarf_version_flag: u32, + debug_info_version_flag: u32, + locations: HashMap<(u32, SourceLocation), u32>, + definitions: Vec, +} + +impl DebugMetadata { + fn new(source_map: &SourceMap) -> Self { + let source_filename = source_map + .primary_path() + .map(path_text) + .unwrap_or_else(|| "oscan_program".to_string()); + let mut metadata = DebugMetadata { + source_filename, + files: HashMap::new(), + primary_file: 0, + compile_unit: 0, + subroutine_type: 0, + dwarf_version_flag: 0, + debug_info_version_flag: 0, + locations: HashMap::new(), + definitions: Vec::new(), + }; + + let compile_unit = metadata.reserve_definition(); + metadata.compile_unit = compile_unit; + + let mut primary_file = None; + for (source_file, path) in source_map.files() { + let (filename, directory) = split_debug_path(path); + let file = metadata.add_definition(format!( + "!DIFile(filename: \"{}\", directory: \"{}\")", + llvm_bytes(filename.as_bytes()), + llvm_bytes(directory.as_bytes()) + )); + primary_file.get_or_insert(file); + metadata.files.insert(source_file, file); + } + let primary_file = match primary_file { + Some(file) => file, + None => metadata.add_definition( + "!DIFile(filename: \"oscan_program\", directory: \"\")".to_string(), + ), + }; + metadata.primary_file = primary_file; + + let empty_types = metadata.add_definition("!{}".to_string()); + metadata.subroutine_type = + metadata.add_definition(format!("!DISubroutineType(types: !{empty_types})")); + metadata.dwarf_version_flag = + metadata.add_definition("!{i32 7, !\"Dwarf Version\", i32 4}".to_string()); + metadata.debug_info_version_flag = + metadata.add_definition("!{i32 2, !\"Debug Info Version\", i32 3}".to_string()); + metadata.set_definition( + compile_unit, + format!( + "distinct !DICompileUnit(language: DW_LANG_C, file: !{primary_file}, producer: \ + \"Oscan\", isOptimized: true, runtimeVersion: 0, emissionKind: LineTablesOnly)" + ), + ); + metadata + } + + fn reserve_definition(&mut self) -> u32 { + let id = self.definitions.len() as u32; + self.definitions.push(String::new()); + id + } + + fn set_definition(&mut self, id: u32, definition: String) { + self.definitions[id as usize] = definition; + } + + fn add_definition(&mut self, definition: String) -> u32 { + let id = self.reserve_definition(); + self.set_definition(id, definition); + id + } + + fn add_subprogram( + &mut self, + source_name: &str, + linkage_name: &str, + location: SourceLocation, + ) -> Option { + let file = *self.files.get(&location.file)?; + Some(self.add_definition(format!( + "distinct !DISubprogram(name: \"{}\", linkageName: \"{}\", scope: !{file}, file: \ + !{file}, line: {}, type: !{}, scopeLine: {}, spFlags: DISPFlagDefinition | \ + DISPFlagOptimized, unit: !{})", + llvm_bytes(source_name.as_bytes()), + llvm_bytes(linkage_name.as_bytes()), + location.line, + self.subroutine_type, + location.line, + self.compile_unit + ))) + } + + fn add_artificial_subprogram(&mut self, linkage_name: &str) -> (u32, u32) { + let subprogram = self.add_definition(format!( + "distinct !DISubprogram(name: \"{}\", linkageName: \"{}\", scope: !{}, file: !{}, \ + line: 0, type: !{}, scopeLine: 0, flags: DIFlagArtificial, spFlags: \ + DISPFlagDefinition | DISPFlagOptimized, unit: !{})", + llvm_bytes(linkage_name.as_bytes()), + llvm_bytes(linkage_name.as_bytes()), + self.primary_file, + self.primary_file, + self.subroutine_type, + self.compile_unit + )); + let location = self.add_definition(format!( + "!DILocation(line: 0, column: 0, scope: !{subprogram})" + )); + (subprogram, location) + } + + fn location(&mut self, scope: u32, location: SourceLocation) -> u32 { + if let Some(existing) = self.locations.get(&(scope, location)) { + return *existing; + } + let column = if location.column <= u32::from(u16::MAX) { + location.column + } else { + 0 + }; + let id = self.add_definition(format!( + "!DILocation(line: {}, column: {}, scope: !{scope})", + location.line, column + )); + self.locations.insert((scope, location), id); + id + } + + fn write_to(&self, out: &mut String) { + let _ = writeln!(out, "!llvm.dbg.cu = !{{!{}}}", self.compile_unit); + let _ = writeln!( + out, + "!llvm.module.flags = !{{!{}, !{}}}", + self.dwarf_version_flag, self.debug_info_version_flag + ); + out.push('\n'); + for (id, definition) in self.definitions.iter().enumerate() { + debug_assert!(!definition.is_empty()); + let _ = writeln!(out, "!{id} = {definition}"); + } + } +} + +fn path_text(path: &Path) -> String { + path.to_string_lossy().into_owned() +} + +fn split_debug_path(path: &Path) -> (String, String) { + let filename = path + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_else(|| path_text(path)); + let directory = path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .map(path_text) + .unwrap_or_default(); + (filename, directory) +} + /// Module-level state, mirroring `lir_cranelift::ModuleState`. struct ModuleState { target_triple: String, data_layout: String, + debug: Option, funcs: Vec, func_handles: HashMap, datas: Vec, @@ -172,6 +349,8 @@ impl ModuleState { sig: sig.clone(), linkage, defined: false, + debug_subprogram: None, + debug_fallback_location: None, }); self.func_handles.insert(symbol.to_string(), handle); Ok(handle) @@ -218,11 +397,19 @@ pub struct LlvmEmitter { } impl LlvmEmitter { - pub fn new(target_triple: &str, data_layout: &str) -> Self { + pub fn new( + target_triple: &str, + data_layout: &str, + debug_info: DebugInfo, + source_map: &SourceMap, + ) -> Self { LlvmEmitter { state: ModuleState { target_triple: target_triple.to_string(), data_layout: data_layout.to_string(), + debug: debug_info + .is_enabled() + .then(|| DebugMetadata::new(source_map)), funcs: Vec::new(), func_handles: HashMap::new(), datas: Vec::new(), @@ -245,6 +432,25 @@ impl LirModule for LlvmEmitter { self.state.declare_function(symbol, sig, linkage) } + fn set_function_source( + &mut self, + func: LFunc, + source_name: &str, + linkage_name: &str, + location: SourceLocation, + ) { + if self.state.funcs[func.index()].debug_subprogram.is_some() { + return; + } + let subprogram = self + .state + .debug + .as_mut() + .and_then(|debug| debug.add_subprogram(source_name, linkage_name, location)); + self.state.funcs[func.index()].debug_subprogram = subprogram; + self.state.funcs[func.index()].debug_fallback_location = None; + } + fn define_function( &mut self, func: LFunc, @@ -258,6 +464,18 @@ impl LirModule for LlvmEmitter { ))); } self.state.funcs[func.index()].defined = true; + if self.state.funcs[func.index()].debug_subprogram.is_none() { + let symbol = self.state.funcs[func.index()].symbol.clone(); + let generated_debug = self + .state + .debug + .as_mut() + .map(|debug| debug.add_artificial_subprogram(&symbol)); + if let Some((subprogram, fallback_location)) = generated_debug { + self.state.funcs[func.index()].debug_subprogram = Some(subprogram); + self.state.funcs[func.index()].debug_fallback_location = Some(fallback_location); + } + } let mut fb = LlvmFuncBuilder::new(&mut self.state, func, sig); let params = fb.entry_params.clone(); @@ -274,7 +492,16 @@ impl LirModule for LlvmEmitter { fn finish(&mut self) -> Result { let mut out = String::new(); out.push_str("; ModuleID = 'oscan_program'\n"); - out.push_str("source_filename = \"oscan_program\"\n"); + match &self.state.debug { + Some(debug) => { + let _ = writeln!( + out, + "source_filename = \"{}\"", + llvm_bytes(debug.source_filename.as_bytes()) + ); + } + None => out.push_str("source_filename = \"oscan_program\"\n"), + } let _ = writeln!(out, "target datalayout = \"{}\"", self.state.data_layout); let _ = writeln!(out, "target triple = \"{}\"", self.state.target_triple); out.push('\n'); @@ -350,6 +577,10 @@ impl LirModule for LlvmEmitter { out.push('\n'); } + if let Some(debug) = &self.state.debug { + debug.write_to(&mut out); + } + Ok(LirArtifact::LlvmIr(out)) } } @@ -391,6 +622,9 @@ pub struct LlvmFuncBuilder<'a> { next_temp: u32, next_label: u32, + debug_subprogram: Option, + debug_fallback_location: Option, + source_location: Option, /// A hard error discovered while emitting (e.g. a type mismatch that /// would produce invalid IR), surfaced by `finish`. error: Option, @@ -398,6 +632,8 @@ pub struct LlvmFuncBuilder<'a> { impl<'a> LlvmFuncBuilder<'a> { fn new(state: &'a mut ModuleState, func: LFunc, sig: &LSig) -> Self { + let debug_subprogram = state.funcs[func.index()].debug_subprogram; + let debug_fallback_location = state.funcs[func.index()].debug_fallback_location; let mut fb = LlvmFuncBuilder { state, func, @@ -412,6 +648,9 @@ impl<'a> LlvmFuncBuilder<'a> { allocas: String::new(), next_temp: 0, next_label: 0, + debug_subprogram, + debug_fallback_location, + source_location: None, error: None, }; fb.new_block(); @@ -487,26 +726,46 @@ impl<'a> LlvmFuncBuilder<'a> { /// `break`/`return` inside a nested expression), and LLVM rejects a /// block with instructions past its terminator. fn emit(&mut self, instruction: String) { - let block = &mut self.blocks[self.current]; - if block.terminated { + if self.blocks[self.current].terminated { return; } + let debug_location = self.debug_location(); + let block = &mut self.blocks[self.current]; block.body.push_str(" "); block.body.push_str(&instruction); + if let Some(debug_location) = debug_location { + let _ = write!(block.body, ", !dbg !{debug_location}"); + } block.body.push('\n'); } fn emit_terminator(&mut self, instruction: String) { - let block = &mut self.blocks[self.current]; - if block.terminated { + if self.blocks[self.current].terminated { return; } + let debug_location = self.debug_location(); + let block = &mut self.blocks[self.current]; block.body.push_str(" "); block.body.push_str(&instruction); + if let Some(debug_location) = debug_location { + let _ = write!(block.body, ", !dbg !{debug_location}"); + } block.body.push('\n'); block.terminated = true; } + fn debug_location(&mut self) -> Option { + let scope = self.debug_subprogram?; + match self.source_location { + Some(location) => self + .state + .debug + .as_mut() + .map(|debug| debug.location(scope, location)), + None => self.debug_fallback_location, + } + } + fn label(&self, block: LBlock) -> String { format!("%b{}", self.blocks[block.index()].index) } @@ -669,9 +928,27 @@ impl<'a> LlvmFuncBuilder<'a> { // `"no-builtins"` is Clang's `-ffreestanding` marker: LLVM must not // assume libc entry points exist. Nothing stronger (no `nofree`, no // `willreturn`, no argument attributes) is promised. + let line_tables = self.state.debug.is_some(); + let unwind_table_attribute = + if line_tables && self.state.target_triple.contains("-windows-") { + // Win64 stack walkers consume the mandatory PE unwind records. + // LLVM emits them for nounwind functions only when requested. + " uwtable" + } else { + "" + }; + let line_table_attributes = if line_tables { + " \"frame-pointer\"=\"all\"" + } else { + "" + }; + let debug_subprogram = decl + .debug_subprogram + .map(|id| format!(" !dbg !{id}")) + .unwrap_or_default(); let _ = writeln!( out, - "define {ret} {}({params}) minsize nounwind optsize \"no-builtins\" {{", + "define {ret} {}({params}) minsize nounwind optsize{unwind_table_attribute} \"no-builtins\"{line_table_attributes}{debug_subprogram} {{", llvm_symbol(&decl.symbol) ); for (i, block) in self.blocks.iter().enumerate() { @@ -722,6 +999,10 @@ impl LirBuilder for LlvmFuncBuilder<'_> { self.state.declare_function(symbol, sig, linkage) } + fn set_source_location(&mut self, location: Option) { + self.source_location = location; + } + fn string_literal_data(&mut self, value: &str) -> LData { self.state.string_literal_data(value) } @@ -1174,9 +1455,34 @@ impl LirBuilder for LlvmFuncBuilder<'_> { #[cfg(test)] mod tests { use super::*; + use crate::debuginfo::SourceMapBuilder; + use std::path::PathBuf; fn ir_of(build: impl FnOnce(&mut LlvmEmitter)) -> String { - let mut emitter = LlvmEmitter::new("x86_64-unknown-linux-gnu", "e-m:e-p:64:64-i64:64"); + let source_map = SourceMap::default(); + ir_of_with_debug(DebugInfo::None, &source_map, build) + } + + fn ir_of_with_debug( + debug_info: DebugInfo, + source_map: &SourceMap, + build: impl FnOnce(&mut LlvmEmitter), + ) -> String { + ir_of_for_target_with_debug("x86_64-unknown-linux-gnu", debug_info, source_map, build) + } + + fn ir_of_for_target_with_debug( + target_triple: &str, + debug_info: DebugInfo, + source_map: &SourceMap, + build: impl FnOnce(&mut LlvmEmitter), + ) -> String { + let mut emitter = LlvmEmitter::new( + target_triple, + "e-m:e-p:64:64-i64:64", + debug_info, + source_map, + ); build(&mut emitter); match emitter.finish().expect("finish") { LirArtifact::LlvmIr(text) => text, @@ -1184,6 +1490,44 @@ mod tests { } } + #[test] + fn line_tables_request_frame_pointers_and_windows_unwind_tables() { + let source_map = SourceMap::default(); + for (target, debug_info, frame_pointer, unwind_table) in [ + ( + "x86_64-unknown-linux-gnu", + DebugInfo::LineTables, + true, + false, + ), + ("x86_64-w64-windows-gnu", DebugInfo::LineTables, true, true), + ("x86_64-w64-windows-gnu", DebugInfo::None, false, false), + ] { + let text = ir_of_for_target_with_debug(target, debug_info, &source_map, |emitter| { + let sig = LSig::new(vec![], None); + let wrapper = emitter + .declare_function("main", &sig, LLinkage::Export) + .expect("declare wrapper"); + emitter + .define_function(wrapper, &sig, &mut |b, _params| { + b.ret(None); + Ok(()) + }) + .unwrap_or_else(|_| panic!("define wrapper failed")); + }); + let header = text + .lines() + .find(|line| line.starts_with("define ")) + .expect("function definition"); + assert_eq!( + header.contains("\"frame-pointer\"=\"all\""), + frame_pointer, + "{header}" + ); + assert_eq!(header.contains(" uwtable "), unwind_table, "{header}"); + } + } + #[test] fn doubles_use_llvm_exact_hex_form() { assert_eq!(llvm_double(0.0), "0x0000000000000000"); @@ -1217,7 +1561,13 @@ mod tests { #[test] fn conflicting_signatures_for_one_symbol_are_rejected() { - let mut emitter = LlvmEmitter::new("x86_64-unknown-linux-gnu", "e-m:e-p:64:64"); + let source_map = SourceMap::default(); + let mut emitter = LlvmEmitter::new( + "x86_64-unknown-linux-gnu", + "e-m:e-p:64:64", + DebugInfo::None, + &source_map, + ); let a = LSig::new(vec![LType::I32], Some(LType::I32)); let b = LSig::new(vec![LType::I64], Some(LType::I32)); assert!(emitter.declare_function("f", &a, LLinkage::Import).is_ok()); @@ -1228,6 +1578,409 @@ mod tests { assert!(err.contains("conflicting signatures"), "{err}"); } + #[test] + fn debug_info_none_preserves_the_metadata_free_module_shape() { + let mut source_map = SourceMapBuilder::default(); + let file = source_map.intern_file(PathBuf::from("ignored").join("actual.osc")); + let source_map = source_map.finish(Vec::new()); + let text = ir_of_with_debug(DebugInfo::None, &source_map, |emitter| { + let sig = LSig::new(vec![], Some(LType::I32)); + let func = emitter + .declare_function("plain", &sig, LLinkage::Export) + .expect("declare"); + emitter.set_function_source( + func, + "plain", + "plain", + SourceLocation { + file, + line: 3, + column: 1, + }, + ); + emitter + .define_function(func, &sig, &mut |b, _params| { + b.set_source_location(Some(SourceLocation { + file, + line: 4, + column: 2, + })); + let one = b.iconst(LType::I32, 1); + let two = b.iconst(LType::I32, 2); + let sum = b.iadd(one, two); + b.ret(Some(sum)); + Ok(()) + }) + .unwrap_or_else(|_| panic!("define failed")); + }); + + assert!( + text.contains("source_filename = \"oscan_program\""), + "{text}" + ); + for marker in [ + "!llvm.dbg.cu", + "!llvm.module.flags", + "!DI", + "!dbg", + "\"frame-pointer\"", + ] { + assert!( + !text.contains(marker), + "DebugInfo::None must not emit '{marker}':\n{text}" + ); + } + } + + #[test] + fn line_tables_emit_multi_file_function_and_location_metadata() { + let root_path = PathBuf::from("project").join("src").join("root.osc"); + let imported_path = PathBuf::from("project").join("libs").join("math\"core.osc"); + let mut source_map = SourceMapBuilder::default(); + let root = source_map.intern_file(root_path.clone()); + let imported = source_map.intern_file(imported_path.clone()); + let source_map = source_map.finish(Vec::new()); + + let text = ir_of_with_debug(DebugInfo::LineTables, &source_map, |emitter| { + let wrapper_sig = LSig::new(vec![], Some(LType::I32)); + let wrapper = emitter + .declare_function("main", &wrapper_sig, LLinkage::Export) + .expect("declare wrapper"); + emitter + .define_function(wrapper, &wrapper_sig, &mut |b, _params| { + let one = b.iconst(LType::I32, 1); + let two = b.iconst(LType::I32, 2); + let sum = b.iadd(one, two); + b.ret(Some(sum)); + Ok(()) + }) + .unwrap_or_else(|_| panic!("define wrapper failed")); + + let foo_sig = LSig::new(vec![LType::Ptr], Some(LType::I32)); + let foo = emitter + .declare_function("__osc_foo", &foo_sig, LLinkage::Export) + .expect("declare foo"); + emitter.set_function_source( + foo, + "foo", + "__osc_foo", + SourceLocation { + file: root, + line: 3, + column: 4, + }, + ); + emitter + .define_function(foo, &foo_sig, &mut |b, _params| { + let slot = b.declare_var(LType::I32); + b.set_source_location(Some(SourceLocation { + file: root, + line: 4, + column: 2, + })); + let one = b.iconst(LType::I32, 1); + let two = b.iconst(LType::I32, 2); + let sum = b.iadd(one, two); + b.def_var(slot, sum); + b.set_source_location(Some(SourceLocation { + file: root, + line: 8, + column: 7, + })); + let result = b.use_var(slot); + b.ret(Some(result)); + Ok(()) + }) + .unwrap_or_else(|_| panic!("define foo failed")); + + let bar_sig = LSig::new(vec![], Some(LType::I32)); + let bar = emitter + .declare_function("__osc_bar", &bar_sig, LLinkage::Export) + .expect("declare bar"); + emitter.set_function_source( + bar, + "bar", + "__osc_bar", + SourceLocation { + file: imported, + line: 12, + column: 3, + }, + ); + emitter + .define_function(bar, &bar_sig, &mut |b, _params| { + b.set_source_location(Some(SourceLocation { + file: imported, + line: 13, + column: u32::MAX, + })); + let one = b.iconst(LType::I32, 1); + let two = b.iconst(LType::I32, 2); + let sum = b.iadd(one, two); + b.ret(Some(sum)); + Ok(()) + }) + .unwrap_or_else(|_| panic!("define bar failed")); + }); + + let source_filename = llvm_bytes(root_path.to_string_lossy().as_bytes()); + assert!( + text.contains(&format!("source_filename = \"{source_filename}\"")), + "{text}" + ); + assert!(text.contains("!llvm.dbg.cu = !{!"), "{text}"); + assert!(text.contains("!llvm.module.flags = !{!"), "{text}"); + assert!( + text.contains("!{i32 7, !\"Dwarf Version\", i32 4}"), + "{text}" + ); + assert!( + text.contains("!{i32 2, !\"Debug Info Version\", i32 3}"), + "{text}" + ); + assert_eq!(text.matches("!DIFile(").count(), 2, "{text}"); + assert!(text.contains("filename: \"root.osc\""), "{text}"); + assert!( + text.contains("filename: \"math\\22core.osc\""), + "LLVM strings must escape quotes:\n{text}" + ); + assert_eq!( + text.matches("distinct !DICompileUnit(").count(), + 1, + "{text}" + ); + assert!(text.contains("emissionKind: LineTablesOnly"), "{text}"); + assert_eq!(text.matches("!DISubroutineType(").count(), 1, "{text}"); + for symbol in ["main", "__osc_foo", "__osc_bar"] { + let marker = format!("@{symbol}("); + let header = text + .lines() + .find(|line| line.starts_with("define ") && line.contains(&marker)) + .expect("function definition"); + assert!( + header.contains("\"frame-pointer\"=\"all\""), + "line-table definition lacks a frame pointer attribute: {header}" + ); + } + + let root_file = text + .lines() + .find(|line| line.contains("!DIFile(filename: \"root.osc\"")) + .expect("root DIFile"); + let imported_file = text + .lines() + .find(|line| line.contains("!DIFile(filename: \"math\\22core.osc\"")) + .expect("imported DIFile"); + let root_directory = llvm_bytes( + root_path + .parent() + .expect("root directory") + .to_string_lossy() + .as_bytes(), + ); + let imported_directory = llvm_bytes( + imported_path + .parent() + .expect("imported directory") + .to_string_lossy() + .as_bytes(), + ); + assert!( + root_file.contains(&format!("directory: \"{root_directory}\"")), + "{root_file}" + ); + assert!( + imported_file.contains(&format!("directory: \"{imported_directory}\"")), + "{imported_file}" + ); + let root_file_id = root_file + .strip_prefix('!') + .and_then(|line| line.split_once(" = ")) + .map(|(id, _)| id) + .expect("root metadata id"); + let imported_file_id = imported_file + .strip_prefix('!') + .and_then(|line| line.split_once(" = ")) + .map(|(id, _)| id) + .expect("imported metadata id"); + + let subprograms: Vec<&str> = text + .lines() + .filter(|line| line.contains("distinct !DISubprogram(")) + .collect(); + assert_eq!(subprograms.len(), 3, "{text}"); + let wrapper_subprogram = subprograms + .iter() + .copied() + .find(|line| line.contains("linkageName: \"main\"")) + .expect("wrapper subprogram"); + let foo_subprogram = subprograms + .iter() + .copied() + .find(|line| line.contains("name: \"foo\"")) + .expect("foo subprogram"); + let bar_subprogram = subprograms + .iter() + .copied() + .find(|line| line.contains("name: \"bar\"")) + .expect("bar subprogram"); + assert!( + wrapper_subprogram.contains("line: 0") + && wrapper_subprogram.contains("flags: DIFlagArtificial"), + "{wrapper_subprogram}" + ); + assert!( + foo_subprogram.contains(&format!( + "linkageName: \"__osc_foo\", scope: !{root_file_id}, file: !{root_file_id}, line: 3" + )), + "{foo_subprogram}" + ); + assert!( + bar_subprogram.contains(&format!( + "linkageName: \"__osc_bar\", scope: !{imported_file_id}, file: !{imported_file_id}, line: 12" + )), + "{bar_subprogram}" + ); + for subprogram in &subprograms { + assert!( + subprogram.contains("spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !"), + "{subprogram}" + ); + for obsolete in ["isLocal:", "isDefinition:", "isOptimized:"] { + assert!( + !subprogram.contains(obsolete), + "obsolete DISubprogram field '{obsolete}': {subprogram}" + ); + } + } + let foo_type = foo_subprogram + .split("type: !") + .nth(1) + .and_then(|tail| tail.split(',').next()) + .expect("foo type"); + let bar_type = bar_subprogram + .split("type: !") + .nth(1) + .and_then(|tail| tail.split(',').next()) + .expect("bar type"); + assert_eq!(foo_type, bar_type, "subroutine type must be shared"); + + assert_eq!(text.matches("!DILocation(").count(), 4, "{text}"); + assert!( + text.contains("!DILocation(line: 0, column: 0, scope: !"), + "{text}" + ); + assert!( + text.contains("!DILocation(line: 13, column: 0, scope: !"), + "{text}" + ); + assert!(!text.contains("column: 4294967295"), "{text}"); + let foo_subprogram_id = foo_subprogram + .strip_prefix('!') + .and_then(|line| line.split_once(" = ")) + .map(|(id, _)| id) + .expect("foo subprogram id"); + let bar_subprogram_id = bar_subprogram + .strip_prefix('!') + .and_then(|line| line.split_once(" = ")) + .map(|(id, _)| id) + .expect("bar subprogram id"); + assert_eq!( + text.lines() + .filter(|line| { + line.contains("!DILocation(") + && line.contains(&format!("scope: !{foo_subprogram_id})")) + }) + .count(), + 2, + "{text}" + ); + assert_eq!( + text.lines() + .filter(|line| { + line.contains("!DILocation(") + && line.contains(&format!("scope: !{bar_subprogram_id})")) + }) + .count(), + 1, + "{text}" + ); + + let function_text = |symbol: &str| { + let marker = format!("@{symbol}("); + let start = text + .lines() + .position(|line| line.starts_with("define ") && line.contains(&marker)) + .expect("function definition"); + text.lines() + .skip(start) + .take_while(|line| *line != "}") + .chain(std::iter::once("}")) + .collect::>() + .join("\n") + }; + let wrapper = function_text("main"); + assert!( + wrapper + .lines() + .next() + .unwrap_or_default() + .contains("!dbg !"), + "{wrapper}" + ); + for instruction in wrapper.lines().filter(|line| line.starts_with(" ")) { + assert!( + instruction.contains("!dbg !"), + "generated wrapper instruction lacks its artificial location: {instruction}" + ); + } + let foo = function_text("__osc_foo"); + let foo_header = foo.lines().next().expect("foo header"); + assert!(foo_header.contains("!dbg !"), "{foo_header}"); + let alloca = foo + .lines() + .find(|line| line.contains(" = alloca ")) + .expect("hoisted alloca"); + let parameter_setup = foo + .lines() + .find(|line| line.contains("ptrtoint ptr %arg0")) + .expect("parameter setup"); + assert!(!alloca.contains("!dbg"), "{alloca}"); + assert!(!parameter_setup.contains("!dbg"), "{parameter_setup}"); + let add = foo + .lines() + .find(|line| line.contains(" = add i32 ")) + .expect("add instruction"); + let store = foo + .lines() + .find(|line| line.contains("store i32 ")) + .expect("store instruction"); + let add_location = add.split("!dbg !").nth(1).expect("add location"); + let store_location = store.split("!dbg !").nth(1).expect("store location"); + assert_eq!( + add_location, store_location, + "equal source locations must share one DILocation" + ); + for instruction in foo.lines().filter(|line| { + line.starts_with(" ") + && !line.contains(" = alloca ") + && !line.contains("ptrtoint ptr %arg") + }) { + assert!( + instruction.contains("!dbg !"), + "normal user instruction lacks debug location: {instruction}" + ); + } + let bar = function_text("__osc_bar"); + assert!(bar.lines().next().unwrap_or_default().contains("!dbg !")); + for instruction in bar.lines().filter(|line| line.starts_with(" ")) { + assert!( + instruction.contains("!dbg !"), + "bar instruction lacks debug location: {instruction}" + ); + } + } + #[test] fn emitted_module_is_deterministic_and_carries_no_poison_flags() { let build = || { diff --git a/src/backend/llvm/mod.rs b/src/backend/llvm/mod.rs index cb17f16..6b0ac5f 100644 --- a/src/backend/llvm/mod.rs +++ b/src/backend/llvm/mod.rs @@ -128,6 +128,8 @@ pub fn compile_object( target: NativeTarget, runtime_mode: RuntimeMode, emit_object: bool, + debug_info: crate::debuginfo::DebugInfo, + source_map: &crate::debuginfo::SourceMap, ) -> Result { backend.require_target(target)?; let triple = target_triple(target); @@ -140,8 +142,8 @@ pub fn compile_object( .data_layout_for(triple) .map_err(compile_error)?; - let mut emitter = emit::LlvmEmitter::new(triple, &data_layout); - let mut ctx = BackendContext::new(program, runtime_mode); + let mut emitter = emit::LlvmEmitter::new(triple, &data_layout, debug_info, source_map); + let mut ctx = BackendContext::new(program, runtime_mode, debug_info, source_map); let generated_extern_shim_c = super::translate_program(&mut ctx, &mut emitter)?; let ir_text = match emitter.finish() { diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 15c9df9..de8a56b 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -71,6 +71,8 @@ // The shared object-backend machinery (semantic lowering onto the // backend-independent `lir` interface) exists only when at least one // object backend does. +#[cfg(feature = "backend-cranelift")] +mod cranelift_debug; #[cfg(any(feature = "backend-cranelift", feature = "backend-llvm"))] mod ctx; pub mod distribution_contract; @@ -147,10 +149,12 @@ pub fn compile_object( program: &ir::Program, target: NativeTarget, runtime_mode: RuntimeMode, + debug_info: crate::debuginfo::DebugInfo, + source_map: &crate::debuginfo::SourceMap, ) -> Result { - let mut lir = lir_cranelift::CraneliftLir::new(target) + let mut lir = lir_cranelift::CraneliftLir::new(target, debug_info, source_map) .map_err(|e| CompileError::new(crate::token::Span::new(1, 1), e))?; - let mut ctx = BackendContext::new(program, runtime_mode); + let mut ctx = BackendContext::new(program, runtime_mode, debug_info, source_map); let generated_extern_shim_c = translate_program(&mut ctx, &mut lir)?; let object_bytes = match lir.finish() { Ok(LirArtifact::Object(bytes)) => bytes, diff --git a/src/backend/target.rs b/src/backend/target.rs index db37b7a..36187c2 100644 --- a/src/backend/target.rs +++ b/src/backend/target.rs @@ -207,7 +207,10 @@ fn env_arch() -> &'static str { /// `SpeedAndSize`-specific size optimization is picked up automatically /// without another change here. #[cfg(feature = "backend-cranelift")] -pub fn build_isa(target: NativeTarget) -> Result, String> { +pub fn build_isa( + target: NativeTarget, + debug_info: crate::debuginfo::DebugInfo, +) -> Result, String> { let triple = target.triple(); let mut flag_builder = settings::builder(); flag_builder @@ -219,6 +222,11 @@ pub fn build_isa(target: NativeTarget) -> Result, String> { flag_builder .set("enable_probestack", "false") .map_err(|e| format!("internal error configuring Cranelift settings: {e}"))?; + if debug_info.is_enabled() { + flag_builder + .set("preserve_frame_pointers", "true") + .map_err(|e| format!("internal error configuring Cranelift settings: {e}"))?; + } let isa_builder = cranelift_codegen::isa::lookup(triple.clone()).map_err(|e| { format!( @@ -255,7 +263,8 @@ mod tests { NativeTarget::LinuxAarch64, NativeTarget::LinuxRiscv64, ] { - let isa = build_isa(target).expect("every built-in target must configure an ISA"); + let isa = build_isa(target, crate::debuginfo::DebugInfo::None) + .expect("every built-in target must configure an ISA"); let flags = isa.flags(); assert_eq!( flags.opt_level(), @@ -267,9 +276,24 @@ mod tests { !flags.enable_probestack(), "{target} must not enable stack probes" ); + assert!( + !flags.preserve_frame_pointers(), + "{target} must preserve the release default without debug info" + ); } } + #[cfg(feature = "backend-cranelift")] + #[test] + fn debug_isa_preserves_frame_pointers() { + let isa = build_isa( + NativeTarget::WindowsX64, + crate::debuginfo::DebugInfo::LineTables, + ) + .expect("debug ISA"); + assert!(isa.flags().preserve_frame_pointers()); + } + #[test] fn detect_recognizes_every_supported_host() { assert_eq!( diff --git a/src/codegen.rs b/src/codegen.rs index b94a083..b0e4290 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet}; use crate::ast::{BinOp, UnaryOp}; use crate::collection::{array_element_type, CollectionIntrinsic, CollectionOp, CoreType}; +use crate::debuginfo::{DebugInfo, SourceLocation, SourceMap}; use crate::ir; use crate::types::*; @@ -21,6 +22,9 @@ pub struct CodeGenerator { fn_ptr_types: Vec, freestanding: bool, embed_runtime: bool, + debug_info: DebugInfo, + source_map: SourceMap, + current_source: Option, } enum CTypeDecl { @@ -31,14 +35,21 @@ enum CTypeDecl { } impl CodeGenerator { - pub fn generate(program: &ir::Program, freestanding: bool) -> String { - Self::generate_with_runtime(program, freestanding, true) + pub fn generate( + program: &ir::Program, + freestanding: bool, + debug_info: DebugInfo, + source_map: &SourceMap, + ) -> String { + Self::generate_with_runtime(program, freestanding, true, debug_info, source_map) } fn generate_with_runtime( program: &ir::Program, freestanding: bool, embed_runtime: bool, + debug_info: DebugInfo, + source_map: &SourceMap, ) -> String { let mut cg = Self { out: String::new(), @@ -52,6 +63,9 @@ impl CodeGenerator { fn_ptr_types: Vec::new(), freestanding, embed_runtime, + debug_info, + source_map: source_map.clone(), + current_source: None, }; // Collect all unique Result types used in the program @@ -388,6 +402,16 @@ impl CodeGenerator { // ----------------------------------------------------------------------- fn line(&mut self, s: &str) { + if self.debug_info.is_enabled() { + if let Some(location) = self.current_source { + let path = self.source_map.path(location.file); + let path = Self::debug_line_path(path); + self.out + .push_str(&format!("#line {} \"{}\"\n", location.line, path)); + } else { + self.out.push_str("#line 1 \"\"\n"); + } + } for _ in 0..self.indent { self.out.push_str(" "); } @@ -399,6 +423,22 @@ impl CodeGenerator { self.out.push('\n'); } + fn debug_line_path(path: &std::path::Path) -> String { + let mut escaped = String::new(); + for ch in path.to_string_lossy().chars() { + match ch { + '\\' if cfg!(windows) => escaped.push('/'), + '\\' => escaped.push_str("\\\\"), + '"' => escaped.push_str("\\\""), + '\n' => escaped.push_str("\\n"), + '\r' => escaped.push_str("\\r"), + '\t' => escaped.push_str("\\t"), + other => escaped.push(other), + } + } + escaped + } + fn fresh_tmp(&mut self) -> String { let n = self.temp_counter; self.temp_counter += 1; @@ -738,6 +778,8 @@ impl CodeGenerator { } fn emit_function(&mut self, f: &ir::FnDef) { + let saved_source = self.current_source; + self.current_source = self.source_map.location(f.span); let ret = self.fn_return_c(f); let params = self.fn_params_c(f); let name = if f.name == "main" { @@ -756,6 +798,9 @@ impl CodeGenerator { // Emit body let body_val = self.emit_block_body(&f.body); + if let Some(tail) = f.body.tail_expr.as_deref() { + self.current_source = self.source_map.location(tail.span()); + } // If the body has a tail expression and function returns non-unit, emit return if f.body.tail_expr.is_some() { @@ -780,6 +825,7 @@ impl CodeGenerator { self.blank(); self.current_fn_return_type = None; self.deferred_exprs = saved_deferred; + self.current_source = saved_source; } /// Emit all deferred expressions in LIFO order. @@ -839,6 +885,7 @@ impl CodeGenerator { // ----------------------------------------------------------------------- fn emit_main_wrapper(&mut self) { + self.current_source = None; self.line("int main(int argc, char *argv[]) {"); self.indent += 1; self.line("osc_global_argc = argc;"); @@ -893,7 +940,11 @@ impl CodeGenerator { self.emit_stmt(stmt); } if let Some(tail) = &block.tail_expr { - self.emit_expr(tail) + let saved_source = self.current_source; + self.current_source = self.source_map.location(tail.span()); + let result = self.emit_expr(tail); + self.current_source = saved_source; + result } else { "(void)0".to_string() } @@ -945,6 +996,8 @@ impl CodeGenerator { // ----------------------------------------------------------------------- fn emit_stmt(&mut self, stmt: &ir::Stmt) { + let saved_source = self.current_source; + self.current_source = self.source_map.location(stmt.span()); match stmt { ir::Stmt::Let(ls) => { let c_ty = self.type_to_c(&ls.ty); @@ -1117,6 +1170,7 @@ impl CodeGenerator { self.deferred_exprs.push(c_code); } } + self.current_source = saved_source; } fn emit_place(&mut self, place: &ir::Place, stored_ty: &BcType) -> String { @@ -1156,6 +1210,14 @@ impl CodeGenerator { // ----------------------------------------------------------------------- fn emit_expr(&mut self, expr: &ir::Expr) -> String { + let saved_source = self.current_source; + self.current_source = self.source_map.location(expr.span()); + let result = self.emit_expr_inner(expr); + self.current_source = saved_source; + result + } + + fn emit_expr_inner(&mut self, expr: &ir::Expr) -> String { match expr { ir::Expr::IntLit(v, _) => format!("{}", v), ir::Expr::FloatLit(v, _) => { diff --git a/src/debuginfo.rs b/src/debuginfo.rs new file mode 100644 index 0000000..db5caa4 --- /dev/null +++ b/src/debuginfo.rs @@ -0,0 +1,186 @@ +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use crate::token::Span; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum DebugInfo { + #[default] + None, + LineTables, +} + +impl DebugInfo { + pub fn is_enabled(self) -> bool { + self != Self::None + } +} + +impl FromStr for DebugInfo { + type Err = String; + + fn from_str(value: &str) -> Result { + match value { + "none" => Ok(Self::None), + "line-tables" => Ok(Self::LineTables), + other => Err(format!( + "unknown debug-info level '{other}' (supported: none, line-tables)" + )), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct SourceFileId(u32); + +impl SourceFileId { + pub fn index(self) -> usize { + self.0 as usize + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct SourceLine { + pub file: SourceFileId, + pub line: u32, +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct SourceLocation { + pub file: SourceFileId, + pub line: u32, + pub column: u32, +} + +#[derive(Clone, Debug, Default)] +pub struct SourceMap { + files: Vec, + lines: Vec>, +} + +impl SourceMap { + pub fn location(&self, span: Span) -> Option { + let flat_line = usize::try_from(span.line).ok()?.checked_sub(1)?; + let origin = self.lines.get(flat_line)?.as_ref()?; + Some(SourceLocation { + file: origin.file, + line: origin.line, + column: u32::try_from(span.column.max(1)).unwrap_or(u32::MAX), + }) + } + + pub fn path(&self, file: SourceFileId) -> &Path { + &self.files[file.index()] + } + + pub fn files(&self) -> impl Iterator { + self.files + .iter() + .enumerate() + .map(|(index, path)| (SourceFileId(index as u32), path.as_path())) + } + + pub fn primary_path(&self) -> Option<&Path> { + self.files.first().map(PathBuf::as_path) + } +} + +#[derive(Default)] +pub struct SourceMapBuilder { + files: Vec, + file_ids: HashMap, +} + +impl SourceMapBuilder { + pub fn intern_file(&mut self, path: PathBuf) -> SourceFileId { + let path = Self::debugger_path(path); + if let Some(id) = self.file_ids.get(&path) { + return *id; + } + let id = SourceFileId( + u32::try_from(self.files.len()).expect("too many source files for debug information"), + ); + self.files.push(path.clone()); + self.file_ids.insert(path, id); + id + } + + pub fn finish(self, lines: Vec>) -> SourceMap { + SourceMap { + files: self.files, + lines, + } + } + + #[cfg(windows)] + fn debugger_path(path: PathBuf) -> PathBuf { + let text = path.to_string_lossy(); + if let Some(rest) = text.strip_prefix(r"\\?\UNC\") { + PathBuf::from(format!(r"\\{rest}")) + } else if let Some(rest) = text.strip_prefix(r"\\?\") { + PathBuf::from(rest) + } else { + path + } + } + + #[cfg(not(windows))] + fn debugger_path(path: PathBuf) -> PathBuf { + path + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolves_flattened_lines_to_original_files() { + let mut builder = SourceMapBuilder::default(); + let root = builder.intern_file(PathBuf::from("root.osc")); + let imported = builder.intern_file(PathBuf::from("lib.osc")); + let map = builder.finish(vec![ + Some(SourceLine { + file: imported, + line: 7, + }), + None, + Some(SourceLine { + file: root, + line: 2, + }), + ]); + + assert_eq!( + map.location(Span::new(1, 4)), + Some(SourceLocation { + file: imported, + line: 7, + column: 4, + }) + ); + assert_eq!(map.location(Span::new(2, 1)), None); + assert_eq!(map.path(root), Path::new("root.osc")); + } + + #[test] + fn parses_supported_debug_levels() { + assert_eq!("none".parse(), Ok(DebugInfo::None)); + assert_eq!("line-tables".parse(), Ok(DebugInfo::LineTables)); + assert!("full".parse::().is_err()); + } + + #[cfg(windows)] + #[test] + fn removes_windows_verbatim_prefix_from_debugger_paths() { + assert_eq!( + SourceMapBuilder::debugger_path(PathBuf::from(r"\\?\C:\src\main.osc")), + PathBuf::from(r"C:\src\main.osc") + ); + assert_eq!( + SourceMapBuilder::debugger_path(PathBuf::from(r"\\?\UNC\server\share\main.osc")), + PathBuf::from(r"\\server\share\main.osc") + ); + } +} diff --git a/src/ir.rs b/src/ir.rs index 67be349..344ec9e 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -144,6 +144,23 @@ pub enum Stmt { Continue(Span), } +impl Stmt { + pub fn span(&self) -> Span { + match self { + Stmt::Let(stmt) => stmt.span, + Stmt::Assign(stmt) => stmt.span, + Stmt::CompoundAssign(stmt) => stmt.span, + Stmt::Expr(stmt) => stmt.span, + Stmt::While(stmt) => stmt.span, + Stmt::For(stmt) => stmt.span, + Stmt::ForIn(stmt) => stmt.span, + Stmt::Return(stmt) => stmt.span, + Stmt::Defer(stmt) => stmt.span, + Stmt::Break(span) | Stmt::Continue(span) => *span, + } + } +} + pub struct LetStmt { pub name: String, pub is_mut: bool, diff --git a/src/main.rs b/src/main.rs index 5dc5c5b..462c55c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod backend; mod c_name; mod codegen; mod collection; +mod debuginfo; mod error; mod ir; mod lexer; @@ -24,6 +25,7 @@ use std::sync::atomic::AtomicUsize; use std::sync::atomic::{AtomicBool, Ordering}; use backend::select::{self, Backend, SelectionInputs}; +use debuginfo::{DebugInfo, SourceLine, SourceMap, SourceMapBuilder}; static VERBOSE: AtomicBool = AtomicBool::new(false); @@ -127,24 +129,45 @@ mod c_embed { #[cfg(feature = "backend-c")] use c_embed::*; +#[derive(Default)] +struct ResolvedSource { + text: String, + lines: Vec>, +} + +impl ResolvedSource { + fn append(&mut self, other: ResolvedSource) { + self.text.push_str(&other.text); + self.lines.extend(other.lines); + } + + fn push_line(&mut self, text: &str, origin: Option) { + self.text.push_str(text); + self.text.push('\n'); + self.lines.push(origin); + } +} + fn resolve_imports( path: &Path, visited: &mut HashSet, namespaces: &mut Vec, -) -> Result { + source_files: &mut SourceMapBuilder, +) -> Result { let canonical = path .canonicalize() .map_err(|e| format!("cannot resolve {}: {}", path.display(), e))?; if !visited.insert(canonical.clone()) { - return Ok(String::new()); + return Ok(ResolvedSource::default()); } + let file_id = source_files.intern_file(canonical.clone()); let source = fs::read_to_string(&canonical) .map_err(|e| format!("cannot read {}: {}", path.display(), e))?; - let mut result = String::new(); + let mut result = ResolvedSource::default(); let base_dir = canonical.parent().unwrap_or(Path::new(".")); - for line in source.lines() { + for (line_index, line) in source.lines().enumerate() { let trimmed = line.trim(); if trimmed.starts_with("use \"") { if let Some(end) = trimmed[5..].find('"') { @@ -157,25 +180,35 @@ fn resolve_imports( // can be imported under different namespaces. let mut ns_visited = visited.clone(); let mut ns_namespaces = Vec::new(); - let imported = - resolve_imports(&full_path, &mut ns_visited, &mut ns_namespaces)?; - let mangled = mangle_top_level_names(&imported, &ns); - result.push_str(&mangled); - result.push('\n'); + let mut imported = resolve_imports( + &full_path, + &mut ns_visited, + &mut ns_namespaces, + source_files, + )?; + imported.text = mangle_top_level_names(&imported.text, &ns); + result.append(imported); + result.push_line("", None); namespaces.push(ns); namespaces.extend(ns_namespaces); continue; } else { // Flat import (existing behavior) - let imported = resolve_imports(&full_path, visited, namespaces)?; - result.push_str(&imported); - result.push('\n'); + let imported = resolve_imports(&full_path, visited, namespaces, source_files)?; + result.append(imported); + result.push_line("", None); continue; } } } - result.push_str(line); - result.push('\n'); + result.push_line( + line, + Some(SourceLine { + file: file_id, + line: u32::try_from(line_index + 1) + .expect("source file has too many lines for debug information"), + }), + ); } Ok(result) } @@ -311,7 +344,7 @@ fn print_usage(to_stderr: bool) { // option list below, so a backend-specific build never advertises a // flag it can only ever refuse. let mut usage = String::from( - "usage: oscan [--help] [-h] [--version] [-V] [--verbose] [--warnings] [-W] [--dump-tokens] [--dump-ast] [--run]", + "usage: oscan [--help] [-h] [--version] [-V] [--verbose] [--warnings] [-W] [--dump-tokens] [--dump-ast] [--run] [--debuginfo none|line-tables]", ); if has_c { usage.push_str(" [--emit-c]"); @@ -352,6 +385,7 @@ fn print_usage(to_stderr: bool) { " --libc Use the hosted libc runtime" }); } + print_line(" --debuginfo Debug information: none or line-tables (default: none)"); if has_c { print_line(" --target Cross-compile for target (riscv64, wasi) — C backend only"); } @@ -444,6 +478,7 @@ fn main() { let mut emit_llvm_ir = false; let mut use_libc = false; let mut show_warnings = false; + let mut debug_info = DebugInfo::None; let mut verbose = false; let mut target: Option = None; let mut explicit_backend: Option = None; @@ -467,6 +502,17 @@ fn main() { "--libc" => use_libc = true, "--allow-elevated-native-link" => allow_elevated_native_link = true, "--warnings" | "-W" => show_warnings = true, + "--debuginfo" => { + i += 1; + let value = args.get(i).unwrap_or_else(|| { + eprintln!("error: --debuginfo requires an argument (none, line-tables)"); + process::exit(1); + }); + debug_info = value.parse().unwrap_or_else(|error: String| { + eprintln!("error: {error}"); + process::exit(1); + }); + } "--verbose" => verbose = true, "--version" | "-V" => { // The build-metadata block lets tests and packaged release @@ -785,20 +831,26 @@ fn main() { // Set global verbose flag so all functions can use is_verbose() VERBOSE.store(verbose, Ordering::Relaxed); - let source = { + let (source, source_map) = { if is_verbose() { eprintln!("[verbose] Resolving imports for {}", path); } let file_path_buf = PathBuf::from(&path); let mut visited = HashSet::new(); let mut namespaces = Vec::new(); - match resolve_imports(&file_path_buf, &mut visited, &mut namespaces) { - Ok(s) => { - let mut s = s; + let mut source_files = SourceMapBuilder::default(); + match resolve_imports( + &file_path_buf, + &mut visited, + &mut namespaces, + &mut source_files, + ) { + Ok(mut resolved) => { for ns in &namespaces { - s = replace_ns_dot(&s, ns); + resolved.text = replace_ns_dot(&resolved.text, ns); } - s + let source_map = source_files.finish(resolved.lines); + (resolved.text, source_map) } Err(e) => { eprintln!("error: {e}"); @@ -884,6 +936,8 @@ fn main() { llvm_backend, native_target, runtime_mode, + debug_info, + &source_map, &path, output_path, run_mode, @@ -924,7 +978,8 @@ fn main() { } }; - let c_code = codegen::CodeGenerator::generate(&ir_program, freestanding); + let c_code = + codegen::CodeGenerator::generate(&ir_program, freestanding, debug_info, &source_map); if is_verbose() { eprintln!( "[verbose] Generated C code ({} bytes, freestanding={})", @@ -947,6 +1002,7 @@ fn main() { &path, &c_code, freestanding, + debug_info, show_warnings, &program_args, &extra_c_files, @@ -1003,6 +1059,7 @@ fn main() { &exe_path, freestanding, target.as_ref(), + debug_info, show_warnings, &extra_c_files, &extra_cflags, @@ -1092,6 +1149,8 @@ fn run_object_backend( llvm_backend: PreloadedLlvmBackend, native_target: backend::NativeTarget, runtime_mode: backend::RuntimeMode, + debug_info: DebugInfo, + source_map: &SourceMap, source_path: &str, output_path: Option, run_mode: bool, @@ -1116,7 +1175,13 @@ fn run_object_backend( let compile_output = match backend_kind { #[cfg(feature = "backend-cranelift")] Backend::Cranelift => { - match backend::compile_object(ir_program, native_target, runtime_mode) { + match backend::compile_object( + ir_program, + native_target, + runtime_mode, + debug_info, + source_map, + ) { Ok(output) => output, Err(e) => { eprintln!("{}", e.with_file(source_path)); @@ -1150,6 +1215,8 @@ fn run_object_backend( native_target, runtime_mode, !emit_llvm_ir, + debug_info, + source_map, ) { Ok(output) => output, Err(e) => { @@ -1327,6 +1394,7 @@ fn run_object_backend( } let link_options = backend::link::NativeLinkOptions { runtime_mode, + debug_info, show_warnings, allow_elevated_native_link, extra_c_files, @@ -1452,6 +1520,7 @@ fn run_object_backend( } let link_options = backend::link::NativeLinkOptions { runtime_mode, + debug_info, show_warnings, allow_elevated_native_link, extra_c_files: &native_extra_c_files, @@ -2090,6 +2159,7 @@ fn compile_to_executable( exe_path: &Path, freestanding: bool, target: Option<&CrossTarget>, + debug_info: DebugInfo, show_warnings: bool, extra_c_files: &[String], extra_cflags: &[String], @@ -2203,6 +2273,7 @@ fn compile_to_executable( &include_dirs, freestanding, target, + debug_info, show_warnings, extra_c_files, extra_cflags, @@ -2225,6 +2296,7 @@ fn run_program( source_path: &str, c_code: &str, freestanding: bool, + debug_info: DebugInfo, show_warnings: bool, program_args: &[String], extra_c_files: &[String], @@ -2338,6 +2410,7 @@ fn run_program( &include_dirs, freestanding, None, + debug_info, show_warnings, extra_c_files, extra_cflags, @@ -2375,6 +2448,7 @@ fn invoke_c_compiler( include_dirs: &[PathBuf], freestanding: bool, target: Option<&CrossTarget>, + debug_info: DebugInfo, show_warnings: bool, extra_c_files: &[String], extra_cflags: &[String], @@ -2397,6 +2471,7 @@ fn invoke_c_compiler( c_file, exe_file, include_dirs, + debug_info, show_warnings, extra_c_files, extra_cflags, @@ -2427,6 +2502,7 @@ fn invoke_c_compiler( runtime_c, include_dirs, &sysroot, + debug_info, show_warnings, extra_c_files, extra_cflags, @@ -2489,6 +2565,7 @@ fn invoke_c_compiler( include_dirs, freestanding, CompilerSource::Host, + debug_info, show_warnings, extra_c_files, extra_cflags, @@ -2510,6 +2587,7 @@ fn invoke_c_compiler( include_dirs, freestanding, *source, + debug_info, show_warnings, extra_c_files, extra_cflags, @@ -2534,6 +2612,7 @@ fn invoke_c_compiler( include_dirs, freestanding, CompilerSource::Host, + debug_info, show_warnings, extra_c_files, extra_cflags, @@ -2555,6 +2634,7 @@ fn invoke_c_compiler( include_dirs, freestanding, *source, + debug_info, show_warnings, extra_c_files, extra_cflags, @@ -2583,6 +2663,7 @@ fn invoke_c_compiler( include_dirs, freestanding, CompilerSource::Host, + debug_info, show_warnings, extra_c_files, extra_cflags, @@ -2607,6 +2688,7 @@ fn invoke_c_compiler( include_dirs, false, freestanding, + debug_info, show_warnings, extra_c_files, extra_cflags, @@ -2624,6 +2706,7 @@ fn compile_with_gcc_or_clang( include_dirs: &[PathBuf], freestanding: bool, _source: CompilerSource, + debug_info: DebugInfo, show_warnings: bool, extra_c_files: &[String], extra_cflags: &[String], @@ -2631,6 +2714,7 @@ fn compile_with_gcc_or_clang( extra_lib_files: &[String], ) -> bool { let mut command = Command::new(cmd); + let is_clang = cmd.to_ascii_lowercase().contains("clang"); if freestanding { // Freestanding: single TU (runtime is #included), no libc @@ -2643,25 +2727,38 @@ fn compile_with_gcc_or_clang( // BearSSL headers use memcpy in inline functions after l_tls.h undefs // the macro alias. Clang C99+ treats implicit function declarations as // errors. Downgrade to warning so the __asm__ linker shims still work. - if cmd.contains("clang") { + if is_clang { command.arg("-Wno-error=implicit-function-declaration"); } // -Oz is Clang-only; GCC uses -Os for size optimization - let size_opt = if cmd.contains("clang") { "-Oz" } else { "-Os" }; - command - .arg(size_opt) - .arg("-fno-builtin") - .arg("-fno-asynchronous-unwind-tables") - .arg("-fomit-frame-pointer") - .arg("-ffunction-sections") - .arg("-fdata-sections") - .arg("-s"); // strip symbols + let size_opt = if is_clang { "-Oz" } else { "-Os" }; + command.arg(size_opt).arg("-fno-builtin"); + if debug_info.is_enabled() { + command + .arg(if is_clang { + "-gline-tables-only" + } else { + "-g1" + }) + .arg("-fno-omit-frame-pointer"); + } else { + command + .arg("-fno-asynchronous-unwind-tables") + .arg("-fomit-frame-pointer"); + } + command.arg("-ffunction-sections").arg("-fdata-sections"); + if !debug_info.is_enabled() { + command.arg("-s"); // strip symbols + } if cfg!(windows) { // GNU-target compilers must omit MinGW startup objects because the // runtime supplies mainCRTStartup/atexit. MSVC-targeting Clang uses // lld-link instead: GNU ld flags are ignored there, while // -nostartfiles also removes the static __chkstk/_fltused helpers. let msvc_abi = compiler_uses_msvc_abi(cmd); + if debug_info.is_enabled() && msvc_abi { + command.arg("-Wl,/INCREMENTAL:NO,/OPT:REF,/OPT:ICF"); + } for arg in windows_freestanding_link_args(msvc_abi) { command.arg(arg); } @@ -2701,6 +2798,18 @@ fn compile_with_gcc_or_clang( if !show_warnings { command.arg("-w"); } + if debug_info.is_enabled() { + command + .arg(if is_clang { + "-gline-tables-only" + } else { + "-g1" + }) + .arg("-fno-omit-frame-pointer"); + if cfg!(windows) && compiler_uses_msvc_abi(cmd) { + command.arg("-Wl,/INCREMENTAL:NO,/OPT:REF,/OPT:ICF"); + } + } command.arg(c_file).arg(runtime_c); for dir in include_dirs { command.arg(format!("-I{}", dir.display())); @@ -2767,6 +2876,7 @@ fn compile_cross_riscv64( c_file: &Path, exe_file: &Path, include_dirs: &[PathBuf], + debug_info: DebugInfo, show_warnings: bool, extra_c_files: &[String], extra_cflags: &[String], @@ -2781,12 +2891,19 @@ fn compile_cross_riscv64( } command .arg("-Os") // RISC-V gcc doesn't support -Oz - .arg("-fno-builtin") - .arg("-fno-asynchronous-unwind-tables") - .arg("-fomit-frame-pointer") - .arg("-ffunction-sections") - .arg("-fdata-sections") - .arg("-s") + .arg("-fno-builtin"); + if debug_info.is_enabled() { + command.arg("-g1").arg("-fno-omit-frame-pointer"); + } else { + command + .arg("-fno-asynchronous-unwind-tables") + .arg("-fomit-frame-pointer"); + } + command.arg("-ffunction-sections").arg("-fdata-sections"); + if !debug_info.is_enabled() { + command.arg("-s"); + } + command .arg("-march=rv64gc") .arg("-mabi=lp64d") .arg("-nostdlib") @@ -2837,6 +2954,7 @@ fn compile_cross_wasi( runtime_c: &Path, include_dirs: &[PathBuf], sysroot: &str, + debug_info: DebugInfo, show_warnings: bool, extra_c_files: &[String], extra_cflags: &[String], @@ -2852,6 +2970,11 @@ fn compile_cross_wasi( if !show_warnings { command.arg("-w"); } + if debug_info.is_enabled() { + command + .arg("-gline-tables-only") + .arg("-fno-omit-frame-pointer"); + } command.arg(c_file).arg(runtime_c); for dir in include_dirs { command.arg(format!("-I{}", dir.display())); @@ -2899,6 +3022,7 @@ fn compile_with_msvc( include_dirs: &[PathBuf], freestanding: bool, needs_nofreestanding: bool, + debug_info: DebugInfo, show_warnings: bool, extra_c_files: &[String], extra_cflags: &[String], @@ -2911,6 +3035,12 @@ fn compile_with_msvc( "" }; let warn_flag = if show_warnings { "" } else { " /w" }; + let debug_compile_flag = if debug_info.is_enabled() { " /Z7" } else { "" }; + let debug_link_flag = if debug_info.is_enabled() { + " /DEBUG:FULL /INCREMENTAL:NO /OPT:REF /OPT:ICF" + } else { + "" + }; // Build extra files/flags strings for MSVC let extra_c_str: String = extra_c_files @@ -2931,16 +3061,16 @@ fn compile_with_msvc( let bat_content = if freestanding { // Freestanding: single TU, no CRT, optimize for size format!( - "@echo off\r\ncall \"{}\" x64 >nul 2>&1\r\n\"{}\" /nologo{} /std:c11 /Os /GS-{} \"{}\"{}{} /Fe:\"{}\" /link /NODEFAULTLIB kernel32.lib ws2_32.lib secur32.lib crypt32.lib\r\n", - vcvars_path, cl_path, warn_flag, - all_includes, c_file.display(), extra_c_str, extra_flags_str, exe_file.display(), + "@echo off\r\ncall \"{}\" x64 >nul 2>&1\r\n\"{}\" /nologo{} /std:c11 /Os /GS-{}{} \"{}\"{}{} /Fe:\"{}\" /link{} /NODEFAULTLIB kernel32.lib ws2_32.lib secur32.lib crypt32.lib\r\n", + vcvars_path, cl_path, warn_flag, debug_compile_flag, + all_includes, c_file.display(), extra_c_str, extra_flags_str, exe_file.display(), debug_link_flag, ) } else { // libc mode: two TUs, default CRT format!( - "@echo off\r\ncall \"{}\" x64 >nul 2>&1\r\n\"{}\" /nologo{} /std:c11{}{} \"{}\" \"{}\"{}{} /Fe:\"{}\" /link\r\n", - vcvars_path, cl_path, warn_flag, nofree_flag, - all_includes, c_file.display(), runtime_c.display(), extra_c_str, extra_flags_str, exe_file.display(), + "@echo off\r\ncall \"{}\" x64 >nul 2>&1\r\n\"{}\" /nologo{} /std:c11{}{}{} \"{}\" \"{}\"{}{} /Fe:\"{}\" /link{}\r\n", + vcvars_path, cl_path, warn_flag, nofree_flag, debug_compile_flag, + all_includes, c_file.display(), runtime_c.display(), extra_c_str, extra_flags_str, exe_file.display(), debug_link_flag, ) }; @@ -2982,13 +3112,24 @@ fn compile_with_msvc( .arg("/std:c11") .arg("/Os") // optimize for size .arg("/GS-"); + if debug_info.is_enabled() { + command.arg("/Z7"); + } for dir in include_dirs { command.arg(format!("/I{}", dir.display())); } command .arg(c_file) .arg(format!("/Fe:{}", exe_file.display())) - .arg("/link") + .arg("/link"); + if debug_info.is_enabled() { + command + .arg("/DEBUG:FULL") + .arg("/INCREMENTAL:NO") + .arg("/OPT:REF") + .arg("/OPT:ICF"); + } + command .arg("/NODEFAULTLIB") .arg("kernel32.lib") .arg("ws2_32.lib") @@ -3000,6 +3141,9 @@ fn compile_with_msvc( command.arg("/w"); } command.arg("/std:c11"); + if debug_info.is_enabled() { + command.arg("/Z7"); + } if needs_nofreestanding { command.arg("/DOSC_NOFREESTANDING"); } @@ -3011,6 +3155,13 @@ fn compile_with_msvc( .arg(runtime_c) .arg(format!("/Fe:{}", exe_file.display())) .arg("/link"); + if debug_info.is_enabled() { + command + .arg("/DEBUG:FULL") + .arg("/INCREMENTAL:NO") + .arg("/OPT:REF") + .arg("/OPT:ICF"); + } } // Add user-supplied extra C source files and compiler flags diff --git a/tests/cli_help.rs b/tests/cli_help.rs index 7854c83..cb7e2de 100644 --- a/tests/cli_help.rs +++ b/tests/cli_help.rs @@ -132,6 +132,8 @@ fn long_help_flag_prints_usage_and_succeeds() { let stdout = help_output(); assert!(stdout.contains("usage: oscan")); assert!(stdout.contains("--extra-obj")); + assert!(stdout.contains("--debuginfo ")); + assert!(stdout.contains("none or line-tables (default: none)")); if cfg!(feature = "backend-c") { assert!(stdout.contains("--target ")); } @@ -547,6 +549,104 @@ fn implicit_emit_c_matches_explicit_c_source_output() { assert_eq!(default_output.stdout, explicit_c_output.stdout); } +#[cfg(feature = "backend-c")] +#[test] +fn c_line_tables_map_root_and_imported_sources_without_changing_the_default() { + let dir = std::env::temp_dir().join(format!("oscan-c-debuginfo-{}", process::id())); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).expect("create source directory"); + let imported_path = dir.join("lib.osc"); + let root_path = dir.join("main.osc"); + fs::write( + &imported_path, + "fn! greet() {\n println(\"from import\");\n}\n", + ) + .expect("write imported source"); + fs::write( + &root_path, + "use \"lib.osc\"\nfn! main() {\n greet();\n}\n", + ) + .expect("write root source"); + + let default = oscan_command() + .arg(&root_path) + .args(["--backend", "c", "--emit-c"]) + .output() + .expect("emit default C"); + let explicit_none = oscan_command() + .arg(&root_path) + .args(["--backend", "c", "--debuginfo", "none", "--emit-c"]) + .output() + .expect("emit explicit no-debug C"); + let debug = oscan_command() + .arg(&root_path) + .args(["--backend", "c", "--debuginfo", "line-tables", "--emit-c"]) + .output() + .expect("emit debug C"); + assert!( + default.status.success(), + "{}", + String::from_utf8_lossy(&default.stderr) + ); + assert!( + debug.status.success(), + "{}", + String::from_utf8_lossy(&debug.stderr) + ); + assert!( + explicit_none.status.success(), + "{}", + String::from_utf8_lossy(&explicit_none.stderr) + ); + assert_eq!( + default.stdout, explicit_none.stdout, + "the explicit default must not change generated C" + ); + + let default_c = String::from_utf8(default.stdout).expect("default C is UTF-8"); + let debug_c = String::from_utf8(debug.stdout).expect("debug C is UTF-8"); + assert!(!default_c.contains("#line "), "{default_c}"); + assert!( + debug_c.contains("#line 1 \"\""), + "compiler-generated C must not inherit the previous Oscan location\n{debug_c}" + ); + for (path, line) in [ + (&imported_path, 1), + (&imported_path, 2), + (&root_path, 2), + (&root_path, 3), + ] { + let path = path + .canonicalize() + .expect("canonical source") + .to_string_lossy() + .replace('\\', "/"); + let path = path.strip_prefix("//?/").unwrap_or(&path); + let marker = format!("#line {line} \"{path}\""); + assert!(debug_c.contains(&marker), "missing {marker:?}\n{debug_c}"); + } + assert!( + !debug_c.contains("program.c"), + "source mappings must never point at the temporary C file\n{debug_c}" + ); + fs::remove_dir_all(&dir).expect("remove source directory"); +} + +#[test] +fn rejects_unknown_debug_info_level() { + let output = oscan_command() + .args(["--debuginfo", "full"]) + .output() + .expect("run oscan"); + assert!(!output.status.success()); + assert!( + String::from_utf8_lossy(&output.stderr) + .contains("unknown debug-info level 'full' (supported: none, line-tables)"), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + #[cfg(any( all(target_os = "windows", target_arch = "x86_64"), all(