diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..570221a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,43 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "wasmtime: debug dwarf-playground/program.wasm", + "type": "lldb", + "request": "launch", + "program": "/opt/homebrew/bin/wasmtime", + "args": [ + "run", + "-D", + "debug-info=y", + "--invoke", + "compute", + "program.wasm", + "5" + ], + // Deliberately no `-O opt-level=0`: at that opt level, any local + // with a multi-field (DW_OP_piece-composite) location — i.e. any + // struct local, and program.wx has one — crashes wasmtime's own + // DWARF transform (see wasmtime-dwarf-transform-bug.md, "Bug 3"). + // Default opt level avoids that path; the tradeoff is some + // scalar locals show "optimized out" right at a function's + // entry (normal — becomes available a few instructions in). + "cwd": "${workspaceFolder}/dwarf-playground", + "stopOnEntry": false, + "initCommands": [ + // Required on macOS for lldb to pick up the GDB/LLDB JIT + // interface that wasmtime uses to register debug info for + // the JIT-compiled module at runtime. + "settings set plugin.jit-loader.gdb.enable on", + // Our DWARF's compile-unit directory is a bare "." (see + // dwarf/mod.rs's LineProgramBuilder — there's no real + // directory/filename split to model for a single-file + // build). lldb resolves that "." against its *own* cwd, + // not the debuggee's `cwd` above, so without this it can't + // find program.wx on disk and falls back to disassembly + // view even though the line-table resolution is correct. + "settings set target.source-map . ${workspaceFolder}/dwarf-playground" + ] + } + ] +} diff --git a/Cargo.lock b/Cargo.lock index 7b8bc1a..0a3a764 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9698bf0769c641b18618039fe2ebd41eb3541f98433000f64e663fab7cea2c87" dependencies = [ + "fallible-iterator", "gimli", ] @@ -34,9 +35,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -542,6 +543,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "fastrand" version = "2.3.0" @@ -2239,7 +2246,9 @@ dependencies = [ name = "wx-compiler" version = "0.4.0" dependencies = [ + "addr2line", "codespan-reporting", + "gimli", "indoc", "insta", "leb128fmt", diff --git a/crates/wx-cli/src/main.rs b/crates/wx-cli/src/main.rs index 7e1b5e4..e0c9a87 100644 --- a/crates/wx-cli/src/main.rs +++ b/crates/wx-cli/src/main.rs @@ -39,6 +39,12 @@ fn main() { for stdout (default: .wasm)", ), ) + .arg( + clap::Arg::new("debug") + .long("debug") + .action(clap::ArgAction::SetTrue) + .help("Skip optimizations"), + ) .arg(message_format.clone()), ) .subcommand( @@ -65,7 +71,12 @@ fn main() { let format = parse_message_format( sub.get_one::("message-format").unwrap(), ); - cmd_compile(path, output, format); + let mode = if sub.get_flag("debug") { + CompilationMode::Debug + } else { + CompilationMode::Release + }; + cmd_compile(path, output, format, mode); } Some(("check", sub)) => { let path = sub.get_one::("path").unwrap(); @@ -242,7 +253,12 @@ fn abort_if_errors(count: usize) { std::process::exit(1); } -fn cmd_compile(file_path: &str, output: Option<&str>, format: MessageFormat) { +fn cmd_compile( + file_path: &str, + output: Option<&str>, + format: MessageFormat, + mode: CompilationMode, +) { let mut compilation = load_compilation(file_path); for crate_graph in &compilation.crates { @@ -266,12 +282,26 @@ fn cmd_compile(file_path: &str, output: Option<&str>, format: MessageFormat) { .count(), ); - let mir = - mir::MIR::build(&tir, &compilation.interner, compilation.id_generator); - let module = codegen::Builder::build(&mir, &compilation.interner).unwrap(); - let bytecode = module.encode(); + let mut mir = mir::MIR::build( + &tir, + &compilation.interner, + compilation.id_generator, + mode, + ); + let mut graph = mir::CallGraph::build(&mir.functions, &mir.call_edges); + if mode == CompilationMode::Release { + mir.inline_calls(&mut graph); + } + mir.dead_code_eliminate(&graph); + let module = + codegen::Builder::build(&mir, &compilation.interner, mode).unwrap(); if output == Some("-") { + // No debug info for stdout output, regardless of mode — same + // reasoning as `--debug` always being self-contained for file + // output: nothing here would have anywhere to attribute embedded + // DWARF, since there's no file path to reason about. + let bytecode = module.encode(); std::io::stdout().write_all(&bytecode).unwrap(); return; } @@ -280,6 +310,34 @@ fn cmd_compile(file_path: &str, output: Option<&str>, format: MessageFormat) { Some(path) => path.to_string(), None => format!("{}.wasm", output_stem(file_path)), }; + + let bytecode = match mode { + CompilationMode::Release => module.encode(), + CompilationMode::Debug => { + let (mut bytecode, debug_spans, function_debug_info) = + module.encode_with_debug_spans(); + let sections = dwarf::build( + &debug_spans, + &function_debug_info, + &mir, + &compilation.interner, + &compilation.files, + ); + for (name, payload) in [ + (".debug_abbrev", §ions.debug_abbrev), + (".debug_info", §ions.debug_info), + (".debug_line", §ions.debug_line), + (".debug_str", §ions.debug_str), + (".debug_line_str", §ions.debug_line_str), + (".debug_aranges", §ions.debug_aranges), + ] { + bytecode + .extend_from_slice(&dwarf::custom_section(name, payload)); + } + bytecode + } + }; + let mut file = fs::File::create(&out_path).unwrap(); file.write_all(&bytecode).unwrap(); eprintln!("Wrote {} bytes to {out_path}", bytecode.len()); diff --git a/crates/wx-compiler-wasm/src/lib.rs b/crates/wx-compiler-wasm/src/lib.rs index dad545c..8cb996b 100644 --- a/crates/wx-compiler-wasm/src/lib.rs +++ b/crates/wx-compiler-wasm/src/lib.rs @@ -100,10 +100,21 @@ pub fn compile( .into_js()); } - let mir = - mir::MIR::build(&hir, &compilation.interner, compilation.id_generator); - let module = codegen::Builder::build(&mir, &compilation.interner) - .map_err(|_| "codegen failed".to_string())?; + let mut mir = mir::MIR::build( + &hir, + &compilation.interner, + compilation.id_generator, + CompilationMode::Release, + ); + let mut graph = mir::CallGraph::build(&mir.functions, &mir.call_edges); + mir.inline_calls(&mut graph); + mir.dead_code_eliminate(&graph); + let module = codegen::Builder::build( + &mir, + &compilation.interner, + CompilationMode::Release, + ) + .map_err(|_| "codegen failed".to_string())?; let bytecode = module.encode(); Ok(CompilationResult { diff --git a/crates/wx-compiler/Cargo.toml b/crates/wx-compiler/Cargo.toml index e396003..5bc98a0 100644 --- a/crates/wx-compiler/Cargo.toml +++ b/crates/wx-compiler/Cargo.toml @@ -24,3 +24,7 @@ serde_json = { workspace = true } insta = { version = "1.43", features = ["yaml"] } wasmtime = "46.0.1" wasmprinter = "0.252.0" +gimli = { version = "0.33", default-features = false, features = ["read"] } +addr2line = { version = "0.26", default-features = false, features = [ + "fallible-iterator", +] } diff --git a/crates/wx-compiler/src/codegen/mod.rs b/crates/wx-compiler/src/codegen/mod.rs index 7203fbf..97e585a 100644 --- a/crates/wx-compiler/src/codegen/mod.rs +++ b/crates/wx-compiler/src/codegen/mod.rs @@ -2,37 +2,12 @@ use std::collections::HashMap; use leb128fmt; -use crate::{ast, mir}; - -#[derive(Clone, Copy, PartialEq, Hash, Eq)] -#[cfg_attr(debug_assertions, derive(Debug))] -#[cfg_attr(test, derive(serde::Serialize))] -pub enum ValueType { - I32, - I64, - F32, - F64, -} - -#[derive(Clone, Copy)] -#[cfg_attr(debug_assertions, derive(Debug))] -#[cfg_attr(test, derive(serde::Serialize))] -pub enum BlockResult { - Empty, - SingleValue(ValueType), - MultiValue(SignatureIndex), -} +use crate::wasm::{self, ScalarType}; +use crate::{ast, mir, vfs}; #[derive(Debug, Clone, Copy, serde::Serialize)] pub struct LocalIndex(pub u32); -#[derive(Clone)] -#[cfg_attr(debug_assertions, derive(Debug))] -#[cfg_attr(test, derive(serde::Serialize))] -pub struct Local { - ty: ValueType, -} - #[derive(Debug, Clone, Copy, serde::Serialize)] pub struct FuncIndex(pub u32); @@ -44,148 +19,19 @@ pub struct GlobalIndex(pub u32); #[derive(Clone, PartialEq, Eq, Hash)] pub struct FunctionSignature { pub param_count: usize, - pub param_results: Box<[ValueType]>, + pub param_results: Box<[ScalarType]>, } impl FunctionSignature { - pub fn params(&self) -> &[ValueType] { + pub fn params(&self) -> &[ScalarType] { self.param_results.get(..self.param_count).unwrap_or(&[]) } - pub fn results(&self) -> &[ValueType] { + pub fn results(&self) -> &[ScalarType] { self.param_results.get(self.param_count..).unwrap_or(&[]) } } -#[derive(Clone)] -#[cfg_attr(debug_assertions, derive(Debug))] -#[cfg_attr(test, derive(serde::Serialize))] -pub enum Expression { - Nop, - I32Const { - value: i32, - }, - I64Const { - value: i64, - }, - F32Const { - value: f32, - }, - F64Const { - value: f64, - }, - LocalGet { - local_index: LocalIndex, - }, - LocalSet { - local_index: LocalIndex, - }, - GlobalGet { - global_index: GlobalIndex, - }, - GlobalSet { - global: GlobalIndex, - }, - Return, - Block { - expressions: Box<[Expression]>, - result: BlockResult, - }, - Break { - depth: u32, - }, - Unreachable, - Loop { - expressions: Box<[Expression]>, - result: BlockResult, - }, - IfElse { - result: BlockResult, - then_branch: Box, - else_branch: Option>, - }, - Drop, - Call { - function: FuncIndex, - }, - CallIndirect { - table_index: TableIndex, - type_index: SignatureIndex, - }, - I32Add, - I32Sub, - I32Mul, - I32DivS, - I32DivU, - I32RemS, - I32RemU, - I32Eq, - I32Ne, - I32And, - I32Or, - I32Xor, - I32Eqz, - I32Shl, - I32ShrS, - I32ShrU, - I32LtS, - I32LtU, - I32GtS, - I32GtU, - I32LeS, - I32LeU, - I32GeS, - I32GeU, - I64Add, - I64Sub, - I64Mul, - I64DivS, - I64DivU, - I64RemS, - I64RemU, - I64Eq, - I64Eqz, - I64Ne, - I64And, - I64Or, - I64Xor, - I64Shl, - I64ShrS, - I64ShrU, - I64LtS, - I64LtU, - I64GtS, - I64GtU, - I64LeS, - I64LeU, - I64GeS, - I64GeU, - F32Add, - F32Sub, - F32Mul, - F64Add, - F64Sub, - F64Mul, - F32Eq, - F64Eq, - F32Ne, - F64Ne, - F32Lt, - F64Lt, - F32Gt, - F64Gt, - F32Le, - F64Le, - F32Ge, - F64Ge, - F32Div, - F64Div, - F32Neg, - F64Neg, - F32Trunc, - F64Trunc, -} - #[cfg_attr(test, derive(serde::Serialize))] pub struct TypeSection { signatures: Box<[FunctionSignature]>, @@ -224,10 +70,51 @@ pub struct ExportSection { items: Box<[ExportItem]>, } +/// One instruction's source position. `offset` starts out relative to the +/// start of its own function's `expressions` bytes (set by +/// `Builder::encode_scheduled`, the only place that still has both the +/// instruction and its span together) and is corrected in place — `offset +/// += ` — as it passes up through +/// `FunctionBody::encode_with_debug_spans` and +/// `CodeSection::encode_with_debug_spans`, ending up absolute within the +/// fully encoded module. +#[cfg_attr(test, derive(serde::Serialize))] +#[derive(Clone, Copy)] +pub struct DebugSpan { + pub offset: u32, + pub file_id: vfs::FileId, + pub span: ast::TextSpan, +} + +/// One function's absolute byte range within the encoded module, plus its +/// declared locals resolved to wasm-local indices — everything about a +/// function that's only knowable during encoding (`start`/`end`, corrected +/// to absolute module offsets the same way `DebugSpan.offset` is) or only +/// available transiently during scheduling (`locals`, from +/// `wasm::Function`, which is otherwise dropped once `encode_scheduled` +/// consumes it). Deliberately doesn't carry the function's name, and uses +/// plain byte-offset terms rather than DWARF's `low_pc`/`high_pc` +/// vocabulary: the caller already has `&mir::MIR` (whose +/// `mir.functions[i].name` lines up index-for-index with this) and knows +/// what it's going to do with these facts — codegen doesn't need to know +/// anything about that (e.g. DWARF). +#[cfg_attr(test, derive(serde::Serialize))] +#[derive(Clone)] +pub struct FunctionDebugInfo { + pub start: u32, + pub end: u32, + pub locals: Vec, +} + #[cfg_attr(test, derive(serde::Serialize))] pub struct FunctionBody { - locals: Box<[Local]>, + locals: Box<[wasm::Local]>, expressions: Box<[u8]>, + /// One entry per instruction in `expressions`. Empty outside `--debug` + /// builds; see `wasm::Function::spans`. + debug_spans: Vec, + /// Empty outside `--debug` builds; see `wasm::Function::locals_debug`. + locals_debug: Vec, } #[cfg_attr(test, derive(serde::Serialize))] @@ -284,9 +171,11 @@ pub enum Mutability { #[cfg_attr(test, derive(serde::Serialize))] struct Global { - ty: ValueType, + ty: ScalarType, mutability: Mutability, - value: Expression, + /// Only the four `*Const` variants are valid WASM global init + /// expressions; anything else is a construction bug upstream. + value: crate::wasm::Instruction, } #[derive(Clone)] @@ -296,7 +185,7 @@ pub enum ImportDesc { signature_index: SignatureIndex, }, Global { - ty: ValueType, + ty: ScalarType, mutability: Mutability, }, Memory { @@ -344,7 +233,10 @@ struct MemoryEntry { static_size: u32, } -pub struct Builder { +pub struct Builder<'a> { + /// The MIR being encoded — fixed for the whole module, so it lives here + /// once instead of being re-passed to every encoding call. + mir: &'a mir::MIR, table: Vec, /// Byte offset of each live static entry in the assembled data segment. /// Keyed by `MIR.static_entries` index. @@ -365,50 +257,7 @@ pub struct Builder { signatures: HashMap, } -impl TryFrom for ValueType { - type Error = (); - - fn try_from(value: mir::Type) -> Result { - match value { - mir::Type::Bool - | mir::Type::I8 - | mir::Type::U8 - | mir::Type::I16 - | mir::Type::U16 - | mir::Type::I32 - | mir::Type::U32 - | mir::Type::Function { .. } => Ok(ValueType::I32), - mir::Type::I64 | mir::Type::U64 => Ok(ValueType::I64), - mir::Type::Pointer { kind, .. } => match kind { - mir::MemoryKind::Memory32 => Ok(ValueType::I32), - mir::MemoryKind::Memory64 => Ok(ValueType::I64), - }, - mir::Type::F32 => Ok(ValueType::F32), - mir::Type::F64 => Ok(ValueType::F64), - _ => unreachable!(), - } - } -} - -impl Builder { - /// Recursively expand a MIR type into its flat wasm `ValueType`s. - /// Unit/Never produce zero slots; Aggregate recurses into its fields. - fn flatten_type( - ty: mir::Type, - aggregates: &[mir::Aggregate], - ) -> Vec { - match ty { - mir::Type::Unit | mir::Type::Never => vec![], - mir::Type::Aggregate { aggregate_index } => aggregates - [aggregate_index as usize] - .values - .iter() - .flat_map(|&f| Self::flatten_type(f, aggregates)) - .collect(), - t => vec![ValueType::try_from(t).unwrap()], - } - } - +impl<'a> Builder<'a> { /// Intern a function signature built from a MIR signature + the aggregate /// pool, correctly flattening any aggregate params/results into /// individual wasm types. @@ -419,10 +268,15 @@ impl Builder { ) -> SignatureIndex { let mut param_results = Vec::new(); for ¶m in sig.params() { - param_results.extend(Self::flatten_type(param, aggregates)); + param_results.extend(crate::wasm::flatten_type_to_scalars( + param, aggregates, + )); } let param_count = param_results.len(); - param_results.extend(Self::flatten_type(sig.result(), aggregates)); + param_results.extend(crate::wasm::flatten_type_to_scalars( + sig.result(), + aggregates, + )); let signature = FunctionSignature { param_count, param_results: param_results.into_boxed_slice(), @@ -432,8 +286,9 @@ impl Builder { } pub fn build( - mir: &mir::MIR, + mir: &'a mir::MIR, interner: &ast::StringInterner, + mode: crate::CompilationMode, ) -> Result { // Layout static data segment: collect live entries from all functions, // sort largest-align-first for minimal padding, then lay out bytes. @@ -463,6 +318,7 @@ impl Builder { } let mut builder = Builder { + mir, table: Vec::new(), entry_offsets, func_wasm_index: HashMap::new(), @@ -523,7 +379,7 @@ impl Builder { module: import_module.name.clone(), name: interner.resolve(*name).unwrap().to_string(), desc: ImportDesc::Global { - ty: ValueType::I32, + ty: ScalarType::I32, mutability: Mutability::Immutable, }, }); @@ -590,10 +446,17 @@ impl Builder { ); function_signatures.push(signature_index); - let opt_func = crate::opt::builder::Builder::build(mir, func); - let scheduled = - crate::opt::scheduler::Scheduler::schedule(&opt_func, mir); - let body = builder.encode_scheduled(&scheduled, mir); + let scheduled = match mode { + crate::CompilationMode::Release => { + let opt_func = + crate::opt::builder::Builder::build(mir, func); + crate::opt::scheduler::Scheduler::schedule(&opt_func, mir) + } + crate::CompilationMode::Debug => { + crate::mir::scheduler::schedule(func, mir) + } + }; + let body = builder.encode_scheduled(scheduled, func.file_id); functions.push(body); } @@ -605,24 +468,24 @@ impl Builder { .map(|global| { let init_value = match ( global.const_init, - ValueType::try_from(global.ty).unwrap(), + ScalarType::try_from(global.ty).unwrap(), ) { - (mir::ConstInit::Int(v), ValueType::I32) => { - Expression::I32Const { value: v as i32 } + (mir::ConstInit::Int(v), ScalarType::I32) => { + crate::wasm::Instruction::I32Const(v as i32) } - (mir::ConstInit::Int(v), ValueType::I64) => { - Expression::I64Const { value: v } + (mir::ConstInit::Int(v), ScalarType::I64) => { + crate::wasm::Instruction::I64Const(v) } - (mir::ConstInit::Float(v), ValueType::F32) => { - Expression::F32Const { value: v as f32 } + (mir::ConstInit::Float(v), ScalarType::F32) => { + crate::wasm::Instruction::F32Const(v as f32) } - (mir::ConstInit::Float(v), ValueType::F64) => { - Expression::F64Const { value: v } + (mir::ConstInit::Float(v), ScalarType::F64) => { + crate::wasm::Instruction::F64Const(v) } _ => unreachable!(), }; Global { - ty: ValueType::try_from(global.ty).unwrap(), + ty: ScalarType::try_from(global.ty).unwrap(), mutability: match global.mutability { mir::Mutability::Mutable => Mutability::Mutable, mir::Mutability::Immutable => Mutability::Immutable, @@ -742,35 +605,45 @@ impl Builder { /// pools already built on `self`. fn encode_scheduled( &mut self, - scheduled: &crate::opt::scheduler::ScheduledFunction, - mir: &mir::MIR, + func: wasm::Function, + file_id: vfs::FileId, ) -> FunctionBody { - let locals: Box<[Local]> = scheduled - .locals - .iter() - .map(|l| Local { - ty: ValueType::from(l.ty), - }) - .collect(); - + // `spans` is either empty (opt::scheduler's Release output) or + // exactly `body`-length (mir::scheduler's --debug output) — never + // partially populated, so indexing by position is safe here. + let has_spans = !func.spans.is_empty(); let mut sink = Vec::new(); - for instr in scheduled.body.iter().cloned() { - self.encode_scheduled_instr(instr, mir, &mut sink); + let mut debug_spans = Vec::new(); + for (i, instr) in func.body.iter().cloned().enumerate() { + if has_spans { + debug_spans.push(DebugSpan { + offset: sink.len() as u32, + file_id, + span: func.spans[i], + }); + } + self.encode_scheduled_instr( + instr, + &func.br_table_depths, + &mut sink, + ); } FunctionBody { - locals, + locals: func.locals.into_boxed_slice(), expressions: sink.into_boxed_slice(), + debug_spans, + locals_debug: func.locals_debug, } } fn encode_scheduled_instr( &mut self, - instr: crate::opt::scheduler::Instruction, - mir: &mir::MIR, + instr: crate::wasm::Instruction, + br_table_depths: &[u32], sink: &mut Vec, ) { - use crate::opt::scheduler::Instruction as SI; + use crate::wasm::Instruction as SI; match instr { SI::I32Const(v) => { sink.push(Instruction::I32Const as u8); @@ -892,15 +765,15 @@ impl Builder { SI::F64Ge => sink.push(Instruction::F64Ge as u8), SI::Block { ty } => { sink.push(Instruction::Block as u8); - Self::encode_block_type(ty, sink); + self.encode_block_type(ty, sink); } SI::Loop { ty } => { sink.push(Instruction::Loop as u8); - Self::encode_block_type(ty, sink); + self.encode_block_type(ty, sink); } SI::If { ty } => { sink.push(Instruction::If as u8); - Self::encode_block_type(ty, sink); + self.encode_block_type(ty, sink); } SI::Else => sink.push(Instruction::Else as u8), SI::End => sink.push(Instruction::End as u8), @@ -912,13 +785,14 @@ impl Builder { sink.push(Instruction::BrIf as u8); depth.encode(sink); } - SI::BrTable(depths) => { + SI::BrTable { start, len } => { sink.push(Instruction::BrTable as u8); // WASM's `br_table` encodes as `vec(labelidx) labelidx` — a - // table of length `depths.len() - 1` followed by the - // default depth as a separate trailing immediate. The - // scheduler folds both into one slice (its trailing element - // *is* the default) since both stages need the same split. + // table followed by the default depth as a separate trailing + // immediate. The scheduler's range's last element *is* the + // default, since both stages need the same split. + let depths = + &br_table_depths[start as usize..(start + len) as usize]; let (default_depth, table) = depths.split_last().expect("BrTable is never empty"); (table.len() as u32).encode(sink); @@ -935,10 +809,10 @@ impl Builder { sink.push(Instruction::Call as u8); wasm_idx.encode(sink); } - SI::CallIndirectSym { mir_sig_index } => { + SI::CallIndirectSym { signature_index } => { let type_index = self.register_signature( - &mir.signatures[mir_sig_index as usize], - &mir.aggregates, + &self.mir.signatures[signature_index as usize], + &self.mir.aggregates, ); sink.push(Instruction::CallIndirect as u8); type_index.0.encode(sink); @@ -1106,7 +980,7 @@ impl Builder { SI::StaticDataPointer { data_index, ty } => { let offset = self.entry_offsets[&data_index]; match ty { - crate::opt::ScalarType::I64 => { + crate::wasm::ScalarType::I64 => { sink.push(Instruction::I64Const as u8); (offset as i64).encode(sink); } @@ -1133,13 +1007,21 @@ impl Builder { } fn encode_block_type( - ty: crate::opt::scheduler::BlockType, + &mut self, + ty: crate::wasm::BlockType, sink: &mut Vec, ) { - use crate::opt::scheduler::BlockType; + use crate::wasm::BlockType; match ty { BlockType::Empty => sink.push(0x40), BlockType::Value(vt) => vt.encode(sink), + BlockType::MultiValue(signature_index) => { + let type_index = self.register_signature( + &self.mir.signatures[signature_index as usize], + &self.mir.aggregates, + ); + type_index.0.encode(sink); + } } } } @@ -1441,31 +1323,19 @@ impl Encode for u32 { } } -impl Encode for ValueType { +impl Encode for ScalarType { fn encode(&self, sink: &mut Vec) { let opcode = match self { - ValueType::I32 => 0x7F, - ValueType::I64 => 0x7E, - ValueType::F32 => 0x7D, - ValueType::F64 => 0x7C, + ScalarType::I32 => 0x7F, + ScalarType::I64 => 0x7E, + ScalarType::F32 => 0x7D, + ScalarType::F64 => 0x7C, }; sink.push(opcode); } } -impl Encode for BlockResult { - fn encode(&self, sink: &mut Vec) { - match self { - BlockResult::Empty => sink.push(0x40), - BlockResult::SingleValue(ty) => ty.encode(sink), - // Multi-value block types are encoded as a type-section index (s33). - // Type indices are always small positive integers, so s33 == u32 LEB128. - BlockResult::MultiValue(idx) => idx.0.encode(sink), - } - } -} - impl Encode for FunctionSignature { fn encode(&self, sink: &mut Vec) { sink.push(0x60); // Function type @@ -1616,19 +1486,19 @@ impl Encode for Global { }); match self.value { - Expression::I32Const { value } => { + crate::wasm::Instruction::I32Const(value) => { sink.push(Instruction::I32Const as u8); value.encode(sink); } - Expression::F32Const { value } => { + crate::wasm::Instruction::F32Const(value) => { sink.push(Instruction::F32Const as u8); value.encode(sink); } - Expression::I64Const { value } => { + crate::wasm::Instruction::I64Const(value) => { sink.push(Instruction::I64Const as u8); value.encode(sink); } - Expression::F64Const { value } => { + crate::wasm::Instruction::F64Const(value) => { sink.push(Instruction::F64Const as u8); value.encode(sink); } @@ -1667,19 +1537,11 @@ impl Encode for StartSection { } impl FunctionBody { - fn encode( - &self, - sink: &mut Vec, - module: &WasmModule, - func_index: FuncIndex, - ) { - let mut body_content: Vec = Vec::new(); - - let type_index = module.functions.types[func_index.0 as usize]; - let func_type = module.types.signatures[type_index.0 as usize].clone(); + fn encode_locals(&self, param_count: usize) -> Vec { + let mut sink = Vec::new(); - let mut grouped_locals = Vec::<(ValueType, u32)>::new(); - for local in self.locals.iter().skip(func_type.param_count) { + let mut grouped_locals = Vec::<(ScalarType, u32)>::new(); + for local in self.locals.iter().skip(param_count) { match grouped_locals.last_mut() { Some((last_ty, count)) if *last_ty == local.ty => { *count += 1; @@ -1690,12 +1552,66 @@ impl FunctionBody { } } - (grouped_locals.len() as u32).encode(&mut body_content); + (grouped_locals.len() as u32).encode(&mut sink); for (group_type, count) in grouped_locals { - count.encode(&mut body_content); - group_type.encode(&mut body_content); + count.encode(&mut sink); + group_type.encode(&mut sink); + } + + sink + } + + /// Plain encode — returns nothing, never touches `debug_spans`. + fn encode( + &self, + sink: &mut Vec, + module: &WasmModule, + func_index: FuncIndex, + ) { + self.encode_body(sink, module, func_index); + } + + /// Same as `encode`, but also returns `self.debug_spans` and a + /// `FunctionDebugInfo` for this function, both corrected to offsets + /// relative to `sink` (the caller still needs to add where `sink` + /// itself lands in its own caller, exactly as + /// `CodeSection::encode_with_debug_spans` does). + fn encode_with_debug_spans( + &self, + sink: &mut Vec, + module: &WasmModule, + func_index: FuncIndex, + ) -> (Vec, FunctionDebugInfo) { + let expr_start = self.encode_body(sink, module, func_index); + let mut spans = self.debug_spans.clone(); + for span in &mut spans { + span.offset += expr_start; } + let info = FunctionDebugInfo { + start: expr_start, + end: expr_start + self.expressions.len() as u32, + locals: self.locals_debug.clone(), + }; + (spans, info) + } + + /// Writes the actual body bytes, returning where `expressions`' first + /// byte lands in `sink` — the one piece of position information + /// `debug_spans`' offsets (relative to `expressions`) still need to + /// become absolute. Computed from values already needed for the real + /// encoding (`body_content`'s length up to that point, and `sink`'s + /// length right before it's extended), not re-derived separately. + fn encode_body( + &self, + sink: &mut Vec, + module: &WasmModule, + func_index: FuncIndex, + ) -> u32 { + let type_index = module.functions.types[func_index.0 as usize]; + let func_type = module.types.signatures[type_index.0 as usize].clone(); + let mut body_content = self.encode_locals(func_type.param_count); + let locals_offset = body_content.len() as u32; body_content.extend_from_slice( &module.code.functions[func_index.0 as usize].expressions, ); @@ -1703,15 +1619,16 @@ impl FunctionBody { let body_size = body_content.len() as u32; body_size.encode(sink); + let body_start = sink.len() as u32; sink.extend_from_slice(&body_content); - } -} -trait ContextEncode { - fn encode(&self, sink: &mut Vec, module: &WasmModule); + body_start + locals_offset + } } -impl ContextEncode for CodeSection { +impl CodeSection { + /// Plain encode — never touches `debug_spans`, so a `--debug` module + /// encoded this way pays nothing for spans it wasn't asked to resolve. fn encode(&self, sink: &mut Vec, module: &WasmModule) { sink.push(SectionId::Code as u8); @@ -1726,6 +1643,48 @@ impl ContextEncode for CodeSection { section_size.encode(sink); sink.extend_from_slice(§ion_sink); } + + /// Same as `encode`, but also returns every function's `debug_spans` and + /// `FunctionDebugInfo`, corrected to offsets relative to `sink` (the + /// caller still needs to add where `sink` itself lands in its own + /// caller, exactly as `WasmModule::encode_with_debug_spans` does). + fn encode_with_debug_spans( + &self, + sink: &mut Vec, + module: &WasmModule, + ) -> (Vec, Vec) { + sink.push(SectionId::Code as u8); + + let mut section_sink: Vec = Vec::new(); + let function_count = self.functions.len() as u32; + function_count.encode(&mut section_sink); + + let mut spans = Vec::new(); + let mut infos = Vec::with_capacity(self.functions.len()); + for (index, func) in self.functions.iter().enumerate() { + let (func_spans, info) = func.encode_with_debug_spans( + &mut section_sink, + module, + FuncIndex(index as u32), + ); + spans.extend(func_spans); + infos.push(info); + } + + let section_size = section_sink.len() as u32; + section_size.encode(sink); + let content_start = sink.len() as u32; + sink.extend_from_slice(§ion_sink); + + for s in &mut spans { + s.offset += content_start; + } + for info in &mut infos { + info.start += content_start; + info.end += content_start; + } + (spans, infos) + } } impl Encode for RefType { @@ -1927,53 +1886,82 @@ impl Encode for Memory { } impl WasmModule { - pub fn encode(&self) -> Vec { - let mut sink = [ + /// Every section that precedes Code, appended to `sink` — shared by + /// `encode` and `encode_with_debug_spans`, since neither needs anything + /// module-specific about how this part is written. + fn write_preamble(&self, sink: &mut Vec) { + sink.extend_from_slice(&[ 0x00, 0x61, 0x73, 0x6D, // Magic 0x01, 0x00, 0x00, 0x00, // Version - ] - .to_vec(); + ]); - self.types.encode(&mut sink); + self.types.encode(sink); match self.imports.imports.len() { 0 => {} - _ => self.imports.encode(&mut sink), + _ => self.imports.encode(sink), } match self.functions.types.len() { 0 => {} - _ => self.functions.encode(&mut sink), + _ => self.functions.encode(sink), } match self.tables.tables.len() { 0 => {} - _ => self.tables.encode(&mut sink), + _ => self.tables.encode(sink), } match self.memory.memories.len() { 0 => {} - _ => self.memory.encode(&mut sink), + _ => self.memory.encode(sink), } match self.globals.globals.len() { 0 => {} - _ => self.globals.encode(&mut sink), + _ => self.globals.encode(sink), } match self.exports.items.len() { 0 => {} - _ => self.exports.encode(&mut sink), + _ => self.exports.encode(sink), } if let Some(ref start) = self.start { - start.encode(&mut sink); + start.encode(sink); } match self.elements.segments.len() { 0 => {} - _ => self.elements.encode(&mut sink), + _ => self.elements.encode(sink), } - self.code.encode(&mut sink, self); + } + + fn write_data_section(&self, sink: &mut Vec) { match self.data.segments.len() { 0 => {} - _ => self.data.encode(&mut sink), + _ => self.data.encode(sink), } + } + /// Plain encode — routes through `CodeSection::encode`, which never + /// touches `debug_spans`, so this pays nothing for spans it wasn't asked + /// to resolve even when the module is a `--debug` build. + pub fn encode(&self) -> Vec { + let mut sink = Vec::new(); + self.write_preamble(&mut sink); + self.code.encode(&mut sink, self); + self.write_data_section(&mut sink); sink } + + /// Same as `encode`, but also returns every `--debug` instruction's span + /// and every function's `FunctionDebugInfo`, resolved to absolute + /// offsets into the returned bytes — both empty for a + /// `CompilationMode::Release` module, since only `mir::scheduler` + /// populates `FunctionBody::debug_spans`/`locals_debug`. + pub fn encode_with_debug_spans( + &self, + ) -> (Vec, Vec, Vec) { + let mut sink = Vec::new(); + self.write_preamble(&mut sink); + let (debug_spans, function_debug_info) = + self.code.encode_with_debug_spans(&mut sink, self); + self.write_data_section(&mut sink); + (sink, debug_spans, function_debug_info) + } } #[cfg(test)] diff --git a/crates/wx-compiler/src/codegen/tests.rs b/crates/wx-compiler/src/codegen/tests.rs index 4d37a62..6e6ad3f 100644 --- a/crates/wx-compiler/src/codegen/tests.rs +++ b/crates/wx-compiler/src/codegen/tests.rs @@ -66,8 +66,22 @@ impl TestCase { } std::process::exit(1); } - let mir = mir::MIR::build(&tir, &graph.interner, graph.id_generator); - let wasm = Builder::build(&mir, &graph.interner).unwrap(); + let mut mir = mir::MIR::build( + &tir, + &graph.interner, + graph.id_generator, + crate::CompilationMode::Release, + ); + let mut call_graph = + mir::CallGraph::build(&mir.functions, &mir.call_edges); + mir.inline_calls(&mut call_graph); + mir.dead_code_eliminate(&call_graph); + let wasm = Builder::build( + &mir, + &graph.interner, + crate::CompilationMode::Release, + ) + .unwrap(); let bytecode = wasm.encode(); TestCase { @@ -3616,3 +3630,160 @@ fn test_pure_value_shared_via_reassignment_across_if_else_branches() { // else: m=10, side_dist=11, step=1 -> 1101 assert_eq!(f.call(&mut store, (0, 5)).unwrap(), 1101); } + +/// `encode_with_debug_spans`'s offsets are verified against +/// `wasm-objdump -d`'s independently-computed ground truth (not just +/// re-checked against our own encoder): for this function, `wasm-objdump` +/// reports `local.get 0` at file offset `0x28` (40), `i32.const 1` at +/// `0x2a` (42), `i32.add` at `0x2c` (44), `local.set 1` at `0x2d` (45), +/// `local.get 1` at `0x2f` (47), `i32.const 2` at `0x31` (49), `i32.mul` at +/// `0x33` (51) — matching every offset asserted below exactly. +#[test] +fn encode_with_debug_spans_locates_correct_module_offsets() { + let source = indoc! {" + fn compute(x: i32) -> i32 { + local y = x + 1; + y * 2 + } + export { compute } + "}; + let mut builder = vfs::CompilationGraphBuilder::new(); + let stdlib_id = builder.load_stdlib(); + let prefixed = format!("use std::*;\n{source}"); + let root_id = builder + .load_binary( + "main.wx".to_string(), + &vfs::VirtualFileSource::new(HashMap::from([( + "main.wx".to_string(), + prefixed.clone(), + )])), + ) + .unwrap(); + let mut graph = builder.build(root_id, stdlib_id); + let tir = tir::TIR::build(&mut graph); + let mut mir = mir::MIR::build( + &tir, + &graph.interner, + graph.id_generator, + crate::CompilationMode::Debug, + ); + let call_graph = mir::CallGraph::build(&mir.functions, &mir.call_edges); + mir.dead_code_eliminate(&call_graph); + let wasm = + Builder::build(&mir, &graph.interner, crate::CompilationMode::Debug) + .unwrap(); + let (bytecode, debug_spans, function_debug_info) = + wasm.encode_with_debug_spans(); + + // Sanity-check the offsets actually land where `wasm-objdump` says the + // corresponding instruction starts, not just that they're plausible. + assert_eq!(bytecode[40], 0x20); // local.get + assert_eq!(bytecode[42], 0x41); // i32.const + assert_eq!(bytecode[44], 0x6a); // i32.add + assert_eq!(bytecode[45], 0x21); // local.set + assert_eq!(bytecode[47], 0x20); // local.get + assert_eq!(bytecode[49], 0x41); // i32.const + assert_eq!(bytecode[51], 0x6c); // i32.mul + + // `compute` is the only function: its range covers offset 40 (the first + // instruction, `local.get`) through 52 (one past the final 1-byte + // `i32.mul` at 51), and its two locals (param `x`, declared local `y`) + // each resolve to their own single wasm local slot. + assert_eq!(function_debug_info.len(), 1); + let info = &function_debug_info[0]; + assert_eq!((info.start, info.end), (40, 52)); + let names: Vec<(&str, u32, u32)> = info + .locals + .iter() + .map(|l| { + ( + graph.interner.resolve(l.name).unwrap(), + l.wasm_local_start, + l.wasm_local_count, + ) + }) + .collect(); + assert_eq!(names, vec![("x", 0, 1), ("y", 1, 1)]); + + let text_at = + |span: ast::TextSpan| &prefixed[span.start as usize..span.end as usize]; + let actual: Vec<(u32, &str)> = debug_spans + .iter() + .map(|d| (d.offset, text_at(d.span))) + .collect(); + assert_eq!( + actual, + vec![ + (40, "x"), + (42, "1"), + (44, "x + 1"), + (45, "local y = x + 1"), + (47, "y"), + (49, "2"), + (51, "y * 2"), + ] + ); +} + +/// Regression test: `mir::scheduler`'s `ExprKind::LocalGet` used to always +/// emit exactly one `local.get`, even when the local's type was an +/// aggregate occupying several consecutive wasm locals (e.g. reading a +/// whole struct-typed local to pass it as a by-value call argument) — only +/// the first flattened field ever reached the stack, producing an +/// under-full wasm `call` that fails to validate. Caught by hand while +/// building a `--debug` demo, not by any prior test: nothing previously +/// exercised reading (as opposed to writing) a whole aggregate-typed local +/// under `CompilationMode::Debug`. +#[test] +fn debug_mode_reading_whole_struct_local_pushes_every_field() { + let source = indoc! {" + struct Vec2 { x: i32, y: i32 } + fn magnitude_sq(v: Vec2) -> i32 { + v.x * v.x + v.y * v.y + } + fn compute(x: i32, y: i32) -> i32 { + local v: Vec2 = Vec2::{ x: x, y: y }; + magnitude_sq(v) + } + export { compute } + "}; + let mut builder = vfs::CompilationGraphBuilder::new(); + let stdlib_id = builder.load_stdlib(); + let prefixed = format!("use std::*;\n{source}"); + let root_id = builder + .load_binary( + "main.wx".to_string(), + &vfs::VirtualFileSource::new(HashMap::from([( + "main.wx".to_string(), + prefixed, + )])), + ) + .unwrap(); + let mut graph = builder.build(root_id, stdlib_id); + let tir = tir::TIR::build(&mut graph); + let mut mir = mir::MIR::build( + &tir, + &graph.interner, + graph.id_generator, + crate::CompilationMode::Debug, + ); + let call_graph = mir::CallGraph::build(&mir.functions, &mir.call_edges); + mir.dead_code_eliminate(&call_graph); + let wasm = + Builder::build(&mir, &graph.interner, crate::CompilationMode::Debug) + .unwrap(); + let bytecode = wasm.encode(); + + // Validating with wasmtime is the point: the bug produced a wasm + // `call` with too few arguments on the stack, which fails to validate + // at module-compile time, before execution even starts. + let engine = wasmtime::Engine::default(); + let module = wasmtime::Module::new(&engine, &bytecode).unwrap(); + let mut store = wasmtime::Store::new(&engine, ()); + let instance = wasmtime::Instance::new(&mut store, &module, &[]).unwrap(); + let compute = instance + .get_typed_func::<(i32, i32), i32>(&mut store, "compute") + .unwrap(); + + assert_eq!(compute.call(&mut store, (3, 4)).unwrap(), 25); +} diff --git a/crates/wx-compiler/src/dwarf/mod.rs b/crates/wx-compiler/src/dwarf/mod.rs new file mode 100644 index 0000000..2c62880 --- /dev/null +++ b/crates/wx-compiler/src/dwarf/mod.rs @@ -0,0 +1,939 @@ +//! Hand-rolled DWARF5 debug-info encoder for `--debug` wasm builds. See +//! `WebAssembly/tool-conventions/Dwarf.md` for how DWARF is adapted to +//! wasm: addresses are Code-section-relative byte offsets (exactly what +//! `codegen::DebugSpan`/`FunctionDebugInfo` already carry), and variable +//! locations use the vendor `DW_OP_WASM_location` extension instead of +//! registers. +//! +//! Deliberately hand-rolled rather than built on `gimli::write`: the +//! feature surface actually needed is narrow — one compile unit, no +//! relocations (every address here is already a resolved constant), no +//! location lists, no range lists, no call-frame info, no split-DWARF — and +//! this matches the rest of the compiler's style: `codegen/mod.rs` already +//! hand-rolls the entire wasm binary encoder the same way, with +//! `leb128fmt` as the only real primitive dependency. `gimli`'s *read* side +//! is used only in this module's own tests (a dev-dependency), to verify +//! this encoder's output actually parses the way a real consumer would see +//! it — never shipped in the release binary. +//! +//! Every byte layout below (DWARF5's CU header field order, the +//! `.debug_line` v5 header's directory/file-entry-format tables, form +//! widths) was cross-checked against `gimli`'s own parser +//! (`gimli::read::unit::parse_unit_header`, `gimli::read::line::parse`) — +//! not just recalled from memory. +//! +//! Scope, matching what wasm's `--debug` lowering (`mir::scheduler`) +//! actually produces: +//! - No `DW_TAG_lexical_block` — every variable/parameter DIE is a direct +//! child of its function's `DW_TAG_subprogram`, visible for the whole +//! function. This matches wasm's own local-storage model: every declared +//! local gets function-lifetime storage regardless of which source block +//! declared it, so this isn't a loss of precision. +//! - Struct-typed locals use `DW_OP_piece` composite locations: one +//! `DW_OP_WASM_location`+`DW_OP_piece` pair per flattened leaf field, in +//! the same physical order `mir::Aggregate::values`/`::offsets` already +//! use — the standard DWARF mechanism for "this value's storage is split +//! across several locations" (the textbook case being a struct promoted +//! into two registers), not a hack. +//! - Struct type DIEs are never deduplicated across the compile unit +//! (unlike base/pointer types, which are): `mir::Aggregate` is shared +//! structurally, so two differently-named, identically-shaped structs +//! can point at the same `AggregateIndex` — building one shared, named +//! `DW_TAG_structure_type` per `AggregateIndex` would mean one of them +//! shows the other's name. Building a fresh structure DIE per +//! `LocalDebugInfo::struct_debug` occurrence sidesteps that entirely, at +//! the cost of some duplicate DIEs when the same struct type is used by +//! several locals — a reasonable size/simplicity trade for a first cut. +//! - Struct field names are one level deep only: a field that's itself a +//! struct falls back to positional member names (`field0`, `field1`, +//! ...), matching the same limit already established on +//! `mir::StructDebugInfo`. +//! - The line-number program uses only the general opcodes +//! (`DW_LNS_advance_pc`/`DW_LNS_advance_line`/`DW_LNS_set_file`/ +//! `DW_LNS_set_column`/`DW_LNS_copy` + `DW_LNE_end_sequence`/ +//! `DW_LNE_set_address`) — no special-opcode compression table, which is +//! a size optimization, not a correctness requirement. + +use std::collections::HashMap; + +use crate::codegen::{DebugSpan, FunctionDebugInfo}; +use crate::{ast, mir, vfs, wasm}; + +#[cfg(test)] +mod tests; + +mod constants { + // DW_TAG_* + pub const DW_TAG_FORMAL_PARAMETER: u64 = 0x05; + pub const DW_TAG_MEMBER: u64 = 0x0d; + pub const DW_TAG_POINTER_TYPE: u64 = 0x0f; + pub const DW_TAG_COMPILE_UNIT: u64 = 0x11; + pub const DW_TAG_STRUCTURE_TYPE: u64 = 0x13; + pub const DW_TAG_BASE_TYPE: u64 = 0x24; + pub const DW_TAG_SUBPROGRAM: u64 = 0x2e; + pub const DW_TAG_VARIABLE: u64 = 0x34; + + // DW_AT_* + pub const DW_AT_LOCATION: u64 = 0x02; + pub const DW_AT_NAME: u64 = 0x03; + pub const DW_AT_BYTE_SIZE: u64 = 0x0b; + pub const DW_AT_STMT_LIST: u64 = 0x10; + pub const DW_AT_LOW_PC: u64 = 0x11; + pub const DW_AT_HIGH_PC: u64 = 0x12; + pub const DW_AT_COMP_DIR: u64 = 0x1b; + pub const DW_AT_PRODUCER: u64 = 0x25; + pub const DW_AT_DATA_MEMBER_LOCATION: u64 = 0x38; + pub const DW_AT_ENCODING: u64 = 0x3e; + pub const DW_AT_TYPE: u64 = 0x49; + + // DW_FORM_* + pub const DW_FORM_ADDR: u64 = 0x01; + pub const DW_FORM_DATA1: u64 = 0x0b; + pub const DW_FORM_DATA4: u64 = 0x06; + pub const DW_FORM_STRP: u64 = 0x0e; + pub const DW_FORM_UDATA: u64 = 0x0f; + pub const DW_FORM_REF4: u64 = 0x13; + pub const DW_FORM_SEC_OFFSET: u64 = 0x17; + pub const DW_FORM_EXPRLOC: u64 = 0x18; + pub const DW_FORM_LINE_STRP: u64 = 0x1f; + + // DW_ATE_* (base_type encodings) + pub const DW_ATE_BOOLEAN: u8 = 0x02; + pub const DW_ATE_FLOAT: u8 = 0x04; + pub const DW_ATE_SIGNED: u8 = 0x05; + pub const DW_ATE_UNSIGNED: u8 = 0x07; + + pub const DW_UT_COMPILE: u8 = 0x01; + + // Standard line-number opcodes actually emitted (opcode_base = 13 + // covers 1..=12; the table above 12 exists purely for reader + // compatibility — see `standard_opcode_lengths` in `LineProgramBuilder`). + pub const DW_LNS_COPY: u8 = 0x01; + pub const DW_LNS_ADVANCE_PC: u8 = 0x02; + pub const DW_LNS_ADVANCE_LINE: u8 = 0x03; + pub const DW_LNS_SET_FILE: u8 = 0x04; + pub const DW_LNS_SET_COLUMN: u8 = 0x05; + + pub const DW_LNE_END_SEQUENCE: u8 = 0x01; + pub const DW_LNE_SET_ADDRESS: u8 = 0x02; + + pub const DW_LNCT_PATH: u64 = 0x1; + pub const DW_LNCT_DIRECTORY_INDEX: u64 = 0x2; + + /// Vendor extension opcode for wasm-specific location descriptions — + /// not part of core DWARF. See + /// . + pub const DW_OP_WASM_LOCATION: u8 = 0xED; + /// `wasm-op` selector for `DW_OP_WASM_LOCATION`: value is in a wasm + /// local, ULEB128 index follows. + pub const WASM_LOCATION_LOCAL: u8 = 0x00; + pub const DW_OP_PIECE: u8 = 0x93; + /// Marks the preceding operations as having computed the value itself + /// (not an address to dereference) — see `build_location_expr`. + pub const DW_OP_STACK_VALUE: u8 = 0x9f; + + /// Fixed abbreviation codes — one per DIE shape this module ever + /// emits, hand-written rather than dynamically deduplicated (see the + /// module doc comment). + pub mod abbrev_code { + pub const COMPILE_UNIT: u64 = 1; + pub const SUBPROGRAM: u64 = 2; + pub const FORMAL_PARAMETER: u64 = 3; + pub const VARIABLE: u64 = 4; + pub const BASE_TYPE: u64 = 5; + pub const POINTER_TYPE: u64 = 6; + pub const STRUCTURE_TYPE: u64 = 7; + pub const MEMBER: u64 = 8; + } +} + +use constants::*; + +fn push_uleb128(sink: &mut Vec, value: u64) { + let (bytes, len) = leb128fmt::encode_u64(value).unwrap(); + sink.extend_from_slice(&bytes[..len]); +} + +fn push_sleb128(sink: &mut Vec, value: i64) { + let (bytes, len) = leb128fmt::encode_s64(value).unwrap(); + sink.extend_from_slice(&bytes[..len]); +} + +/// An append-only, deduplicating string pool — the shared shape behind +/// `.debug_str` and `.debug_line_str` (kept as two separate instances, +/// never cross-referenced, matching DWARF5's convention that regular names +/// and line-table file/directory names live in different sections). +#[derive(Default)] +struct StringTable { + bytes: Vec, + offsets: HashMap, +} + +impl StringTable { + fn intern(&mut self, s: &str) -> u32 { + if let Some(&offset) = self.offsets.get(s) { + return offset; + } + let offset = self.bytes.len() as u32; + self.bytes.extend_from_slice(s.as_bytes()); + self.bytes.push(0); + self.offsets.insert(s.to_string(), offset); + offset + } +} + +/// The DWARF sections this module produces, ready to embed as wasm custom +/// sections via [`custom_section`]. +pub struct Sections { + pub debug_abbrev: Vec, + pub debug_info: Vec, + pub debug_line: Vec, + pub debug_str: Vec, + pub debug_line_str: Vec, + pub debug_aranges: Vec, +} + +/// Builds every DWARF section for a `--debug` module from the data +/// `codegen::WasmModule::encode_with_debug_spans` already produced, plus +/// `mir`/`interner`/`files` for names, struct layouts, and source +/// positions. `function_debug_info` must be index-aligned with +/// `mir.functions` (true by construction: both come from the same +/// `mir.functions.iter()` walk in `codegen::Builder::build`). +pub fn build( + debug_spans: &[DebugSpan], + function_debug_info: &[FunctionDebugInfo], + mir: &mir::MIR, + interner: &ast::StringInterner, + files: &vfs::Files, +) -> Sections { + let debug_abbrev = build_debug_abbrev(); + + let mut info_builder = DebugInfoBuilder::new(); + let debug_info = info_builder.build(function_debug_info, mir, interner); + + let mut line_builder = LineProgramBuilder::new(); + let debug_line = + line_builder.build(debug_spans, function_debug_info, files); + + let cu_low_pc = function_debug_info + .iter() + .map(|f| f.start) + .min() + .unwrap_or(0); + let cu_high_pc = + function_debug_info.iter().map(|f| f.end).max().unwrap_or(0); + let debug_aranges = build_debug_aranges(cu_low_pc, cu_high_pc); + + Sections { + debug_abbrev, + debug_info, + debug_line, + debug_str: info_builder.debug_str.bytes, + debug_line_str: line_builder.debug_line_str.bytes, + debug_aranges, + } +} + +/// `.debug_aranges`: a fast address-range → compile-unit index, separate +/// from (and not required by) the DIE tree in `.debug_info` — some DWARF +/// consumers use it specifically to resolve "which compile unit/function +/// contains this PC" at runtime (e.g. when a debugger pauses), rather than +/// walking the DIE tree, which is otherwise sufficient for everything else +/// this module does (source-line lookups, variable locations, ...). One +/// entry, covering the whole module, since there's only ever one compile +/// unit. Byte layout verified against `gimli::read::aranges::ArangeHeader:: +/// parse` — notably its version field is *always* 2 regardless of the +/// referenced CU's own DWARF version (a real, spec-mandated quirk, not a +/// typo), and the header is padded to a `2 * address_size` boundary before +/// the first `(address, length)` tuple. +fn build_debug_aranges(low_pc: u32, high_pc: u32) -> Vec { + let mut sink = Vec::new(); + sink.extend_from_slice(&0u32.to_le_bytes()); // unit_length placeholder + let body_start = sink.len(); + + sink.extend_from_slice(&2u16.to_le_bytes()); // version — always 2 + sink.extend_from_slice(&0u32.to_le_bytes()); // debug_info_offset: our one CU, at offset 0 + sink.push(4); // address_size + sink.push(0); // segment_selector_size + sink.extend_from_slice(&[0u8; 4]); // pad header (12 bytes) to a 8-byte tuple boundary + + sink.extend_from_slice(&low_pc.to_le_bytes()); + sink.extend_from_slice(&(high_pc - low_pc).to_le_bytes()); + sink.extend_from_slice(&0u32.to_le_bytes()); // terminator: (0, 0) + sink.extend_from_slice(&0u32.to_le_bytes()); + + let unit_length = (sink.len() - body_start) as u32; + sink[0..4].copy_from_slice(&unit_length.to_le_bytes()); + sink +} + +/// Encodes the wasm custom section framing (section id 0, name, +/// length-prefixed payload) around raw section bytes — the same shape for +/// every DWARF section (`.debug_info`, `.debug_line`, ...): a `name` (with +/// its leading dot, per `WebAssembly/tool-conventions`) and the section's +/// own bytes verbatim as the payload. +pub fn custom_section(name: &str, payload: &[u8]) -> Vec { + let mut content = Vec::new(); + push_uleb128(&mut content, name.len() as u64); + content.extend_from_slice(name.as_bytes()); + content.extend_from_slice(payload); + + let mut section = vec![0x00]; // wasm SectionId::Custom + push_uleb128(&mut section, content.len() as u64); + section.extend_from_slice(&content); + section +} + +/// Writes one `(tag, has_children, [(attr, form), ...])` abbreviation +/// declaration. +fn push_abbrev_decl( + sink: &mut Vec, + code: u64, + tag: u64, + has_children: bool, + attrs: &[(u64, u64)], +) { + push_uleb128(sink, code); + push_uleb128(sink, tag); + sink.push(has_children as u8); + for &(attr, form) in attrs { + push_uleb128(sink, attr); + push_uleb128(sink, form); + } + push_uleb128(sink, 0); + push_uleb128(sink, 0); +} + +/// The fixed `.debug_abbrev` table — one declaration per DIE shape this +/// module ever emits (see `constants::abbrev_code`), never dynamically +/// deduplicated. +fn build_debug_abbrev() -> Vec { + let mut sink = Vec::new(); + + push_abbrev_decl( + &mut sink, + abbrev_code::COMPILE_UNIT, + DW_TAG_COMPILE_UNIT, + true, + &[ + (DW_AT_PRODUCER, DW_FORM_STRP), + (DW_AT_NAME, DW_FORM_STRP), + (DW_AT_COMP_DIR, DW_FORM_STRP), + (DW_AT_LOW_PC, DW_FORM_ADDR), + // DW_AT_high_pc as DW_FORM_data4: a *size* (high_pc = low_pc + + // this value), not an absolute address. Both are spec-legal — + // DW_AT_high_pc's class is address-or-constant, and the reader + // is supposed to branch on the attribute's form — but the + // size-relative-to-low_pc encoding is the near-universal + // real-world convention (what LLVM/GCC always emit), and at + // least one real consumer (Chrome's C/C++ DWARF extension) + // turned out not to handle the address-class alternative + // correctly, silently rejecting the DIE. + (DW_AT_HIGH_PC, DW_FORM_DATA4), + (DW_AT_STMT_LIST, DW_FORM_SEC_OFFSET), + ], + ); + push_abbrev_decl( + &mut sink, + abbrev_code::SUBPROGRAM, + DW_TAG_SUBPROGRAM, + true, + &[ + (DW_AT_NAME, DW_FORM_STRP), + (DW_AT_LOW_PC, DW_FORM_ADDR), + (DW_AT_HIGH_PC, DW_FORM_DATA4), + ], + ); + push_abbrev_decl( + &mut sink, + abbrev_code::FORMAL_PARAMETER, + DW_TAG_FORMAL_PARAMETER, + false, + &[ + (DW_AT_NAME, DW_FORM_STRP), + (DW_AT_TYPE, DW_FORM_REF4), + (DW_AT_LOCATION, DW_FORM_EXPRLOC), + ], + ); + push_abbrev_decl( + &mut sink, + abbrev_code::VARIABLE, + DW_TAG_VARIABLE, + false, + &[ + (DW_AT_NAME, DW_FORM_STRP), + (DW_AT_TYPE, DW_FORM_REF4), + (DW_AT_LOCATION, DW_FORM_EXPRLOC), + ], + ); + push_abbrev_decl( + &mut sink, + abbrev_code::BASE_TYPE, + DW_TAG_BASE_TYPE, + false, + &[ + (DW_AT_NAME, DW_FORM_STRP), + (DW_AT_ENCODING, DW_FORM_DATA1), + (DW_AT_BYTE_SIZE, DW_FORM_DATA1), + ], + ); + push_abbrev_decl( + &mut sink, + abbrev_code::POINTER_TYPE, + DW_TAG_POINTER_TYPE, + false, + &[(DW_AT_TYPE, DW_FORM_REF4), (DW_AT_BYTE_SIZE, DW_FORM_DATA1)], + ); + push_abbrev_decl( + &mut sink, + abbrev_code::STRUCTURE_TYPE, + DW_TAG_STRUCTURE_TYPE, + true, + &[(DW_AT_NAME, DW_FORM_STRP), (DW_AT_BYTE_SIZE, DW_FORM_UDATA)], + ); + push_abbrev_decl( + &mut sink, + abbrev_code::MEMBER, + DW_TAG_MEMBER, + false, + &[ + (DW_AT_NAME, DW_FORM_STRP), + (DW_AT_TYPE, DW_FORM_REF4), + (DW_AT_DATA_MEMBER_LOCATION, DW_FORM_UDATA), + ], + ); + + push_uleb128(&mut sink, 0); // table terminator + sink +} + +/// Builds `.debug_info` (and, alongside it, `.debug_str`, since every name +/// referenced from `.debug_info` needs to be interned there as it's +/// written). +struct DebugInfoBuilder { + sink: Vec, + debug_str: StringTable, + /// Base/pointer/function-reference type DIEs, deduplicated by + /// `mir::Type` — safe because these carry no ambiguous names (see the + /// module doc comment for why struct types can't use the same + /// treatment). + type_cache: HashMap, +} + +impl DebugInfoBuilder { + fn new() -> Self { + DebugInfoBuilder { + sink: Vec::new(), + debug_str: StringTable::default(), + type_cache: HashMap::new(), + } + } + + fn push_strp(&mut self, s: &str) { + let offset = self.debug_str.intern(s); + self.sink.extend_from_slice(&offset.to_le_bytes()); + } + + fn build( + &mut self, + function_debug_info: &[FunctionDebugInfo], + mir: &mir::MIR, + interner: &ast::StringInterner, + ) -> Vec { + // `unit_length` (the CU's total byte size, excluding this field + // itself) can only be known once everything after it has been + // written — reserve it here and patch it in place at the end, + // rather than building into a separate buffer first: everything + // else in this builder already writes straight into `self.sink`. + self.sink.extend_from_slice(&0u32.to_le_bytes()); + let unit_length_at = 0; + let body_start = self.sink.len(); + + self.sink.extend_from_slice(&5u16.to_le_bytes()); // version + self.sink.push(DW_UT_COMPILE); + self.sink.push(4); // address_size — matches wasm's u32 code offsets + self.sink.extend_from_slice(&0u32.to_le_bytes()); // debug_abbrev_offset (single CU, always 0) + + let low_pc = function_debug_info + .iter() + .map(|f| f.start) + .min() + .unwrap_or(0); + let high_pc = + function_debug_info.iter().map(|f| f.end).max().unwrap_or(0); + let cu_name = mir + .functions + .iter() + .find_map(|f| f.name) + .and_then(|s| interner.resolve(s)) + .unwrap_or("wx"); + + push_uleb128(&mut self.sink, abbrev_code::COMPILE_UNIT); + self.push_strp(concat!("wx ", env!("CARGO_PKG_VERSION"))); + self.push_strp(cu_name); + self.push_strp("."); + self.sink.extend_from_slice(&low_pc.to_le_bytes()); + self.sink + .extend_from_slice(&(high_pc - low_pc).to_le_bytes()); + self.sink.extend_from_slice(&0u32.to_le_bytes()); // DW_AT_stmt_list: .debug_line has one CU, at offset 0 + + // Phase 1: every local's type DIE, across every function — fully + // written (and, for structs, closed) before any subprogram DIE + // references them, so every DW_FORM_ref4 below is a completed, + // known offset, never a forward reference needing a later patch. + let local_types: Vec> = function_debug_info + .iter() + .map(|info| { + info.locals + .iter() + .map(|local| self.build_local_type(local, mir, interner)) + .collect() + }) + .collect(); + + // Phase 2: subprogram DIEs, each with its own formal_parameter/ + // variable children (see the module doc comment for why there's no + // lexical-block nesting). + for (func, (info, types)) in mir + .functions + .iter() + .zip(function_debug_info.iter().zip(&local_types)) + { + self.build_subprogram(func, info, types, interner, mir); + } + + push_uleb128(&mut self.sink, 0); // close compile_unit's children + + let unit_length = (self.sink.len() - body_start) as u32; + self.sink[unit_length_at..unit_length_at + 4] + .copy_from_slice(&unit_length.to_le_bytes()); + std::mem::take(&mut self.sink) + } + + fn build_subprogram( + &mut self, + func: &mir::Function, + info: &FunctionDebugInfo, + local_type_offsets: &[u32], + interner: &ast::StringInterner, + mir: &mir::MIR, + ) { + let name = func + .name + .and_then(|s| interner.resolve(s)) + .unwrap_or("$start"); + + push_uleb128(&mut self.sink, abbrev_code::SUBPROGRAM); + self.push_strp(name); + self.sink.extend_from_slice(&info.start.to_le_bytes()); + self.sink + .extend_from_slice(&(info.end - info.start).to_le_bytes()); + + let params_count = + mir.signatures[func.signature_index as usize].params_count; + for (i, (local, &type_offset)) in + info.locals.iter().zip(local_type_offsets).enumerate() + { + let leaf_count = + wasm::flatten_type_to_scalars(local.ty, &mir.aggregates).len(); + if leaf_count == 0 { + // Unit/Never-typed local: no meaningful location to + // describe (shouldn't occur for a real source local, but + // guard rather than emit a malformed empty exprloc). + continue; + } + let abbrev = if i < params_count { + abbrev_code::FORMAL_PARAMETER + } else { + abbrev_code::VARIABLE + }; + push_uleb128(&mut self.sink, abbrev); + let local_name = interner.resolve(local.name).unwrap_or("?"); + self.push_strp(local_name); + self.sink.extend_from_slice(&type_offset.to_le_bytes()); + let expr = build_location_expr(local, &mir.aggregates); + push_uleb128(&mut self.sink, expr.len() as u64); + self.sink.extend_from_slice(&expr); + } + + push_uleb128(&mut self.sink, 0); // close subprogram's children + } + + fn build_local_type( + &mut self, + local: &wasm::LocalDebugInfo, + mir: &mir::MIR, + interner: &ast::StringInterner, + ) -> u32 { + self.build_type(local.ty, local.struct_debug.as_ref(), mir, interner) + } + + fn build_type( + &mut self, + ty: mir::Type, + name_hint: Option<&mir::StructDebugInfo>, + mir: &mir::MIR, + interner: &ast::StringInterner, + ) -> u32 { + match ty { + mir::Type::Aggregate { aggregate_index } => { + self.build_struct_die(aggregate_index, name_hint, mir, interner) + } + _ => self.build_scalar_type(ty), + } + } + + /// Base/pointer/function-reference types — deduplicated (see + /// `type_cache`'s doc comment). + fn build_scalar_type(&mut self, ty: mir::Type) -> u32 { + if let Some(&offset) = self.type_cache.get(&ty) { + return offset; + } + if let mir::Type::Pointer { kind, .. } = ty { + // Resolve the pointee *before* capturing this DIE's own + // offset below — same reasoning as `build_struct_die`: a + // dependency built inline here would otherwise land its bytes + // between `offset` and this DIE's own header, corrupting the + // very offset this call is about to return. Pointee isn't + // tracked at the MIR level for `Pointer`, so this points at a + // generic byte-sized type rather than a precise pointee — a + // stated simplification (module doc comment). + let pointee = self.build_scalar_type(mir::Type::U8); + let byte_size = kind.pointer_size() as u8; + let offset = self.sink.len() as u32; + push_uleb128(&mut self.sink, abbrev_code::POINTER_TYPE); + self.sink.extend_from_slice(&pointee.to_le_bytes()); + self.sink.push(byte_size); + self.type_cache.insert(ty, offset); + return offset; + } + let (name, encoding, byte_size) = base_type_info(ty); + let offset = self.sink.len() as u32; + push_uleb128(&mut self.sink, abbrev_code::BASE_TYPE); + self.push_strp(name); + self.sink.push(encoding); + self.sink.push(byte_size); + self.type_cache.insert(ty, offset); + offset + } + + /// Always builds a fresh structure DIE — see the module doc comment for + /// why struct types are never deduplicated by `AggregateIndex`. + fn build_struct_die( + &mut self, + aggregate_index: mir::AggregateIndex, + name_hint: Option<&mir::StructDebugInfo>, + mir: &mir::MIR, + interner: &ast::StringInterner, + ) -> u32 { + let aggregate = &mir.aggregates[aggregate_index as usize]; + let struct_name = name_hint + .and_then(|s| interner.resolve(s.name)) + .unwrap_or("tuple") + .to_string(); + let byte_size = aggregate.layout.size; + + // Build every member's type DIE first (recursing one level with no + // further name hint — see the module doc comment), so this + // struct's own bytes (written below) never end up straddling a + // nested type DIE's bytes. + let members: Vec<(String, u32, u32)> = (0..aggregate.values.len()) + .map(|i| { + let field_name = name_hint + .and_then(|s| s.field_names.get(i)) + .and_then(|&sym| interner.resolve(sym)) + .map(str::to_string) + .unwrap_or_else(|| format!("field{i}")); + let field_type = + self.build_type(aggregate.values[i], None, mir, interner); + (field_name, field_type, aggregate.offsets[i]) + }) + .collect(); + + let struct_offset = self.sink.len() as u32; + push_uleb128(&mut self.sink, abbrev_code::STRUCTURE_TYPE); + self.push_strp(&struct_name); + push_uleb128(&mut self.sink, byte_size as u64); + + for (field_name, field_type, field_offset) in &members { + push_uleb128(&mut self.sink, abbrev_code::MEMBER); + self.push_strp(field_name); + self.sink.extend_from_slice(&field_type.to_le_bytes()); + push_uleb128(&mut self.sink, *field_offset as u64); + } + push_uleb128(&mut self.sink, 0); // close structure_type's children + + struct_offset + } +} + +fn base_type_info(ty: mir::Type) -> (&'static str, u8, u8) { + match ty { + mir::Type::I8 => ("i8", DW_ATE_SIGNED, 1), + mir::Type::U8 => ("u8", DW_ATE_UNSIGNED, 1), + mir::Type::I16 => ("i16", DW_ATE_SIGNED, 2), + mir::Type::U16 => ("u16", DW_ATE_UNSIGNED, 2), + mir::Type::I32 => ("i32", DW_ATE_SIGNED, 4), + mir::Type::U32 => ("u32", DW_ATE_UNSIGNED, 4), + mir::Type::I64 => ("i64", DW_ATE_SIGNED, 8), + mir::Type::U64 => ("u64", DW_ATE_UNSIGNED, 8), + mir::Type::F32 => ("f32", DW_ATE_FLOAT, 4), + mir::Type::F64 => ("f64", DW_ATE_FLOAT, 8), + // Matches the actual wasm i32 storage width a bool local occupies, + // not the 1-byte logical size — see the module doc comment. + mir::Type::Bool => ("bool", DW_ATE_BOOLEAN, 4), + mir::Type::Function { .. } => ("function", DW_ATE_UNSIGNED, 4), + mir::Type::Pointer { .. } + | mir::Type::Aggregate { .. } + | mir::Type::Unit + | mir::Type::Never => { + unreachable!("base_type_info called on a non-scalar mir::Type") + } + } +} + +fn scalar_byte_size(ty: wasm::ScalarType) -> u64 { + match ty { + wasm::ScalarType::I32 | wasm::ScalarType::F32 => 4, + wasm::ScalarType::I64 | wasm::ScalarType::F64 => 8, + } +} + +/// Builds a `DW_AT_location` expression for one local: a single +/// `DW_OP_WASM_location` for a scalar, or a chained +/// `DW_OP_WASM_location`+`DW_OP_piece` pair per flattened leaf field for an +/// aggregate — see the module doc comment. +/// +/// Each `DW_OP_WASM_location` is followed by `DW_OP_stack_value`: without +/// it, a consumer must treat the wasm-local reference as an *address* to +/// dereference (standard DWARF location-expression default) rather than the +/// value itself — real producers (LLVM's wasm backend) always emit this +/// pairing. Confirmed the hard way: wasmtime's own DWARF→native transform +/// (`crates/cranelift/src/debug/transform/expression.rs`) sets +/// `need_deref = true` unconditionally per operation and only clears it on +/// `DW_OP_stack_value`, so a bare `DW_OP_WASM_location` sent it down the +/// memory-dereference path — which then hard-errored on modules with no +/// declared linear memory (`ModuleMemoryOffset::None`). +fn build_location_expr( + local: &wasm::LocalDebugInfo, + aggregates: &[mir::Aggregate], +) -> Vec { + let leaves = wasm::flatten_type_to_scalars(local.ty, aggregates); + let mut expr = Vec::new(); + if leaves.len() == 1 { + expr.push(DW_OP_WASM_LOCATION); + expr.push(WASM_LOCATION_LOCAL); + push_uleb128(&mut expr, local.wasm_local_start as u64); + expr.push(DW_OP_STACK_VALUE); + } else { + for (i, &leaf) in leaves.iter().enumerate() { + expr.push(DW_OP_WASM_LOCATION); + expr.push(WASM_LOCATION_LOCAL); + push_uleb128(&mut expr, (local.wasm_local_start + i as u32) as u64); + expr.push(DW_OP_STACK_VALUE); + expr.push(DW_OP_PIECE); + push_uleb128(&mut expr, scalar_byte_size(leaf)); + } + } + expr +} + +/// Builds `.debug_line` (and, alongside it, `.debug_line_str`). +struct LineProgramBuilder { + debug_line_str: StringTable, + file_index: HashMap, + file_names: Vec<(u32, u32)>, // (path line_strp offset, directory_index) +} + +impl LineProgramBuilder { + fn new() -> Self { + LineProgramBuilder { + debug_line_str: StringTable::default(), + file_index: HashMap::new(), + file_names: Vec::new(), + } + } + + fn file_index_for( + &mut self, + file_id: vfs::FileId, + files: &vfs::Files, + ) -> u32 { + if let Some(&index) = self.file_index.get(&file_id) { + return index; + } + use codespan_reporting::files::Files as _; + let name = files.name(file_id).unwrap_or(""); + let path_offset = self.debug_line_str.intern(name); + let index = self.file_names.len() as u32; + self.file_names.push((path_offset, 0)); + self.file_index.insert(file_id, index); + index + } + + fn build( + &mut self, + debug_spans: &[DebugSpan], + function_debug_info: &[FunctionDebugInfo], + files: &vfs::Files, + ) -> Vec { + // Assign file indices (and intern their names) before building any + // row, since row emission just references indices by number. + for span in debug_spans { + self.file_index_for(span.file_id, files); + } + // A single shared directory (index 0, ".") — every file's own name + // is used as-is, so there's no real directory/filename split to + // model here. Deliberately not an empty string: legal per raw + // DWARF, but real consumers assume non-empty (e.g. wasmtime's own + // `gimli::write`-based DWARF transform panics on it — + // `assert!(!val.is_empty())` in `gimli::write::line`). + let directory_offset = self.debug_line_str.intern("."); + + let program = + self.build_program(debug_spans, function_debug_info, files); + let header_content = self.build_header_content(directory_offset); + + let mut body = Vec::new(); + body.extend_from_slice(&5u16.to_le_bytes()); // version + body.push(4); // address_size + body.push(0); // segment_selector_size + body.extend_from_slice(&(header_content.len() as u32).to_le_bytes()); + body.extend_from_slice(&header_content); + body.extend_from_slice(&program); + + let mut sink = Vec::new(); + sink.extend_from_slice(&(body.len() as u32).to_le_bytes()); + sink.extend_from_slice(&body); + sink + } + + /// Everything from `minimum_instruction_length` through the file-name + /// table — the part `header_length` itself measures. + fn build_header_content(&mut self, directory_offset: u32) -> Vec { + let mut h = Vec::new(); + h.push(1); // minimum_instruction_length: every wasm byte is addressable + h.push(1); // maximum_operations_per_instruction: non-VLIW + h.push(1); // default_is_stmt: true + h.push((-5i8) as u8); // line_base + h.push(14); // line_range + h.push(13); // opcode_base: standard opcodes 1..=12 + // standard_opcode_lengths[opcode - 1], opcodes 1..=12 — required + // even though only a subset is ever emitted, so a reader that + // doesn't implement every standard opcode can still skip unknown + // ones by their declared operand count. + h.extend_from_slice(&[0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1]); + + // directory_entry_format: one (DW_LNCT_path, DW_FORM_line_strp) pair. + h.push(1); + push_uleb128(&mut h, DW_LNCT_PATH); + push_uleb128(&mut h, DW_FORM_LINE_STRP); + push_uleb128(&mut h, 1); // directories_count + h.extend_from_slice(&directory_offset.to_le_bytes()); + + // file_name_entry_format: (path, line_strp) + (directory_index, udata). + h.push(2); + push_uleb128(&mut h, DW_LNCT_PATH); + push_uleb128(&mut h, DW_FORM_LINE_STRP); + push_uleb128(&mut h, DW_LNCT_DIRECTORY_INDEX); + push_uleb128(&mut h, DW_FORM_UDATA); + push_uleb128(&mut h, self.file_names.len() as u64); + for &(path_offset, dir_index) in &self.file_names { + h.extend_from_slice(&path_offset.to_le_bytes()); + push_uleb128(&mut h, dir_index as u64); + } + + h + } + + /// The actual line-number program: one sequence per function that has + /// any spans, terminated by `DW_LNE_end_sequence`. Functions are + /// matched to their spans by absolute offset range (`DebugSpan`s are + /// globally sorted ascending — see `codegen::WasmModule:: + /// encode_with_debug_spans` — and `FunctionDebugInfo` ranges are + /// non-overlapping and in the same order), not by an explicit + /// per-span function index. + fn build_program( + &mut self, + debug_spans: &[DebugSpan], + function_debug_info: &[FunctionDebugInfo], + files: &vfs::Files, + ) -> Vec { + let mut program = Vec::new(); + let mut cursor = 0; + for info in function_debug_info { + let start = cursor; + while cursor < debug_spans.len() + && debug_spans[cursor].offset < info.end + { + cursor += 1; + } + let spans = &debug_spans[start..cursor]; + if !spans.is_empty() { + self.emit_sequence(&mut program, spans, info, files); + } + } + program + } + + fn emit_sequence( + &mut self, + program: &mut Vec, + spans: &[DebugSpan], + info: &FunctionDebugInfo, + files: &vfs::Files, + ) { + use codespan_reporting::files::Files as _; + + program.push(0x00); // extended opcode escape + push_uleb128(program, 1 + 4); // length: sub-opcode + 4-byte address + program.push(DW_LNE_SET_ADDRESS); + program.extend_from_slice(&spans[0].offset.to_le_bytes()); + + let mut address = spans[0].offset; + let mut file = None; + let mut line: i64 = 1; + let mut column = 0u64; + + for span in spans { + if span.offset != address { + program.push(DW_LNS_ADVANCE_PC); + push_uleb128(program, (span.offset - address) as u64); + address = span.offset; + } + let file_index = self.file_index_for(span.file_id, files); + if file != Some(file_index) { + program.push(DW_LNS_SET_FILE); + push_uleb128(program, file_index as u64); + file = Some(file_index); + } + let location = files + .location(span.file_id, span.span.start as usize) + .expect("debug span's start offset is within its file"); + // Both already 1-indexed, matching DWARF's line/column register + // convention directly (column 0 is reserved as a "left edge of + // the line" sentinel, unlike source maps' 0-indexed columns). + let span_line = location.line_number as i64; + let span_column = location.column_number as u64; + if span_line != line { + program.push(DW_LNS_ADVANCE_LINE); + push_sleb128(program, span_line - line); + line = span_line; + } + if span_column != column { + program.push(DW_LNS_SET_COLUMN); + push_uleb128(program, span_column); + column = span_column; + } + program.push(DW_LNS_COPY); + } + + if info.end != address { + program.push(DW_LNS_ADVANCE_PC); + push_uleb128(program, (info.end - address) as u64); + } + program.push(0x00); + push_uleb128(program, 1); + program.push(DW_LNE_END_SEQUENCE); + } +} diff --git a/crates/wx-compiler/src/dwarf/tests.rs b/crates/wx-compiler/src/dwarf/tests.rs new file mode 100644 index 0000000..10bb0ad --- /dev/null +++ b/crates/wx-compiler/src/dwarf/tests.rs @@ -0,0 +1,543 @@ +use std::collections::HashMap; + +use gimli::{EndianSlice, LittleEndian, Reader as _}; +use indoc::indoc; + +use super::*; +use crate::{codegen, mir, tir}; + +/// Compiles `source` in `--debug` mode through the full pipeline and +/// returns everything needed to build (and then parse back) its DWARF +/// sections. +#[allow(unused)] +struct DebugBuild { + graph: vfs::CompilationGraph, + mir: mir::MIR, + bytecode: Vec, + debug_spans: Vec, + function_debug_info: Vec, +} + +impl DebugBuild { + fn new(source: &str) -> Self { + let mut builder = vfs::CompilationGraphBuilder::new(); + let stdlib_id = builder.load_stdlib(); + let prefixed = format!("use std::*;\n{source}"); + let root_id = builder + .load_binary( + "main.wx".to_string(), + &vfs::VirtualFileSource::new(HashMap::from([( + "main.wx".to_string(), + prefixed, + )])), + ) + .unwrap(); + let mut graph = builder.build(root_id, stdlib_id); + let tir = tir::TIR::build(&mut graph); + assert!( + tir.diagnostics.is_empty(), + "unexpected diagnostics: {:?}", + tir.diagnostics + ); + let mut mir = mir::MIR::build( + &tir, + &graph.interner, + graph.id_generator, + crate::CompilationMode::Debug, + ); + let call_graph = mir::CallGraph::build(&mir.functions, &mir.call_edges); + mir.dead_code_eliminate(&call_graph); + let wasm = codegen::Builder::build( + &mir, + &graph.interner, + crate::CompilationMode::Debug, + ) + .unwrap(); + let (bytecode, debug_spans, function_debug_info) = + wasm.encode_with_debug_spans(); + + DebugBuild { + graph, + mir, + bytecode, + debug_spans, + function_debug_info, + } + } + + fn sections(&self) -> Sections { + build( + &self.debug_spans, + &self.function_debug_info, + &self.mir, + &self.graph.interner, + &self.graph.files, + ) + } +} + +/// Loads a `gimli::Dwarf` view directly over `Sections`' byte buffers — no +/// object-file container involved, matching how `codegen`'s own tests +/// operate on raw wasm bytes rather than a `.wasm` file on disk. +fn load_dwarf( + sections: &Sections, +) -> gimli::Dwarf> { + gimli::Dwarf::load(|id| -> Result<_, gimli::Error> { + let data: &[u8] = match id { + gimli::SectionId::DebugAbbrev => §ions.debug_abbrev, + gimli::SectionId::DebugInfo => §ions.debug_info, + gimli::SectionId::DebugLine => §ions.debug_line, + gimli::SectionId::DebugStr => §ions.debug_str, + gimli::SectionId::DebugLineStr => §ions.debug_line_str, + gimli::SectionId::DebugAranges => §ions.debug_aranges, + _ => &[], + }; + Ok(EndianSlice::new(data, LittleEndian)) + }) + .unwrap() +} + +/// Decodes a `DW_OP_WASM_location 0x00 DW_OP_stack_value` +/// expression's wasm-local index — independent of the encoder (manual byte +/// parsing), so this exercises the real wire format rather than checking +/// the encoder against itself. Panics if `expr` isn't exactly that +/// one-operation shape. The trailing `DW_OP_stack_value` is required for +/// real consumers (see `build_location_expr`'s doc comment) — without it, a +/// wasm-local reference is (mis)read as an address to dereference. +fn wasm_local_index(expr: &[u8]) -> u32 { + assert_eq!(expr[0], DW_OP_WASM_LOCATION); + assert_eq!(expr[1], WASM_LOCATION_LOCAL); + let (index, index_len) = decode_uleb128(&expr[2..]); + assert_eq!(expr[2 + index_len], DW_OP_STACK_VALUE); + assert_eq!(expr.len(), 2 + index_len + 1); + index +} + +/// Decodes a chain of `(DW_OP_WASM_location 0x00 DW_OP_stack_value, +/// DW_OP_piece )` pairs into `(index, size)` tuples. +fn wasm_pieces(expr: &[u8]) -> Vec<(u32, u32)> { + let mut out = Vec::new(); + let mut pos = 0; + while pos < expr.len() { + assert_eq!(expr[pos], DW_OP_WASM_LOCATION); + assert_eq!(expr[pos + 1], WASM_LOCATION_LOCAL); + let (index, index_len) = decode_uleb128(&expr[pos + 2..]); + pos += 2 + index_len; + assert_eq!(expr[pos], DW_OP_STACK_VALUE); + pos += 1; + assert_eq!(expr[pos], DW_OP_PIECE); + let (size, size_len) = decode_uleb128(&expr[pos + 1..]); + pos += 1 + size_len; + out.push((index, size)); + } + out +} + +#[test] +fn custom_section_has_wasm_custom_section_shape() { + let bytes = custom_section(".debug_info", &[1, 2, 3]); + + assert_eq!(bytes[0], 0x00); // wasm SectionId::Custom + let (section_len, len_bytes) = decode_uleb128(&bytes[1..]); + assert_eq!(1 + len_bytes + section_len as usize, bytes.len()); + + let mut pos = 1 + len_bytes; + let (name_len, extra) = decode_uleb128(&bytes[pos..]); + pos += extra; + assert_eq!(name_len, ".debug_info".len() as u32); + assert_eq!(&bytes[pos..pos + name_len as usize], b".debug_info"); + pos += name_len as usize; + + assert_eq!(&bytes[pos..], &[1, 2, 3]); +} + +/// Plain unsigned LEB128, decoded byte-by-byte — independent of +/// `push_uleb128`, matching the round-trip philosophy used throughout this +/// module's tests. +fn decode_uleb128(bytes: &[u8]) -> (u32, usize) { + let mut result = 0u32; + let mut shift = 0; + let mut pos = 0; + loop { + let byte = bytes[pos]; + result |= ((byte & 0x7F) as u32) << shift; + pos += 1; + if byte & 0x80 == 0 { + break; + } + shift += 7; + } + (result, pos) +} + +#[test] +fn scalar_function_produces_valid_dwarf5_compile_unit_and_subprogram() { + let build = DebugBuild::new(indoc! {" + fn compute(x: i32) -> i32 { + local y = x + 1; + y * 2 + } + export { compute } + "}); + let sections = build.sections(); + let dwarf = load_dwarf(§ions); + + let header = dwarf.units().next().unwrap().unwrap(); + assert_eq!(header.version(), 5); + assert_eq!(header.address_size(), 4); + + let unit = dwarf.unit(header).unwrap(); + let mut cursor = unit.entries(); + + // compile_unit + let root = cursor.next_dfs().unwrap().unwrap(); + assert_eq!(root.tag(), gimli::DW_TAG_compile_unit); + + // Every local's type DIE is emitted as a compile_unit child *before* + // any subprogram DIE (see the module doc comment on Phase 1/Phase 2), + // so skip forward past those rather than assuming the subprogram comes + // immediately after compile_unit. + let subprogram = loop { + let entry = cursor.next_dfs().unwrap().unwrap(); + if entry.tag() == gimli::DW_TAG_subprogram { + break entry.clone(); + } + }; + let name = dwarf + .attr_string(&unit, subprogram.attr_value(gimli::DW_AT_name).unwrap()) + .unwrap(); + assert_eq!(name.to_string_lossy(), "compute"); + + let expected_info = &build.function_debug_info[0]; + let gimli::AttributeValue::Addr(low_pc) = + subprogram.attr_value(gimli::DW_AT_low_pc).unwrap() + else { + panic!("expected Addr"); + }; + // DW_AT_high_pc is encoded as a size relative to low_pc (DW_FORM_data4), + // not an absolute address — see `DebugInfoBuilder::build_subprogram`. + let high_pc_size = subprogram + .attr_value(gimli::DW_AT_high_pc) + .unwrap() + .udata_value() + .unwrap(); + assert_eq!( + (low_pc as u32, low_pc as u32 + high_pc_size as u32), + (expected_info.start, expected_info.end) + ); + + // formal_parameter `x`, wasm local 0 + let param = cursor.next_dfs().unwrap().unwrap(); + assert_eq!(param.tag(), gimli::DW_TAG_formal_parameter); + let param_name = dwarf + .attr_string(&unit, param.attr_value(gimli::DW_AT_name).unwrap()) + .unwrap(); + assert_eq!(param_name.to_string_lossy(), "x"); + let param_loc = param.attr_value(gimli::DW_AT_location).unwrap(); + let gimli::AttributeValue::Exprloc(expr) = param_loc else { + panic!("expected Exprloc"); + }; + assert_eq!( + wasm_local_index(&expr.0.to_slice().unwrap()), + expected_info.locals[0].wasm_local_start + ); + + // variable `y`, its own wasm local + let var = cursor.next_dfs().unwrap().unwrap(); + assert_eq!(var.tag(), gimli::DW_TAG_variable); + let var_name = dwarf + .attr_string(&unit, var.attr_value(gimli::DW_AT_name).unwrap()) + .unwrap(); + assert_eq!(var_name.to_string_lossy(), "y"); + let var_loc = var.attr_value(gimli::DW_AT_location).unwrap(); + let gimli::AttributeValue::Exprloc(expr) = var_loc else { + panic!("expected Exprloc"); + }; + assert_eq!( + wasm_local_index(&expr.0.to_slice().unwrap()), + expected_info.locals[1].wasm_local_start + ); +} + +#[test] +fn line_program_rows_match_debug_spans() { + let build = DebugBuild::new(indoc! {" + fn compute(x: i32) -> i32 { + local y = x + 1; + y * 2 + } + export { compute } + "}); + let sections = build.sections(); + let dwarf = load_dwarf(§ions); + + let header = dwarf.units().next().unwrap().unwrap(); + let unit = dwarf.unit(header).unwrap(); + let program = unit.line_program.clone().unwrap(); + let mut rows = program.rows(); + + let mut actual: Vec<(u32, u64, u64)> = Vec::new(); + while let Some((_, row)) = rows.next_row().unwrap() { + if row.end_sequence() { + continue; + } + let column = match row.column() { + gimli::ColumnType::LeftEdge => 0, + gimli::ColumnType::Column(n) => n.get(), + }; + actual.push((row.address() as u32, row.line().unwrap().get(), column)); + } + + use codespan_reporting::files::Files as _; + let expected: Vec<(u32, u64, u64)> = build + .debug_spans + .iter() + .map(|d| { + let location = build + .graph + .files + .location(d.file_id, d.span.start as usize) + .unwrap(); + ( + d.offset, + location.line_number as u64, + location.column_number as u64, + ) + }) + .collect(); + + assert_eq!(actual, expected); +} + +#[test] +fn struct_local_uses_dw_op_piece_and_real_field_names() { + let build = DebugBuild::new(indoc! {" + struct Vec2 { x: i32, y: i32 } + fn compute() -> i32 { + local v: Vec2 = Vec2::{ x: 1, y: 2 }; + v.x + v.y + } + export { compute } + "}); + let sections = build.sections(); + let dwarf = load_dwarf(§ions); + + let header = dwarf.units().next().unwrap().unwrap(); + let unit = dwarf.unit(header).unwrap(); + let mut cursor = unit.entries(); + + // Find the `variable` DIE for `v` and, separately, the structure_type + // DIE it points at. + let mut var_entry = None; + let mut struct_offset = None; + while let Some(entry) = cursor.next_dfs().unwrap() { + if entry.tag() == gimli::DW_TAG_variable { + let name = dwarf + .attr_string( + &unit, + entry.attr_value(gimli::DW_AT_name).unwrap(), + ) + .unwrap(); + if name.to_string_lossy() == "v" { + let gimli::AttributeValue::UnitRef(offset) = + entry.attr_value(gimli::DW_AT_type).unwrap() + else { + panic!("expected UnitRef"); + }; + struct_offset = Some(offset); + var_entry = Some(entry.clone()); + } + } + } + let var_entry = var_entry.expect("variable `v` not found"); + let struct_offset = struct_offset.expect("`v` has no type reference"); + + // The struct type itself: name "Vec2", byte_size 8, two members with + // real field names in physical order. + let struct_die = unit.entry(struct_offset).unwrap(); + assert_eq!(struct_die.tag(), gimli::DW_TAG_structure_type); + let struct_name = dwarf + .attr_string(&unit, struct_die.attr_value(gimli::DW_AT_name).unwrap()) + .unwrap(); + assert_eq!(struct_name.to_string_lossy(), "Vec2"); + assert_eq!( + struct_die + .attr_value(gimli::DW_AT_byte_size) + .unwrap() + .udata_value() + .unwrap(), + 8 + ); + + let mut tree = unit.entries_tree(Some(struct_offset)).unwrap(); + let root = tree.root().unwrap(); + let mut children = root.children(); + let mut members = Vec::new(); + while let Some(node) = children.next().unwrap() { + let entry = node.entry(); + assert_eq!(entry.tag(), gimli::DW_TAG_member); + let name = dwarf + .attr_string(&unit, entry.attr_value(gimli::DW_AT_name).unwrap()) + .unwrap(); + let offset = entry + .attr_value(gimli::DW_AT_data_member_location) + .unwrap() + .udata_value() + .unwrap(); + members.push((name.to_string_lossy().into_owned(), offset)); + } + assert_eq!(members, vec![("x".to_string(), 0), ("y".to_string(), 4)]); + + // `v`'s own location: two DW_OP_WASM_location+DW_OP_piece pairs, 4 + // bytes each, one per flattened field. + let loc = var_entry.attr_value(gimli::DW_AT_location).unwrap(); + let gimli::AttributeValue::Exprloc(expr) = loc else { + panic!("expected Exprloc"); + }; + let pieces = wasm_pieces(&expr.0.to_slice().unwrap()); + assert_eq!(pieces.len(), 2); + assert_eq!(pieces[0].1, 4); + assert_eq!(pieces[1].1, 4); + assert_eq!(pieces[1].0, pieces[0].0 + 1); // consecutive wasm locals +} + +#[test] +fn debug_aranges_covers_every_function_debug_info_range() { + let build = DebugBuild::new(indoc! {" + fn compute(x: i32) -> i32 { + local y = x + 1; + y * 2 + } + export { compute } + "}); + let sections = build.sections(); + + let debug_aranges = + gimli::DebugAranges::new(§ions.debug_aranges, LittleEndian); + let mut headers = debug_aranges.headers(); + let header = headers.next().unwrap().expect("one arange header"); + assert!(headers.next().unwrap().is_none(), "only one compile unit"); + // Points at our one compile unit, which starts at offset 0. + assert_eq!(header.debug_info_offset().0, 0); + + let mut entries = header.entries(); + let entry = entries.next().unwrap().expect("one address range"); + assert!(entries.next().unwrap().is_none(), "only one range emitted"); + + let expected_low = build + .function_debug_info + .iter() + .map(|f| f.start) + .min() + .unwrap(); + let expected_high = build + .function_debug_info + .iter() + .map(|f| f.end) + .max() + .unwrap(); + assert_eq!(entry.address() as u32, expected_low); + assert_eq!(entry.length() as u32, expected_high - expected_low); +} + +/// Regression test for a real bug this module used to have: every +/// `DW_OP_WASM_location` must be followed by `DW_OP_stack_value` (see +/// `build_location_expr`'s doc comment) — without it, wasmtime's own +/// DWARF→native transform (`crates/cranelift/src/debug/transform/ +/// expression.rs`) treats the wasm-local reference as an *address* that +/// needs dereferencing through the module's linear memory, and hard-errors +/// with gimli's `Error::InvalidAttributeValue` ("The attribute value is an +/// invalid for writing") on any module with zero declared memories +/// (`ModuleMemoryOffset::None`). This module deliberately has no `memory` +/// declaration, to pin the fix rather than accidentally re-passing via the +/// workaround of declaring one. +#[test] +fn wasmtime_accepts_our_debug_info_for_a_module_with_no_memory() { + let build = DebugBuild::new(indoc! {" + fn add(a: i32, b: i32) -> i32 { + local sum = a + b; + sum + } + + fn compute(n: i32) -> i32 { + local mut total: i32 = 0; + local mut i: i32 = 0; + loop { + if i >= n { break }; + total = add(total, i); + i += 1; + } + total + } + + export { compute } + "}); + let sections = build.sections(); + let mut bytecode = build.bytecode.clone(); + for (name, payload) in [ + (".debug_abbrev", §ions.debug_abbrev), + (".debug_info", §ions.debug_info), + (".debug_line", §ions.debug_line), + (".debug_str", §ions.debug_str), + (".debug_line_str", §ions.debug_line_str), + (".debug_aranges", §ions.debug_aranges), + ] { + bytecode.extend_from_slice(&custom_section(name, payload)); + } + + let mut config = wasmtime::Config::new(); + config.debug_info(true); + let engine = wasmtime::Engine::new(&config).unwrap(); + let module = wasmtime::Module::new(&engine, &bytecode).unwrap(); + let mut store = wasmtime::Store::new(&engine, ()); + let instance = + wasmtime::Instance::new(&mut store, &module, &[]).unwrap(); + let compute = instance + .get_typed_func::(&mut store, "compute") + .unwrap(); + let result = compute.call(&mut store, 5).unwrap(); + assert_eq!(result, 10); +} + +/// Independent, real-world verification beyond this module's own tests: +/// `addr2line` (same `gimli-rs` org as `gimli` itself, the standard Rust +/// tool for "what function/file/line is this address in") performs exactly +/// the address→function resolution that Chrome's DWARF extension was +/// observed failing at in manual browser testing — this gives a fast, +/// scriptable way to check that resolution independently of a browser. +#[test] +fn addr2line_resolves_function_and_location_for_a_mid_function_pc() { + let build = DebugBuild::new(indoc! {" + fn compute(x: i32) -> i32 { + local y = x + 1; + y * 2 + } + export { compute } + "}); + let sections = build.sections(); + let dwarf = load_dwarf(§ions); + let context = addr2line::Context::from_dwarf(dwarf).unwrap(); + + // Probe a PC in the middle of the function, not just its first + // instruction — mirrors "paused mid-function", the exact scenario + // Chrome's extension failed to resolve to a function. + let info = &build.function_debug_info[0]; + let probe = info.start + (info.end - info.start) / 2; + + let mut frames = + context.find_frames(probe as u64).skip_all_loads().unwrap(); + let frame = frames + .next() + .unwrap() + .expect("addr2line found no frame for this pc"); + + let function = frame.function.unwrap(); + assert_eq!(function.raw_name().unwrap(), "compute"); + + let location = frame.location.expect("addr2line found no source location"); + // "./main.wx", not "main.wx": addr2line joins our (empty, by design — + // see `LineProgramBuilder::build`) directory entry with the file name, + // normalizing an empty directory to "." — not a bug in our encoder. + assert_eq!(location.file, Some("./main.wx")); + assert!(location.line.is_some()); +} diff --git a/crates/wx-compiler/src/lib.rs b/crates/wx-compiler/src/lib.rs index 5ed6e17..dc67c67 100644 --- a/crates/wx-compiler/src/lib.rs +++ b/crates/wx-compiler/src/lib.rs @@ -1,8 +1,27 @@ pub mod ast; pub mod codegen; +pub mod dwarf; pub mod mir; pub mod opt; #[cfg(test)] pub mod testing; pub mod tir; pub mod vfs; +pub mod wasm; + +/// Selects which per-function lowering pipeline `codegen::Builder::build` +/// uses, and (via each caller's own MIR-cleanup step) whether `#[inline]` +/// substitution runs. +/// +/// `Release` — the sea-of-nodes `opt::builder`/`opt::scheduler` pipeline: +/// `#[inline]` substitution, CSE, spill decisions. Today's only behavior. +/// +/// `Debug` — `mir::scheduler`, lowering the MIR expression tree directly: +/// no inlining, no CSE, no scheduling decisions — every declared local gets +/// its own fixed WASM slot and instructions come out in exactly the order +/// the source implies. +#[derive(Clone, Copy, PartialEq)] +pub enum CompilationMode { + Release, + Debug, +} diff --git a/crates/wx-compiler/src/mir/inlining.rs b/crates/wx-compiler/src/mir/inlining.rs index d0dc85b..864dc6f 100644 --- a/crates/wx-compiler/src/mir/inlining.rs +++ b/crates/wx-compiler/src/mir/inlining.rs @@ -214,6 +214,7 @@ fn inline_call( arguments: Box<[Expression]>, caller_scopes: &mut Vec, call_site_scope: ScopeIndex, + call_span: ast::TextSpan, ) -> Expression { let result_ty = callee.block.ty; @@ -223,6 +224,7 @@ fn inline_call( kind: tir::BlockKind::Block, parent: Some(call_site_scope), locals: vec![], + locals_debug: vec![], result: result_ty, }); @@ -244,13 +246,17 @@ fn inline_call( .into_vec() .into_iter() .enumerate() - .map(|(i, arg)| Expression { - ty: Type::Unit, - kind: ExprKind::LocalSet { - scope_index: body_scope_offset, - local_index: i as LocalIndex, - value: Box::new(arg), - }, + .map(|(i, arg)| { + let arg_span = arg.span; + Expression { + ty: Type::Unit, + kind: ExprKind::LocalSet { + scope_index: body_scope_offset, + local_index: i as LocalIndex, + value: Box::new(arg), + }, + span: arg_span, + } }) .collect(); @@ -264,6 +270,7 @@ fn inline_call( scope_index: wrapper_scope, expressions: exprs.into_boxed_slice(), }, + span: call_span, } } @@ -583,11 +590,17 @@ fn inline_expr( ExprKind::Call { arguments, .. } => arguments, _ => unreachable!(), }; - *expr = inline_call(inline_body, arguments, caller_scopes, current_scope); + *expr = inline_call( + inline_body, + arguments, + caller_scopes, + current_scope, + expr.span, + ); } /// Directed call graph over MIR function `DefId`s. -struct CallGraph { +pub struct CallGraph { /// `callees[A]` = functions that A calls. callees: HashMap>, /// `callers[A]` = functions that call A. @@ -595,7 +608,7 @@ struct CallGraph { } impl CallGraph { - fn build( + pub fn build( functions: &[Function], call_edges: &[(ast::DefId, ast::DefId)], ) -> Self { @@ -621,87 +634,117 @@ impl CallGraph { } } -/// Inlines all `#[inline]` functions in topological order, then removes -/// unreachable functions — and unreachable imported functions — via dead -/// code elimination from export roots. -pub fn run_inlining_pass(mir: &mut MIR) { - let mut graph = CallGraph::build(&mir.functions, &mir.call_edges); +impl MIR { + /// Inlines all `#[inline]` functions in topological order, patching + /// `graph` in place to reflect inlined edges as it goes (see the "Update + /// graph" step below) — `self.call_edges` itself is never touched, so a + /// caller that needs an accurate post-inlining graph (e.g. for + /// `dead_code_eliminate`) must reuse this same `graph` rather than + /// rebuilding one from `self.call_edges`. Named `inline_calls` rather + /// than `inline_functions` to avoid colliding with `MIR::inline_functions`, + /// the set of DefIds this pass reads to decide what's eligible. + pub fn inline_calls(&mut self, graph: &mut CallGraph) { + // DefId → index in self.functions for O(1) mutation during inlining. + let func_idx: HashMap = self + .functions + .iter() + .enumerate() + .map(|(i, f)| (f.id, i)) + .collect(); - // DefId → index in mir.functions for O(1) mutation during inlining. - let func_idx: HashMap = mir - .functions - .iter() - .enumerate() - .map(|(i, f)| (f.id, i)) - .collect(); + // Kahn's algorithm on the inline subgraph: + // in-degree = number of inline callees not yet processed. + let mut inline_callee_count: HashMap = self + .inline_functions + .iter() + .map(|&id| { + let count = graph.callees[&id] + .iter() + .filter(|c| self.inline_functions.contains(c)) + .count(); + (id, count) + }) + .collect(); - // Kahn's algorithm on the inline subgraph: - // in-degree = number of inline callees not yet processed. - let mut inline_callee_count: HashMap = mir - .inline_functions - .iter() - .map(|&id| { - let count = graph.callees[&id] - .iter() - .filter(|c| mir.inline_functions.contains(c)) - .count(); - (id, count) - }) - .collect(); + let mut queue: VecDeque = inline_callee_count + .iter() + .filter(|(_, n)| **n == 0) + .map(|(&id, _)| id) + .collect(); - let mut queue: VecDeque = inline_callee_count - .iter() - .filter(|(_, n)| **n == 0) - .map(|(&id, _)| id) - .collect(); + // Outer loop: run Kahn's, then break one mutual-recursion cycle at a + // time. When all inline callees have been processed the inner while + // loop drains to empty and there are no stalled nodes left, so we + // break out. + loop { + while let Some(f_id) = queue.pop_front() { + // f's body is clean: all of its inline callees were processed first. + // Clone once here; inline_call will clone scopes+block again per call site. + let f_body = self.functions[func_idx[&f_id]].clone(); - // Outer loop: run Kahn's, then break one mutual-recursion cycle at a time. - // When all inline callees have been processed the inner while loop drains - // to empty and there are no stalled nodes left, so we break out. - loop { - while let Some(f_id) = queue.pop_front() { - // f's body is clean: all of its inline callees were processed first. - // Clone once here; inline_call will clone scopes+block again per call site. - let f_body = mir.functions[func_idx[&f_id]].clone(); + let caller_ids: Vec = + graph.callers[&f_id].iter().copied().collect(); + // f's own callee set doesn't change while its callers are being + // processed below (only caller/f edges are touched), so collect + // it once here instead of re-cloning it on every caller. + let f_callees: Vec = + graph.callees[&f_id].iter().copied().collect(); + for caller_id in caller_ids { + let ci = func_idx[&caller_id]; + let caller_func = &mut self.functions[ci]; + inline_expr( + &mut caller_func.block, + &mut caller_func.scopes, + f_id, + &f_body, + 0, + ); + caller_func + .static_data + .extend_from_slice(&f_body.static_data); - let caller_ids: Vec = - graph.callers[&f_id].iter().copied().collect(); - // f's own callee set doesn't change while its callers are being - // processed below (only caller/f edges are touched), so collect - // it once here instead of re-cloning it on every caller. - let f_callees: Vec = - graph.callees[&f_id].iter().copied().collect(); - for caller_id in caller_ids { - let ci = func_idx[&caller_id]; - let caller_func = &mut mir.functions[ci]; - inline_expr( - &mut caller_func.block, - &mut caller_func.scopes, - f_id, - &f_body, - 0, - ); - caller_func - .static_data - .extend_from_slice(&f_body.static_data); + // Update graph: remove caller → f, propagate f's callees to caller. + graph.callees.get_mut(&caller_id).unwrap().remove(&f_id); + graph.callers.get_mut(&f_id).unwrap().remove(&caller_id); + for callee_id in f_callees.iter().copied() { + graph + .callees + .get_mut(&caller_id) + .unwrap() + .insert(callee_id); + graph + .callers + .get_mut(&callee_id) + .unwrap() + .insert(caller_id); + } - // Update graph: remove caller → f, propagate f's callees to caller. - graph.callees.get_mut(&caller_id).unwrap().remove(&f_id); - graph.callers.get_mut(&f_id).unwrap().remove(&caller_id); - for callee_id in f_callees.iter().copied() { - graph - .callees - .get_mut(&caller_id) - .unwrap() - .insert(callee_id); - graph - .callers - .get_mut(&callee_id) - .unwrap() - .insert(caller_id); + // If caller is also inline, one of its pending inline callees is done. + if let Some(count) = inline_callee_count.get_mut(&caller_id) + { + *count -= 1; + if *count == 0 { + queue.push_back(caller_id); + } + } } + // graph.callers[f_id] is now empty — f is dead. + } - // If caller is also inline, one of its pending inline callees is done. + // Cycle-breaker: any inline function still with count > 0 is part of a + // mutual-recursion cycle. Inlining it fully would require infinite + // expansion, so we evict one "anchor" per iteration — it stays as an + // ordinary call target — then decrement its inline callers so they may + // become unblocked and get inlined on the next inner-loop pass. + let anchor = inline_callee_count + .iter() + .find(|(_, n)| **n > 0) + .map(|(&id, _)| id); + let Some(anchor) = anchor else { break }; + inline_callee_count.remove(&anchor); + for caller_id in + graph.callers[&anchor].iter().copied().collect::>() + { if let Some(count) = inline_callee_count.get_mut(&caller_id) { *count -= 1; if *count == 0 { @@ -709,65 +752,50 @@ pub fn run_inlining_pass(mir: &mut MIR) { } } } - // graph.callers[f_id] is now empty — f is dead. } + } - // Cycle-breaker: any inline function still with count > 0 is part of a - // mutual-recursion cycle. Inlining it fully would require infinite - // expansion, so we evict one "anchor" per iteration — it stays as an - // ordinary call target — then decrement its inline callers so they may - // become unblocked and get inlined on the next inner-loop pass. - let anchor = inline_callee_count + /// Removes functions — and unreachable imported functions — that aren't + /// reachable from export roots or the start function, via BFS over + /// `graph`. Pass the same `graph` `inline_calls` patched if inlining ran + /// first, or a fresh `CallGraph::build(&self.functions, &self.call_edges)` + /// if not. + pub fn dead_code_eliminate(&mut self, graph: &CallGraph) { + // Dead code elimination: BFS from exported functions and the start function. + let mut live: HashSet = self + .exports .iter() - .find(|(_, n)| **n > 0) - .map(|(&id, _)| id); - let Some(anchor) = anchor else { break }; - inline_callee_count.remove(&anchor); - for caller_id in - graph.callers[&anchor].iter().copied().collect::>() - { - if let Some(count) = inline_callee_count.get_mut(&caller_id) { - *count -= 1; - if *count == 0 { - queue.push_back(caller_id); + .filter_map(|e| match e { + ExportItem::Function { id, .. } => Some(*id), + _ => None, + }) + .collect(); + if let Some(start_id) = self.start_function { + live.insert(start_id); + } + let mut dce_queue: VecDeque = + live.iter().copied().collect(); + while let Some(id) = dce_queue.pop_front() { + for &callee_id in graph.callees.get(&id).into_iter().flatten() { + if live.insert(callee_id) { + dce_queue.push_back(callee_id); } } } - } + self.functions.retain(|f| live.contains(&f.id)); - // Dead code elimination: BFS from exported functions and the start function. - let mut live: HashSet = mir - .exports - .iter() - .filter_map(|e| match e { - ExportItem::Function { id, .. } => Some(*id), - _ => None, - }) - .collect(); - if let Some(start_id) = mir.start_function { - live.insert(start_id); - } - let mut dce_queue: VecDeque = live.iter().copied().collect(); - while let Some(id) = dce_queue.pop_front() { - for &callee_id in graph.callees.get(&id).into_iter().flatten() { - if live.insert(callee_id) { - dce_queue.push_back(callee_id); - } + // Imported functions share the same DefId space and flow through the + // same call_edges as regular calls (see `record_call_edge`), so `live` + // already tells us which imported functions are actually reachable. + // Imported globals/memories aren't tracked here yet — they're not part + // of `call_edges` — so leave those import kinds untouched for now. + for module in &mut self.imports { + module.items.retain(|item| match item { + ImportModuleItem::Function { id, .. } => live.contains(id), + ImportModuleItem::Global { .. } + | ImportModuleItem::Memory { .. } => true, + }); } + self.imports.retain(|module| !module.items.is_empty()); } - mir.functions.retain(|f| live.contains(&f.id)); - - // Imported functions share the same DefId space and flow through the - // same call_edges as regular calls (see `record_call_edge`), so `live` - // already tells us which imported functions are actually reachable. - // Imported globals/memories aren't tracked here yet — they're not part - // of `call_edges` — so leave those import kinds untouched for now. - for module in &mut mir.imports { - module.items.retain(|item| match item { - ImportModuleItem::Function { id, .. } => live.contains(id), - ImportModuleItem::Global { .. } - | ImportModuleItem::Memory { .. } => true, - }); - } - mir.imports.retain(|module| !module.items.is_empty()); } diff --git a/crates/wx-compiler/src/mir/mod.rs b/crates/wx-compiler/src/mir/mod.rs index cbf3889..13ba084 100644 --- a/crates/wx-compiler/src/mir/mod.rs +++ b/crates/wx-compiler/src/mir/mod.rs @@ -7,9 +7,13 @@ use string_interner::symbol::SymbolU32; use crate::ast::{self, DefIdGenerator}; use crate::tir::{self, ItemAttribute}; +use crate::vfs; mod inlining; -use inlining::{rebase_scope, run_inlining_pass}; +pub use inlining::CallGraph; +use inlining::rebase_scope; + +pub mod scheduler; #[cfg(test)] mod tests; @@ -373,6 +377,7 @@ impl Type { pub struct Expression { pub kind: ExprKind, pub ty: Type, + pub span: ast::TextSpan, } #[derive(Clone)] @@ -386,6 +391,25 @@ pub struct Aggregate { decl_to_phys: Box<[u32]>, } +/// Recursively flatten a MIR type into its constituent scalar leaf types — +/// `Unit`/`Never` produce no slots, `Aggregate` recurses field by field +/// (in physical/layout order, matching `Aggregate::values`), everything else +/// is already a leaf. Shared by `opt::scheduler` (which converts leaves to +/// `ScalarType`) and `mir::scheduler` (which does the same for its own +/// direct-lowering locals) — kept MIR-only so it has no dependency on `opt`. +pub fn flatten_type(ty: Type, aggregates: &[Aggregate]) -> Vec { + match ty { + Type::Unit | Type::Never => vec![], + Type::Aggregate { aggregate_index } => aggregates + [aggregate_index as usize] + .values + .iter() + .flat_map(|&f| flatten_type(f, aggregates)) + .collect(), + scalar => vec![scalar], + } +} + /// Whether a memory is locally defined or provided by the WASM host. /// `External < Internal` so a stable sort puts imported memories first, /// matching the WASM binary format requirement. @@ -465,7 +489,7 @@ pub struct MIR { pub aggregates: Box<[Aggregate]>, pub static_entries: Vec, /// Direct call edges collected during lowering: (caller_mir_id, - /// callee_mir_id). Consumed by `run_inlining_pass` to build the call + /// callee_mir_id). Consumed by `CallGraph::build` to build the call /// graph. #[cfg_attr(test, serde(skip))] pub call_edges: Vec<(ast::DefId, ast::DefId)>, @@ -518,12 +542,57 @@ pub struct Local { pub mutability: Mutability, } +/// Debug-only metadata for one declared local: its source name and, for a +/// directly struct-typed local, its struct's own name and field names. +/// Lives in `BlockScope::locals_debug`, a side table parallel to `locals` +/// rather than fields on `Local` itself — `Local` is shared by every build +/// (including Release, which never reads this), so keeping it out entirely +/// means Release pays nothing: `locals_debug` stays an empty, unallocated +/// `Vec` unless `MIR::build` is asked for debug info (see `CompilationMode`). +/// +/// Only covers a scope's originally-declared locals (params + `local` +/// statements, known from TIR before lowering starts) — compiler- +/// synthesized temporaries appended later during expression lowering (see +/// the `frame[0].locals.push` call sites below) never get an entry, so +/// `locals_debug.get(local_index)` returning `None` doubles as "this local +/// has no debug info" for exactly those temporaries, with no separate +/// tracking needed. +#[cfg_attr(test, derive(serde::Serialize))] +#[derive(Clone)] +pub struct LocalDebugInfo { + pub name: SymbolU32, + pub struct_debug: Option, +} + +/// Present on a [`LocalDebugInfo`] when its local's type is directly a named +/// struct (not a tuple, pointer-to-struct, or a struct nested inside +/// another aggregate) — that struct's own name and its fields' names in +/// physical/layout order (matching `Aggregate::values`/`::offsets`' order). +/// Resolved fresh from TIR per `Local` rather than stored on `Aggregate` +/// itself: `Aggregate` is deduplicated purely by structural shape, so two +/// differently-named, identically-shaped structs (or a struct and a +/// same-shaped tuple) can share one `Aggregate` entry — storing names there +/// would mean one of them shows the other's names. `None` (missing this +/// struct — not represented) for anything not a struct one level deep; +/// `--debug` info for those falls back to positional field labels. +#[cfg_attr(test, derive(serde::Serialize))] +#[derive(Clone)] +pub struct StructDebugInfo { + pub name: SymbolU32, + /// Field names in physical (layout) order — same order as + /// `Aggregate::values`/`::offsets` for this local's aggregate. + pub field_names: Box<[SymbolU32]>, +} + #[cfg_attr(test, derive(serde::Serialize))] #[derive(Clone)] pub struct BlockScope { pub kind: tir::BlockKind, pub parent: Option, pub locals: Vec, + /// Empty unless `MIR::build` was asked to collect `--debug` info — see + /// [`LocalDebugInfo`]. + pub locals_debug: Vec, pub result: Type, } @@ -555,6 +624,16 @@ pub struct Function { /// Codegen unions these across all live functions to determine which /// entries to include in the WASM data segment. pub static_data: Vec, + /// The file this function was declared in — every span on `block` is + /// relative to this file's text. Safe to treat as a single file per + /// function only because `--debug` (the only mode that reads spans) + /// never inlines, so a function's body never mixes expressions lowered + /// from a different source file. + pub file_id: vfs::FileId, + /// The function's declared name — `None` unless `MIR::build` was asked + /// for `--debug` info (see `CompilationMode`), and always `None` for the + /// synthetic `__wx_start` function, which has no single source name. + pub name: Option, } #[cfg_attr(test, derive(serde::Serialize))] @@ -607,10 +686,12 @@ impl MIR { tir: &tir::TIR, interner: &ast::StringInterner, id_generator: ast::DefIdGenerator, + mode: crate::CompilationMode, ) -> MIR { let mut builder = Builder { tir, interner, + mode, aggregate_index_lookup: HashMap::new(), aggregates: Vec::new(), signature_pool: Vec::new(), @@ -742,7 +823,7 @@ impl MIR { functions.push(f.clone()); } - let mut mir = MIR { + let mir = MIR { functions, inline_functions, globals, @@ -809,7 +890,6 @@ impl MIR { static_entries: builder.static_entries, }; - run_inlining_pass(&mut mir); mir } } @@ -862,6 +942,10 @@ enum IndexAddress { struct Builder<'tir> { tir: &'tir tir::TIR, interner: &'tir ast::StringInterner, + /// Set once from `MIR::build`'s own `mode` argument. `lower_function`/ + /// `build_start_function` read it to decide whether to populate + /// `BlockScope::locals_debug` — see [`LocalDebugInfo`]. + mode: crate::CompilationMode, aggregate_index_lookup: HashMap<(FieldOrder, Box<[Type]>), AggregateIndex>, aggregates: Vec, /// Concrete function signatures, interned on demand. The index into this @@ -1217,6 +1301,49 @@ impl<'tir> Builder<'tir> { aggregate_index } + /// See [`StructDebugInfo`]. `mir_ty` is `tir_ty` already lowered (via + /// `lower_type_index`, immediately before this is called) — reused here + /// rather than re-derived, just to confirm it's actually an aggregate + /// and to find that aggregate's `decl_to_phys` mapping. Must run before + /// `current_substitutions` changes again (e.g. before recursing into a + /// different generic context), since `resolve_tir_type` reads it. + fn struct_debug_info( + &self, + tir_ty: tir::TypeIndex, + mir_ty: Type, + ) -> Option { + let Type::Aggregate { aggregate_index } = mir_ty else { + return None; + }; + let resolved = self.resolve_tir_type(tir_ty); + let tir::Type::Struct { struct_index, .. } = + &self.tir.types[resolved.as_usize()] + else { + return None; + }; + let tir_struct = &self.tir.structs[*struct_index as usize]; + let aggregate = &self.aggregates[aggregate_index as usize]; + + // `decl_to_phys` already maps each field straight to its physical + // slot — scatter names directly there in one pass instead of + // collecting-then-sorting. + let mut field_names: Vec> = + vec![None; tir_struct.fields.len()]; + for (decl, field) in tir_struct.fields.iter().enumerate() { + field_names[aggregate.decl_to_phys[decl] as usize] = + Some(field.name.inner); + } + Some(StructDebugInfo { + name: tir_struct.name.inner, + field_names: field_names + .into_iter() + .map(|n| { + n.expect("decl_to_phys is a bijection over declared fields") + }) + .collect(), + }) + } + fn lower_type_index(&mut self, type_idx: tir::TypeIndex) -> Type { match type_idx { idx if idx == tir::TypeIndex::ERROR => unreachable!(), @@ -1397,22 +1524,31 @@ impl<'tir> Builder<'tir> { .map(|scope| { let result_type_idx = scope.inferred_type.infer_or(tir::TypeIndex::UNIT); - let locals = scope - .locals - .iter() - .map(|tir_local| Local { - ty: self.lower_type_index(tir_local.ty), + let mut locals = Vec::with_capacity(scope.locals.len()); + let mut locals_debug = Vec::new(); + for tir_local in &scope.locals { + let ty = self.lower_type_index(tir_local.ty); + if self.mode == crate::CompilationMode::Debug { + locals_debug.push(LocalDebugInfo { + name: tir_local.name.inner, + struct_debug: self + .struct_debug_info(tir_local.ty, ty), + }); + } + locals.push(Local { + ty, mutability: if tir_local.mut_span.is_some() { Mutability::Mutable } else { Mutability::Immutable }, - }) - .collect(); + }); + } BlockScope { kind: scope.kind, parent: scope.parent, locals, + locals_debug, result: self.lower_type_index(result_type_idx), } }) @@ -1434,6 +1570,9 @@ impl<'tir> Builder<'tir> { scopes: ctx.frame, block, static_data: ctx.static_data, + file_id: func.file_id, + name: (self.mode == crate::CompilationMode::Debug) + .then_some(func.name.inner), } } @@ -1490,6 +1629,11 @@ impl<'tir> Builder<'tir> { if globals_with_init.is_empty() { return None; } + // Synthetic function stitching together every global's initializer, + // which could in principle span multiple files — arbitrarily + // attributed to the first one, since this body already has no real + // single source span either (see the `Block`'s span below). + let file_id = globals_with_init[0].file_id; self.current_function_id = Some(start_id); @@ -1498,6 +1642,7 @@ impl<'tir> Builder<'tir> { kind: tir::BlockKind::Block, parent: None, locals: vec![], + locals_debug: vec![], result: Type::Unit, }; let mut combined_frame: Vec = vec![root_scope]; @@ -1514,22 +1659,31 @@ impl<'tir> Builder<'tir> { .map(|scope| { let result_ty = scope.inferred_type.infer_or(tir::TypeIndex::UNIT); - let locals = scope - .locals - .iter() - .map(|tir_local| Local { - ty: self.lower_type_index(tir_local.ty), + let mut locals = Vec::with_capacity(scope.locals.len()); + let mut locals_debug = Vec::new(); + for tir_local in &scope.locals { + let ty = self.lower_type_index(tir_local.ty); + if self.mode == crate::CompilationMode::Debug { + locals_debug.push(LocalDebugInfo { + name: tir_local.name.inner, + struct_debug: self + .struct_debug_info(tir_local.ty, ty), + }); + } + locals.push(Local { + ty, mutability: if tir_local.mut_span.is_some() { Mutability::Mutable } else { Mutability::Immutable }, - }) - .collect(); + }); + } BlockScope { kind: scope.kind, parent: scope.parent, locals, + locals_debug, result: self.lower_type_index(result_ty), } }) @@ -1570,6 +1724,7 @@ impl<'tir> Builder<'tir> { value: Box::new(lowered), }, ty: Type::Unit, + span: body.block.span, }); combined_static_data.extend(ctx.static_data); } @@ -1590,8 +1745,13 @@ impl<'tir> Builder<'tir> { expressions: combined_body.into_boxed_slice(), }, ty: Type::Unit, + // Synthetic: this block stitches together every global's + // initializer, so it has no single originating source span. + span: ast::TextSpan { start: 0, end: 0 }, }, static_data: combined_static_data, + file_id, + name: None, }) } @@ -1720,6 +1880,7 @@ impl<'tir> Builder<'tir> { value: Box::new(lowered), }, ty: Type::Unit, + span: object.span, }); (0, temp) } @@ -1731,6 +1892,7 @@ impl<'tir> Builder<'tir> { value_index: 0, }, ty: ptr_ty, + span: object.span, }; (ptr, ptr_ty) } else { @@ -1749,6 +1911,9 @@ impl<'tir> Builder<'tir> { let idx_ty = self.lower_type_index(index.ty); let idx = self.lower_expression(func_ctx, index, sink); + // Spans the whole indexing expression (`object[index]`), since none + // of these arithmetic nodes has a source counterpart of its own. + let span = ast::TextSpan::new(object.span.start, index.span.end); IndexAddress::Dynamic(Expression { kind: ExprKind::Add { left: Box::new(base), @@ -1760,12 +1925,15 @@ impl<'tir> Builder<'tir> { value: elem_size as i64, }, ty: idx_ty, + span, }), }, ty: idx_ty, + span, }), }, ty: ptr_ty, + span, }) } @@ -1773,25 +1941,33 @@ impl<'tir> Builder<'tir> { /// scalar — shared by every place that reads a `ConstValue` cached on TIR /// (`Constant`, `EnumVariant`) so codegen never has to re-walk the original /// expression tree just to rediscover a value TIR already computed. - fn lower_const_value(const_value: tir::ConstValue, ty: Type) -> Expression { + fn lower_const_value( + const_value: tir::ConstValue, + ty: Type, + span: ast::TextSpan, + ) -> Expression { match const_value { tir::ConstValue::Int(value) => Expression { kind: ExprKind::Int { value }, ty, + span, }, tir::ConstValue::Float(value) => Expression { kind: ExprKind::Float { value }, ty, + span, }, tir::ConstValue::Bool(value) => Expression { kind: ExprKind::Bool { value }, ty, + span, }, tir::ConstValue::Char(value) => Expression { kind: ExprKind::Int { value: value as i64, }, ty, + span, }, } } @@ -1810,26 +1986,32 @@ impl<'tir> Builder<'tir> { | tir::ExprKind::Memory { .. } => Expression { kind: ExprKind::Noop, ty: Type::Unit, + span: expr.span, }, tir::ExprKind::Unreachable => Expression { kind: ExprKind::Unreachable, ty: Type::Never, + span: expr.span, }, tir::ExprKind::Int { value } => Expression { kind: ExprKind::Int { value: *value }, ty: self.lower_type_index(expr.ty), + span: expr.span, }, tir::ExprKind::Float { value } => Expression { kind: ExprKind::Float { value: *value }, ty: self.lower_type_index(expr.ty), + span: expr.span, }, tir::ExprKind::Bool { value } => Expression { kind: ExprKind::Bool { value: *value }, ty: Type::Bool, + span: expr.span, }, tir::ExprKind::Global { id } => Expression { kind: ExprKind::Global { id: *id }, ty: self.lower_type_index(expr.ty), + span: expr.span, }, tir::ExprKind::Local { scope_index, @@ -1840,6 +2022,7 @@ impl<'tir> Builder<'tir> { local_index: *local_index, }, ty: self.lower_type_index(expr.ty), + span: expr.span, }, tir::ExprKind::Function { id } => { // If the FunctionItem carries non-empty type_args the reference is a @@ -1878,6 +2061,7 @@ impl<'tir> Builder<'tir> { Expression { kind: ExprKind::Function { id: mono_id }, ty: Type::Function { signature_index }, + span: expr.span, } } _ => { @@ -1885,6 +2069,7 @@ impl<'tir> Builder<'tir> { Expression { kind: ExprKind::Function { id: *id }, ty: self.lower_type_index(expr.ty), + span: expr.span, } } } @@ -1894,6 +2079,7 @@ impl<'tir> Builder<'tir> { value: *value as i64, }, ty: Type::U32, + span: expr.span, }, tir::ExprKind::String { symbol } => { // The literal's slice type says which memory its bytes are @@ -1914,6 +2100,7 @@ impl<'tir> Builder<'tir> { Expression { kind: ExprKind::StaticPointer { data_index }, ty: self.pointer_type(memory_id), + span: expr.span, }, Expression { kind: ExprKind::Int { value: size as i64 }, @@ -1922,10 +2109,12 @@ impl<'tir> Builder<'tir> { ty: self.lower_type_index( self.tir.memories[mem_idx].size.inner, ), + span: expr.span, }, ]), }, ty, + span: expr.span, } } tir::ExprKind::Return { value } => Expression { @@ -1935,6 +2124,7 @@ impl<'tir> Builder<'tir> { }), }, ty: Type::Never, + span: expr.span, }, tir::ExprKind::EnumVariant { enum_index, @@ -1946,6 +2136,7 @@ impl<'tir> Builder<'tir> { Some(const_value) => Self::lower_const_value( const_value, self.lower_type_index(expr.ty), + expr.span, ), // Error-free TIR guarantees every variant folds to a // constant — see the `NotConstEvaluatable`/range checks @@ -1971,6 +2162,7 @@ impl<'tir> Builder<'tir> { func_ctx, func.name.inner, expr.ty, + expr.span, type_args, arguments, sink, @@ -2024,10 +2216,12 @@ impl<'tir> Builder<'tir> { ty: Type::Function { signature_index: callee_sig_idx, }, + span: expr.span, }), arguments: lowered_args, }, ty: self.lower_type_index(expr.ty), + span: expr.span, } } tir::ExprKind::GenericMethodCall { @@ -2116,10 +2310,12 @@ impl<'tir> Builder<'tir> { ty: Type::Function { signature_index: callee_sig_idx, }, + span: expr.span, }), arguments: lowered_args, }, ty: self.lower_type_index(expr.ty), + span: expr.span, } } tir::ExprKind::Call { callee, arguments } => { @@ -2136,6 +2332,7 @@ impl<'tir> Builder<'tir> { func_ctx, func.name.inner, expr.ty, + expr.span, &[], arguments, sink, @@ -2149,6 +2346,7 @@ impl<'tir> Builder<'tir> { Expression { kind: ExprKind::Call { callee, arguments }, ty: self.lower_type_index(expr.ty), + span: expr.span, } } tir::ExprKind::MethodCall { arguments, id } => { @@ -2162,6 +2360,7 @@ impl<'tir> Builder<'tir> { ty: Type::Function { signature_index: callee_sig_idx, }, + span: expr.span, }); let arguments: Box<_> = arguments .iter() @@ -2170,6 +2369,7 @@ impl<'tir> Builder<'tir> { Expression { kind: ExprKind::Call { callee, arguments }, ty: self.lower_type_index(expr.ty), + span: expr.span, } } tir::ExprKind::NamespaceAccess { namespace, member } => { @@ -2192,6 +2392,7 @@ impl<'tir> Builder<'tir> { memory: *id, }, ty: result_ty, + span: expr.span, }; } "MEMORY_INDEX" => { @@ -2200,6 +2401,7 @@ impl<'tir> Builder<'tir> { memory: *id, }, ty: result_ty, + span: expr.span, }; } _ => unreachable!(), @@ -2207,9 +2409,11 @@ impl<'tir> Builder<'tir> { }; match self.tir.constants[const_idx].const_value { - Some(const_value) => { - Self::lower_const_value(const_value, result_ty) - } + Some(const_value) => Self::lower_const_value( + const_value, + result_ty, + expr.span, + ), None => unreachable!(), } } @@ -2222,7 +2426,7 @@ impl<'tir> Builder<'tir> { if let Some(const_value) = self.tir.constants[const_idx].const_value { - Self::lower_const_value(const_value, result_ty) + Self::lower_const_value(const_value, result_ty, expr.span) } else if self.tir.constants[const_idx].value.is_some() { todo!("complex const expression in MIR lowering") } else { @@ -2261,6 +2465,7 @@ impl<'tir> Builder<'tir> { value_index: phys_index, }, ty: field_ty, + span: expr.span, }, _ => { let object_ty = self.lower_type_index(object.ty); @@ -2280,6 +2485,7 @@ impl<'tir> Builder<'tir> { value: Box::new(object_lowered), }, ty: Type::Unit, + span: expr.span, }); Expression { @@ -2289,6 +2495,7 @@ impl<'tir> Builder<'tir> { value_index: phys_index, }, ty: field_ty, + span: expr.span, } } } @@ -2319,6 +2526,7 @@ impl<'tir> Builder<'tir> { Expression { kind: ExprKind::Aggregate { values }, ty: Type::Aggregate { aggregate_index }, + span: expr.span, } } tir::ExprKind::TupleInit { elements } => { @@ -2351,6 +2559,7 @@ impl<'tir> Builder<'tir> { Expression { kind: ExprKind::Aggregate { values }, ty: Type::Aggregate { aggregate_index }, + span: expr.span, } } tir::ExprKind::IfElse { @@ -2372,6 +2581,7 @@ impl<'tir> Builder<'tir> { else_block, }, ty: self.lower_type_index(expr.ty), + span: expr.span, } } tir::ExprKind::Match { scrutinee, arms } => { @@ -2416,6 +2626,7 @@ impl<'tir> Builder<'tir> { default, }, ty: self.lower_type_index(expr.ty), + span: expr.span, } } tir::ExprKind::Break { scope_index, value } => Expression { @@ -2426,12 +2637,14 @@ impl<'tir> Builder<'tir> { }), }, ty: self.lower_type_index(expr.ty), + span: expr.span, }, tir::ExprKind::Continue { scope_index } => Expression { kind: ExprKind::Continue { scope_index: *scope_index, }, ty: Type::Never, + span: expr.span, }, tir::ExprKind::Loop { scope_index, block } => Expression { kind: ExprKind::Loop { @@ -2441,6 +2654,7 @@ impl<'tir> Builder<'tir> { ), }, ty: self.lower_type_index(expr.ty), + span: expr.span, }, tir::ExprKind::Block { scope_index, @@ -2470,6 +2684,7 @@ impl<'tir> Builder<'tir> { expressions: inner_sink.into_boxed_slice(), }, ty: self.lower_type_index(expr.ty), + span: expr.span, } } tir::ExprKind::LocalDeclaration { @@ -2486,6 +2701,7 @@ impl<'tir> Builder<'tir> { ), }, ty: self.lower_type_index(expr.ty), + span: expr.span, }, tir::ExprKind::Unary { operator, operand } => { let operand = @@ -2497,6 +2713,7 @@ impl<'tir> Builder<'tir> { UnaryOp::BitNot => ExprKind::BitNot { value: operand }, }, ty: self.lower_type_index(expr.ty), + span: expr.span, } } tir::ExprKind::Binary { @@ -2685,6 +2902,7 @@ impl<'tir> Builder<'tir> { Expression { kind, ty: self.lower_type_index(expr.ty), + span: expr.span, } } tir::ExprKind::ArrayLiteral { elements, memory } => { @@ -2702,6 +2920,7 @@ impl<'tir> Builder<'tir> { return Expression { kind: ExprKind::Int { value: 0 }, ty: self.pointer_type(memory_id), + span: expr.span, }; } let (data_index, _) = @@ -2709,6 +2928,7 @@ impl<'tir> Builder<'tir> { Expression { kind: ExprKind::StaticPointer { data_index }, ty: self.pointer_type(memory_id), + span: expr.span, } } tir::ExprKind::ArrayRepeat { @@ -2729,6 +2949,7 @@ impl<'tir> Builder<'tir> { return Expression { kind: ExprKind::Int { value: 0 }, ty: self.pointer_type(memory_id), + span: expr.span, }; } let (data_index, _) = @@ -2736,6 +2957,7 @@ impl<'tir> Builder<'tir> { Expression { kind: ExprKind::StaticPointer { data_index }, ty: self.pointer_type(memory_id), + span: expr.span, } } tir::ExprKind::SliceRange { object, start, end } => { @@ -2786,6 +3008,7 @@ impl<'tir> Builder<'tir> { value: Box::new(lowered_obj), }, ty: Type::Unit, + span: expr.span, }); (0, temp) } @@ -2797,6 +3020,7 @@ impl<'tir> Builder<'tir> { value_index: 0, }, ty: ptr_ty, + span: expr.span, }; let len = Expression { kind: ExprKind::AggregateGet { @@ -2805,6 +3029,7 @@ impl<'tir> Builder<'tir> { value_index: 1, }, ty: idx_ty, + span: expr.span, }; (ptr, Some(len)) } @@ -2830,6 +3055,7 @@ impl<'tir> Builder<'tir> { value: Box::new(s_lowered), }, ty: Type::Unit, + span: expr.span, }); Some(temp) } else { @@ -2846,6 +3072,7 @@ impl<'tir> Builder<'tir> { local_index: li, }, ty: idx_ty, + span: expr.span, }; let byte_offset = if elem_size == 1 { start_val @@ -2858,9 +3085,11 @@ impl<'tir> Builder<'tir> { value: elem_size as i64, }, ty: idx_ty, + span: expr.span, }), }, ty: idx_ty, + span: expr.span, } }; Expression { @@ -2869,6 +3098,7 @@ impl<'tir> Builder<'tir> { right: Box::new(byte_offset), }, ty: ptr_ty, + span: expr.span, } } }; @@ -2898,6 +3128,7 @@ impl<'tir> Builder<'tir> { value: Box::new(e_lowered), }, ty: Type::Unit, + span: expr.span, }); // Allocate a synthetic block scope for the trap branch. @@ -2908,6 +3139,7 @@ impl<'tir> Builder<'tir> { func_ctx.current_scope_index as u32, ), locals: vec![], + locals_debug: vec![], result: Type::Never, }); @@ -2922,6 +3154,7 @@ impl<'tir> Builder<'tir> { local_index: s_li, }, ty: idx_ty, + span: expr.span, }), right: Box::new(Expression { kind: ExprKind::LocalGet { @@ -2929,9 +3162,11 @@ impl<'tir> Builder<'tir> { local_index: e_temp, }, ty: idx_ty, + span: expr.span, }), }, ty: Type::Bool, + span: expr.span, }), then_block: Box::new(Expression { kind: ExprKind::Block { @@ -2940,14 +3175,17 @@ impl<'tir> Builder<'tir> { Expression { kind: ExprKind::Unreachable, ty: Type::Never, + span: expr.span, }, ]), }, ty: Type::Never, + span: expr.span, }), else_block: None, }, ty: Type::Unit, + span: expr.span, }); Expression { @@ -2956,6 +3194,7 @@ impl<'tir> Builder<'tir> { local_index: e_temp, }, ty: idx_ty, + span: expr.span, } } else { e_lowered @@ -2965,6 +3204,7 @@ impl<'tir> Builder<'tir> { Some(sz) => Expression { kind: ExprKind::Int { value: sz as i64 }, ty: idx_ty, + span: expr.span, }, None => opt_slice_len.unwrap(), }, @@ -2982,9 +3222,11 @@ impl<'tir> Builder<'tir> { local_index: li, }, ty: idx_ty, + span: expr.span, }), }, ty: idx_ty, + span: expr.span, }, }; @@ -2994,6 +3236,7 @@ impl<'tir> Builder<'tir> { values: Box::new([offset_ptr, new_len]), }, ty: result_ty, + span: expr.span, } } tir::ExprKind::Load { place } => { @@ -3006,6 +3249,7 @@ impl<'tir> Builder<'tir> { memory, }, ty: self.lower_type_index(place.ty), + span: expr.span, } } tir::ExprKind::AddressOf { place, .. } => { @@ -3023,9 +3267,11 @@ impl<'tir> Builder<'tir> { value: offset as i64, }, ty: ptr_ty, + span: expr.span, }), }, ty: ptr_ty, + span: expr.span, } } } @@ -3042,6 +3288,7 @@ impl<'tir> Builder<'tir> { memory, }, ty: Type::Unit, + span: expr.span, } } } @@ -3052,6 +3299,7 @@ impl<'tir> Builder<'tir> { func_ctx: &mut FunctionContext, name: SymbolU32, expr_ty: tir::TypeIndex, + span: ast::TextSpan, type_args: &[tir::TypeIndex], arguments: &[tir::Expression], sink: &mut Vec, @@ -3082,6 +3330,7 @@ impl<'tir> Builder<'tir> { Expression { kind: ExprKind::MemoryGrow { memory, delta }, ty: self.lower_type_index(expr_ty), + span, } } "memory_size" => { @@ -3103,6 +3352,7 @@ impl<'tir> Builder<'tir> { Expression { kind: ExprKind::MemorySize { memory }, ty: self.lower_type_index(expr_ty), + span, } } "slice_len" => { @@ -3119,6 +3369,7 @@ impl<'tir> Builder<'tir> { value_index: 1, }, ty: result_ty, + span, }, _ => { let slice_ty = self.lower_type_index(slice_arg.ty); @@ -3136,6 +3387,7 @@ impl<'tir> Builder<'tir> { value: Box::new(lowered), }, ty: Type::Unit, + span, }); Expression { kind: ExprKind::AggregateGet { @@ -3144,6 +3396,7 @@ impl<'tir> Builder<'tir> { value_index: 1, }, ty: result_ty, + span, } } } @@ -3162,6 +3415,7 @@ impl<'tir> Builder<'tir> { value_index: 0, }, ty: result_ty, + span, }, _ => { let slice_ty = self.lower_type_index(slice_arg.ty); @@ -3179,6 +3433,7 @@ impl<'tir> Builder<'tir> { value: Box::new(lowered), }, ty: Type::Unit, + span, }); Expression { kind: ExprKind::AggregateGet { @@ -3187,6 +3442,7 @@ impl<'tir> Builder<'tir> { value_index: 0, }, ty: result_ty, + span, } } } @@ -3200,6 +3456,7 @@ impl<'tir> Builder<'tir> { values: Box::new([data, len]), }, ty: result_ty, + span, } } "size_of" => { @@ -3218,6 +3475,7 @@ impl<'tir> Builder<'tir> { value: layout.size as i64, }, ty: self.lower_type_index(expr_ty), + span, } } "align_of" => { @@ -3236,6 +3494,7 @@ impl<'tir> Builder<'tir> { value: layout.align as i64, }, ty: self.lower_type_index(expr_ty), + span, } } "f32_sqrt" | "f64_sqrt" => Expression { @@ -3247,6 +3506,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "f32_abs" | "f64_abs" => Expression { kind: ExprKind::Abs { @@ -3257,6 +3517,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "f32_floor" | "f64_floor" => Expression { kind: ExprKind::Floor { @@ -3267,6 +3528,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "f32_ceil" | "f64_ceil" => Expression { kind: ExprKind::Ceil { @@ -3277,6 +3539,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "i64_extend_i32" => Expression { kind: ExprKind::I64ExtendI32S { @@ -3287,6 +3550,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "u64_extend_u32" => Expression { kind: ExprKind::I64ExtendI32U { @@ -3297,6 +3561,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "i32_wrap_i64" => Expression { kind: ExprKind::I32WrapI64 { @@ -3307,6 +3572,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "f32_convert_i32" => Expression { kind: ExprKind::F32ConvertI32 { @@ -3317,6 +3583,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "f32_convert_u32" => Expression { kind: ExprKind::F32ConvertU32 { @@ -3327,6 +3594,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "f32_convert_i64" => Expression { kind: ExprKind::F32ConvertI64 { @@ -3337,6 +3605,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "f32_convert_u64" => Expression { kind: ExprKind::F32ConvertU64 { @@ -3347,6 +3616,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "f64_convert_i32" => Expression { kind: ExprKind::F64ConvertI32 { @@ -3357,6 +3627,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "f64_convert_u32" => Expression { kind: ExprKind::F64ConvertU32 { @@ -3367,6 +3638,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "f64_convert_i64" => Expression { kind: ExprKind::F64ConvertI64 { @@ -3377,6 +3649,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "f64_convert_u64" => Expression { kind: ExprKind::F64ConvertU64 { @@ -3387,6 +3660,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "i32_trunc_f32" => Expression { kind: ExprKind::I32TruncF32 { @@ -3397,6 +3671,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "u32_trunc_f32" => Expression { kind: ExprKind::U32TruncF32 { @@ -3407,6 +3682,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "i32_trunc_f64" => Expression { kind: ExprKind::I32TruncF64 { @@ -3417,6 +3693,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "u32_trunc_f64" => Expression { kind: ExprKind::U32TruncF64 { @@ -3427,6 +3704,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "i64_trunc_f32" => Expression { kind: ExprKind::I64TruncF32 { @@ -3437,6 +3715,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "u64_trunc_f32" => Expression { kind: ExprKind::U64TruncF32 { @@ -3447,6 +3726,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "i64_trunc_f64" => Expression { kind: ExprKind::I64TruncF64 { @@ -3457,6 +3737,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "u64_trunc_f64" => Expression { kind: ExprKind::U64TruncF64 { @@ -3467,6 +3748,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "f64_promote_f32" => Expression { kind: ExprKind::F64PromoteF32 { @@ -3477,6 +3759,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "f32_demote_f64" => Expression { kind: ExprKind::F32DemoteF64 { @@ -3487,6 +3770,7 @@ impl<'tir> Builder<'tir> { )), }, ty: self.lower_type_index(expr_ty), + span, }, "memory_fill" => { let raw_ty = type_args[0]; @@ -3527,6 +3811,7 @@ impl<'tir> Builder<'tir> { len, }, ty: Type::Unit, + span, } } "memory_copy" => { @@ -3572,6 +3857,7 @@ impl<'tir> Builder<'tir> { len, }, ty: Type::Unit, + span, } } name => unreachable!("cannot lower unknown intrinsic `{name}`"), @@ -3750,6 +4036,7 @@ impl<'tir> Builder<'tir> { let binary_expr = Expression { kind: binary_expr_kind, ty: self.lower_type_index(left.ty), + span: ast::TextSpan::new(left.span.start, right.span.end), }; // Now assign the result back to left: x = (x + y) diff --git a/crates/wx-compiler/src/mir/scheduler.rs b/crates/wx-compiler/src/mir/scheduler.rs new file mode 100644 index 0000000..834bfb9 --- /dev/null +++ b/crates/wx-compiler/src/mir/scheduler.rs @@ -0,0 +1,1744 @@ +//! Lower a [`mir::Function`] directly to a [`wasm::Function`], bypassing the +//! sea-of-nodes `opt` pipeline entirely — no CSE, no scheduling decisions, +//! no `#[inline]` substitution assumed. Instructions come out in exactly the +//! order the source expression tree implies, and every declared local gets +//! its own fixed WASM slot. This is the debug-mode counterpart to +//! `opt::scheduler`: source-faithful by construction rather than optimized. +//! +//! Every `ExprKind` is handled — `emit_expr`'s match is exhaustive, checked +//! by the compiler. Aggregates (as locals, loads/stores, and call args/ +//! results) are just their flattened leaf fields as consecutive WASM stack +//! values / consecutive local slots, matching the convention WASM's own +//! multi-value params/results already use — no separate representation +//! needed. `match` always lowers to an if/else-if chain rather than +//! replicating `opt::builder`'s dense-vs-sparse `br_table` choice — simpler, +//! and correctness doesn't need the optimization. + +use crate::ast; +use crate::mir::{self, ExprKind, Expression}; +use crate::opt::MemAccess; +use crate::wasm::{self, BlockType, Instruction, MemArg, ScalarType}; + +/// Maps each MIR scope's locals to WASM local indices. Every local gets its +/// own dedicated slot — no sharing across sibling scopes, unlike +/// `opt::builder`'s `compute_locals_offsets`: that scheme is safe for that +/// module's internal bookkeeping (never emitted as WASM), but a WASM local +/// has one fixed type for the whole function, so two sibling branches +/// declaring locals of different types could never safely share a slot. +/// `wasm::coalesce_locals` can safely reclaim unused slots afterward — it +/// already does type-aware reuse. +struct LocalTable { + /// `starts[scope_index][local_index]` = first WASM local index for that + /// MIR local (aggregates occupy `starts[..] .. starts[..] + N` for their + /// `N` flattened leaf fields). + starts: Vec>, + /// Declared WASM locals, in slot order (params first, per `scopes[0]`). + locals: Vec, + /// One entry per named local (i.e. every `scope.locals_debug` entry — + /// see `mir::LocalDebugInfo`, only ever populated when `mir::MIR` was + /// built with `CompilationMode::Debug`, which is the only mode that + /// ever reaches this scheduler), resolved to the wasm-local range it + /// occupies. + locals_debug: Vec, +} + +impl LocalTable { + fn build( + scopes: &[mir::BlockScope], + aggregates: &[mir::Aggregate], + ) -> Self { + let mut starts = Vec::with_capacity(scopes.len()); + let mut locals = Vec::new(); + let mut locals_debug = Vec::new(); + for scope in scopes { + let mut scope_starts = Vec::with_capacity(scope.locals.len()); + for (local_index, local) in scope.locals.iter().enumerate() { + let wasm_local_start = locals.len() as u32; + scope_starts.push(wasm_local_start); + for ty in wasm::flatten_type_to_scalars(local.ty, aggregates) { + locals.push(wasm::Local { ty }); + } + // `locals_debug` only covers a scope's originally-declared + // locals, in the same order — anything past its end is a + // compiler-synthesized temporary with no debug info. + if let Some(debug) = scope.locals_debug.get(local_index) { + locals_debug.push(wasm::LocalDebugInfo { + name: debug.name, + ty: local.ty, + struct_debug: debug.struct_debug.clone(), + wasm_local_start, + wasm_local_count: locals.len() as u32 + - wasm_local_start, + }); + } + } + starts.push(scope_starts); + } + LocalTable { + starts, + locals, + locals_debug, + } + } + + fn wasm_index( + &self, + scope_index: mir::ScopeIndex, + local_index: mir::LocalIndex, + ) -> u32 { + self.starts[scope_index as usize][local_index as usize] + } +} + +/// One currently-open WASM construct that `break`/`continue` can jump to, +/// tagged with the MIR scope it corresponds to. Depth is computed by +/// walking this stack from the top, charging each entry its real WASM +/// nesting cost (mirrors `opt::scheduler::break_depth`'s per-node cost). +enum BranchTarget { + /// An ordinary `block`, or one arm of an `if` (an if's two arms share + /// one real WASM level, so each gets its own entry but they're never + /// open simultaneously) — costs 1 level to walk through. + Block(mir::ScopeIndex), + /// A `loop` — an outer `block` (the `break` target) wrapping an inner + /// `loop` (the `continue` target), two real WASM levels tracked as one + /// logical entry. `result_local` is where a `break ` stores its + /// value for code after the loop to read; `None` if the loop's type is + /// `Unit`/`Never`. + Loop { + scope_index: mir::ScopeIndex, + result_local: Option, + }, +} + +struct Scheduler<'f> { + mir: &'f mir::MIR, + /// Needed (alongside `mir`) to look up a local's declared MIR type — + /// e.g. resolving `AggregateGet`'s `aggregate_index`, which isn't + /// carried on the node itself, only recoverable from what the local + /// was declared as. + mir_func: &'f mir::Function, + table: LocalTable, + body: Vec, + /// `spans[i]` is the source span for `body[i]` — see `emit`. Tracks + /// `current_span`, which every `emit_expr` call saves and restores + /// around its own span, so instructions pushed while lowering a nested + /// expression get that expression's span rather than its ancestor's. + spans: Vec, + current_span: ast::TextSpan, + br_table_depths: Vec, + branch_targets: Vec, +} + +/// Lower one MIR function directly into a [`wasm::Function`]. +pub fn schedule(mir_func: &mir::Function, mir: &mir::MIR) -> wasm::Function { + let mut sched = Scheduler { + mir, + mir_func, + table: LocalTable::build(&mir_func.scopes, &mir.aggregates), + body: Vec::new(), + spans: Vec::new(), + current_span: mir_func.block.span, + br_table_depths: Vec::new(), + branch_targets: Vec::new(), + }; + + let body_exprs = match &mir_func.block.kind { + ExprKind::Block { expressions, .. } => expressions, + _ => unreachable!("function body must be a Block"), + }; + sched.emit_sequence(body_exprs); + + if matches!(sched.body.last(), Some(Instruction::Return)) { + sched.body.pop(); + sched.spans.pop(); + } + + wasm::Function { + locals: sched.table.locals, + body: sched.body, + br_table_depths: sched.br_table_depths, + spans: sched.spans, + locals_debug: sched.table.locals_debug, + } +} + +fn scalar_ty(ty: mir::Type) -> ScalarType { + ScalarType::try_from(ty).expect("must be scalar") +} + +/// The WASM block-signature type for a construct whose MIR result type is +/// `ty` — `Empty` for `Unit`/`Never` (nothing left on the stack), otherwise +/// the single scalar value produced. Divergent arms (type `Never`) validate +/// under any declared block type since WASM treats code after an +/// unconditional `return`/`br`/`unreachable` as stack-polymorphic — no +/// special-casing needed here. +fn block_type(ty: mir::Type) -> BlockType { + match ty { + mir::Type::Unit | mir::Type::Never => BlockType::Empty, + scalar => BlockType::Value(scalar_ty(scalar)), + } +} + +/// Extract `(scope_index, expressions)` from a `Block` expression — every +/// `if`/`loop` arm's body is one, matching `opt::builder::unwrap_block`. +fn unwrap_block(expr: &Expression) -> (mir::ScopeIndex, &[Expression]) { + match &expr.kind { + ExprKind::Block { + scope_index, + expressions, + } => (*scope_index, expressions), + _ => panic!("expected Block expression"), + } +} + +fn add_instr(ty: ScalarType) -> Instruction { + match ty { + ScalarType::I32 => Instruction::I32Add, + ScalarType::I64 => Instruction::I64Add, + ScalarType::F32 => Instruction::F32Add, + ScalarType::F64 => Instruction::F64Add, + } +} + +fn sub_instr(ty: ScalarType) -> Instruction { + match ty { + ScalarType::I32 => Instruction::I32Sub, + ScalarType::I64 => Instruction::I64Sub, + ScalarType::F32 => Instruction::F32Sub, + ScalarType::F64 => Instruction::F64Sub, + } +} + +fn mul_instr(ty: ScalarType) -> Instruction { + match ty { + ScalarType::I32 => Instruction::I32Mul, + ScalarType::I64 => Instruction::I64Mul, + ScalarType::F32 => Instruction::F32Mul, + ScalarType::F64 => Instruction::F64Mul, + } +} + +fn div_instr(ty: ScalarType, unsigned: bool) -> Instruction { + match (ty, unsigned) { + (ScalarType::I32, true) => Instruction::I32DivU, + (ScalarType::I32, false) => Instruction::I32DivS, + (ScalarType::I64, true) => Instruction::I64DivU, + (ScalarType::I64, false) => Instruction::I64DivS, + (ScalarType::F32, _) => Instruction::F32Div, + (ScalarType::F64, _) => Instruction::F64Div, + } +} + +fn rem_instr(ty: ScalarType, unsigned: bool) -> Instruction { + match (ty, unsigned) { + (ScalarType::I32, true) => Instruction::I32RemU, + (ScalarType::I32, false) => Instruction::I32RemS, + (ScalarType::I64, true) => Instruction::I64RemU, + (ScalarType::I64, false) => Instruction::I64RemS, + _ => unreachable!("Rem is only valid on integers"), + } +} + +fn eq_instr(ty: ScalarType) -> Instruction { + match ty { + ScalarType::I32 => Instruction::I32Eq, + ScalarType::I64 => Instruction::I64Eq, + ScalarType::F32 => Instruction::F32Eq, + ScalarType::F64 => Instruction::F64Eq, + } +} + +fn ne_instr(ty: ScalarType) -> Instruction { + match ty { + ScalarType::I32 => Instruction::I32Ne, + ScalarType::I64 => Instruction::I64Ne, + ScalarType::F32 => Instruction::F32Ne, + ScalarType::F64 => Instruction::F64Ne, + } +} + +fn lt_instr(ty: ScalarType, unsigned: bool) -> Instruction { + match (ty, unsigned) { + (ScalarType::I32, true) => Instruction::I32LtU, + (ScalarType::I32, false) => Instruction::I32LtS, + (ScalarType::I64, true) => Instruction::I64LtU, + (ScalarType::I64, false) => Instruction::I64LtS, + (ScalarType::F32, _) => Instruction::F32Lt, + (ScalarType::F64, _) => Instruction::F64Lt, + } +} + +fn le_instr(ty: ScalarType, unsigned: bool) -> Instruction { + match (ty, unsigned) { + (ScalarType::I32, true) => Instruction::I32LeU, + (ScalarType::I32, false) => Instruction::I32LeS, + (ScalarType::I64, true) => Instruction::I64LeU, + (ScalarType::I64, false) => Instruction::I64LeS, + (ScalarType::F32, _) => Instruction::F32Le, + (ScalarType::F64, _) => Instruction::F64Le, + } +} + +fn gt_instr(ty: ScalarType, unsigned: bool) -> Instruction { + match (ty, unsigned) { + (ScalarType::I32, true) => Instruction::I32GtU, + (ScalarType::I32, false) => Instruction::I32GtS, + (ScalarType::I64, true) => Instruction::I64GtU, + (ScalarType::I64, false) => Instruction::I64GtS, + (ScalarType::F32, _) => Instruction::F32Gt, + (ScalarType::F64, _) => Instruction::F64Gt, + } +} + +fn ge_instr(ty: ScalarType, unsigned: bool) -> Instruction { + match (ty, unsigned) { + (ScalarType::I32, true) => Instruction::I32GeU, + (ScalarType::I32, false) => Instruction::I32GeS, + (ScalarType::I64, true) => Instruction::I64GeU, + (ScalarType::I64, false) => Instruction::I64GeS, + (ScalarType::F32, _) => Instruction::F32Ge, + (ScalarType::F64, _) => Instruction::F64Ge, + } +} + +/// `And`/`Or` and `BitAnd`/`BitOr` compile to the same instruction — MIR's +/// `bool` is just `i32` 0/1, so bitwise-and/or already gives the right +/// truth table without short-circuiting. `opt::builder` treats them +/// identically for the same reason. +fn bitand_instr(ty: ScalarType) -> Instruction { + match ty { + ScalarType::I32 => Instruction::I32And, + ScalarType::I64 => Instruction::I64And, + _ => unreachable!("And/BitAnd is only valid on integers"), + } +} + +fn bitor_instr(ty: ScalarType) -> Instruction { + match ty { + ScalarType::I32 => Instruction::I32Or, + ScalarType::I64 => Instruction::I64Or, + _ => unreachable!("Or/BitOr is only valid on integers"), + } +} + +fn bitxor_instr(ty: ScalarType) -> Instruction { + match ty { + ScalarType::I32 => Instruction::I32Xor, + ScalarType::I64 => Instruction::I64Xor, + _ => unreachable!("BitXor is only valid on integers"), + } +} + +fn shl_instr(ty: ScalarType) -> Instruction { + match ty { + ScalarType::I32 => Instruction::I32Shl, + ScalarType::I64 => Instruction::I64Shl, + _ => unreachable!("LeftShift is only valid on integers"), + } +} + +fn shr_instr(ty: ScalarType, unsigned: bool) -> Instruction { + match (ty, unsigned) { + (ScalarType::I32, true) => Instruction::I32ShrU, + (ScalarType::I32, false) => Instruction::I32ShrS, + (ScalarType::I64, true) => Instruction::I64ShrU, + (ScalarType::I64, false) => Instruction::I64ShrS, + _ => unreachable!("RightShift is only valid on integers"), + } +} + +fn cast_instr(kind: &ExprKind) -> Instruction { + match kind { + ExprKind::I64ExtendI32S { .. } => Instruction::I64ExtendI32S, + ExprKind::I64ExtendI32U { .. } => Instruction::I64ExtendI32U, + ExprKind::I32WrapI64 { .. } => Instruction::I32WrapI64, + ExprKind::F32ConvertI32 { .. } => Instruction::F32ConvertI32S, + ExprKind::F32ConvertU32 { .. } => Instruction::F32ConvertI32U, + ExprKind::F32ConvertI64 { .. } => Instruction::F32ConvertI64S, + ExprKind::F32ConvertU64 { .. } => Instruction::F32ConvertI64U, + ExprKind::F64ConvertI32 { .. } => Instruction::F64ConvertI32S, + ExprKind::F64ConvertU32 { .. } => Instruction::F64ConvertI32U, + ExprKind::F64ConvertI64 { .. } => Instruction::F64ConvertI64S, + ExprKind::F64ConvertU64 { .. } => Instruction::F64ConvertI64U, + ExprKind::I32TruncF32 { .. } => Instruction::I32TruncF32S, + ExprKind::U32TruncF32 { .. } => Instruction::I32TruncF32U, + ExprKind::I32TruncF64 { .. } => Instruction::I32TruncF64S, + ExprKind::U32TruncF64 { .. } => Instruction::I32TruncF64U, + ExprKind::I64TruncF32 { .. } => Instruction::I64TruncF32S, + ExprKind::U64TruncF32 { .. } => Instruction::I64TruncF32U, + ExprKind::I64TruncF64 { .. } => Instruction::I64TruncF64S, + ExprKind::U64TruncF64 { .. } => Instruction::I64TruncF64U, + ExprKind::F64PromoteF32 { .. } => Instruction::F64PromoteF32, + ExprKind::F32DemoteF64 { .. } => Instruction::F32DemoteF64, + _ => unreachable!("cast_instr called on a non-cast ExprKind"), + } +} + +impl Scheduler<'_> { + /// Allocate a fresh WASM local beyond what `LocalTable` declared from + /// source — used for compiler-synthesized temporaries like a loop's + /// break-value slot. + fn alloc_local(&mut self, ty: ScalarType) -> u32 { + let idx = self.table.locals.len() as u32; + self.table.locals.push(wasm::Local { ty }); + idx + } + + /// The aggregate a local was declared as — recovered from its + /// declaration (`mir_func`), since `AggregateGet`/`AggregateSet` don't + /// carry it on the node itself. + fn local_aggregate_index( + &self, + scope_index: mir::ScopeIndex, + local_index: mir::LocalIndex, + ) -> mir::AggregateIndex { + match self.mir_func.scopes[scope_index as usize].locals + [local_index as usize] + .ty + { + mir::Type::Aggregate { aggregate_index } => aggregate_index, + _ => unreachable!( + "AggregateGet/AggregateSet on a non-aggregate local" + ), + } + } + + /// `(leaf-slot start offset, leaf-slot count)` for field `value_index` + /// within an aggregate, in the same flattened order `LocalTable`/ + /// `wasm::flatten_type_to_scalars` use — `value_index` is already a + /// physical (layout-order) index by this stage of MIR, matching + /// `Aggregate::values`'s own order, so no `decl_to_phys` translation is + /// needed here (that already happened once, upstream during lowering). + fn aggregate_field_slots( + &self, + aggregate_index: mir::AggregateIndex, + value_index: usize, + ) -> (u32, u32) { + let values = &self.mir.aggregates[aggregate_index as usize].values; + let start: u32 = values[..value_index] + .iter() + .map(|&t| { + wasm::flatten_type_to_scalars(t, &self.mir.aggregates).len() + as u32 + }) + .sum(); + let count = wasm::flatten_type_to_scalars( + values[value_index], + &self.mir.aggregates, + ) + .len() as u32; + (start, count) + } + + /// Emit a scalar load — the address must already be on the stack. + fn emit_scalar_load( + &mut self, + ty: mir::Type, + offset: u32, + memory: ast::DefId, + ) { + let access = MemAccess::from_mir(ty); + let m = MemArg { + align: access.align_log2(), + offset, + memory, + }; + self.emit(match access { + MemAccess::I8S => Instruction::I32Load8S(m), + MemAccess::I8U => Instruction::I32Load8U(m), + MemAccess::I16S => Instruction::I32Load16S(m), + MemAccess::I16U => Instruction::I32Load16U(m), + MemAccess::I32 => Instruction::I32Load(m), + MemAccess::I64 => Instruction::I64Load(m), + MemAccess::F32 => Instruction::F32Load(m), + MemAccess::F64 => Instruction::F64Load(m), + }); + } + + /// Emit a scalar store — the address and value must already be on the + /// stack, address pushed first. + fn emit_scalar_store( + &mut self, + ty: mir::Type, + offset: u32, + memory: ast::DefId, + ) { + let access = MemAccess::from_mir(ty); + let m = MemArg { + align: access.align_log2(), + offset, + memory, + }; + self.emit(match access { + MemAccess::I8S | MemAccess::I8U => Instruction::I32Store8(m), + MemAccess::I16S | MemAccess::I16U => Instruction::I32Store16(m), + MemAccess::I32 => Instruction::I32Store(m), + MemAccess::I64 => Instruction::I64Store(m), + MemAccess::F32 => Instruction::F32Store(m), + MemAccess::F64 => Instruction::F64Store(m), + }); + } + + /// Recursively load every leaf field of an aggregate at + /// `addr_local + base_offset`, pushing them in physical order — the + /// address is re-read from `addr_local` per leaf field rather than + /// re-evaluating the pointer expression, which would re-run any side + /// effects it has once per field instead of once total. + fn emit_aggregate_load( + &mut self, + addr_local: u32, + base_offset: u32, + aggregate_index: mir::AggregateIndex, + memory: ast::DefId, + ) { + let n = self.mir.aggregates[aggregate_index as usize].values.len(); + for i in 0..n { + let field_ty = + self.mir.aggregates[aggregate_index as usize].values[i]; + let field_offset = base_offset + + self.mir.aggregates[aggregate_index as usize].offsets[i]; + match field_ty { + mir::Type::Aggregate { + aggregate_index: nested, + } => { + self.emit_aggregate_load( + addr_local, + field_offset, + nested, + memory, + ); + } + _ => { + self.emit(Instruction::LocalGet(addr_local)); + self.emit_scalar_load(field_ty, field_offset, memory); + } + } + } + } + + /// Recursively store every leaf field of an aggregate — `value_locals` + /// holds one already-spilled temp local per leaf field (in the same + /// physical order `emit_aggregate_load`/`Aggregate` use), since storing + /// needs the address pushed fresh before *each* field's value. + fn emit_aggregate_store( + &mut self, + addr_local: u32, + base_offset: u32, + value_locals: &[u32], + aggregate_index: mir::AggregateIndex, + memory: ast::DefId, + ) { + let n = self.mir.aggregates[aggregate_index as usize].values.len(); + let mut consumed = 0usize; + for i in 0..n { + let field_ty = + self.mir.aggregates[aggregate_index as usize].values[i]; + let field_offset = base_offset + + self.mir.aggregates[aggregate_index as usize].offsets[i]; + let field_slots = + wasm::flatten_type_to_scalars(field_ty, &self.mir.aggregates) + .len(); + match field_ty { + mir::Type::Aggregate { + aggregate_index: nested, + } => { + self.emit_aggregate_store( + addr_local, + field_offset, + &value_locals[consumed..consumed + field_slots], + nested, + memory, + ); + } + _ => { + self.emit(Instruction::LocalGet(addr_local)); + self.emit(Instruction::LocalGet(value_locals[consumed])); + self.emit_scalar_store(field_ty, field_offset, memory); + } + } + consumed += field_slots; + } + } + + /// WASM `br` depth for a `break` targeting `target`, walking + /// `branch_targets` from the innermost currently-open construct outward + /// and charging each one its real WASM nesting cost — mirrors + /// `opt::scheduler::break_depth` exactly, just walking a stack instead + /// of a graph's block-parent chain. + fn break_depth(&self, target: mir::ScopeIndex) -> u32 { + let mut depth = 0u32; + for bt in self.branch_targets.iter().rev() { + match bt { + BranchTarget::Block(s) if *s == target => return depth, + BranchTarget::Loop { scope_index, .. } + if *scope_index == target => + { + return depth + 1; + } + BranchTarget::Block(_) => depth += 1, + BranchTarget::Loop { .. } => depth += 2, + } + } + unreachable!("break target scope not found in branch_targets") + } + + /// WASM `br` depth for a `continue` (branch to loop header) targeting + /// `target` — one level shallower than `break`'s, landing on the inner + /// `loop` instead of the outer wrapping `block`. Only ever called with a + /// `target` that's actually a `Loop` entry — TIR guarantees `continue` + /// can't target a non-loop scope. + fn continue_depth(&self, target: mir::ScopeIndex) -> u32 { + self.break_depth(target) - 1 + } + + /// The break-value local for the (currently open) loop `target`, if it + /// has one. + fn loop_result_local(&self, target: mir::ScopeIndex) -> Option { + self.branch_targets.iter().rev().find_map(|bt| match bt { + BranchTarget::Loop { + scope_index, + result_local, + } if *scope_index == target => Some(*result_local), + _ => None, + })? + } + + /// Emit a sequence of statements: every non-final expression's value (if + /// any) is explicitly dropped — unlike `opt::builder`, there's no graph + /// to silently discard unused pure nodes, so a leftover WASM stack value + /// has to be dropped for real or the emitted module is invalid. + fn emit_sequence(&mut self, exprs: &[Expression]) { + for (i, expr) in exprs.iter().enumerate() { + let is_last = i == exprs.len() - 1; + self.emit_expr(expr); + if !is_last { + self.drop_value(expr.ty, expr.span); + } + } + } + + /// Drop a value of MIR type `ty` off the stack — one `Drop` per + /// flattened leaf scalar, since an aggregate value is N separate WASM + /// stack values, not one. No-op for `Unit`/`Never` (nothing was pushed). + /// Tagged with `span` (the dropped expression's own) rather than + /// whatever's currently ambient, since by the time a caller drops a + /// value, `current_span` has already been restored past it. + fn drop_value(&mut self, ty: mir::Type, span: ast::TextSpan) { + self.current_span = span; + let n = wasm::flatten_type_to_scalars(ty, &self.mir.aggregates).len(); + for _ in 0..n { + self.emit(Instruction::Drop); + } + } + + /// Pushes `instr`, tagged with whichever expression's lowering is + /// currently on the stack — see `emit_expr`'s save/restore of + /// `current_span`. + fn emit(&mut self, instr: Instruction) { + self.body.push(instr); + self.spans.push(self.current_span); + debug_assert_eq!( + self.body.len(), + self.spans.len(), + "every push to `body` must go through `emit` so `spans` stays \ + index-aligned with it — some caller pushed to `self.body` \ + directly" + ); + } + + /// Sets `current_span` to `expr.span` for the duration of lowering + /// `expr`, restoring the caller's span on return — so instructions + /// pushed by a nested `emit_expr` call get that nested expression's own + /// span, and control returns to the enclosing span automatically once + /// it's done, with no explicit reset needed at each call site. + fn emit_expr(&mut self, expr: &Expression) { + let saved = self.current_span; + self.current_span = expr.span; + self.emit_expr_inner(expr); + self.current_span = saved; + } + + fn emit_expr_inner(&mut self, expr: &Expression) { + match &expr.kind { + ExprKind::Noop => {} + ExprKind::Bool { value } => { + self.emit(Instruction::I32Const(*value as i32)); + } + ExprKind::Int { value } => self.emit_int_const(expr.ty, *value), + ExprKind::Float { value } => self.emit_float_const(expr.ty, *value), + ExprKind::LocalGet { + scope_index, + local_index, + } => { + // Mirrors `LocalSet` below: `expr.ty` may be aggregate-typed + // (e.g. reading a whole `Point`-typed local to pass it as a + // call argument), occupying N consecutive wasm locals — push + // all N, not just the first. + let idx = self.table.wasm_index(*scope_index, *local_index); + let n = wasm::flatten_type_to_scalars( + expr.ty, + &self.mir.aggregates, + ) + .len() as u32; + for i in 0..n { + self.emit(Instruction::LocalGet(idx + i)); + } + } + ExprKind::LocalSet { + scope_index, + local_index, + value, + } => { + // `value` may itself be aggregate-typed (e.g. `local p: + // Point = Point::{ .. }`), pushing N flattened values — + // pop them into the local's N consecutive slots in + // reverse, same convention as `AggregateSet`. + self.emit_expr(value); + let idx = self.table.wasm_index(*scope_index, *local_index); + let n = wasm::flatten_type_to_scalars( + value.ty, + &self.mir.aggregates, + ) + .len() as u32; + for i in (0..n).rev() { + self.emit(Instruction::LocalSet(idx + i)); + } + } + ExprKind::Add { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(add_instr(scalar_ty(expr.ty))); + } + ExprKind::Sub { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(sub_instr(scalar_ty(expr.ty))); + } + ExprKind::Mul { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(mul_instr(scalar_ty(expr.ty))); + } + ExprKind::Div { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(div_instr(scalar_ty(expr.ty), expr.ty.is_unsigned())); + } + ExprKind::Rem { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(rem_instr(scalar_ty(expr.ty), expr.ty.is_unsigned())); + } + ExprKind::Eq { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(eq_instr(scalar_ty(left.ty))); + } + ExprKind::NotEq { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(ne_instr(scalar_ty(left.ty))); + } + ExprKind::Less { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(lt_instr(scalar_ty(left.ty), left.ty.is_unsigned())); + } + ExprKind::LessEq { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(le_instr(scalar_ty(left.ty), left.ty.is_unsigned())); + } + ExprKind::Greater { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(gt_instr(scalar_ty(left.ty), left.ty.is_unsigned())); + } + ExprKind::GreaterEq { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(ge_instr(scalar_ty(left.ty), left.ty.is_unsigned())); + } + ExprKind::Eqz { value } => { + // Always i32.eqz — matches opt::builder/opt::scheduler exactly: + // `Eqz` is only ever constructed over an already-i32 (bool) + // operand in this language, so `DataNodeKind::Eqz` there + // doesn't even carry a type. + self.emit_expr(value); + self.emit(Instruction::I32Eqz); + } + ExprKind::And { left, right } + | ExprKind::BitAnd { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(bitand_instr(scalar_ty(expr.ty))); + } + ExprKind::Or { left, right } | ExprKind::BitOr { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(bitor_instr(scalar_ty(expr.ty))); + } + ExprKind::BitXor { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(bitxor_instr(scalar_ty(expr.ty))); + } + ExprKind::LeftShift { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(shl_instr(scalar_ty(expr.ty))); + } + ExprKind::RightShift { left, right } => { + self.emit_expr(left); + self.emit_expr(right); + self.emit(shr_instr(scalar_ty(expr.ty), expr.ty.is_unsigned())); + } + ExprKind::BitNot { value } => { + // WASM has no bitwise-not; emit `x ^ -1`, matching + // opt::scheduler's `DataNodeKind::BitNot`. + let ty = scalar_ty(expr.ty); + self.emit_expr(value); + self.emit(match ty { + ScalarType::I32 => Instruction::I32Const(-1), + ScalarType::I64 => Instruction::I64Const(-1), + _ => unreachable!("BitNot is only valid on integers"), + }); + self.emit(bitxor_instr(ty)); + } + ExprKind::Neg { value } => { + // WASM only has neg for floats; ints synthesize `0 - x`. + match scalar_ty(expr.ty) { + ScalarType::F32 => { + self.emit_expr(value); + self.emit(Instruction::F32Neg); + } + ScalarType::F64 => { + self.emit_expr(value); + self.emit(Instruction::F64Neg); + } + ScalarType::I32 => { + self.emit(Instruction::I32Const(0)); + self.emit_expr(value); + self.emit(Instruction::I32Sub); + } + ScalarType::I64 => { + self.emit(Instruction::I64Const(0)); + self.emit_expr(value); + self.emit(Instruction::I64Sub); + } + } + } + ExprKind::Sqrt { value } => { + self.emit_expr(value); + self.emit(match scalar_ty(expr.ty) { + ScalarType::F32 => Instruction::F32Sqrt, + ScalarType::F64 => Instruction::F64Sqrt, + _ => unreachable!("Sqrt is only valid on floats"), + }); + } + ExprKind::Abs { value } => { + self.emit_expr(value); + self.emit(match scalar_ty(expr.ty) { + ScalarType::F32 => Instruction::F32Abs, + ScalarType::F64 => Instruction::F64Abs, + _ => unreachable!("Abs is only valid on floats"), + }); + } + ExprKind::Floor { value } => { + self.emit_expr(value); + self.emit(match scalar_ty(expr.ty) { + ScalarType::F32 => Instruction::F32Floor, + ScalarType::F64 => Instruction::F64Floor, + _ => unreachable!("Floor is only valid on floats"), + }); + } + ExprKind::Ceil { value } => { + self.emit_expr(value); + self.emit(match scalar_ty(expr.ty) { + ScalarType::F32 => Instruction::F32Ceil, + ScalarType::F64 => Instruction::F64Ceil, + _ => unreachable!("Ceil is only valid on floats"), + }); + } + ExprKind::I64ExtendI32S { value } + | ExprKind::I64ExtendI32U { value } + | ExprKind::I32WrapI64 { value } + | ExprKind::F32ConvertI32 { value } + | ExprKind::F32ConvertU32 { value } + | ExprKind::F32ConvertI64 { value } + | ExprKind::F32ConvertU64 { value } + | ExprKind::F64ConvertI32 { value } + | ExprKind::F64ConvertU32 { value } + | ExprKind::F64ConvertI64 { value } + | ExprKind::F64ConvertU64 { value } + | ExprKind::I32TruncF32 { value } + | ExprKind::U32TruncF32 { value } + | ExprKind::I32TruncF64 { value } + | ExprKind::U32TruncF64 { value } + | ExprKind::I64TruncF32 { value } + | ExprKind::U64TruncF32 { value } + | ExprKind::I64TruncF64 { value } + | ExprKind::U64TruncF64 { value } + | ExprKind::F64PromoteF32 { value } + | ExprKind::F32DemoteF64 { value } => { + self.emit_expr(value); + self.emit(cast_instr(&expr.kind)); + } + ExprKind::Global { id } => { + self.emit(Instruction::GlobalGet(*id)); + } + ExprKind::GlobalSet { id, value } => { + self.emit_expr(value); + self.emit(Instruction::GlobalSet(*id)); + } + ExprKind::Function { id } => { + self.emit(Instruction::FunctionPointer(*id)); + } + ExprKind::StaticPointer { data_index } => { + self.emit(Instruction::StaticDataPointer { + data_index: *data_index, + ty: scalar_ty(expr.ty), + }); + } + ExprKind::MemoryOffset { memory } => { + self.emit(Instruction::DataSectionEnd { memory: *memory }); + } + ExprKind::MemoryIndex { memory } => { + self.emit(Instruction::MemoryIndex { memory: *memory }); + } + ExprKind::MemorySize { memory } => { + self.emit(Instruction::MemorySize(*memory)); + } + ExprKind::MemoryGrow { memory, delta } => { + self.emit_expr(delta); + self.emit(Instruction::MemoryGrow(*memory)); + } + ExprKind::MemoryFill { + memory, + dst, + val, + len, + } => { + self.emit_expr(dst); + self.emit_expr(val); + self.emit_expr(len); + self.emit(Instruction::MemoryFill(*memory)); + } + ExprKind::MemoryCopy { + dst_memory, + src_memory, + dst, + src, + len, + } => { + self.emit_expr(dst); + self.emit_expr(src); + self.emit_expr(len); + self.emit(Instruction::MemoryCopy { + dst: *dst_memory, + src: *src_memory, + }); + } + ExprKind::PointerLoad { + pointer, + offset, + memory, + } => { + if let mir::Type::Aggregate { aggregate_index } = expr.ty { + let addr_local = self.alloc_local(scalar_ty(pointer.ty)); + self.emit_expr(pointer); + self.emit(Instruction::LocalSet(addr_local)); + self.emit_aggregate_load( + addr_local, + *offset, + aggregate_index, + *memory, + ); + return; + } + self.emit_expr(pointer); + self.emit_scalar_load(expr.ty, *offset, *memory); + } + ExprKind::PointerStore { + pointer, + value, + offset, + memory, + } => { + if let mir::Type::Aggregate { aggregate_index } = value.ty { + let addr_local = self.alloc_local(scalar_ty(pointer.ty)); + self.emit_expr(pointer); + self.emit(Instruction::LocalSet(addr_local)); + + // Spill the value's flattened fields to temp locals + // first — storing needs the address pushed fresh before + // *each* field's value, so the fields can't just stay on + // the stack in the order `emit_expr` left them in. + self.emit_expr(value); + let field_types = wasm::flatten_type_to_scalars( + value.ty, + &self.mir.aggregates, + ); + let mut value_locals = vec![0u32; field_types.len()]; + for (i, ty) in field_types.iter().enumerate().rev() { + let local = self.alloc_local(*ty); + self.emit(Instruction::LocalSet(local)); + value_locals[i] = local; + } + self.emit_aggregate_store( + addr_local, + *offset, + &value_locals, + aggregate_index, + *memory, + ); + return; + } + self.emit_expr(pointer); + self.emit_expr(value); + self.emit_scalar_store(value.ty, *offset, *memory); + } + ExprKind::Aggregate { values } => { + // An aggregate value is just its flattened fields, in + // physical order, sitting on the WASM stack — the same + // convention as a multi-value call result or an + // aggregate-typed local's slots. + for v in values.iter() { + self.emit_expr(v); + } + } + ExprKind::AggregateGet { + scope_index, + local_index, + value_index, + } => { + let aggregate_index = + self.local_aggregate_index(*scope_index, *local_index); + let (field_start, field_count) = self.aggregate_field_slots( + aggregate_index, + *value_index as usize, + ); + let base = self.table.wasm_index(*scope_index, *local_index); + for i in 0..field_count { + self.emit(Instruction::LocalGet(base + field_start + i)); + } + } + ExprKind::AggregateSet { + scope_index, + local_index, + value_index, + value, + } => { + let aggregate_index = + self.local_aggregate_index(*scope_index, *local_index); + let (field_start, field_count) = self.aggregate_field_slots( + aggregate_index, + *value_index as usize, + ); + let base = self.table.wasm_index(*scope_index, *local_index); + self.emit_expr(value); + for i in (0..field_count).rev() { + self.emit(Instruction::LocalSet(base + field_start + i)); + } + } + ExprKind::Call { callee, arguments } => { + // Aggregate args/results need no special handling here: + // each `emit_expr` on an argument already pushes exactly + // as many stack values as its type flattens to (1 for a + // scalar, N for an aggregate, via `Aggregate`/ + // `AggregateGet`), matching WASM's native multi-value + // params/results — the same convention the callee's own + // flattened signature already expects. + if let ExprKind::Function { id } = &callee.kind { + for arg in arguments { + self.emit_expr(arg); + } + self.emit(Instruction::Call(*id)); + } else { + let signature_index = match callee.ty { + mir::Type::Function { signature_index } => { + signature_index + } + _ => { + unreachable!("call target must be a function type") + } + }; + // WASM's call_indirect pops the table index last, so it + // must be pushed last too — after the args, not before. + for arg in arguments { + self.emit_expr(arg); + } + self.emit_expr(callee); + self.emit(Instruction::CallIndirectSym { signature_index }); + } + } + ExprKind::Return { value } => { + if let Some(v) = value { + self.emit_expr(v); + } + self.emit(Instruction::Return); + } + ExprKind::Drop { value } => { + self.emit_expr(value); + self.drop_value(value.ty, value.span); + } + ExprKind::Unreachable => { + self.emit(Instruction::Unreachable); + } + ExprKind::Block { + scope_index, + expressions, + } => { + // A bare `{ ... }` isn't necessarily a break target, but it + // might be (any block can be labeled), so it always gets a + // real `block ... end` wrapper — unlike an if-arm's body, + // which reuses the `if`'s own level instead (see below). + self.emit(Instruction::Block { + ty: block_type(expr.ty), + }); + self.branch_targets.push(BranchTarget::Block(*scope_index)); + self.emit_sequence(expressions); + self.branch_targets.pop(); + self.emit(Instruction::End); + } + ExprKind::IfElse { + condition, + then_block, + else_block, + } => { + self.emit_expr(condition); + self.emit(Instruction::If { + ty: block_type(expr.ty), + }); + let (then_scope, then_exprs) = unwrap_block(then_block); + self.branch_targets.push(BranchTarget::Block(then_scope)); + self.emit_sequence(then_exprs); + self.branch_targets.pop(); + if let Some(else_block) = else_block { + self.emit(Instruction::Else); + let (else_scope, else_exprs) = unwrap_block(else_block); + self.branch_targets.push(BranchTarget::Block(else_scope)); + self.emit_sequence(else_exprs); + self.branch_targets.pop(); + } + self.emit(Instruction::End); + } + ExprKind::Loop { scope_index, block } => { + let (body_scope, body_exprs) = unwrap_block(block); + let result_local = match block_type(expr.ty) { + BlockType::Empty => None, + BlockType::Value(ty) => Some(self.alloc_local(ty)), + BlockType::MultiValue(_) => { + unreachable!("loop result must be scalar or unit") + } + }; + self.emit(Instruction::Block { + ty: BlockType::Empty, + }); + self.emit(Instruction::Loop { + ty: BlockType::Empty, + }); + self.branch_targets.push(BranchTarget::Loop { + scope_index: *scope_index, + result_local, + }); + // `body_scope` is `*scope_index` itself — MIR gives a `Loop` + // and its body the same scope, so no separate lookup needed. + debug_assert_eq!(body_scope, *scope_index); + self.emit_sequence(body_exprs); + self.branch_targets.pop(); + // Back-edge; unreachable if the body always breaks/returns, + // same as opt::scheduler — WASM allows dead code after an + // unconditional branch. + self.emit(Instruction::Br(0)); + self.emit(Instruction::End); // loop + self.emit(Instruction::End); // block + if let Some(local) = result_local { + self.emit(Instruction::LocalGet(local)); + } + } + ExprKind::Break { scope_index, value } => { + let result_local = self.loop_result_local(*scope_index); + if let Some(v) = value { + self.emit_expr(v); + let local = result_local.expect( + "break with a value must target a loop with a result local", + ); + self.emit(Instruction::LocalSet(local)); + } + self.emit(Instruction::Br(self.break_depth(*scope_index))); + } + ExprKind::Continue { scope_index } => { + self.emit(Instruction::Br(self.continue_depth(*scope_index))); + } + ExprKind::Switch { + selector, + cases, + default, + } => { + // Uniform if/else-if chain — simpler and just as correct as + // opt::builder's dense-vs-sparse `br_table`-or-chain choice, + // without needing to replicate its block-depth bookkeeping. + let sel_ty = scalar_ty(selector.ty); + self.emit_expr(selector); + let sel_local = self.alloc_local(sel_ty); + self.emit(Instruction::LocalSet(sel_local)); + self.emit_switch_chain( + sel_local, + sel_ty, + cases, + default.as_deref(), + block_type(expr.ty), + ); + } + } + } + + /// Recursively emit `cases` as a chain of `local.get sel; const d; eq; + /// if ... else end`, bottoming out at `default` (or + /// `unreachable` if none — TIR only omits `default` when it already + /// proved exhaustiveness). + fn emit_switch_chain( + &mut self, + sel_local: u32, + sel_ty: ScalarType, + cases: &[(i64, Expression)], + default: Option<&Expression>, + ty: BlockType, + ) { + let Some(((discriminant, body), rest)) = cases.split_first() else { + match default { + Some(d) => self.emit_expr(d), + None => self.emit(Instruction::Unreachable), + } + return; + }; + + self.emit(Instruction::LocalGet(sel_local)); + self.emit(match sel_ty { + ScalarType::I32 => Instruction::I32Const(*discriminant as i32), + ScalarType::I64 => Instruction::I64Const(*discriminant), + _ => unreachable!("switch selector must be an integer"), + }); + self.emit(match sel_ty { + ScalarType::I32 => Instruction::I32Eq, + ScalarType::I64 => Instruction::I64Eq, + _ => unreachable!("switch selector must be an integer"), + }); + self.emit(Instruction::If { ty }); + let (case_scope, case_exprs) = unwrap_block(body); + self.branch_targets.push(BranchTarget::Block(case_scope)); + self.emit_sequence(case_exprs); + self.branch_targets.pop(); + self.emit(Instruction::Else); + self.emit_switch_chain(sel_local, sel_ty, rest, default, ty); + self.emit(Instruction::End); + } + + fn emit_int_const(&mut self, ty: mir::Type, value: i64) { + self.emit(match scalar_ty(ty) { + ScalarType::I32 => Instruction::I32Const(value as i32), + ScalarType::I64 => Instruction::I64Const(value), + ScalarType::F32 | ScalarType::F64 => { + unreachable!() + } + }); + } + + fn emit_float_const(&mut self, ty: mir::Type, value: f64) { + self.emit(match scalar_ty(ty) { + ScalarType::F32 => Instruction::F32Const(value as f32), + ScalarType::F64 => Instruction::F64Const(value), + ScalarType::I32 | ScalarType::I64 => { + unreachable!() + } + }); + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use indoc::indoc; + + use super::*; + use crate::{tir, vfs}; + + /// Builds MIR from source, standalone from `mir::tests::TestCase` — this + /// module only needs `mir::MIR` plus one function to hand to `schedule`, + /// not the wider harness. + fn build_mir(source: &str) -> mir::MIR { + let mut builder = vfs::CompilationGraphBuilder::new(); + let stdlib_id = builder.load_stdlib(); + let prefixed = format!("use std::*;\n{source}"); + let root_id = builder + .load_binary( + "main.wx".to_string(), + &vfs::VirtualFileSource::new(HashMap::from([( + "main.wx".to_string(), + prefixed, + )])), + ) + .unwrap(); + let mut graph = builder.build(root_id, stdlib_id); + let tir = tir::TIR::build(&mut graph); + let mut mir_out = mir::MIR::build( + &tir, + &graph.interner, + graph.id_generator, + crate::CompilationMode::Debug, + ); + let mut call_graph = + mir::CallGraph::build(&mir_out.functions, &mir_out.call_edges); + mir_out.inline_calls(&mut call_graph); + mir_out.dead_code_eliminate(&call_graph); + mir_out + } + + #[test] + fn lowers_straight_line_arithmetic() { + let mir_out = build_mir(indoc! {" + fn compute(x: i32) -> i32 { + local y = x + 1; + y * 2 + } + export { compute } + "}); + let func = &mir_out.functions[0]; + let scheduled = schedule(func, &mir_out); + + assert_eq!( + scheduled.body, + vec![ + Instruction::LocalGet(0), + Instruction::I32Const(1), + Instruction::I32Add, + Instruction::LocalSet(1), + Instruction::LocalGet(1), + Instruction::I32Const(2), + Instruction::I32Mul, + ] + ); + } + + #[test] + fn lowers_bitwise_and_shift() { + let mir_out = build_mir(indoc! {" + fn combine(a: i32, b: i32) -> i32 { + (a & b) << 1 + } + export { combine } + "}); + let func = &mir_out.functions[0]; + let scheduled = schedule(func, &mir_out); + + assert_eq!( + scheduled.body, + vec![ + Instruction::LocalGet(0), + Instruction::LocalGet(1), + Instruction::I32And, + Instruction::I32Const(1), + Instruction::I32Shl, + ] + ); + } + + #[test] + fn lowers_global_get_and_set() { + let mir_out = build_mir(indoc! {" + global mut counter: i32 = 0; + fn bump() -> i32 { + counter = counter + 1; + counter + } + export { bump } + "}); + let counter_id = mir_out.globals[0].id; + let func = mir_out + .functions + .iter() + .find(|f| { + matches!( + mir_out.signatures[f.signature_index as usize].params(), + [] + ) + }) + .expect("bump function not found"); + let scheduled = schedule(func, &mir_out); + + assert_eq!( + scheduled.body, + vec![ + Instruction::GlobalGet(counter_id), + Instruction::I32Const(1), + Instruction::I32Add, + Instruction::GlobalSet(counter_id), + Instruction::GlobalGet(counter_id), + ] + ); + } + + #[test] + fn lowers_direct_call() { + let mir_out = build_mir(indoc! {" + fn helper(x: i32) -> i32 { + x + 1 + } + fn caller(x: i32) -> i32 { + helper(x) * 2 + } + export { caller } + "}); + // Only `caller` is exported (`helper` stays live only via the call + // edge), so the single export is unambiguously `caller`. + let caller_id = mir_out + .exports + .iter() + .find_map(|e| match e { + mir::ExportItem::Function { id, .. } => Some(*id), + _ => None, + }) + .expect("caller export not found"); + let func = mir_out + .functions + .iter() + .find(|f| f.id == caller_id) + .expect("caller function not found"); + let helper_id = mir_out + .functions + .iter() + .find(|f| f.id != caller_id) + .expect("helper function not found") + .id; + let scheduled = schedule(func, &mir_out); + + assert_eq!( + scheduled.body, + vec![ + Instruction::LocalGet(0), + Instruction::Call(helper_id), + Instruction::I32Const(2), + Instruction::I32Mul, + ] + ); + } + + #[test] + fn lowers_if_else_with_value() { + let mir_out = build_mir(indoc! {" + fn classify(x: i32) -> i32 { + if x > 0 { + 1 + } else { + 0 - 1 + } + } + export { classify } + "}); + let func = &mir_out.functions[0]; + let scheduled = schedule(func, &mir_out); + + assert_eq!( + scheduled.body, + vec![ + Instruction::LocalGet(0), + Instruction::I32Const(0), + Instruction::I32GtS, + Instruction::If { + ty: BlockType::Value(ScalarType::I32) + }, + Instruction::I32Const(1), + Instruction::Else, + Instruction::I32Const(0), + Instruction::I32Const(1), + Instruction::I32Sub, + Instruction::End, + ] + ); + } + + #[test] + fn lowers_if_without_else() { + let mir_out = build_mir(indoc! {" + global mut flag: i32 = 0; + fn maybe_set(x: i32) { + if x > 0 { + flag = 1; + } + } + export { maybe_set } + "}); + let flag_id = mir_out.globals[0].id; + let func = &mir_out.functions[0]; + let scheduled = schedule(func, &mir_out); + + assert_eq!( + scheduled.body, + vec![ + Instruction::LocalGet(0), + Instruction::I32Const(0), + Instruction::I32GtS, + Instruction::If { + ty: BlockType::Empty + }, + Instruction::I32Const(1), + Instruction::GlobalSet(flag_id), + Instruction::End, + ] + ); + } + + #[test] + fn lowers_loop_with_break_value_and_continue() { + let mir_out = build_mir(indoc! {" + fn count_to(n: i32) -> i32 { + local mut i: i32 = 0; + loop { + i = i + 1; + if i < n { + continue; + } + break i; + } + } + export { count_to } + "}); + let func = &mir_out.functions[0]; + let scheduled = schedule(func, &mir_out); + + // Structural shape: ..., Block, Loop, ..., Br(0) back-edge, End, End, + // then reading the result local. (`local mut i = 0` emits its own + // init instructions before the loop, so Block/Loop aren't at a fixed + // index — precise depth correctness is checked by execution instead, + // in the codegen smoke test.) + let block_pos = scheduled + .body + .iter() + .position(|i| { + matches!( + i, + Instruction::Block { + ty: BlockType::Empty + } + ) + }) + .expect("expected the loop's wrapping Block"); + assert_eq!( + scheduled.body[block_pos + 1], + Instruction::Loop { + ty: BlockType::Empty + } + ); + let len = scheduled.body.len(); + assert_eq!(scheduled.body[len - 4], Instruction::Br(0)); + assert_eq!(scheduled.body[len - 3], Instruction::End); + assert_eq!(scheduled.body[len - 2], Instruction::End); + assert!( + matches!(scheduled.body[len - 1], Instruction::LocalGet(_)), + "loop with a value result must end by reading its result local; got {:#?}", + scheduled.body + ); + } + + #[test] + fn break_from_nested_loop_targets_correct_depth() { + // The inner loop's `break` must exit only the inner loop (depth 1: + // past its own Block+Loop... no — see below), not the outer one. + // Concretely: outer keeps running (increments `outer_count`) while + // the inner loop breaks out after one iteration every time. + let mir_out = build_mir(indoc! {" + fn nested(n: i32) -> i32 { + local mut outer_count: i32 = 0; + loop { + if outer_count >= n { + break outer_count; + } + loop { + break; + } + outer_count = outer_count + 1; + } + } + export { nested } + "}); + let func = &mir_out.functions[0]; + let scheduled = schedule(func, &mir_out); + // Two Loop instructions (outer + inner), two matching Block wrappers. + let loop_count = scheduled + .body + .iter() + .filter(|i| matches!(i, Instruction::Loop { .. })) + .count(); + assert_eq!(loop_count, 2, "got: {:#?}", scheduled.body); + } + + #[test] + fn lowers_match_to_if_else_chain() { + let mir_out = build_mir(indoc! {" + fn classify(x: i32) -> i32 { + match x { + 0 -> { 10 }, + 1 -> { 20 }, + + _ -> { -1 }, + } + } + export { classify } + "}); + let func = &mir_out.functions[0]; + let scheduled = schedule(func, &mir_out); + + // Two nested `if`s (one per real case), no BrTable at all. + let if_count = scheduled + .body + .iter() + .filter(|i| matches!(i, Instruction::If { .. })) + .count(); + assert_eq!( + if_count, 2, + "expected one `if` per real case; got: {:#?}", + scheduled.body + ); + assert!( + !scheduled + .body + .iter() + .any(|i| matches!(i, Instruction::BrTable { .. })), + "direct scheduler always uses an if/else-if chain, never BrTable" + ); + } + + #[test] + fn lowers_struct_construction_and_field_access() { + let mir_out = build_mir(indoc! {" + struct Point { + x: i32, + y: i32, + } + fn make_point(x: i32, y: i32) -> Point { + Point::{ x: x, y: y } + } + fn sum(p: Point) -> i32 { + p.x + p.y + } + export { make_point, sum } + "}); + let make_point = mir_out + .functions + .iter() + .find(|f| { + matches!( + mir_out.signatures[f.signature_index as usize].params(), + [mir::Type::I32, mir::Type::I32] + ) && mir_out.signatures[f.signature_index as usize].result() + == mir::Type::Aggregate { aggregate_index: 0 } + }) + .expect("make_point not found"); + assert_eq!( + schedule(make_point, &mir_out).body, + vec![Instruction::LocalGet(0), Instruction::LocalGet(1)], + ); + + let sum = mir_out + .functions + .iter() + .find(|f| { + mir_out.signatures[f.signature_index as usize].result() + == mir::Type::I32 + }) + .expect("sum not found"); + assert_eq!( + schedule(sum, &mir_out).body, + vec![ + Instruction::LocalGet(0), + Instruction::LocalGet(1), + Instruction::I32Add, + ], + ); + } + + #[test] + fn lowers_local_struct_field_mutation() { + let mir_out = build_mir(indoc! {" + struct Point { + x: i32, + y: i32, + } + fn bump_x(x: i32, y: i32) -> i32 { + local mut p: Point = Point::{ x: x, y: y }; + p.x = p.x + 1; + p.x + p.y + } + export { bump_x } + "}); + let func = &mir_out.functions[0]; + let scheduled = schedule(func, &mir_out); + + // `p` is a 2-slot local starting right after the 2 params (slots 0,1), + // so `p` occupies slots 2 (x) and 3 (y). `LocalSet` on a multi-value + // (aggregate) assignment pops in reverse to land each field in its + // correct slot — see the comment on `ExprKind::LocalSet`. + assert_eq!( + scheduled.body, + vec![ + // local mut p: Point = Point::{ x, y } + Instruction::LocalGet(0), + Instruction::LocalGet(1), + Instruction::LocalSet(3), + Instruction::LocalSet(2), + // p.x = p.x + 1 + Instruction::LocalGet(2), + Instruction::I32Const(1), + Instruction::I32Add, + Instruction::LocalSet(2), + // p.x + p.y + Instruction::LocalGet(2), + Instruction::LocalGet(3), + Instruction::I32Add, + ] + ); + } + + #[test] + fn spans_track_source_positions() { + let src = indoc! {" + fn compute(x: i32) -> i32 { + local y = x + 1; + y * 2 + } + export { compute } + "}; + let mir_out = build_mir(src); + let func = &mir_out.functions[0]; + let scheduled = schedule(func, &mir_out); + assert_eq!(scheduled.body.len(), scheduled.spans.len()); + + // `build_mir` prepends `"use std::*;\n"`, so spans are relative to + // that combined text, not `src` alone. + let full = format!("use std::*;\n{src}"); + let text_at = + |span: ast::TextSpan| &full[span.start as usize..span.end as usize]; + + let spanned: Vec<(&str, &Instruction)> = scheduled + .spans + .iter() + .copied() + .map(text_at) + .zip(scheduled.body.iter()) + .collect(); + assert_eq!( + spanned, + vec![ + ("x", &Instruction::LocalGet(0)), + ("1", &Instruction::I32Const(1)), + ("x + 1", &Instruction::I32Add), + ("local y = x + 1", &Instruction::LocalSet(1)), + ("y", &Instruction::LocalGet(1)), + ("2", &Instruction::I32Const(2)), + ("y * 2", &Instruction::I32Mul), + ] + ); + } +} diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__align_of_lowers_to_const_int.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__align_of_lowers_to_const_int.snap index a1c1a2f..8e71581 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__align_of_lowers_to_const_int.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__align_of_lowers_to_const_int.snap @@ -9,6 +9,7 @@ functions: - kind: Block parent: ~ locals: [] + locals_debug: [] result: U32 block: kind: @@ -19,14 +20,23 @@ functions: Int: value: 1 ty: U32 + span: + start: 78 + end: 100 ty: U32 + span: + start: 76 + end: 102 static_data: [] + file_id: 1 + name: ~ - id: 108 signature_index: 0 scopes: - kind: Block parent: ~ locals: [] + locals_debug: [] result: U32 block: kind: @@ -37,14 +47,23 @@ functions: Int: value: 4 ty: U32 + span: + start: 127 + end: 150 ty: U32 + span: + start: 125 + end: 152 static_data: [] + file_id: 1 + name: ~ - id: 109 signature_index: 0 scopes: - kind: Block parent: ~ locals: [] + locals_debug: [] result: U32 block: kind: @@ -55,8 +74,16 @@ functions: Int: value: 8 ty: U32 + span: + start: 177 + end: 200 ty: U32 + span: + start: 175 + end: 202 static_data: [] + file_id: 1 + name: ~ signatures: - items: - U32 diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__char_lowered_to_u32.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__char_lowered_to_u32.snap index 070a2ab..08e8dca 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__char_lowered_to_u32.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__char_lowered_to_u32.snap @@ -11,6 +11,7 @@ functions: locals: - ty: U32 mutability: Immutable + locals_debug: [] result: U32 block: kind: @@ -22,8 +23,16 @@ functions: scope_index: 0 local_index: 0 ty: U32 + span: + start: 47 + end: 48 ty: U32 + span: + start: 41 + end: 50 static_data: [] + file_id: 1 + name: ~ signatures: - items: - U32 diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__compound_assign_through_ptr_deref_on_struct_field.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__compound_assign_through_ptr_deref_on_struct_field.snap index dc2b61d..f6eb776 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__compound_assign_through_ptr_deref_on_struct_field.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__compound_assign_through_ptr_deref_on_struct_field.snap @@ -14,6 +14,7 @@ functions: memory: 106 kind: Memory32 mutability: Immutable + locals_debug: [] result: Unit block: kind: @@ -31,6 +32,9 @@ functions: Pointer: memory: 106 kind: Memory32 + span: + start: 155 + end: 156 value: kind: Add: @@ -46,20 +50,40 @@ functions: Pointer: memory: 106 kind: Memory32 + span: + start: 155 + end: 156 offset: 4 memory: 106 ty: U32 + span: + start: 155 + end: 162 right: kind: Int: value: 1 ty: U32 + span: + start: 166 + end: 167 ty: U32 + span: + start: 155 + end: 167 offset: 4 memory: 106 ty: Unit + span: + start: 155 + end: 167 ty: Unit + span: + start: 149 + end: 170 static_data: [] + file_id: 1 + name: ~ signatures: - items: - Pointer: diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__enum_folded_arithmetic_variant_lowered.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__enum_folded_arithmetic_variant_lowered.snap index 596fd50..4c6252b 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__enum_folded_arithmetic_variant_lowered.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__enum_folded_arithmetic_variant_lowered.snap @@ -9,6 +9,7 @@ functions: - kind: Block parent: ~ locals: [] + locals_debug: [] result: I32 block: kind: @@ -19,8 +20,16 @@ functions: Int: value: 2 ty: I32 + span: + start: 85 + end: 88 ty: I32 + span: + start: 72 + end: 90 static_data: [] + file_id: 1 + name: ~ signatures: - items: - I32 diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__enum_variant_lowered_to_repr_scalar.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__enum_variant_lowered_to_repr_scalar.snap index 7f6ca94..e989bc3 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__enum_variant_lowered_to_repr_scalar.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__enum_variant_lowered_to_repr_scalar.snap @@ -9,6 +9,7 @@ functions: - kind: Block parent: ~ locals: [] + locals_debug: [] result: I32 block: kind: @@ -19,8 +20,16 @@ functions: Int: value: 2 ty: I32 + span: + start: 104 + end: 109 ty: I32 + span: + start: 91 + end: 111 static_data: [] + file_id: 1 + name: ~ signatures: - items: - I32 diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__generic_impl_slice_count_method_lowers_correctly.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__generic_impl_slice_count_method_lowers_correctly.snap index 35279fe..cb545c5 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__generic_impl_slice_count_method_lowers_correctly.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__generic_impl_slice_count_method_lowers_correctly.snap @@ -13,6 +13,7 @@ functions: Aggregate: aggregate_index: 0 mutability: Immutable + locals_debug: [] result: U32 block: kind: @@ -28,6 +29,9 @@ functions: ty: Function: signature_index: 0 + span: + start: 195 + end: 204 arguments: - kind: LocalGet: @@ -36,9 +40,20 @@ functions: ty: Aggregate: aggregate_index: 0 + span: + start: 195 + end: 196 ty: U32 + span: + start: 195 + end: 204 ty: U32 + span: + start: 189 + end: 206 static_data: [] + file_id: 1 + name: ~ - id: 114 signature_index: 0 scopes: @@ -49,6 +64,7 @@ functions: Aggregate: aggregate_index: 0 mutability: Immutable + locals_debug: [] result: U32 block: kind: @@ -61,8 +77,16 @@ functions: local_index: 0 value_index: 1 ty: U32 + span: + start: 127 + end: 142 ty: U32 + span: + start: 117 + end: 148 static_data: [] + file_id: 1 + name: ~ signatures: - items: - Aggregate: diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__generic_over_memory_monomorphizes_size_type.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__generic_over_memory_monomorphizes_size_type.snap index 0387ade..727f473 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__generic_over_memory_monomorphizes_size_type.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__generic_over_memory_monomorphizes_size_type.snap @@ -11,6 +11,7 @@ functions: locals: - ty: U32 mutability: Immutable + locals_debug: [] result: U32 block: kind: @@ -26,17 +27,34 @@ functions: ty: Function: signature_index: 0 + span: + start: 196 + end: 209 arguments: - kind: Noop ty: Unit + span: + start: 201 + end: 205 - kind: LocalGet: scope_index: 0 local_index: 0 ty: U32 + span: + start: 207 + end: 208 ty: U32 + span: + start: 196 + end: 209 ty: U32 + span: + start: 190 + end: 211 static_data: [] + file_id: 1 + name: ~ - id: 110 signature_index: 3 scopes: @@ -45,6 +63,7 @@ functions: locals: - ty: U64 mutability: Immutable + locals_debug: [] result: U64 block: kind: @@ -60,17 +79,34 @@ functions: ty: Function: signature_index: 2 + span: + start: 251 + end: 265 arguments: - kind: Noop ty: Unit + span: + start: 256 + end: 261 - kind: LocalGet: scope_index: 0 local_index: 0 ty: U64 + span: + start: 263 + end: 264 ty: U64 + span: + start: 251 + end: 265 ty: U64 + span: + start: 245 + end: 267 static_data: [] + file_id: 1 + name: ~ - id: 119 signature_index: 0 scopes: @@ -81,6 +117,7 @@ functions: mutability: Immutable - ty: U32 mutability: Immutable + locals_debug: [] result: U32 block: kind: @@ -92,8 +129,16 @@ functions: scope_index: 0 local_index: 1 ty: U32 + span: + start: 154 + end: 155 ty: U32 + span: + start: 148 + end: 157 static_data: [] + file_id: 1 + name: ~ - id: 120 signature_index: 2 scopes: @@ -104,6 +149,7 @@ functions: mutability: Immutable - ty: U64 mutability: Immutable + locals_debug: [] result: U64 block: kind: @@ -115,8 +161,16 @@ functions: scope_index: 0 local_index: 1 ty: U64 + span: + start: 154 + end: 155 ty: U64 + span: + start: 148 + end: 157 static_data: [] + file_id: 1 + name: ~ signatures: - items: - Unit diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__generic_type_alias_transparent_in_mir.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__generic_type_alias_transparent_in_mir.snap index 733cf09..b86dc39 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__generic_type_alias_transparent_in_mir.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__generic_type_alias_transparent_in_mir.snap @@ -13,6 +13,7 @@ functions: Aggregate: aggregate_index: 0 mutability: Immutable + locals_debug: [] result: U32 block: kind: @@ -25,8 +26,16 @@ functions: local_index: 0 value_index: 0 ty: U32 + span: + start: 113 + end: 120 ty: U32 + span: + start: 107 + end: 122 static_data: [] + file_id: 1 + name: ~ signatures: - items: - Aggregate: diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__global_struct_type.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__global_struct_type.snap index 1cc4db4..87308f3 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__global_struct_type.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__global_struct_type.snap @@ -9,6 +9,7 @@ functions: - kind: Block parent: ~ locals: [] + locals_debug: [] result: Aggregate: aggregate_index: 0 @@ -24,17 +25,31 @@ functions: Int: value: 0 ty: U32 + span: + start: 94 + end: 95 - kind: Int: value: 0 ty: U32 + span: + start: 100 + end: 101 ty: Aggregate: aggregate_index: 0 + span: + start: 83 + end: 103 ty: Aggregate: aggregate_index: 0 + span: + start: 77 + end: 105 static_data: [] + file_id: 1 + name: ~ signatures: - items: - Aggregate: diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__inline_method_is_substituted.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__inline_method_is_substituted.snap index c818c91..c0e0f5e 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__inline_method_is_substituted.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__inline_method_is_substituted.snap @@ -11,34 +11,41 @@ functions: locals: - ty: U32 mutability: Immutable + locals_debug: [] result: U32 - kind: Block parent: 0 locals: [] + locals_debug: [] result: U32 - kind: Block parent: 1 locals: - ty: U32 mutability: Immutable + locals_debug: [] result: U32 - kind: Block parent: 2 locals: [] + locals_debug: [] result: U32 - kind: Block parent: 2 locals: [] + locals_debug: [] result: U32 - kind: Block parent: 2 locals: [] + locals_debug: [] result: Bool - kind: Block parent: 5 locals: - ty: U32 mutability: Immutable + locals_debug: [] result: Bool block: kind: @@ -59,7 +66,13 @@ functions: scope_index: 0 local_index: 0 ty: U32 + span: + start: 405 + end: 406 ty: Unit + span: + start: 405 + end: 406 - kind: Block: scope_index: 2 @@ -81,7 +94,13 @@ functions: scope_index: 2 local_index: 0 ty: U32 + span: + start: 237 + end: 241 ty: Unit + span: + start: 237 + end: 241 - kind: Block: scope_index: 6 @@ -97,12 +116,21 @@ functions: scope_index: 6 local_index: 0 ty: U32 + span: + start: 133 + end: 137 right: kind: Int: value: 97 ty: U32 + span: + start: 141 + end: 144 ty: Bool + span: + start: 133 + end: 144 right: kind: LessEq: @@ -112,15 +140,33 @@ functions: scope_index: 6 local_index: 0 ty: U32 + span: + start: 148 + end: 152 right: kind: Int: value: 122 ty: U32 + span: + start: 156 + end: 159 ty: Bool + span: + start: 148 + end: 159 ty: Bool + span: + start: 133 + end: 159 ty: Bool + span: + start: 123 + end: 165 ty: Bool + span: + start: 237 + end: 261 then_block: kind: Block: @@ -134,13 +180,25 @@ functions: scope_index: 2 local_index: 0 ty: U8 + span: + start: 278 + end: 282 right: kind: Int: value: 32 ty: U8 + span: + start: 292 + end: 307 ty: U32 + span: + start: 277 + end: 307 ty: U32 + span: + start: 262 + end: 326 else_block: kind: Block: @@ -151,12 +209,32 @@ functions: scope_index: 2 local_index: 0 ty: U32 + span: + start: 346 + end: 350 ty: U32 + span: + start: 332 + end: 360 ty: U32 + span: + start: 234 + end: 360 ty: U32 + span: + start: 224 + end: 366 ty: U32 + span: + start: 405 + end: 426 ty: U32 + span: + start: 399 + end: 428 static_data: [] + file_id: 1 + name: ~ signatures: - items: - U32 diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__match_enum_lowers_discriminants_to_repr_values.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__match_enum_lowers_discriminants_to_repr_values.snap index 9719ba5..e0733de 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__match_enum_lowers_discriminants_to_repr_values.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__match_enum_lowers_discriminants_to_repr_values.snap @@ -11,18 +11,22 @@ functions: locals: - ty: I32 mutability: Immutable + locals_debug: [] result: U8 - kind: Block parent: 0 locals: [] + locals_debug: [] result: U8 - kind: Block parent: 0 locals: [] + locals_debug: [] result: U8 - kind: Block parent: 0 locals: [] + locals_debug: [] result: U8 block: kind: @@ -37,6 +41,9 @@ functions: scope_index: 0 local_index: 0 ty: I32 + span: + start: 104 + end: 105 cases: - - 1 - kind: @@ -47,7 +54,13 @@ functions: Int: value: 0 ty: U8 + span: + start: 132 + end: 133 ty: U8 + span: + start: 130 + end: 135 - - 2 - kind: Block: @@ -57,7 +70,13 @@ functions: Int: value: 1 ty: U8 + span: + start: 163 + end: 164 ty: U8 + span: + start: 161 + end: 166 - - 3 - kind: Block: @@ -67,11 +86,25 @@ functions: Int: value: 2 ty: U8 + span: + start: 193 + end: 194 ty: U8 + span: + start: 191 + end: 196 default: ~ ty: U8 + span: + start: 98 + end: 203 ty: U8 + span: + start: 92 + end: 205 static_data: [] + file_id: 1 + name: ~ signatures: - items: - I32 diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__match_int_lowers_to_switch.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__match_int_lowers_to_switch.snap index f78d1da..0659471 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__match_int_lowers_to_switch.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__match_int_lowers_to_switch.snap @@ -11,18 +11,22 @@ functions: locals: - ty: I32 mutability: Immutable + locals_debug: [] result: I32 - kind: Block parent: 0 locals: [] + locals_debug: [] result: I32 - kind: Block parent: 0 locals: [] + locals_debug: [] result: I32 - kind: Block parent: 0 locals: [] + locals_debug: [] result: I32 block: kind: @@ -37,6 +41,9 @@ functions: scope_index: 0 local_index: 0 ty: I32 + span: + start: 47 + end: 48 cases: - - 0 - kind: @@ -47,7 +54,13 @@ functions: Int: value: 0 ty: I32 + span: + start: 66 + end: 67 ty: I32 + span: + start: 64 + end: 69 - - 1 - kind: Block: @@ -57,7 +70,13 @@ functions: Int: value: 1 ty: I32 + span: + start: 86 + end: 87 ty: I32 + span: + start: 84 + end: 89 default: kind: Block: @@ -70,11 +89,28 @@ functions: Int: value: 1 ty: I32 + span: + start: 107 + end: 108 ty: I32 + span: + start: 106 + end: 108 ty: I32 + span: + start: 104 + end: 110 ty: I32 + span: + start: 41 + end: 117 ty: I32 + span: + start: 35 + end: 119 static_data: [] + file_id: 1 + name: ~ signatures: - items: - I32 diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_data_end_lowers_to_memory_offset.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_data_end_lowers_to_memory_offset.snap index a5cd5fe..bbd8d92 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_data_end_lowers_to_memory_offset.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_data_end_lowers_to_memory_offset.snap @@ -9,6 +9,7 @@ functions: - kind: Block parent: ~ locals: [] + locals_debug: [] result: Pointer: memory: 106 @@ -25,11 +26,19 @@ functions: Pointer: memory: 106 kind: Memory32 + span: + start: 85 + end: 99 ty: Pointer: memory: 106 kind: Memory32 + span: + start: 79 + end: 101 static_data: [] + file_id: 1 + name: ~ signatures: - items: - Pointer: diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_grow_lowers_to_memory_grow.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_grow_lowers_to_memory_grow.snap index 843e6bf..9aff286 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_grow_lowers_to_memory_grow.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_grow_lowers_to_memory_grow.snap @@ -11,10 +11,12 @@ functions: locals: - ty: U32 mutability: Immutable + locals_debug: [] result: I32 - kind: Block parent: 0 locals: [] + locals_debug: [] result: I32 - kind: Block parent: 1 @@ -23,6 +25,7 @@ functions: mutability: Immutable - ty: U32 mutability: Immutable + locals_debug: [] result: I32 block: kind: @@ -40,7 +43,13 @@ functions: value: kind: Noop ty: Unit + span: + start: 89 + end: 93 ty: Unit + span: + start: 89 + end: 93 - kind: LocalSet: scope_index: 2 @@ -51,7 +60,13 @@ functions: scope_index: 0 local_index: 0 ty: U32 + span: + start: 99 + end: 104 ty: Unit + span: + start: 99 + end: 104 - kind: Block: scope_index: 2 @@ -65,11 +80,28 @@ functions: scope_index: 2 local_index: 1 ty: U32 + span: + start: 3484 + end: 3489 ty: I32 + span: + start: 3466 + end: 3490 ty: I32 + span: + start: 3456 + end: 3496 ty: I32 + span: + start: 89 + end: 105 ty: I32 + span: + start: 83 + end: 107 static_data: [] + file_id: 1 + name: ~ signatures: - items: - Unit diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_index_lowers_to_int.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_index_lowers_to_int.snap index 426afb7..6d593d0 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_index_lowers_to_int.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_index_lowers_to_int.snap @@ -9,6 +9,7 @@ functions: - kind: Block parent: ~ locals: [] + locals_debug: [] result: U32 block: kind: @@ -19,8 +20,16 @@ functions: MemoryIndex: memory: 106 ty: U32 + span: + start: 79 + end: 97 ty: U32 + span: + start: 73 + end: 99 static_data: [] + file_id: 1 + name: ~ signatures: - items: - U32 diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_size_lowers_to_memory_size.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_size_lowers_to_memory_size.snap index 34e61d0..5f67746 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_size_lowers_to_memory_size.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__memory_size_lowers_to_memory_size.snap @@ -9,16 +9,19 @@ functions: - kind: Block parent: ~ locals: [] + locals_debug: [] result: U32 - kind: Block parent: 0 locals: [] + locals_debug: [] result: U32 - kind: Block parent: 1 locals: - ty: Unit mutability: Immutable + locals_debug: [] result: U32 block: kind: @@ -36,7 +39,13 @@ functions: value: kind: Noop ty: Unit + span: + start: 79 + end: 83 ty: Unit + span: + start: 79 + end: 83 - kind: Block: scope_index: 2 @@ -45,10 +54,24 @@ functions: MemorySize: memory: 106 ty: U32 + span: + start: 3554 + end: 3571 ty: U32 + span: + start: 3544 + end: 3577 ty: U32 + span: + start: 79 + end: 90 ty: U32 + span: + start: 73 + end: 92 static_data: [] + file_id: 1 + name: ~ signatures: - items: - Unit diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__non_inline_callee_survives_dce.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__non_inline_callee_survives_dce.snap index e6ea7d8..3933f2c 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__non_inline_callee_survives_dce.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__non_inline_callee_survives_dce.snap @@ -11,6 +11,7 @@ functions: locals: - ty: U32 mutability: Immutable + locals_debug: [] result: U32 block: kind: @@ -25,14 +26,28 @@ functions: scope_index: 0 local_index: 0 ty: U32 + span: + start: 39 + end: 40 right: kind: Int: value: 1 ty: U32 + span: + start: 43 + end: 44 ty: U32 + span: + start: 39 + end: 44 ty: U32 + span: + start: 37 + end: 46 static_data: [] + file_id: 1 + name: ~ - id: 107 signature_index: 0 scopes: @@ -41,6 +56,7 @@ functions: locals: - ty: U32 mutability: Immutable + locals_debug: [] result: U32 block: kind: @@ -56,15 +72,29 @@ functions: ty: Function: signature_index: 0 + span: + start: 73 + end: 79 arguments: - kind: LocalGet: scope_index: 0 local_index: 0 ty: U32 + span: + start: 80 + end: 81 ty: U32 + span: + start: 73 + end: 82 ty: U32 + span: + start: 71 + end: 84 static_data: [] + file_id: 1 + name: ~ signatures: - items: - U32 diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__size_of_generic_monomorphizes.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__size_of_generic_monomorphizes.snap index bdbbdb2..c0c1c15 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__size_of_generic_monomorphizes.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__size_of_generic_monomorphizes.snap @@ -9,6 +9,7 @@ functions: - kind: Block parent: ~ locals: [] + locals_debug: [] result: U32 block: kind: @@ -24,16 +25,28 @@ functions: ty: Function: signature_index: 0 + span: + start: 141 + end: 165 arguments: [] ty: U32 + span: + start: 141 + end: 165 ty: U32 + span: + start: 139 + end: 167 static_data: [] + file_id: 1 + name: ~ - id: 109 signature_index: 0 scopes: - kind: Block parent: ~ locals: [] + locals_debug: [] result: U32 block: kind: @@ -49,16 +62,28 @@ functions: ty: Function: signature_index: 0 + span: + start: 191 + end: 216 arguments: [] ty: U32 + span: + start: 191 + end: 216 ty: U32 + span: + start: 189 + end: 218 static_data: [] + file_id: 1 + name: ~ - id: 114 signature_index: 0 scopes: - kind: Block parent: ~ locals: [] + locals_debug: [] result: U32 block: kind: @@ -69,14 +94,23 @@ functions: Int: value: 1 ty: U32 + span: + start: 98 + end: 115 ty: U32 + span: + start: 96 + end: 117 static_data: [] + file_id: 1 + name: ~ - id: 115 signature_index: 0 scopes: - kind: Block parent: ~ locals: [] + locals_debug: [] result: U32 block: kind: @@ -87,8 +121,16 @@ functions: Int: value: 4 ty: U32 + span: + start: 98 + end: 115 ty: U32 + span: + start: 96 + end: 117 static_data: [] + file_id: 1 + name: ~ signatures: - items: - U32 diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__size_of_lowers_to_const_int.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__size_of_lowers_to_const_int.snap index 356e5b3..cfef7e1 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__size_of_lowers_to_const_int.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__size_of_lowers_to_const_int.snap @@ -9,6 +9,7 @@ functions: - kind: Block parent: ~ locals: [] + locals_debug: [] result: U32 block: kind: @@ -19,14 +20,23 @@ functions: Int: value: 1 ty: U32 + span: + start: 77 + end: 98 ty: U32 + span: + start: 75 + end: 100 static_data: [] + file_id: 1 + name: ~ - id: 108 signature_index: 0 scopes: - kind: Block parent: ~ locals: [] + locals_debug: [] result: U32 block: kind: @@ -37,14 +47,23 @@ functions: Int: value: 4 ty: U32 + span: + start: 124 + end: 146 ty: U32 + span: + start: 122 + end: 148 static_data: [] + file_id: 1 + name: ~ - id: 109 signature_index: 0 scopes: - kind: Block parent: ~ locals: [] + locals_debug: [] result: U32 block: kind: @@ -55,14 +74,23 @@ functions: Int: value: 8 ty: U32 + span: + start: 172 + end: 194 ty: U32 + span: + start: 170 + end: 196 static_data: [] + file_id: 1 + name: ~ - id: 110 signature_index: 0 scopes: - kind: Block parent: ~ locals: [] + locals_debug: [] result: U32 block: kind: @@ -73,8 +101,16 @@ functions: Int: value: 2 ty: U32 + span: + start: 220 + end: 242 ty: U32 + span: + start: 218 + end: 244 static_data: [] + file_id: 1 + name: ~ signatures: - items: - U32 diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__slice_from_parts_lowers_to_aggregate.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__slice_from_parts_lowers_to_aggregate.snap index d3f0729..9a85742 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__slice_from_parts_lowers_to_aggregate.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__slice_from_parts_lowers_to_aggregate.snap @@ -16,6 +16,7 @@ functions: mutability: Immutable - ty: U32 mutability: Immutable + locals_debug: [] result: Aggregate: aggregate_index: 0 @@ -35,18 +36,32 @@ functions: Pointer: memory: 106 kind: Memory32 + span: + start: 127 + end: 130 - kind: LocalGet: scope_index: 0 local_index: 1 ty: U32 + span: + start: 132 + end: 135 ty: Aggregate: aggregate_index: 0 + span: + start: 110 + end: 136 ty: Aggregate: aggregate_index: 0 + span: + start: 104 + end: 138 static_data: [] + file_id: 1 + name: ~ signatures: - items: - Pointer: diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__slice_len_lowers_to_aggregate_get.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__slice_len_lowers_to_aggregate_get.snap index 2f3201c..62c6f42 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__slice_len_lowers_to_aggregate_get.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__slice_len_lowers_to_aggregate_get.snap @@ -13,6 +13,7 @@ functions: Aggregate: aggregate_index: 0 mutability: Immutable + locals_debug: [] result: U32 block: kind: @@ -25,8 +26,16 @@ functions: local_index: 0 value_index: 1 ty: U32 + span: + start: 92 + end: 104 ty: U32 + span: + start: 86 + end: 106 static_data: [] + file_id: 1 + name: ~ signatures: - items: - Aggregate: diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__struct_field_access_lowered_to_local_tuple_get.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__struct_field_access_lowered_to_local_tuple_get.snap index b8c8829..a940a3d 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__struct_field_access_lowered_to_local_tuple_get.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__struct_field_access_lowered_to_local_tuple_get.snap @@ -13,6 +13,7 @@ functions: Aggregate: aggregate_index: 0 mutability: Immutable + locals_debug: [] result: U32 block: kind: @@ -25,8 +26,16 @@ functions: local_index: 0 value_index: 0 ty: U32 + span: + start: 86 + end: 89 ty: U32 + span: + start: 80 + end: 91 static_data: [] + file_id: 1 + name: ~ signatures: - items: - Aggregate: diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__struct_init_lowered_to_struct_create.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__struct_init_lowered_to_struct_create.snap index f695f74..e0a3759 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__struct_init_lowered_to_struct_create.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__struct_init_lowered_to_struct_create.snap @@ -13,6 +13,7 @@ functions: mutability: Immutable - ty: U32 mutability: Immutable + locals_debug: [] result: Aggregate: aggregate_index: 0 @@ -29,18 +30,32 @@ functions: scope_index: 0 local_index: 0 ty: U32 + span: + start: 111 + end: 112 - kind: LocalGet: scope_index: 0 local_index: 1 ty: U32 + span: + start: 117 + end: 118 ty: Aggregate: aggregate_index: 0 + span: + start: 99 + end: 120 ty: Aggregate: aggregate_index: 0 + span: + start: 93 + end: 122 static_data: [] + file_id: 1 + name: ~ signatures: - items: - U32 diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__struct_method_call.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__struct_method_call.snap index c818c91..c0e0f5e 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__struct_method_call.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__struct_method_call.snap @@ -11,34 +11,41 @@ functions: locals: - ty: U32 mutability: Immutable + locals_debug: [] result: U32 - kind: Block parent: 0 locals: [] + locals_debug: [] result: U32 - kind: Block parent: 1 locals: - ty: U32 mutability: Immutable + locals_debug: [] result: U32 - kind: Block parent: 2 locals: [] + locals_debug: [] result: U32 - kind: Block parent: 2 locals: [] + locals_debug: [] result: U32 - kind: Block parent: 2 locals: [] + locals_debug: [] result: Bool - kind: Block parent: 5 locals: - ty: U32 mutability: Immutable + locals_debug: [] result: Bool block: kind: @@ -59,7 +66,13 @@ functions: scope_index: 0 local_index: 0 ty: U32 + span: + start: 405 + end: 406 ty: Unit + span: + start: 405 + end: 406 - kind: Block: scope_index: 2 @@ -81,7 +94,13 @@ functions: scope_index: 2 local_index: 0 ty: U32 + span: + start: 237 + end: 241 ty: Unit + span: + start: 237 + end: 241 - kind: Block: scope_index: 6 @@ -97,12 +116,21 @@ functions: scope_index: 6 local_index: 0 ty: U32 + span: + start: 133 + end: 137 right: kind: Int: value: 97 ty: U32 + span: + start: 141 + end: 144 ty: Bool + span: + start: 133 + end: 144 right: kind: LessEq: @@ -112,15 +140,33 @@ functions: scope_index: 6 local_index: 0 ty: U32 + span: + start: 148 + end: 152 right: kind: Int: value: 122 ty: U32 + span: + start: 156 + end: 159 ty: Bool + span: + start: 148 + end: 159 ty: Bool + span: + start: 133 + end: 159 ty: Bool + span: + start: 123 + end: 165 ty: Bool + span: + start: 237 + end: 261 then_block: kind: Block: @@ -134,13 +180,25 @@ functions: scope_index: 2 local_index: 0 ty: U8 + span: + start: 278 + end: 282 right: kind: Int: value: 32 ty: U8 + span: + start: 292 + end: 307 ty: U32 + span: + start: 277 + end: 307 ty: U32 + span: + start: 262 + end: 326 else_block: kind: Block: @@ -151,12 +209,32 @@ functions: scope_index: 2 local_index: 0 ty: U32 + span: + start: 346 + end: 350 ty: U32 + span: + start: 332 + end: 360 ty: U32 + span: + start: 234 + end: 360 ty: U32 + span: + start: 224 + end: 366 ty: U32 + span: + start: 405 + end: 426 ty: U32 + span: + start: 399 + end: 428 static_data: [] + file_id: 1 + name: ~ signatures: - items: - U32 diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__tuple_type_alias_transparent_in_mir.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__tuple_type_alias_transparent_in_mir.snap index 99835e1..123950e 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__tuple_type_alias_transparent_in_mir.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__tuple_type_alias_transparent_in_mir.snap @@ -9,6 +9,7 @@ functions: - kind: Block parent: ~ locals: [] + locals_debug: [] result: Aggregate: aggregate_index: 0 @@ -24,17 +25,31 @@ functions: Int: value: 1 ty: U32 + span: + start: 66 + end: 67 - kind: Int: value: 2 ty: U32 + span: + start: 69 + end: 70 ty: Aggregate: aggregate_index: 0 + span: + start: 65 + end: 71 ty: Aggregate: aggregate_index: 0 + span: + start: 59 + end: 73 static_data: [] + file_id: 1 + name: ~ signatures: - items: - Aggregate: diff --git a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__type_alias_to_struct_transparent_in_mir.snap b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__type_alias_to_struct_transparent_in_mir.snap index 8010f07..70b808b 100644 --- a/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__type_alias_to_struct_transparent_in_mir.snap +++ b/crates/wx-compiler/src/mir/snapshots/wx_compiler__mir__tests__type_alias_to_struct_transparent_in_mir.snap @@ -13,6 +13,7 @@ functions: Aggregate: aggregate_index: 0 mutability: Immutable + locals_debug: [] result: U32 block: kind: @@ -25,8 +26,16 @@ functions: local_index: 0 value_index: 0 ty: U32 + span: + start: 107 + end: 110 ty: U32 + span: + start: 101 + end: 112 static_data: [] + file_id: 1 + name: ~ signatures: - items: - Aggregate: diff --git a/crates/wx-compiler/src/mir/tests.rs b/crates/wx-compiler/src/mir/tests.rs index f0959c6..581dc6d 100644 --- a/crates/wx-compiler/src/mir/tests.rs +++ b/crates/wx-compiler/src/mir/tests.rs @@ -29,7 +29,15 @@ impl TestCase { .unwrap(); let mut graph = builder.build(root_id, stdlib_id); let tir = tir::TIR::build(&mut graph); - let mir = MIR::build(&tir, &graph.interner, graph.id_generator); + let mut mir = MIR::build( + &tir, + &graph.interner, + graph.id_generator, + crate::CompilationMode::Release, + ); + let mut call_graph = CallGraph::build(&mir.functions, &mir.call_edges); + mir.inline_calls(&mut call_graph); + mir.dead_code_eliminate(&call_graph); TestCase { graph, tir, mir } } } diff --git a/crates/wx-compiler/src/opt/builder.rs b/crates/wx-compiler/src/opt/builder.rs index 03ca22b..35329be 100644 --- a/crates/wx-compiler/src/opt/builder.rs +++ b/crates/wx-compiler/src/opt/builder.rs @@ -1,8 +1,9 @@ use crate::mir::{self, ExprKind}; use crate::opt::{ Block, BlockIndex, ControlNode, DataNodeIndex, DataNodeKind, Function, - LoopData, MemAccess, NodeType, ScalarType, StackResult, SwitchCase, + LoopData, MemAccess, NodeType, StackResult, SwitchCase, }; +use crate::wasm::ScalarType; pub struct Builder<'mir> { mir: &'mir mir::MIR, diff --git a/crates/wx-compiler/src/opt/mod.rs b/crates/wx-compiler/src/opt/mod.rs index c7f274c..68b479f 100644 --- a/crates/wx-compiler/src/opt/mod.rs +++ b/crates/wx-compiler/src/opt/mod.rs @@ -21,20 +21,12 @@ pub mod scheduler; #[cfg(test)] mod tests; +use crate::wasm::ScalarType; use crate::{ast, mir}; pub type DataNodeIndex = u32; pub type BlockIndex = u32; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(test, derive(serde::Serialize))] -pub enum ScalarType { - I32, - I64, - F32, - F64, -} - /// Sign only matters for narrow loads: `i32.load8_s` vs `i32.load8_u`. /// Full-width loads and stores are always unsigned. #[derive(Clone, Copy, PartialEq, Eq, Hash)] @@ -91,41 +83,6 @@ impl MemAccess { } } -impl TryFrom for ScalarType { - type Error = (); - fn try_from(ty: mir::Type) -> Result { - Ok(match ty { - mir::Type::I32 - | mir::Type::U32 - | mir::Type::Bool - | mir::Type::U8 - | mir::Type::I8 - | mir::Type::U16 - | mir::Type::I16 - | mir::Type::Function { .. } => ScalarType::I32, - mir::Type::I64 | mir::Type::U64 => ScalarType::I64, - mir::Type::Pointer { kind, .. } => match kind { - mir::MemoryKind::Memory32 => ScalarType::I32, - mir::MemoryKind::Memory64 => ScalarType::I64, - }, - mir::Type::F32 => ScalarType::F32, - mir::Type::F64 => ScalarType::F64, - _ => return Err(()), - }) - } -} - -impl From for crate::codegen::ValueType { - fn from(ty: ScalarType) -> Self { - match ty { - ScalarType::I32 => crate::codegen::ValueType::I32, - ScalarType::I64 => crate::codegen::ValueType::I64, - ScalarType::F32 => crate::codegen::ValueType::F32, - ScalarType::F64 => crate::codegen::ValueType::F64, - } - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum NodeType { Scalar(ScalarType), diff --git a/crates/wx-compiler/src/opt/scheduler.rs b/crates/wx-compiler/src/opt/scheduler.rs index 6a257d0..a6be388 100644 --- a/crates/wx-compiler/src/opt/scheduler.rs +++ b/crates/wx-compiler/src/opt/scheduler.rs @@ -18,262 +18,27 @@ //! //! # Output //! -//! The scheduler produces a [`ScheduledFunction`] containing +//! The scheduler produces a [`WasmFunction`] containing //! - the extra WASM locals needed for spilled nodes (appended after params) //! - a flat `Vec` for the function body //! -//! Encoding [`Instruction`]s to bytes is left to the codegen layer. +//! `WasmFunction`/`Instruction` and everything else describing that output +//! shape live in `crate::wasm` — shared with `mir::scheduler`, the other +//! producer of the same representation — not here. Encoding `Instruction`s +//! to bytes is left to the codegen layer. use std::collections::HashMap; -use crate::codegen::ValueType; use crate::mir; use crate::opt::liveness::DataLiveness; use crate::opt::{ BlockIndex, ControlNode, DataNode, DataNodeIndex, DataNodeKind, Function, - MemAccess, ScalarType, StackResult, SwitchCase, + MemAccess, StackResult, SwitchCase, +}; +use crate::wasm::{ + self, BlockType, Function as WasmFunction, Instruction, Local, MemArg, + ScalarType, }; - -// ── Output types -// ────────────────────────────────────────────────────────────── - -/// The `memarg` immediate carried by every WebAssembly memory instruction. -#[cfg_attr(test, derive(Debug, serde::Serialize))] -#[derive(Clone, Copy)] -pub struct MemArg { - /// Log2 of the alignment hint in bytes (e.g. 2 = 4-byte aligned). - pub align: u32, - /// Static byte offset added to the runtime address. - pub offset: u32, - pub memory: crate::ast::DefId, -} - -/// A WASM local variable declaration. -#[cfg_attr(test, derive(serde::Serialize))] -#[cfg_attr(test, serde(transparent))] -pub struct Local { - pub ty: ScalarType, -} - -#[cfg_attr(test, derive(serde::Serialize))] -pub struct ScheduledFunction { - /// Locals in declaration order (params first, then spill slots). - pub locals: Vec, - /// Flat WASM stack-machine instruction sequence for the function body. - pub body: Vec, -} - -/// A subset of WASM instructions produced by the scheduler. -/// Each variant maps 1-to-1 to a WASM opcode; operands are pushed onto the -/// implicit value stack by the preceding instructions. -#[cfg_attr(test, derive(Debug, serde::Serialize))] -#[derive(Clone)] -pub enum Instruction { - // Constants - I32Const(i32), - I64Const(i64), - F32Const(f32), - F64Const(f64), - // Locals - LocalGet(u32), - LocalSet(u32), - LocalTee(u32), - // Globals - GlobalGet(crate::ast::DefId), - GlobalSet(crate::ast::DefId), - // Arithmetic — i32 - I32Add, - I32Sub, - I32Mul, - I32DivS, - I32DivU, - I32RemS, - I32RemU, - I32And, - I32Or, - I32Xor, - I32Shl, - I32ShrS, - I32ShrU, - I32Eqz, - I32Eq, - I32Ne, - I32LtS, - I32LtU, - I32LeS, - I32LeU, - I32GtS, - I32GtU, - I32GeS, - I32GeU, - I32Clz, - I32Ctz, - // Arithmetic — i64 - I64Add, - I64Sub, - I64Mul, - I64DivS, - I64DivU, - I64RemS, - I64RemU, - I64And, - I64Or, - I64Xor, - I64Shl, - I64ShrS, - I64ShrU, - I64Eqz, - I64Eq, - I64Ne, - I64LtS, - I64LtU, - I64LeS, - I64LeU, - I64GtS, - I64GtU, - I64GeS, - I64GeU, - // Arithmetic — f32 / f64 - F32Add, - F32Sub, - F32Mul, - F32Div, - F32Neg, - F32Sqrt, - F32Abs, - F32Floor, - F32Ceil, - F64Add, - F64Sub, - F64Mul, - F64Div, - F64Neg, - F64Sqrt, - F64Abs, - F64Floor, - F64Ceil, - F32Eq, - F32Ne, - F32Lt, - F32Le, - F32Gt, - F32Ge, - F64Eq, - F64Ne, - F64Lt, - F64Le, - F64Gt, - F64Ge, - // Control flow - Block { - ty: BlockType, - }, - Loop { - ty: BlockType, - }, - If { - ty: BlockType, - }, - Else, - End, - Br(u32), // break by depth - BrIf(u32), - /// Branch depth per shifted-selector index, covering every index in the - /// case set's `[min, max]` range (including gaps), followed by the - /// default depth as the trailing element — i.e. `depths[i]` for - /// `i < depths.len() - 1`, `depths[depths.len() - 1]` for anything else - /// (per WASM semantics: an out-of-table index, including one that went - /// negative before being reinterpreted as unsigned, always falls to the - /// default). One field instead of a separate `default_depth` since the - /// encoder needs the exact same split either way. - BrTable(Box<[u32]>), - Return, - Unreachable, - Drop, - // Calls - /// Direct call; the encoder resolves the WASM function index from - /// `func_wasm_index`, covering both internal and imported functions. - Call(crate::ast::DefId), - /// Indirect call via the function table; the encoder resolves `type_index` - /// from the referenced MIR signature. - CallIndirectSym { - mir_sig_index: u32, - }, - // Memory - MemorySize(crate::ast::DefId), - MemoryGrow(crate::ast::DefId), - MemoryFill(crate::ast::DefId), - MemoryCopy { - dst: crate::ast::DefId, - src: crate::ast::DefId, - }, - /// Wasm linear-memory index as an `i32.const`, resolved at codegen. - MemoryIndex { - memory: crate::ast::DefId, - }, - // Pointer load/store - I32Load8S(MemArg), - I32Load8U(MemArg), - I32Load16S(MemArg), - I32Load16U(MemArg), - I32Load(MemArg), - I64Load(MemArg), - F32Load(MemArg), - F64Load(MemArg), - I32Store8(MemArg), - I32Store16(MemArg), - I32Store(MemArg), - I64Store(MemArg), - F32Store(MemArg), - F64Store(MemArg), - // Conversion - I64ExtendI32S, - I64ExtendI32U, - I32WrapI64, - F32ConvertI32S, - F32ConvertI32U, - F32ConvertI64S, - F32ConvertI64U, - F64ConvertI32S, - F64ConvertI32U, - F64ConvertI64S, - F64ConvertI64U, - I32TruncF32S, - I32TruncF32U, - I32TruncF64S, - I32TruncF64U, - I64TruncF32S, - I64TruncF32U, - I64TruncF64S, - I64TruncF64U, - F64PromoteF32, - F32DemoteF64, - // Nop (used as a placeholder) - Nop, - // Symbolic references — resolved to concrete i32.const values by the - // codegen encoder, which has access to the string pool and function table. - /// A function referenced as a value; the encoder pushes it into the - /// function table and emits `i32.const `. - FunctionPointer(crate::ast::DefId), - /// End of the static data section for a given memory (base of writable - /// heap); the encoder emits `i32.const `. - DataSectionEnd { - memory: crate::ast::DefId, - }, - /// A static array; the encoder resolves the index to a byte offset in the - /// data segment and emits `i32.const `. - StaticDataPointer { - data_index: u32, - ty: ScalarType, - }, -} - -#[cfg_attr(test, derive(Debug, serde::Serialize))] -#[derive(Clone, Copy)] -pub enum BlockType { - Empty, - Value(ValueType), -} // ── Scheduler // ───────────────────────────────────────────────────────────────── @@ -293,6 +58,9 @@ pub struct Scheduler<'f> { node_to_aggregate_locals: HashMap>, /// Output instruction stream. body: Vec, + /// Flat arena backing every emitted `Instruction::BrTable`; see + /// `WasmFunction::br_table_depths`. + br_table_depths: Vec, /// Placement decisions for pure values read from more than one block — /// computed once by `compute_value_placement` before emission starts, /// consulted (and drained, one block at a time) by `emit_block`. Indexed @@ -303,10 +71,7 @@ pub struct Scheduler<'f> { } impl<'f> Scheduler<'f> { - pub fn schedule( - func: &'f Function, - mir: &'f mir::MIR, - ) -> ScheduledFunction { + pub fn schedule(func: &'f Function, mir: &'f mir::MIR) -> WasmFunction { let sig = &mir.signatures[{ // Find the function's signature via its DefId. mir.functions @@ -321,7 +86,7 @@ impl<'f> Scheduler<'f> { .params() .iter() .copied() - .flat_map(|ty| Self::flatten_mir_type(ty, &mir.aggregates)) + .flat_map(|ty| wasm::flatten_type_to_scalars(ty, &mir.aggregates)) .map(|ty| Local { ty }) .collect(); let params_count = locals.len(); @@ -334,6 +99,7 @@ impl<'f> Scheduler<'f> { node_to_local: HashMap::new(), node_to_aggregate_locals: HashMap::new(), body: Vec::new(), + br_table_depths: Vec::new(), placement_by_block: Vec::new(), }; sched.placement_by_block = sched.compute_value_placement(); @@ -344,12 +110,15 @@ impl<'f> Scheduler<'f> { sched.body.pop(); } - coalesce_locals(&mut sched.body, &mut sched.locals, params_count); - peephole_local_tee(&mut sched.body); + wasm::coalesce_locals(&mut sched.body, &mut sched.locals, params_count); + wasm::peephole_local_tee(&mut sched.body); - ScheduledFunction { + WasmFunction { locals: sched.locals, body: sched.body, + br_table_depths: sched.br_table_depths, + spans: Vec::new(), + locals_debug: Vec::new(), } } @@ -1812,7 +1581,8 @@ impl<'f> Scheduler<'f> { .values[..field_index as usize] .iter() .map(|&t| { - Self::flatten_mir_type(t, &self.mir.aggregates).len() + wasm::flatten_type_to_scalars(t, &self.mir.aggregates) + .len() }) .sum(); let field_local = @@ -1896,31 +1666,13 @@ impl<'f> Scheduler<'f> { } } - /// Recursively flatten a MIR type into its constituent scalar types. - /// Mirrors `codegen::Builder::flatten_type` but yields `ScalarType`. - fn flatten_mir_type( - ty: mir::Type, - aggregates: &[mir::Aggregate], - ) -> Vec { - match ty { - mir::Type::Unit | mir::Type::Never => vec![], - mir::Type::Aggregate { aggregate_index } => aggregates - [aggregate_index as usize] - .values - .iter() - .flat_map(|&f| Self::flatten_mir_type(f, aggregates)) - .collect(), - _ => vec![ScalarType::try_from(ty).expect("must be scalar")], - } - } - /// Ensure per-*leaf* WASM locals exist for an `Aggregate` node. /// Emits each scalar field expression and spills it to a fresh local; a /// field that is itself an aggregate has no local of its own — it's /// walked into recursively, and its own (already- or newly-computed) /// leaf locals are spliced in directly, so `node_to_aggregate_locals` /// always ends up holding one entry per *leaf* scalar, flattened in the - /// same pre-order as `flatten_mir_type`. Records the mapping in + /// same pre-order as `wasm::flatten_type_to_scalars`. Records the mapping in /// `node_to_aggregate_locals`. /// /// For `AggregateCallResult` nodes this must never be called — their locals @@ -2140,8 +1892,10 @@ impl<'f> Scheduler<'f> { self.body.push(Instruction::LocalGet(selector_local)); self.body.push(Instruction::I32Const(min as i32)); self.body.push(Instruction::I32Sub); - self.body - .push(Instruction::BrTable(depths.into_boxed_slice())); + let start = self.br_table_depths.len() as u32; + let len = depths.len() as u32; + self.br_table_depths.extend(depths); + self.body.push(Instruction::BrTable { start, len }); for (i, case) in cases.iter().enumerate() { self.body.push(Instruction::End); // end $case[i] @@ -2238,7 +1992,7 @@ impl<'f> Scheduler<'f> { _ => { self.emit_value(callee_node); self.body.push(Instruction::CallIndirectSym { - mir_sig_index: callee_sig, + signature_index: callee_sig, }); } } @@ -2249,7 +2003,7 @@ impl<'f> Scheduler<'f> { StackResult::Value(node) => { let ty = self.func.data_nodes[node as usize].kind.unwrap_scalar(); - BlockType::Value(ValueType::from(ty)) + BlockType::Value(ty) } _ => BlockType::Empty, } @@ -2265,233 +2019,3 @@ fn push_unique_block(blocks: &mut Vec, block: BlockIndex) { blocks.push(block); } } - -// ── Local coalescing ────────────────────────────────────────────────────────── - -/// Reuse WASM local slots for spilled values whose live ranges do not overlap. -/// -/// Each spill slot has a live range `[first_write, last_read]` measured in flat -/// instruction-list positions. Two slots of the same WASM type can share a slot -/// number when one range ends strictly before the other begins. -/// -/// Param slots (indices `0..params_count`) are never remapped — WASM passes -/// arguments via the first N locals and the ABI cannot be changed. -fn coalesce_locals( - body: &mut [Instruction], - locals: &mut Vec, - params_count: usize, -) { - let n = locals.len(); - if n <= params_count { - return; - } - - // ── Step 1: compute live ranges ────────────────────────────────────────── - let mut first_write = vec![usize::MAX; n]; - let mut last_read = vec![0usize; n]; - - for (i, instr) in body.iter().enumerate() { - match instr { - Instruction::LocalSet(s) => { - let s = *s as usize; - if s >= params_count { - first_write[s] = first_write[s].min(i); - } - } - Instruction::LocalGet(s) => { - let s = *s as usize; - if s >= params_count { - last_read[s] = last_read[s].max(i); - } - } - Instruction::LocalTee(s) => { - let s = *s as usize; - if s >= params_count { - first_write[s] = first_write[s].min(i); - last_read[s] = last_read[s].max(i); - } - } - _ => {} - } - } - - // A `[first_write, last_read]` window computed from flat textual positions - // is only valid for straight-line code: it implicitly assumes every - // instruction executes at most once. `Loop` is the one construct that - // breaks that assumption — its body is a single textual span that - // actually runs many times, via a back-edge (`Br`) to the `Loop` - // instruction itself. A slot written once before the loop and read once - // inside it gets `last_read` pinned to that first textual occurrence, - // even though the same read recurs on every later iteration. If some - // other slot's write/read pair falls entirely after that position but - // still inside the loop body, the ranges look non-overlapping and the - // allocator happily hands them the same physical local — which then - // gets clobbered by the second slot's write before the first slot's - // value is read again on the next iteration. - // - // Fix: widen every slot touched anywhere inside a loop so its range - // covers that loop's entire `[Loop, End]` span (and transitively every - // loop it's nested in, since the same argument applies at each nesting - // level). Two slots both live inside the same loop then always overlap - // and can never be coalesced together, which is what correctness under - // repeated execution requires. - let mut frame_starts: Vec = Vec::new(); - let mut loop_spans: Vec<(usize, usize)> = Vec::new(); - for (i, instr) in body.iter().enumerate() { - match instr { - Instruction::Block { .. } | Instruction::If { .. } => { - frame_starts.push(usize::MAX); - } - Instruction::Loop { .. } => { - frame_starts.push(i); - } - Instruction::End => { - if let Some(start) = frame_starts.pop() { - if start != usize::MAX { - loop_spans.push((start, i)); - } - } - } - _ => {} - } - } - - for &(ls, le) in &loop_spans { - for instr in &body[ls..=le] { - let s = match instr { - Instruction::LocalGet(s) - | Instruction::LocalSet(s) - | Instruction::LocalTee(s) => *s as usize, - _ => continue, - }; - if s >= params_count { - first_write[s] = first_write[s].min(ls); - last_read[s] = last_read[s].max(le); - } - } - } - - // Normalize: dead stores (written, never read) collapse to a point range. - // Slots never written (shouldn't normally happen) get range [0, 0]. - for s in params_count..n { - if first_write[s] == usize::MAX { - first_write[s] = 0; - } - if last_read[s] < first_write[s] { - last_read[s] = first_write[s]; - } - } - - // ── Step 2: linear scan ────────────────────────────────────────────────── - let mut order: Vec = (params_count..n).collect(); - order.sort_unstable_by_key(|&s| first_write[s]); - - let ty_idx = |ty: ScalarType| match ty { - ScalarType::I32 => 0usize, - ScalarType::I64 => 1, - ScalarType::F32 => 2, - ScalarType::F64 => 3, - }; - - // Per-type free lists of slot numbers available for reuse. - let mut free: [Vec; 4] = - [Vec::new(), Vec::new(), Vec::new(), Vec::new()]; - // Active intervals: (last_read, new_slot_number, type_index). - let mut active: Vec<(usize, u32, usize)> = Vec::new(); - let mut next_slot = params_count as u32; - let mut mapping = vec![0u32; n]; - for (i, slot) in mapping.iter_mut().enumerate().take(params_count) { - *slot = i as u32; - } - - for old in order { - let start = first_write[old]; - let end = last_read[old]; - let ti = ty_idx(locals[old].ty); - - // Expire intervals that ended strictly before this one starts. - let mut i = 0; - while i < active.len() { - if active[i].0 < start { - let (_, freed, freed_ti) = active.swap_remove(i); - free[freed_ti].push(freed); - } else { - i += 1; - } - } - - let new_slot = free[ti].pop().unwrap_or_else(|| { - let s = next_slot; - next_slot += 1; - s - }); - - mapping[old] = new_slot; - active.push((end, new_slot, ti)); - } - - // ── Step 3: rebuild locals and rewrite instructions ────────────────────── - let new_spill_count = (next_slot - params_count as u32) as usize; - let mut spill_types = vec![ScalarType::I32; new_spill_count]; - for old in params_count..n { - let new = mapping[old] as usize; - if new >= params_count { - spill_types[new - params_count] = locals[old].ty; - } - } - locals.truncate(params_count); - locals.extend(spill_types.into_iter().map(|ty| Local { ty })); - - for instr in body.iter_mut() { - match instr { - Instruction::LocalGet(s) - | Instruction::LocalSet(s) - | Instruction::LocalTee(s) => { - *s = mapping[*s as usize]; - } - _ => {} - } - } -} - -/// Replace `LocalSet(n), LocalGet(n)` pairs with a single `LocalTee(n)`, -/// and eliminate `LocalTee(n), LocalSet(n)` by dropping the redundant tee. -/// -/// `local.tee` writes the top-of-stack value to the local *and* leaves a copy -/// on the stack, which is exactly what set+get does in two instructions. -/// Conversely, `local.tee(n)` immediately followed by `local.set(n)` writes -/// the same value to the local twice — the tee is redundant; a plain set suffices. -fn peephole_local_tee(body: &mut Vec) { - let mut out = Vec::with_capacity(body.len()); - let mut i = 0; - while i < body.len() { - if let Instruction::LocalSet(s) = body[i] { - // set(N), get(N), set(N) → set(N): the middle get feeds back into a set - // of the same slot, so both the tee and the redundant write collapse. - if i + 2 < body.len() { - if let (Instruction::LocalGet(g), Instruction::LocalSet(s2)) = - (&body[i + 1], &body[i + 2]) - { - if *g == s && *s2 == s { - out.push(Instruction::LocalSet(s)); - i += 3; - continue; - } - } - } - // set(N), get(N) → tee(N) - if i + 1 < body.len() { - if let Instruction::LocalGet(g) = body[i + 1] { - if s == g { - out.push(Instruction::LocalTee(s)); - i += 2; - continue; - } - } - } - } - out.push(body[i].clone()); - i += 1; - } - *body = out; -} diff --git a/crates/wx-compiler/src/opt/snapshots/wx_compiler__opt__tests__snapshot_sched_call_two_args.snap b/crates/wx-compiler/src/opt/snapshots/wx_compiler__opt__tests__snapshot_sched_call_two_args.snap index ce25037..6c41ae5 100644 --- a/crates/wx-compiler/src/opt/snapshots/wx_compiler__opt__tests__snapshot_sched_call_two_args.snap +++ b/crates/wx-compiler/src/opt/snapshots/wx_compiler__opt__tests__snapshot_sched_call_two_args.snap @@ -11,3 +11,6 @@ body: - LocalGet: 1 - Call: 107 - LocalTee: 2 +br_table_depths: [] +spans: [] +locals_debug: [] diff --git a/crates/wx-compiler/src/opt/snapshots/wx_compiler__opt__tests__snapshot_sched_cse.snap b/crates/wx-compiler/src/opt/snapshots/wx_compiler__opt__tests__snapshot_sched_cse.snap index 5b72909..474110a 100644 --- a/crates/wx-compiler/src/opt/snapshots/wx_compiler__opt__tests__snapshot_sched_cse.snap +++ b/crates/wx-compiler/src/opt/snapshots/wx_compiler__opt__tests__snapshot_sched_cse.snap @@ -12,3 +12,6 @@ body: - LocalTee: 1 - LocalGet: 1 - I32Add +br_table_depths: [] +spans: [] +locals_debug: [] diff --git a/crates/wx-compiler/src/opt/tests.rs b/crates/wx-compiler/src/opt/tests.rs index fcbd47b..932353b 100644 --- a/crates/wx-compiler/src/opt/tests.rs +++ b/crates/wx-compiler/src/opt/tests.rs @@ -13,8 +13,9 @@ use indoc::indoc; use crate::mir::{self, MIR}; use crate::opt::builder::Builder; -use crate::opt::scheduler::{Instruction, Scheduler}; -use crate::opt::{ControlNode, DataNodeKind, ScalarType, StackResult}; +use crate::opt::scheduler::Scheduler; +use crate::opt::{ControlNode, DataNodeKind, StackResult}; +use crate::wasm::{Instruction, ScalarType}; use crate::{tir, vfs}; /// Minimal stdlib definitions required for memory / pointer tests. @@ -42,7 +43,7 @@ impl TestCase { Scheduler::schedule(&opt, &self.mir).body } - fn schedule_full(&self) -> crate::opt::scheduler::ScheduledFunction { + fn schedule_full(&self) -> crate::wasm::Function { let func_mir = self.get_first_func(); let opt = Builder::build(&self.mir, func_mir); Scheduler::schedule(&opt, &self.mir) @@ -65,7 +66,16 @@ impl TestCase { .unwrap(); let mut graph = builder.build(root_id, stdlib_id); let tir = tir::TIR::build(&mut graph); - let mir = MIR::build(&tir, &graph.interner, graph.id_generator); + let mut mir = MIR::build( + &tir, + &graph.interner, + graph.id_generator, + crate::CompilationMode::Release, + ); + let mut call_graph = + mir::CallGraph::build(&mir.functions, &mir.call_edges); + mir.inline_calls(&mut call_graph); + mir.dead_code_eliminate(&call_graph); TestCase { mir } } @@ -566,7 +576,7 @@ fn test_sched_if_else() { matches!( body[ip], Instruction::If { - ty: crate::opt::scheduler::BlockType::Empty + ty: crate::wasm::BlockType::Empty } ), "If block type should be Empty when phi outputs exist; got {:?}", @@ -1234,7 +1244,7 @@ fn test_sched_if_else_phi_stores() { matches!( body[if_pos], Instruction::If { - ty: crate::opt::scheduler::BlockType::Empty + ty: crate::wasm::BlockType::Empty } ), "If block type should be Empty when phi stores are used; got {:?}", @@ -2306,7 +2316,9 @@ fn test_match_schedules_nested_if_else_chain() { assert_eq!(else_count, 2, "one `else` per real case; got: {body:?}"); assert_eq!(eq_count, 2, "one comparison per real case; got: {body:?}"); assert!( - !body.iter().any(|i| matches!(i, Instruction::BrTable(_))), + !body + .iter() + .any(|i| matches!(i, Instruction::BrTable { .. })), "below the br_table threshold, must not emit one; got: {body:?}" ); } @@ -2334,11 +2346,15 @@ fn test_match_schedules_br_table_for_dense_cases() { } export { classify } "}); - let body = case.schedule(); - let br_tables: Vec<_> = body + let sched = case.schedule_full(); + let body = &sched.body; + let br_tables: Vec<&[u32]> = body .iter() .filter_map(|i| match i { - Instruction::BrTable(depths) => Some(depths), + Instruction::BrTable { start, len } => Some( + &sched.br_table_depths + [*start as usize..(*start + *len) as usize], + ), _ => None, }) .collect(); @@ -2348,7 +2364,7 @@ fn test_match_schedules_br_table_for_dense_cases() { "expected exactly one br_table; got: {body:?}" ); assert_eq!( - br_tables[0].as_ref(), + br_tables[0], [0, 1, 2, 3], "depths must be per-case array position, default (== case_count) trailing" ); diff --git a/crates/wx-compiler/src/vfs/mod.rs b/crates/wx-compiler/src/vfs/mod.rs index 495fc14..71a1fd6 100644 --- a/crates/wx-compiler/src/vfs/mod.rs +++ b/crates/wx-compiler/src/vfs/mod.rs @@ -129,7 +129,7 @@ impl File { } #[cfg_attr(debug_assertions, derive(Debug))] -#[derive(Copy, Clone, PartialEq, Eq, serde::Serialize)] +#[derive(Copy, Clone, PartialEq, Eq, Hash, serde::Serialize)] pub struct FileId(u32); #[cfg_attr(test, derive(serde::Serialize))] diff --git a/crates/wx-compiler/src/wasm/mod.rs b/crates/wx-compiler/src/wasm/mod.rs new file mode 100644 index 0000000..7ba3470 --- /dev/null +++ b/crates/wx-compiler/src/wasm/mod.rs @@ -0,0 +1,569 @@ +//! The WASM-shaped instruction representation every lowering pass targets — +//! a flat, linear sequence of instructions for a stack machine, as opposed +//! to the tree (`mir::Function`) or graph (`opt::Function`) representations +//! earlier in the pipeline. Two independent processes build a [`Function`]: +//! `opt::scheduler` (from the sea-of-nodes graph) and, eventually, +//! `mir::scheduler` (directly from MIR). Neither owns these types — they're +//! the shared contract both target and `codegen` consumes — which is also +//! why the few optimization passes that operate purely on this +//! representation ([`coalesce_locals`], [`peephole_local_tee`]) live here +//! rather than inside either producer. + +use string_interner::symbol::SymbolU32; + +use crate::ast; +use crate::mir; + +#[cfg(test)] +mod tests; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(test, derive(serde::Serialize))] +pub enum ScalarType { + I32, + I64, + F32, + F64, +} + +impl TryFrom for ScalarType { + type Error = (); + fn try_from(ty: mir::Type) -> Result { + Ok(match ty { + mir::Type::I32 + | mir::Type::U32 + | mir::Type::Bool + | mir::Type::U8 + | mir::Type::I8 + | mir::Type::U16 + | mir::Type::I16 + | mir::Type::Function { .. } => ScalarType::I32, + mir::Type::I64 | mir::Type::U64 => ScalarType::I64, + mir::Type::Pointer { kind, .. } => match kind { + mir::MemoryKind::Memory32 => ScalarType::I32, + mir::MemoryKind::Memory64 => ScalarType::I64, + }, + mir::Type::F32 => ScalarType::F32, + mir::Type::F64 => ScalarType::F64, + _ => return Err(()), + }) + } +} + +/// Recursively flatten a MIR type into its constituent WASM scalar types — +/// the `ScalarType`-producing counterpart to `mir::flatten_type` (which +/// stops at MIR leaf types). The one place this conversion happens; every +/// producer of a `Function` should call this rather than repeating the +/// `mir::flatten_type` + `ScalarType::try_from` pair itself. +pub fn flatten_type_to_scalars( + ty: mir::Type, + aggregates: &[mir::Aggregate], +) -> Vec { + mir::flatten_type(ty, aggregates) + .into_iter() + .map(|t| ScalarType::try_from(t).expect("must be scalar")) + .collect() +} + +/// The `memarg` immediate carried by every WebAssembly memory instruction. +#[cfg_attr(test, derive(Debug, PartialEq, serde::Serialize))] +#[derive(Clone, Copy)] +pub struct MemArg { + /// Log2 of the alignment hint in bytes (e.g. 2 = 4-byte aligned). + pub align: u32, + /// Static byte offset added to the runtime address. + pub offset: u32, + pub memory: crate::ast::DefId, +} + +/// A WASM local variable declaration. +#[cfg_attr(test, derive(serde::Serialize))] +#[cfg_attr(test, serde(transparent))] +#[derive(Clone, Copy)] +pub struct Local { + pub ty: ScalarType, +} + +/// Debug-only metadata for one source-declared local, resolved to the wasm +/// local slot(s) it actually occupies — the wasm-scheduling-side +/// counterpart to `mir::LocalDebugInfo` (name and, for a struct-typed +/// local, its field names), plus the `wasm_local_start`/`wasm_local_count` +/// range only scheduling knows. Always empty for `opt::scheduler`'s output, +/// same as `Function::spans` — see there for why. +#[cfg_attr(test, derive(serde::Serialize))] +#[derive(Clone)] +pub struct LocalDebugInfo { + pub name: SymbolU32, + /// The local's MIR type — flattening it (`mir::flatten_type`) recovers + /// each leaf's byte size for `DW_OP_piece` chains, and it's also what a + /// type-DIE builder walks for struct member types. Not derivable from + /// `wasm_local_count` alone (that's just a scalar count, not enough to + /// reconstruct e.g. a struct's field types). + pub ty: mir::Type, + pub struct_debug: Option, + pub wasm_local_start: u32, + pub wasm_local_count: u32, +} + +#[cfg_attr(test, derive(serde::Serialize))] +pub struct Function { + /// Locals in declaration order (params first, then spill slots). + pub locals: Vec, + /// Flat WASM stack-machine instruction sequence for the function body. + pub body: Vec, + /// Flat arena backing every [`Instruction::BrTable`] in `body` — each + /// instruction stores a `(start, len)` range into this `Vec` instead of + /// its own heap allocation, so `br_table`-heavy functions don't pay one + /// allocation per switch. + pub br_table_depths: Vec, + /// Source span for `body[i]`, one-to-one with `body` — empty for + /// `opt::scheduler`'s output, since CSE/scheduling there means an + /// instruction no longer corresponds to a single source position. + /// Populated by `mir::scheduler`, whose no-reordering guarantee is + /// exactly what makes a per-instruction span meaningful. + pub spans: Vec, + /// One entry per named local actually emitted — empty for + /// `opt::scheduler`'s output; see [`LocalDebugInfo`]. + pub locals_debug: Vec, +} + +/// A subset of WASM instructions produced by a scheduling pass. +/// Each variant maps 1-to-1 to a WASM opcode; operands are pushed onto the +/// implicit value stack by the preceding instructions. +#[cfg_attr(test, derive(Debug, PartialEq, serde::Serialize))] +#[derive(Clone)] +pub enum Instruction { + // Constants + I32Const(i32), + I64Const(i64), + F32Const(f32), + F64Const(f64), + // Locals + LocalGet(u32), + LocalSet(u32), + LocalTee(u32), + // Globals + GlobalGet(crate::ast::DefId), + GlobalSet(crate::ast::DefId), + // Arithmetic — i32 + I32Add, + I32Sub, + I32Mul, + I32DivS, + I32DivU, + I32RemS, + I32RemU, + I32And, + I32Or, + I32Xor, + I32Shl, + I32ShrS, + I32ShrU, + I32Eqz, + I32Eq, + I32Ne, + I32LtS, + I32LtU, + I32LeS, + I32LeU, + I32GtS, + I32GtU, + I32GeS, + I32GeU, + I32Clz, + I32Ctz, + // Arithmetic — i64 + I64Add, + I64Sub, + I64Mul, + I64DivS, + I64DivU, + I64RemS, + I64RemU, + I64And, + I64Or, + I64Xor, + I64Shl, + I64ShrS, + I64ShrU, + I64Eqz, + I64Eq, + I64Ne, + I64LtS, + I64LtU, + I64LeS, + I64LeU, + I64GtS, + I64GtU, + I64GeS, + I64GeU, + // Arithmetic — f32 / f64 + F32Add, + F32Sub, + F32Mul, + F32Div, + F32Neg, + F32Sqrt, + F32Abs, + F32Floor, + F32Ceil, + F64Add, + F64Sub, + F64Mul, + F64Div, + F64Neg, + F64Sqrt, + F64Abs, + F64Floor, + F64Ceil, + F32Eq, + F32Ne, + F32Lt, + F32Le, + F32Gt, + F32Ge, + F64Eq, + F64Ne, + F64Lt, + F64Le, + F64Gt, + F64Ge, + // Control flow + Block { + ty: BlockType, + }, + Loop { + ty: BlockType, + }, + If { + ty: BlockType, + }, + Else, + End, + Br(u32), // break by depth + BrIf(u32), + /// `[start, start + len)` range into [`Function::br_table_depths`]. + /// All entries but the last are the concrete per-case branch depths; the + /// last entry is the default branch depth. + BrTable { + start: u32, + len: u32, + }, + Return, + Unreachable, + Drop, + // Calls + /// Direct call; the encoder resolves the WASM function index from + /// `func_wasm_index`, covering both internal and imported functions. + Call(crate::ast::DefId), + /// Indirect call via the function table; the encoder resolves `type_index` + /// from the referenced MIR signature. + CallIndirectSym { + signature_index: mir::SignatureIndex, + }, + // Memory + MemorySize(crate::ast::DefId), + MemoryGrow(crate::ast::DefId), + MemoryFill(crate::ast::DefId), + MemoryCopy { + dst: crate::ast::DefId, + src: crate::ast::DefId, + }, + /// Wasm linear-memory index as an `i32.const`, resolved at codegen. + MemoryIndex { + memory: crate::ast::DefId, + }, + // Pointer load/store + I32Load8S(MemArg), + I32Load8U(MemArg), + I32Load16S(MemArg), + I32Load16U(MemArg), + I32Load(MemArg), + I64Load(MemArg), + F32Load(MemArg), + F64Load(MemArg), + I32Store8(MemArg), + I32Store16(MemArg), + I32Store(MemArg), + I64Store(MemArg), + F32Store(MemArg), + F64Store(MemArg), + // Conversion + I64ExtendI32S, + I64ExtendI32U, + I32WrapI64, + F32ConvertI32S, + F32ConvertI32U, + F32ConvertI64S, + F32ConvertI64U, + F64ConvertI32S, + F64ConvertI32U, + F64ConvertI64S, + F64ConvertI64U, + I32TruncF32S, + I32TruncF32U, + I32TruncF64S, + I32TruncF64U, + I64TruncF32S, + I64TruncF32U, + I64TruncF64S, + I64TruncF64U, + F64PromoteF32, + F32DemoteF64, + // Nop (used as a placeholder) + Nop, + // Symbolic references — resolved to concrete i32.const values by the + // codegen encoder, which has access to the string pool and function table. + /// A function referenced as a value; the encoder pushes it into the + /// function table and emits `i32.const `. + FunctionPointer(crate::ast::DefId), + /// End of the static data section for a given memory (base of writable + /// heap); the encoder emits `i32.const `. + DataSectionEnd { + memory: crate::ast::DefId, + }, + /// A static array; the encoder resolves the index to a byte offset in the + /// data segment and emits `i32.const `. + StaticDataPointer { + data_index: u32, + ty: ScalarType, + }, +} + +#[cfg_attr(test, derive(Debug, PartialEq, serde::Serialize))] +#[derive(Clone, Copy)] +pub enum BlockType { + Empty, + Value(ScalarType), + MultiValue(mir::SignatureIndex), +} + +// ── Local coalescing ────────────────────────────────────────────────────────── + +/// Reuse WASM local slots for spilled values whose live ranges do not overlap. +/// +/// Each spill slot has a live range `[first_write, last_read]` measured in flat +/// instruction-list positions. Two slots of the same WASM type can share a slot +/// number when one range ends strictly before the other begins. +/// +/// Param slots (indices `0..params_count`) are never remapped — WASM passes +/// arguments via the first N locals and the ABI cannot be changed. +pub fn coalesce_locals( + body: &mut [Instruction], + locals: &mut Vec, + params_count: usize, +) { + let n = locals.len(); + if n <= params_count { + return; + } + + // ── Step 1: compute live ranges ────────────────────────────────────────── + let mut first_write = vec![usize::MAX; n]; + let mut last_read = vec![0usize; n]; + + for (i, instr) in body.iter().enumerate() { + match instr { + Instruction::LocalSet(s) => { + let s = *s as usize; + if s >= params_count { + first_write[s] = first_write[s].min(i); + } + } + Instruction::LocalGet(s) => { + let s = *s as usize; + if s >= params_count { + last_read[s] = last_read[s].max(i); + } + } + Instruction::LocalTee(s) => { + let s = *s as usize; + if s >= params_count { + first_write[s] = first_write[s].min(i); + last_read[s] = last_read[s].max(i); + } + } + _ => {} + } + } + + // A `[first_write, last_read]` window computed from flat textual positions + // is only valid for straight-line code: it implicitly assumes every + // instruction executes at most once. `Loop` is the one construct that + // breaks that assumption — its body is a single textual span that + // actually runs many times, via a back-edge (`Br`) to the `Loop` + // instruction itself. A slot written once before the loop and read once + // inside it gets `last_read` pinned to that first textual occurrence, + // even though the same read recurs on every later iteration. If some + // other slot's write/read pair falls entirely after that position but + // still inside the loop body, the ranges look non-overlapping and the + // allocator happily hands them the same physical local — which then + // gets clobbered by the second slot's write before the first slot's + // value is read again on the next iteration. + // + // Fix: widen every slot touched anywhere inside a loop so its range + // covers that loop's entire `[Loop, End]` span (and transitively every + // loop it's nested in, since the same argument applies at each nesting + // level). Two slots both live inside the same loop then always overlap + // and can never be coalesced together, which is what correctness under + // repeated execution requires. + let mut frame_starts: Vec = Vec::new(); + let mut loop_spans: Vec<(usize, usize)> = Vec::new(); + for (i, instr) in body.iter().enumerate() { + match instr { + Instruction::Block { .. } | Instruction::If { .. } => { + frame_starts.push(usize::MAX); + } + Instruction::Loop { .. } => { + frame_starts.push(i); + } + Instruction::End => { + if let Some(start) = frame_starts.pop() { + if start != usize::MAX { + loop_spans.push((start, i)); + } + } + } + _ => {} + } + } + + for &(ls, le) in &loop_spans { + for instr in &body[ls..=le] { + let s = match instr { + Instruction::LocalGet(s) + | Instruction::LocalSet(s) + | Instruction::LocalTee(s) => *s as usize, + _ => continue, + }; + if s >= params_count { + first_write[s] = first_write[s].min(ls); + last_read[s] = last_read[s].max(le); + } + } + } + + // Normalize: dead stores (written, never read) collapse to a point range. + // Slots never written (shouldn't normally happen) get range [0, 0]. + for s in params_count..n { + if first_write[s] == usize::MAX { + first_write[s] = 0; + } + if last_read[s] < first_write[s] { + last_read[s] = first_write[s]; + } + } + + // ── Step 2: linear scan ────────────────────────────────────────────────── + let mut order: Vec = (params_count..n).collect(); + order.sort_unstable_by_key(|&s| first_write[s]); + + let ty_idx = |ty: ScalarType| match ty { + ScalarType::I32 => 0usize, + ScalarType::I64 => 1, + ScalarType::F32 => 2, + ScalarType::F64 => 3, + }; + + // Per-type free lists of slot numbers available for reuse. + let mut free: [Vec; 4] = + [Vec::new(), Vec::new(), Vec::new(), Vec::new()]; + // Active intervals: (last_read, new_slot_number, type_index). + let mut active: Vec<(usize, u32, usize)> = Vec::new(); + let mut next_slot = params_count as u32; + let mut mapping = vec![0u32; n]; + for (i, slot) in mapping.iter_mut().enumerate().take(params_count) { + *slot = i as u32; + } + + for old in order { + let start = first_write[old]; + let end = last_read[old]; + let ti = ty_idx(locals[old].ty); + + // Expire intervals that ended strictly before this one starts. + let mut i = 0; + while i < active.len() { + if active[i].0 < start { + let (_, freed, freed_ti) = active.swap_remove(i); + free[freed_ti].push(freed); + } else { + i += 1; + } + } + + let new_slot = free[ti].pop().unwrap_or_else(|| { + let s = next_slot; + next_slot += 1; + s + }); + + mapping[old] = new_slot; + active.push((end, new_slot, ti)); + } + + // ── Step 3: rebuild locals and rewrite instructions ────────────────────── + let new_spill_count = (next_slot - params_count as u32) as usize; + let mut spill_types = vec![ScalarType::I32; new_spill_count]; + for old in params_count..n { + let new = mapping[old] as usize; + if new >= params_count { + spill_types[new - params_count] = locals[old].ty; + } + } + locals.truncate(params_count); + locals.extend(spill_types.into_iter().map(|ty| Local { ty })); + + for instr in body.iter_mut() { + match instr { + Instruction::LocalGet(s) + | Instruction::LocalSet(s) + | Instruction::LocalTee(s) => { + *s = mapping[*s as usize]; + } + _ => {} + } + } +} + +/// Replace `LocalSet(n), LocalGet(n)` pairs with a single `LocalTee(n)`, +/// and eliminate `LocalTee(n), LocalSet(n)` by dropping the redundant tee. +/// +/// `local.tee` writes the top-of-stack value to the local *and* leaves a copy +/// on the stack, which is exactly what set+get does in two instructions. +/// Conversely, `local.tee(n)` immediately followed by `local.set(n)` writes +/// the same value to the local twice — the tee is redundant; a plain set suffices. +pub fn peephole_local_tee(body: &mut Vec) { + let mut out = Vec::with_capacity(body.len()); + let mut i = 0; + while i < body.len() { + if let Instruction::LocalSet(s) = body[i] { + // set(N), get(N), set(N) → set(N): the middle get feeds back into a set + // of the same slot, so both the tee and the redundant write collapse. + if i + 2 < body.len() { + if let (Instruction::LocalGet(g), Instruction::LocalSet(s2)) = + (&body[i + 1], &body[i + 2]) + { + if *g == s && *s2 == s { + out.push(Instruction::LocalSet(s)); + i += 3; + continue; + } + } + } + // set(N), get(N) → tee(N) + if i + 1 < body.len() { + if let Instruction::LocalGet(g) = body[i + 1] { + if s == g { + out.push(Instruction::LocalTee(s)); + i += 2; + continue; + } + } + } + } + out.push(body[i].clone()); + i += 1; + } + *body = out; +} diff --git a/crates/wx-compiler/src/wasm/tests.rs b/crates/wx-compiler/src/wasm/tests.rs new file mode 100644 index 0000000..3bc0b5a --- /dev/null +++ b/crates/wx-compiler/src/wasm/tests.rs @@ -0,0 +1,15 @@ +use super::*; + +#[test] +fn flatten_type_to_scalars_unit_and_never_produce_no_slots() { + assert_eq!(flatten_type_to_scalars(mir::Type::Unit, &[]), vec![]); + assert_eq!(flatten_type_to_scalars(mir::Type::Never, &[]), vec![]); +} + +#[test] +fn flatten_type_to_scalars_scalar_is_one_slot() { + assert_eq!( + flatten_type_to_scalars(mir::Type::I64, &[]), + vec![ScalarType::I64] + ); +} diff --git a/dwarf-playground/index.html b/dwarf-playground/index.html new file mode 100644 index 0000000..9e85726 --- /dev/null +++ b/dwarf-playground/index.html @@ -0,0 +1,110 @@ + + + + +wx DWARF debugging playground + + + +

wx — DWARF debugging playground

+

+ program.wasm was compiled from program.wx via + wx compile --debug. It has real DWARF5 debug info embedded + directly in it (.debug_info, .debug_line, ...) — + no separate source map, nothing else to load. +

+ +
+ compute(n) — sums magnitude_sq(Vec2{x:i,y:2i}) + i + for i in 0..n, via helper functions add/magnitude_sq. +
+ n = + +   result: +
+
+ +

How to debug it in Chrome DevTools

+
    +
  1. Install the + C/C++ DevTools Support (DWARF) + extension (once, if you don't already have it). +
  2. +
  3. Serve this folder over HTTP — fetch() won't load + program.wasm from a file:// page. From this + directory: +
    python3 -m http.server 8000
    + then open http://localhost:8000/. +
  4. +
  5. Open DevTools (Cmd+Opt+I) → Sources tab. Once the + page loads and instantiates the module, a program.wasm entry + with a file tree icon should appear in the sources list — + that's the DWARF extension recognizing the embedded debug info and + exposing program.wx as an actual source file, not + disassembly. +
  6. +
  7. Open program.wx under that entry, click a line number + inside compute, add, or magnitude_sq + to set a breakpoint (e.g. the total = add(...) line, or + inside magnitude_sq). +
  8. +
  9. Click call compute(n) above. Execution should pause at + your breakpoint, in wx source, with a real call stack across the three + functions and the locals/params (i, total, + v, v.x/v.y, ...) inspectable in + the Scope pane. +
  10. +
+ +

+ If the DWARF extension doesn't pick it up as expected, or the Scope pane + doesn't show real names/values, that's useful signal too — it means + either the extension's expectations differ from what this module provides + (e.g. it may expect an external_debug_info section or a + specific loading path rather than picking up embedded DWARF directly from + WebAssembly.instantiateStreaming), which is exactly the kind + of gap worth knowing about. +

+ +

Recompiling after editing program.wx

+

From the repo root:

+
cargo run -p wx-cli --bin wx -- compile dwarf-playground/program.wx --debug -o dwarf-playground/program.wasm
+ + + + diff --git a/dwarf-playground/program.wasm b/dwarf-playground/program.wasm new file mode 100644 index 0000000..7b8f42e Binary files /dev/null and b/dwarf-playground/program.wasm differ diff --git a/dwarf-playground/program.wx b/dwarf-playground/program.wx new file mode 100644 index 0000000..1de44fb --- /dev/null +++ b/dwarf-playground/program.wx @@ -0,0 +1,28 @@ +struct Vec2 { + x: i32, + y: i32, +} + +fn add(a: i32, b: i32) -> i32 { + local sum = a + b; + sum +} + +fn magnitude_sq(v: Vec2) -> i32 { + v.x * v.x + v.y * v.y +} + +fn compute(n: i32) -> i32 { + local mut total: i32 = 0; + local mut i: i32 = 0; + loop { + if i >= n { break }; + local v: Vec2 = Vec2::{ x: i, y: i * 2 }; + local step = add(magnitude_sq(v), i); + total = add(total, step); + i += 1; + } + total +} + +export { compute } diff --git a/dwarf-playground/wasmtime-dwarf-transform-bug.md b/dwarf-playground/wasmtime-dwarf-transform-bug.md new file mode 100644 index 0000000..f20fbab --- /dev/null +++ b/dwarf-playground/wasmtime-dwarf-transform-bug.md @@ -0,0 +1,342 @@ +# Findings: getting wasmtime + LLDB/CodeLLDB debugging working against wx's DWARF + +## Summary + +Getting `wasmtime run -D debug-info=y` (and, by extension, LLDB/CodeLLDB in +VS Code) working against wx's embedded DWARF5 debug info surfaced four +separate issues, two ours and two wasmtime's: + +1. **Ours (fixed).** Our DWARF encoder omitted `DW_OP_stack_value` after + `DW_OP_WASM_location`, so wasmtime read every scalar local as an address + needing a memory dereference — and hard-crashed on any module with no + declared memory. This was the actual root cause of the original crash. +2. **wasmtime's, still open.** Even granting that a consumer might read our + (pre-fix) output as address-yielding, wasmtime's response to that is + disproportionate (kills the whole `wasmtime run`, not just debug-info + generation) and illegible (a generic gimli error with no indication of + what or why). +3. **wasmtime's, still open, newly found.** Independent of bug 1: any + multi-field struct local (anything using `DW_OP_piece`) still crashes + the same way, but only at `-O opt-level=0` — a `need_deref` tracking bug + in wasmtime's expression parser that our encoding can't work around. +4. **wasmtime's, acknowledged by a maintainer, not really "fixable" by us + or practically by them right now.** Gutter/file-line breakpoints against + wasmtime's JIT-registered debug info never bind in lldb on macOS, even + after the module is loaded. A wasmtime core maintainer has stated native + debugger attachment is "best-effort... more or less unmaintained" and + they're building a replacement rather than continuing to harden this + path. Workaround found: **symbol/function-name breakpoints work fully** + (correct source location, working `frame variable`) — see the setup + guide at the bottom. + +## Bug 1 (ours, fixed): missing `DW_OP_stack_value` + +`crates/wx-compiler/src/dwarf/mod.rs`, `build_location_expr`, used to emit: + +``` +DW_OP_WASM_location 0x00 +``` + +for a scalar local, or a chain of + +``` +DW_OP_WASM_location 0x00 DW_OP_piece +``` + +per flattened field for an aggregate. Neither includes `DW_OP_stack_value`. + +Real producers (LLVM's wasm backend) always terminate one of these +expressions with `DW_OP_stack_value` to mark "the thing I just computed +*is* the value" — and wasmtime's consumer code assumes this. From +`crates/cranelift/src/debug/transform/expression.rs` (wasmtime v46.0.1): + +```rust +while !pc.is_empty() { + ... + need_deref = true; // <- reset to true before every operation + let op = Operation::parse(&mut pc, encoding)?; + match op { + ... + Operation::StackValue => { + need_deref = false; // <- only operation that ever clears it + ... + } + Operation::WasmLocal { index } => { + // no effect on need_deref + ... + } +``` + +So a bare `DW_OP_WASM_location` operation leaves `need_deref == true`, and +later, when building the actual location list/exprloc bytes, that flag +triggers a call to `append_memory_deref`, which needs to know how to compute +the module's linear-memory base pointer from `vmctx`. If the module has no +memory, that's `ModuleMemoryOffset::None`, and: + +```rust +// crates/cranelift/src/debug/transform/expression.rs +ModuleMemoryOffset::None => return Err(write::Error::InvalidAttributeValue), +``` + +— which is the exact error text we saw, propagated up through +`wasmtime_environ::error::Error`'s `Caused by:` chain. + +**Fix applied**: `build_location_expr` now appends `DW_OP_stack_value` +(`0x9f`) after every `DW_OP_WASM_location` — once for a scalar, and once per +piece for an aggregate (right before that piece's `DW_OP_piece`). +Regression test: `dwarf::tests::wasmtime_accepts_our_debug_info_for_a_module_with_no_memory` +(deliberately declares no `memory` block, to pin the real fix rather than +the memory-declaration workaround). Verified end-to-end against the real +`wx` CLI + `wasmtime` binary too — `dwarf-playground/program.wx` (which +declares no memory) runs clean under +`wasmtime run -D debug-info=y --invoke compute program.wasm 5`. + +## Bug 2 (wasmtime's, still open): disproportionate, illegible failure — not "rejecting is wrong" + +To be precise about what's actually wrong here, since "reject bad input" is +a perfectly legitimate design choice and arguably *better* than silently +limping along on it: the problem isn't that wasmtime rejects our (pre-fix) +input. It's two separable things, both independent of whether rejecting is +the right call in the abstract: + +1. **Blast radius.** `-D debug-info=y` / `Config::debug_info(true)` is + opt-in tooling layered on top of running the module — it's not required + for correctness of execution. When wasmtime can't honor it for some + input, the current behavior kills the *entire* `wasmtime run` + invocation: the module doesn't run at all, not even without debug info. + That's disproportionate to what failed. A narrower failure — refuse to + attach debug info (for the one variable, the one function, or worst + case the whole module) but still execute the wasm — would still be + "rejecting the bad input," just scoped to the thing that's actually + broken. +2. **Diagnostic quality.** Even a hard, whole-module rejection would be + defensible if it told you *why*. What you actually get is a generic + gimli string — + `Caused by: The attribute value is an invalid for writing.` — with no + DIE offset, no attribute name, no mention of "no memory declared," + nothing pointing at the actual cause. + +For context on how wasmtime handles similar situations elsewhere: the same +file (`crates/cranelift/src/debug/transform/attr.rs`) has a fallback path +for other malformed attributes that logs and skips rather than aborting: + +```rust +// No other attributes contain addresses or address offsets. +_ => match convert_unit.convert_attribute_value(unit, attr, &|_| None) { + Ok(value) => value, + Err(e) => { + // Invalid `FileIndex` was seen in #8884 and #8904. In general it's + // better to ignore invalid or unknown DWARF rather then failing outright. + dbi_log!(...); + continue; + } +}, +``` + +That's one legitimate way to fix the blast-radius problem (scope the +failure to the one attribute), but it's not the only one — a louder, +scoped rejection (skip debug info for the module, print a real diagnostic, +still run the wasm) would address both problems above just as well without +taking a position on "should invalid DWARF be silently dropped," which is +a separate, more debatable question. + +## Bug 3 (wasmtime's, still open, newly found): `DW_OP_piece` doesn't reset `need_deref` + +Even with bug 1 fixed, a **struct-typed local still crashes wasmtime the +same way**, but only under `-O opt-level=0`. Reproduced via +`dwarf-playground/program.wx` (has a `Vec2` struct param/local): + +``` +wasmtime run -D debug-info=y --invoke compute program.wasm 5 # works +wasmtime run -D debug-info=y -O opt-level=0 --invoke compute program.wasm 5 # crashes, + # same "attribute value is invalid for writing" +``` + +A scalar-only program (no structs) works fine at `-O opt-level=0`, so this +is specifically about `DW_OP_piece`-composite locations. + +Root cause (same file, `expression.rs`): `need_deref` is a single flag for +the *whole* expression, computed from whichever operation was parsed last: + +```rust +while !pc.is_empty() { + ... + need_deref = true; // reset unconditionally, every iteration + let op = Operation::parse(&mut pc, encoding)?; + match op { + ... + Operation::StackValue => { need_deref = false; ... } + ... + Operation::Piece { .. } => (), // <- grouped with inert arithmetic + // ops; doesn't touch need_deref +``` + +and later, unconditionally at the end of building the expression: + +```rust +if self.need_deref { + ranges_builder.process_label(vmctx_label); + ... + deref!(); // -> append_memory_deref -> same ModuleMemoryOffset::None crash +} +``` + +Our struct-local expression is `[WasmLocal, StackValue, Piece, WasmLocal, +StackValue, Piece]`. The *last* operation processed is `Piece`, which +doesn't reset `need_deref` — so despite every individual piece correctly +being marked with `DW_OP_stack_value`, the expression-wide flag comes out +`true` and wasmtime tries to memory-dereference the whole thing anyway. + +This isn't something we can encode around: `DW_OP_piece` is a structural +marker (not a value-computing op), and any spec-legal multi-piece location +description built from register/stack values (not just ours) will end on a +`Piece`, not a `StackValue`. The real fix has to be on wasmtime's side — +either treat `Operation::Piece` as resetting/tracking `need_deref` +per-piece (mirroring how it already tracks per-`Local` `trailing` state), +or stop taking the whole-expression flag from "whatever the last op +happened to be." + +**Workaround for now**: don't pass `-O opt-level=0` when the module may +have struct locals (i.e. always, unless you know the program is +scalar-only). Default opt level works cleanly; the tradeoff is coarser +local-variable visibility (values are more likely to show "optimized out" +near a function's start). + +## Bug 4 (wasmtime's, acknowledged by a maintainer): gutter breakpoints never bind on macOS lldb + +Confirmed directly, with the fix from bug 1 applied and without `-O +opt-level=0` (bugs 2/3 out of the way): + +- `wasmtime run -D debug-info=y --invoke compute program.wasm 5` runs + clean. +- The GDB/LLDB JIT interface *is* working: `target modules lookup -f + program.wx -l 7` and `target modules lookup -n compute`, run interactively + in lldb once the JIT module is loaded, both resolve correctly to real + addresses inside the JIT-registered module. +- But `breakpoint set --file program.wx --line 7` — the exact mechanism VS + Code's gutter/line breakpoints compile down to — stays "no locations + (pending)" forever, **even when set after the module is already loaded** + (confirmed by setting it only after stopping at a breakpoint on + `__jit_debug_register_code`, at which point the module was already + visible in `image list`). +- `breakpoint set --name compute` (symbol/function-name based), by + contrast, **does** bind and stop correctly — reports the right file + (`program.wx`) and line, shows source context, and `frame variable` + returns real values for locals (arguments right at function entry can + show "optimized out," which is normal debugger behavior, not specific to + this). + +This matches an existing, unrelated wasmtime issue +([#11830](https://github.com/bytecodealliance/wasmtime/issues/11830), +"Couldn't materialize any expression on macOS with LLDB"), where a +different symptom on the same subsystem got this reply from wasmtime core +maintainer `cfallin`: + +> "our native-debugger support has been quite best-effort, and is more or +> less unmaintained at the moment. The DWARF translation has had known +> bugs, and we know that various aspects of our lowering can cause values +> to become unavailable... I wouldn't recommend relying on debugging by +> attaching a native debugger to wasmtime at the moment." + +They also mention actively working on "guest debugging" as a *replacement* +for native-debugger attach, not a fix to the current path. A different +contributor also noted they could **not** reproduce a related crash on +Linux with `lldb-20`, suggesting the macOS lldb + wasmtime JIT interface +combination specifically is the weaker one — consistent with what we found. + +**Practical takeaway**: this one probably isn't worth chasing upstream +right now — it's a known, maintainer-acknowledged gap on a path they're +already planning to replace, not a bug nobody's aware of. The workaround +(function-name breakpoints) is fully usable in the meantime. + +## Existing mentions upstream, for bugs 1-3 + +Searched `bytecodealliance/wasmtime` issues and PRs (via `gh search +issues`/`gh search prs`) for: `ModuleMemoryOffset`, `InvalidAttributeValue`, +`"attribute value is an invalid for writing"`, `"no memory" debug`, `vmctx +debug panic`. No existing issue matches bugs 1-3 (the crash cases) exactly. + +Related, but not the same: + +- **[#894](https://github.com/bytecodealliance/wasmtime/issues/894) / + [#896](https://github.com/bytecodealliance/wasmtime/issues/896)** — the + original author of this code flagged that `ModuleMemoryOffset::Imported` + handling was "temporarily stubbed" and incomplete. Same subsystem, same + file, but about *imported* memories specifically, not the *zero-memories* + case, and doesn't mention a crash. +- **[#5537](https://github.com/bytecodealliance/wasmtime/issues/5537)** — + "Reimplement Wasmtime's DWARF transform and debugging support", the + general tracking issue acknowledging this whole subsystem is + known-incomplete. Bugs 1-3 would be data points for that issue, not + duplicates of it. +- **[#887](https://github.com/bytecodealliance/wasmtime/issues/887)** — + "Assertion failure generating debuginfo on a module with no functions" — + same *flavor* of problem (an edge-case module shape crashing debug-info + generation), different trigger, already fixed. +- **[#11830](https://github.com/bytecodealliance/wasmtime/issues/11830)** — + the macOS-lldb issue quoted above for bug 4. + +## Could we help fix bugs 1-3? How hard would it be? + +Yes, these look like reasonable upstream contributions: + +- **Root-causing them** (what took most of the effort here): genuinely + hard without local reproduction — the error message is generic and + raised one level removed from the actual cause (inside gimli's `write` + module). Confirming it required building a small local repro against the + `wasmtime` crate directly (with `RUST_LOG=debug-info-transform=trace` on + a debug build) to watch exactly which DIE/attribute/operation it died on, + for both bug 1 and bug 3 separately. +- **The fixes themselves, once found: easy — for bug 1/2.** A single, + localized change in `attr.rs`, following an idiom already in the same + file (catch the error, degrade instead of hard-propagating). +- **Bug 3 is a bit more involved**: it needs `Operation::Piece` to actually + participate in `need_deref` tracking (or the whole-expression flag + approach reworked to be piece-aware), which touches the core parsing + loop rather than just an error-handling call site — still localized to + one function, but requires understanding the per-piece semantics rather + than just adding a `continue`. +- **Testing: easy for all three.** Minimal repros are small `.wat`/2-line + wx programs; we already have equivalent `wx-compiler` unit tests using + the `wasmtime` crate directly that could be adapted. + +If you want, I can draft the actual wasmtime patches + minimal `.wat` +regression tests and open the issues/PRs, but that's a call for you to +make given it involves posting to a repo we don't control. + +## Practical setup that works today: wasmtime + CodeLLDB in VS Code + +`.vscode/launch.json` (already added to this repo): + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "wasmtime: debug dwarf-playground/program.wasm", + "type": "lldb", + "request": "launch", + "program": "/opt/homebrew/bin/wasmtime", + "args": ["run", "-D", "debug-info=y", "--invoke", "compute", "program.wasm", "5"], + "cwd": "${workspaceFolder}/dwarf-playground", + "stopOnEntry": false, + "initCommands": ["settings set plugin.jit-loader.gdb.enable on"] + } + ] +} +``` + +To actually hit a breakpoint (given bug 4 above): + +1. Open the **Run and Debug** panel, open the **Breakpoints** section, click + the **+** ("Add Function Breakpoint") button — *not* a gutter click in + `program.wx`. +2. Type a function name, e.g. `compute` or `add`. +3. Press F5 (or select this launch config and start debugging). +4. It should stop inside the named function, with correct source location + and working variable inspection (`frame variable` / hover / Watch). + +Gutter/line breakpoints in `program.wx` will show as set but will never +actually bind or stop execution (bug 4) — this is expected given the +current state of wasmtime's native-debugger support, not something to +debug further on our end.