diff --git a/crates/rue-air/src/inference.rs b/crates/rue-air/src/inference.rs index be63f6ec..aef2369e 100644 --- a/crates/rue-air/src/inference.rs +++ b/crates/rue-air/src/inference.rs @@ -1549,9 +1549,16 @@ impl<'a> ConstraintGenerator<'a> { } // Type declarations don't produce values - InstData::FnDecl { .. } | InstData::StructDecl { .. } | InstData::EnumDecl { .. } => { - InferType::Concrete(Type::Unit) - } + InstData::FnDecl { .. } + | InstData::StructDecl { .. } + | InstData::EnumDecl { .. } + | InstData::ImplDecl { .. } => InferType::Concrete(Type::Unit), + + // Method call - placeholder for Phase 3 (see ADR-0009) + InstData::MethodCall { .. } => InferType::Concrete(Type::Unit), + + // Associated function call - placeholder for Phase 3 (see ADR-0009) + InstData::AssocFnCall { .. } => InferType::Concrete(Type::Unit), }; // Record the type for this expression diff --git a/crates/rue-air/src/inst.rs b/crates/rue-air/src/inst.rs index 0002baa2..c8e0e46d 100644 --- a/crates/rue-air/src/inst.rs +++ b/crates/rue-air/src/inst.rs @@ -334,6 +334,15 @@ pub enum AirInstData { /// The variant index (0-based) variant_index: u32, }, + + // Drop/destructor operations + /// Drop a value, running its destructor if the type has one. + /// For trivially droppable types, this is a no-op. + /// The type is stored in the AirInst.ty field. + Drop { + /// The value to drop + value: AirRef, + }, } impl fmt::Display for AirRef { @@ -526,6 +535,9 @@ impl fmt::Display for Air { } => { writeln!(f, "enum_variant #{}::{}", enum_id.0, variant_index)?; } + AirInstData::Drop { value } => { + writeln!(f, "drop {}", value)?; + } } } writeln!(f, "}}") diff --git a/crates/rue-air/src/sema.rs b/crates/rue-air/src/sema.rs index 3b1b250b..45fcfb51 100644 --- a/crates/rue-air/src/sema.rs +++ b/crates/rue-air/src/sema.rs @@ -2600,6 +2600,37 @@ impl<'a> Sema<'a> { }); Ok(AnalysisResult::new(air_ref, ty)) } + + // Impl block declarations are processed during collection phase, skip here + InstData::ImplDecl { .. } => { + // Return Unit - impl blocks don't produce a value + let air_ref = air.add_inst(AirInst { + data: AirInstData::UnitConst, + ty: Type::Unit, + span: inst.span, + }); + Ok(AnalysisResult::new(air_ref, Type::Unit)) + } + + // Method call - placeholder for Phase 3 (see ADR-0009) + InstData::MethodCall { .. } => { + let air_ref = air.add_inst(AirInst { + data: AirInstData::UnitConst, + ty: Type::Unit, + span: inst.span, + }); + Ok(AnalysisResult::new(air_ref, Type::Unit)) + } + + // Associated function call - placeholder for Phase 3 (see ADR-0009) + InstData::AssocFnCall { .. } => { + let air_ref = air.add_inst(AirInst { + data: AirInstData::UnitConst, + ty: Type::Unit, + span: inst.span, + }); + Ok(AnalysisResult::new(air_ref, Type::Unit)) + } } } diff --git a/crates/rue-cfg/src/build.rs b/crates/rue-cfg/src/build.rs index de26653f..730d43e5 100644 --- a/crates/rue-cfg/src/build.rs +++ b/crates/rue-cfg/src/build.rs @@ -3,7 +3,7 @@ //! This module converts the structured control flow in AIR (Branch, Loop) //! into explicit basic blocks with terminators. -use rue_air::{Air, AirInstData, AirPattern, AirRef, Type}; +use rue_air::{Air, AirInstData, AirPattern, AirRef, ArrayTypeDef, StructDef, Type}; use rue_error::{CompileWarning, WarningKind}; use crate::CfgOutput; @@ -37,6 +37,10 @@ struct LoopContext { pub struct CfgBuilder<'a> { air: &'a Air, cfg: Cfg, + /// Struct definitions for type queries (e.g., needs_drop) + struct_defs: &'a [StructDef], + /// Array type definitions for type queries + array_types: &'a [ArrayTypeDef], /// Current block we're building current_block: BlockId, /// Stack of loop contexts for nested loops @@ -49,7 +53,17 @@ pub struct CfgBuilder<'a> { impl<'a> CfgBuilder<'a> { /// Build a CFG from AIR, returning the CFG and any warnings. - pub fn build(air: &'a Air, num_locals: u32, num_params: u32, fn_name: &str) -> CfgOutput { + /// + /// The `struct_defs` and `array_types` parameters provide type definitions + /// needed for queries like `type_needs_drop`. + pub fn build( + air: &'a Air, + num_locals: u32, + num_params: u32, + fn_name: &str, + struct_defs: &'a [StructDef], + array_types: &'a [ArrayTypeDef], + ) -> CfgOutput { let mut builder = CfgBuilder { air, cfg: Cfg::new( @@ -58,6 +72,8 @@ impl<'a> CfgBuilder<'a> { num_params, fn_name.to_string(), ), + struct_defs, + array_types, current_block: BlockId(0), loop_stack: Vec::new(), value_cache: vec![None; air.len()], @@ -1192,6 +1208,26 @@ impl<'a> CfgBuilder<'a> { continuation: Continuation::Continues, } } + + AirInstData::Drop { value } => { + // Lower the value to drop + let val = self.lower_inst(*value).value.unwrap(); + let val_ty = self.air.get(*value).ty; + + // Only emit a Drop instruction if the type needs drop. + // For trivially droppable types, this is a no-op. + // We use self.type_needs_drop() which has access to struct/array + // definitions to recursively check if fields need drop. + if self.type_needs_drop(val_ty) { + self.emit(CfgInstData::Drop { value: val }, Type::Unit, span); + } + + // Drop is a statement, produces no value + ExprResult { + value: None, + continuation: Continuation::Continues, + } + } } } @@ -1205,6 +1241,53 @@ impl<'a> CfgBuilder<'a> { fn cache(&mut self, air_ref: AirRef, value: CfgValue) { self.value_cache[air_ref.as_u32() as usize] = Some(value); } + + /// Check if a type needs to be dropped (has a destructor). + /// + /// This method has access to struct and array definitions, allowing it to + /// recursively check if struct fields or array elements need drop. + /// + /// A type needs drop if dropping it requires cleanup actions: + /// - Primitives, bool, unit, never, error, enums: trivially droppable (no) + /// - String: will need drop when mutable strings land (currently no) + /// - Struct: needs drop if any field needs drop + /// - Array: needs drop if element type needs drop + fn type_needs_drop(&self, ty: Type) -> bool { + match ty { + // Primitive types are trivially droppable + Type::I8 + | Type::I16 + | Type::I32 + | Type::I64 + | Type::U8 + | Type::U16 + | Type::U32 + | Type::U64 + | Type::Bool + | Type::Unit + | Type::Never + | Type::Error => false, + + // Enum types are trivially droppable (just discriminant values) + Type::Enum(_) => false, + + // String will need drop when mutable strings land (heap allocation) + // For now, string literals are static and don't need drop + Type::String => false, + + // Struct types need drop if any field needs drop + Type::Struct(struct_id) => { + let struct_def = &self.struct_defs[struct_id.0 as usize]; + struct_def.fields.iter().any(|f| self.type_needs_drop(f.ty)) + } + + // Array types need drop if element type needs drop + Type::Array(array_id) => { + let array_def = &self.array_types[array_id.0 as usize]; + self.type_needs_drop(array_def.element_type) + } + } + } } #[cfg(test)] @@ -1230,7 +1313,15 @@ mod tests { let output = sema.analyze_all().unwrap(); let func = &output.functions[0]; - CfgBuilder::build(&func.air, func.num_locals, func.num_param_slots, &func.name).cfg + CfgBuilder::build( + &func.air, + func.num_locals, + func.num_param_slots, + &func.name, + &output.struct_defs, + &output.array_types, + ) + .cfg } #[test] diff --git a/crates/rue-cfg/src/inst.rs b/crates/rue-cfg/src/inst.rs index eb8c302f..340603bb 100644 --- a/crates/rue-cfg/src/inst.rs +++ b/crates/rue-cfg/src/inst.rs @@ -189,6 +189,13 @@ pub enum CfgInstData { enum_id: EnumId, variant_index: u32, }, + + // Drop/destructor operations + /// Drop a value, running its destructor if the type has one. + /// For trivially droppable types, this is a no-op that will be elided. + Drop { + value: CfgValue, + }, } /// Block terminator - how control leaves a basic block. @@ -673,6 +680,9 @@ impl Cfg { } => { write!(f, "enum_variant #{}::{}", enum_id.0, variant_index) } + CfgInstData::Drop { value } => { + write!(f, "drop {}", value) + } } } } diff --git a/crates/rue-codegen/BUCK b/crates/rue-codegen/BUCK index f2c78a00..d0e9d258 100644 --- a/crates/rue-codegen/BUCK +++ b/crates/rue-codegen/BUCK @@ -4,6 +4,7 @@ rust_library( deps = [ "//crates/rue-air:rue-air", "//crates/rue-cfg:rue-cfg", + "//crates/rue-error:rue-error", "//crates/rue-span:rue-span", ], visibility = ["PUBLIC"], @@ -15,6 +16,7 @@ rust_test( deps = [ "//crates/rue-air:rue-air", "//crates/rue-cfg:rue-cfg", + "//crates/rue-error:rue-error", "//crates/rue-intern:rue-intern", "//crates/rue-lexer:rue-lexer", "//crates/rue-parser:rue-parser", diff --git a/crates/rue-codegen/src/aarch64/cfg_lower.rs b/crates/rue-codegen/src/aarch64/cfg_lower.rs index 806aabad..d2de69ed 100644 --- a/crates/rue-codegen/src/aarch64/cfg_lower.rs +++ b/crates/rue-codegen/src/aarch64/cfg_lower.rs @@ -1770,6 +1770,19 @@ impl<'a> CfgLower<'a> { imm: *variant_index as i64, }); } + + CfgInstData::Drop { value: _ } => { + // Drop instruction - runs destructor if the type needs one. + // The CFG builder already elides Drop for trivially droppable types, + // so reaching here means we need to emit actual cleanup code. + // + // This will be implemented in Phase 3 (rue-wjha.9) when we add + // types with destructors (e.g., mutable strings). + debug_assert!( + false, + "Drop instruction reached codegen but no types currently need drop" + ); + } } } @@ -2628,8 +2641,14 @@ mod tests { let struct_defs = &output.struct_defs; let array_types = &output.array_types; let strings = &output.strings; - let cfg_output = - CfgBuilder::build(&func.air, func.num_locals, func.num_param_slots, &func.name); + let cfg_output = CfgBuilder::build( + &func.air, + func.num_locals, + func.num_param_slots, + &func.name, + struct_defs, + array_types, + ); CfgLower::new(&cfg_output.cfg, struct_defs, array_types, strings).lower() } diff --git a/crates/rue-codegen/src/aarch64/mod.rs b/crates/rue-codegen/src/aarch64/mod.rs index 15ad9d04..895736b2 100644 --- a/crates/rue-codegen/src/aarch64/mod.rs +++ b/crates/rue-codegen/src/aarch64/mod.rs @@ -24,6 +24,7 @@ pub use regalloc::RegAlloc; use rue_air::{ArrayTypeDef, StructDef}; use rue_cfg::Cfg; +use rue_error::CompileResult; use crate::MachineCode; @@ -35,7 +36,7 @@ pub fn generate( struct_defs: &[StructDef], array_types: &[ArrayTypeDef], strings: &[String], -) -> MachineCode { +) -> CompileResult { let num_locals = cfg.num_locals(); let num_params = cfg.num_params(); @@ -45,16 +46,16 @@ pub fn generate( // Allocate physical registers let existing_slots = num_locals + num_params; let (mir, num_spills, used_callee_saved) = - RegAlloc::new(mir, existing_slots).allocate_with_spills(); + RegAlloc::new(mir, existing_slots).allocate_with_spills()?; // Emit machine code bytes let total_locals = num_locals + num_spills; let (code, relocations) = Emitter::new(&mir, total_locals, num_params, &used_callee_saved, strings).emit(); - MachineCode { + Ok(MachineCode { code, relocations, strings: strings.to_vec(), - } + }) } diff --git a/crates/rue-codegen/src/aarch64/regalloc.rs b/crates/rue-codegen/src/aarch64/regalloc.rs index dae9beb6..17aac92f 100644 --- a/crates/rue-codegen/src/aarch64/regalloc.rs +++ b/crates/rue-codegen/src/aarch64/regalloc.rs @@ -5,6 +5,8 @@ use std::collections::HashSet; +use rue_error::{CompileError, CompileResult, ErrorKind}; + use super::liveness::{self, LiveRange, LivenessInfo}; use super::mir::{Aarch64Inst, Aarch64Mir, Operand, Reg, VReg}; @@ -77,19 +79,19 @@ impl RegAlloc { } /// Perform register allocation and return the updated MIR. - pub fn allocate(mut self) -> Aarch64Mir { + pub fn allocate(mut self) -> CompileResult { self.assign_registers(); - self.rewrite_instructions(); - self.mir + self.rewrite_instructions()?; + Ok(self.mir) } /// Perform register allocation and return the MIR, spill count, and used callee-saved registers. - pub fn allocate_with_spills(mut self) -> (Aarch64Mir, u32, Vec) { + pub fn allocate_with_spills(mut self) -> CompileResult<(Aarch64Mir, u32, Vec)> { self.assign_registers(); - self.rewrite_instructions(); + self.rewrite_instructions()?; let num_spills = self.num_spills; let used_callee_saved = self.used_callee_saved; - (self.mir, num_spills, used_callee_saved) + Ok((self.mir, num_spills, used_callee_saved)) } /// Assign physical registers to all virtual registers using linear scan. @@ -156,18 +158,19 @@ impl RegAlloc { offset } - fn rewrite_instructions(&mut self) { + fn rewrite_instructions(&mut self) -> CompileResult<()> { let old_instructions = std::mem::take(&mut self.mir).into_instructions(); let mut new_mir = Aarch64Mir::new(); for inst in old_instructions { - self.rewrite_inst(&mut new_mir, inst); + self.rewrite_inst(&mut new_mir, inst)?; } self.mir = new_mir; + Ok(()) } - fn rewrite_inst(&self, mir: &mut Aarch64Mir, inst: Aarch64Inst) { + fn rewrite_inst(&self, mir: &mut Aarch64Mir, inst: Aarch64Inst) -> CompileResult<()> { match inst { Aarch64Inst::MovImm { dst, imm } => { match self.get_allocation(dst) { @@ -196,7 +199,7 @@ impl RegAlloc { } Aarch64Inst::MovRR { dst, src } => { - let src_op = self.load_operand(mir, src, Reg::X9); + let src_op = self.load_operand(mir, src, Reg::X9)?; let dst_alloc = self.get_allocation(dst); match dst_alloc { @@ -251,7 +254,7 @@ impl RegAlloc { }, Aarch64Inst::Str { src, base, offset } => { - let src_op = self.load_operand(mir, src, Reg::X9); + let src_op = self.load_operand(mir, src, Reg::X9)?; mir.push(Aarch64Inst::Str { src: src_op, base, @@ -264,7 +267,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::AddsRR { dst, src1, src2 } => { @@ -272,7 +275,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::AddsRR64 { dst, src1, src2 } => { @@ -280,7 +283,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::AddImm { dst, src, imm } => { @@ -288,7 +291,7 @@ impl RegAlloc { dst: d, src: s, imm: i, - }); + })?; } Aarch64Inst::SubRR { dst, src1, src2 } => { @@ -296,7 +299,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::SubsRR { dst, src1, src2 } => { @@ -304,7 +307,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::SubsRR64 { dst, src1, src2 } => { @@ -312,7 +315,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::SubImm { dst, src, imm } => { @@ -320,7 +323,7 @@ impl RegAlloc { dst: d, src: s, imm: i, - }); + })?; } Aarch64Inst::MulRR { dst, src1, src2 } => { @@ -328,7 +331,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::SmullRR { dst, src1, src2 } => { @@ -336,7 +339,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::UmullRR { dst, src1, src2 } => { @@ -344,7 +347,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::SmulhRR { dst, src1, src2 } => { @@ -352,7 +355,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::UmulhRR { dst, src1, src2 } => { @@ -360,7 +363,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::Lsr64Imm { dst, src, imm } => { @@ -368,7 +371,7 @@ impl RegAlloc { dst: d, src: s, imm, - }); + })?; } Aarch64Inst::Asr64Imm { dst, src, imm } => { @@ -376,7 +379,7 @@ impl RegAlloc { dst: d, src: s, imm, - }); + })?; } Aarch64Inst::SdivRR { dst, src1, src2 } => { @@ -384,7 +387,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::Msub { @@ -395,9 +398,9 @@ impl RegAlloc { } => { // Use X10, X11, X12 for sources to avoid conflict with X9 used for spilled dst. // X9 is reserved for the destination when it's spilled. - let src1_op = self.load_operand(mir, src1, Reg::X10); - let src2_op = self.load_operand(mir, src2, Reg::X11); - let src3_op = self.load_operand(mir, src3, Reg::X12); + let src1_op = self.load_operand(mir, src1, Reg::X10)?; + let src2_op = self.load_operand(mir, src2, Reg::X11)?; + let src3_op = self.load_operand(mir, src3, Reg::X12)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { mir.push(Aarch64Inst::Msub { @@ -432,15 +435,15 @@ impl RegAlloc { } Aarch64Inst::Neg { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Neg { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Neg { dst: d, src: s })?; } Aarch64Inst::Negs { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Negs { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Negs { dst: d, src: s })?; } Aarch64Inst::Negs32 { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Negs32 { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Negs32 { dst: d, src: s })?; } Aarch64Inst::AndRR { dst, src1, src2 } => { @@ -448,7 +451,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::OrrRR { dst, src1, src2 } => { @@ -456,7 +459,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::EorRR { dst, src1, src2 } => { @@ -464,11 +467,11 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::EorImm { dst, src, imm } => { - let src_op = self.load_operand(mir, src, Reg::X10); + let src_op = self.load_operand(mir, src, Reg::X10)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { mir.push(Aarch64Inst::EorImm { @@ -500,7 +503,7 @@ impl RegAlloc { } Aarch64Inst::MvnRR { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::MvnRR { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::MvnRR { dst: d, src: s })?; } Aarch64Inst::LslRR { dst, src1, src2 } => { @@ -508,7 +511,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::Lsl32RR { dst, src1, src2 } => { @@ -516,7 +519,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::LsrRR { dst, src1, src2 } => { @@ -524,7 +527,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::Lsr32RR { dst, src1, src2 } => { @@ -532,7 +535,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::AsrRR { dst, src1, src2 } => { @@ -540,7 +543,7 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::Asr32RR { dst, src1, src2 } => { @@ -548,12 +551,12 @@ impl RegAlloc { dst: d, src1: s1, src2: s2, - }); + })?; } Aarch64Inst::CmpRR { src1, src2 } => { - let src1_op = self.load_operand(mir, src1, Reg::X9); - let src2_op = self.load_operand(mir, src2, Reg::X10); + let src1_op = self.load_operand(mir, src1, Reg::X9)?; + let src2_op = self.load_operand(mir, src2, Reg::X10)?; mir.push(Aarch64Inst::CmpRR { src1: src1_op, src2: src2_op, @@ -561,8 +564,8 @@ impl RegAlloc { } Aarch64Inst::Cmp64RR { src1, src2 } => { - let src1_op = self.load_operand(mir, src1, Reg::X9); - let src2_op = self.load_operand(mir, src2, Reg::X10); + let src1_op = self.load_operand(mir, src1, Reg::X9)?; + let src2_op = self.load_operand(mir, src2, Reg::X10)?; mir.push(Aarch64Inst::Cmp64RR { src1: src1_op, src2: src2_op, @@ -570,17 +573,17 @@ impl RegAlloc { } Aarch64Inst::CmpImm { src, imm } => { - let src_op = self.load_operand(mir, src, Reg::X9); + let src_op = self.load_operand(mir, src, Reg::X9)?; mir.push(Aarch64Inst::CmpImm { src: src_op, imm }); } Aarch64Inst::Cbz { src, label } => { - let src_op = self.load_operand(mir, src, Reg::X9); + let src_op = self.load_operand(mir, src, Reg::X9)?; mir.push(Aarch64Inst::Cbz { src: src_op, label }); } Aarch64Inst::Cbnz { src, label } => { - let src_op = self.load_operand(mir, src, Reg::X9); + let src_op = self.load_operand(mir, src, Reg::X9)?; mir.push(Aarch64Inst::Cbnz { src: src_op, label }); } @@ -608,8 +611,8 @@ impl RegAlloc { }, Aarch64Inst::TstRR { src1, src2 } => { - let src1_op = self.load_operand(mir, src1, Reg::X9); - let src2_op = self.load_operand(mir, src2, Reg::X10); + let src1_op = self.load_operand(mir, src1, Reg::X9)?; + let src2_op = self.load_operand(mir, src2, Reg::X10)?; mir.push(Aarch64Inst::TstRR { src1: src1_op, src2: src2_op, @@ -617,28 +620,28 @@ impl RegAlloc { } Aarch64Inst::Sxtb { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Sxtb { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Sxtb { dst: d, src: s })?; } Aarch64Inst::Sxth { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Sxth { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Sxth { dst: d, src: s })?; } Aarch64Inst::Sxtw { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Sxtw { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Sxtw { dst: d, src: s })?; } Aarch64Inst::Uxtb { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Uxtb { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Uxtb { dst: d, src: s })?; } Aarch64Inst::Uxth { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Uxth { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| Aarch64Inst::Uxth { dst: d, src: s })?; } Aarch64Inst::StpPre { src1, src2, offset } => { - let src1_op = self.load_operand(mir, src1, Reg::X9); - let src2_op = self.load_operand(mir, src2, Reg::X10); + let src1_op = self.load_operand(mir, src1, Reg::X9)?; + let src2_op = self.load_operand(mir, src2, Reg::X10)?; mir.push(Aarch64Inst::StpPre { src1: src1_op, src2: src2_op, @@ -683,7 +686,7 @@ impl RegAlloc { Aarch64Inst::LdrIndexed { dst, base } => { // Load base vreg into scratch, then emit load with the result allocation let base_op = Operand::Virtual(base); - let base_reg = self.load_operand(mir, base_op, Reg::X9); + let base_reg = self.load_operand(mir, base_op, Reg::X9)?; let base_phys = match base_reg { Operand::Physical(r) => r, _ => Reg::X9, @@ -720,9 +723,9 @@ impl RegAlloc { } Aarch64Inst::StrIndexed { src, base } => { - let src_op = self.load_operand(mir, src, Reg::X9); + let src_op = self.load_operand(mir, src, Reg::X9)?; let base_vreg_op = Operand::Virtual(base); - let base_reg = self.load_operand(mir, base_vreg_op, Reg::X10); + let base_reg = self.load_operand(mir, base_vreg_op, Reg::X10)?; let base_phys = match base_reg { Operand::Physical(r) => r, _ => Reg::X10, @@ -735,7 +738,7 @@ impl RegAlloc { } Aarch64Inst::LslImm { dst, src, imm } => { - let src_op = self.load_operand(mir, src, Reg::X10); + let src_op = self.load_operand(mir, src, Reg::X10)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { @@ -768,7 +771,7 @@ impl RegAlloc { } Aarch64Inst::Lsl32Imm { dst, src, imm } => { - let src_op = self.load_operand(mir, src, Reg::X10); + let src_op = self.load_operand(mir, src, Reg::X10)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { @@ -801,7 +804,7 @@ impl RegAlloc { } Aarch64Inst::Lsr32Imm { dst, src, imm } => { - let src_op = self.load_operand(mir, src, Reg::X10); + let src_op = self.load_operand(mir, src, Reg::X10)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { @@ -834,7 +837,7 @@ impl RegAlloc { } Aarch64Inst::Asr32Imm { dst, src, imm } => { - let src_op = self.load_operand(mir, src, Reg::X10); + let src_op = self.load_operand(mir, src, Reg::X10)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { @@ -921,6 +924,7 @@ impl RegAlloc { Aarch64Inst::Bl { symbol } => mir.push(Aarch64Inst::Bl { symbol }), Aarch64Inst::Ret => mir.push(Aarch64Inst::Ret), } + Ok(()) } fn get_allocation(&self, operand: Operand) -> Option { @@ -930,29 +934,43 @@ impl RegAlloc { } } - fn load_operand(&self, mir: &mut Aarch64Mir, operand: Operand, scratch: Reg) -> Operand { + fn load_operand( + &self, + mir: &mut Aarch64Mir, + operand: Operand, + scratch: Reg, + ) -> CompileResult { match operand { Operand::Virtual(vreg) => match self.allocation[vreg.index() as usize] { - Some(Allocation::Register(reg)) => Operand::Physical(reg), + Some(Allocation::Register(reg)) => Ok(Operand::Physical(reg)), Some(Allocation::Spill(offset)) => { mir.push(Aarch64Inst::Ldr { dst: Operand::Physical(scratch), base: Reg::Fp, offset, }); - Operand::Physical(scratch) + Ok(Operand::Physical(scratch)) } - None => panic!("vreg {} not allocated", vreg.index()), + None => Err(CompileError::without_span(ErrorKind::LinkError(format!( + "internal codegen error: virtual register {} was not allocated", + vreg.index() + )))), }, - Operand::Physical(reg) => Operand::Physical(reg), + Operand::Physical(reg) => Ok(Operand::Physical(reg)), } } - fn emit_binop(&self, mir: &mut Aarch64Mir, dst: Operand, src: Operand, make_inst: F) + fn emit_binop( + &self, + mir: &mut Aarch64Mir, + dst: Operand, + src: Operand, + make_inst: F, + ) -> CompileResult<()> where F: FnOnce(Operand, Operand) -> Aarch64Inst, { - let src_op = self.load_operand(mir, src, Reg::X10); + let src_op = self.load_operand(mir, src, Reg::X10)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { mir.push(make_inst(Operand::Physical(reg), src_op)); @@ -969,6 +987,7 @@ impl RegAlloc { mir.push(make_inst(dst, src_op)); } } + Ok(()) } fn emit_ternop( @@ -978,11 +997,12 @@ impl RegAlloc { src1: Operand, src2: Operand, make_inst: F, - ) where + ) -> CompileResult<()> + where F: FnOnce(Operand, Operand, Operand) -> Aarch64Inst, { - let src1_op = self.load_operand(mir, src1, Reg::X10); - let src2_op = self.load_operand(mir, src2, Reg::X11); + let src1_op = self.load_operand(mir, src1, Reg::X10)?; + let src2_op = self.load_operand(mir, src2, Reg::X11)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { mir.push(make_inst(Operand::Physical(reg), src1_op, src2_op)); @@ -999,6 +1019,7 @@ impl RegAlloc { mir.push(make_inst(dst, src1_op, src2_op)); } } + Ok(()) } fn emit_binop_imm( @@ -1008,10 +1029,11 @@ impl RegAlloc { src: Operand, imm: i32, make_inst: F, - ) where + ) -> CompileResult<()> + where F: FnOnce(Operand, Operand, i32) -> Aarch64Inst, { - let src_op = self.load_operand(mir, src, Reg::X10); + let src_op = self.load_operand(mir, src, Reg::X10)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { mir.push(make_inst(Operand::Physical(reg), src_op, imm)); @@ -1028,6 +1050,7 @@ impl RegAlloc { mir.push(make_inst(dst, src_op, imm)); } } + Ok(()) } } @@ -1045,11 +1068,11 @@ mod tests { imm: 42, }); - let mir = RegAlloc::new(mir, 0).allocate(); + let mir = RegAlloc::new(mir, 0).allocate().unwrap(); match &mir.instructions()[0] { Aarch64Inst::MovImm { dst, imm } => { - assert_eq!(*dst, Operand::Physical(Reg::X19)); + assert_eq!(dst, &Operand::Physical(Reg::X19)); assert_eq!(*imm, 42); } _ => panic!("expected MovImm"), @@ -1065,11 +1088,11 @@ mod tests { imm: 60, }); - let mir = RegAlloc::new(mir, 0).allocate(); + let mir = RegAlloc::new(mir, 0).allocate().unwrap(); match &mir.instructions()[0] { Aarch64Inst::MovImm { dst, imm } => { - assert_eq!(*dst, Operand::Physical(Reg::X0)); + assert_eq!(dst, &Operand::Physical(Reg::X0)); assert_eq!(*imm, 60); } _ => panic!("expected MovImm"), @@ -1111,7 +1134,7 @@ mod tests { } // Allocate - this should succeed without panicking - let result = RegAlloc::new(mir, 0).allocate(); + let result = RegAlloc::new(mir, 0).allocate().unwrap(); // Verify the Msub instruction was generated let has_msub = result @@ -1146,7 +1169,7 @@ mod tests { src2: Operand::Virtual(v1), }); - let mir = RegAlloc::new(mir, 0).allocate(); + let mir = RegAlloc::new(mir, 0).allocate().unwrap(); // Verify all instructions have physical registers for inst in mir.instructions() { @@ -1188,7 +1211,7 @@ mod tests { }); } - let (mir, num_spills, _) = RegAlloc::new(mir, 0).allocate_with_spills(); + let (mir, num_spills, _) = RegAlloc::new(mir, 0).allocate_with_spills().unwrap(); // With 15 vregs and 10 allocatable registers, we should have spills assert!( diff --git a/crates/rue-codegen/src/lib.rs b/crates/rue-codegen/src/lib.rs index cb22c419..505679e1 100644 --- a/crates/rue-codegen/src/lib.rs +++ b/crates/rue-codegen/src/lib.rs @@ -75,11 +75,11 @@ mod tests { span: Span::new(0, 2), }); - // Build CFG from AIR - let cfg_output = CfgBuilder::build(&air, 0, 0, "main"); + // Build CFG from AIR (no struct/array types in this simple test) + let cfg_output = CfgBuilder::build(&air, 0, 0, "main", &[], &[]); // Test the generate function - let machine_code = generate(&cfg_output.cfg, &[], &[], &[]); + let machine_code = generate(&cfg_output.cfg, &[], &[], &[]).unwrap(); // Should generate working code assert!(!machine_code.code.is_empty()); diff --git a/crates/rue-codegen/src/x86_64/cfg_lower.rs b/crates/rue-codegen/src/x86_64/cfg_lower.rs index ba7d6863..4204b7e5 100644 --- a/crates/rue-codegen/src/x86_64/cfg_lower.rs +++ b/crates/rue-codegen/src/x86_64/cfg_lower.rs @@ -1891,6 +1891,19 @@ impl<'a> CfgLower<'a> { imm: *variant_index as i32, }); } + + CfgInstData::Drop { value: _ } => { + // Drop instruction - runs destructor if the type needs one. + // The CFG builder already elides Drop for trivially droppable types, + // so reaching here means we need to emit actual cleanup code. + // + // This will be implemented in Phase 3 (rue-wjha.9) when we add + // types with destructors (e.g., mutable strings). + debug_assert!( + false, + "Drop instruction reached codegen but no types currently need drop" + ); + } } } @@ -2465,8 +2478,14 @@ mod tests { let struct_defs = &output.struct_defs; let array_types = &output.array_types; let strings = &output.strings; - let cfg_output = - CfgBuilder::build(&func.air, func.num_locals, func.num_param_slots, &func.name); + let cfg_output = CfgBuilder::build( + &func.air, + func.num_locals, + func.num_param_slots, + &func.name, + struct_defs, + array_types, + ); CfgLower::new(&cfg_output.cfg, struct_defs, array_types, strings).lower() } diff --git a/crates/rue-codegen/src/x86_64/mod.rs b/crates/rue-codegen/src/x86_64/mod.rs index 4991cd78..7eca3625 100644 --- a/crates/rue-codegen/src/x86_64/mod.rs +++ b/crates/rue-codegen/src/x86_64/mod.rs @@ -24,6 +24,7 @@ pub use regalloc::RegAlloc; use rue_air::{ArrayTypeDef, StructDef}; use rue_cfg::Cfg; +use rue_error::CompileResult; // Re-export from parent pub use super::{EmittedRelocation, MachineCode}; @@ -37,7 +38,7 @@ pub fn generate( struct_defs: &[StructDef], array_types: &[ArrayTypeDef], strings: &[String], -) -> MachineCode { +) -> CompileResult { let num_locals = cfg.num_locals(); let num_params = cfg.num_params(); @@ -48,7 +49,7 @@ pub fn generate( // Spill slots go after both locals AND parameters to avoid conflicts let existing_slots = num_locals + num_params; let (mir, num_spills, used_callee_saved) = - RegAlloc::new(mir, existing_slots).allocate_with_spills(); + RegAlloc::new(mir, existing_slots).allocate_with_spills()?; // Emit machine code bytes (with prologue for stack frame setup) // Total local slots = local variables + spill slots (params handled separately) @@ -65,9 +66,9 @@ pub fn generate( ) .emit(); - MachineCode { + Ok(MachineCode { code, relocations, strings: strings.to_vec(), - } + }) } diff --git a/crates/rue-codegen/src/x86_64/regalloc.rs b/crates/rue-codegen/src/x86_64/regalloc.rs index a83397af..5ed28b2d 100644 --- a/crates/rue-codegen/src/x86_64/regalloc.rs +++ b/crates/rue-codegen/src/x86_64/regalloc.rs @@ -12,6 +12,8 @@ use std::collections::HashSet; +use rue_error::{CompileError, CompileResult, ErrorKind}; + use super::liveness::{self, LiveRange, LivenessInfo}; use super::mir::{Operand, Reg, VReg, X86Inst, X86Mir}; @@ -90,27 +92,27 @@ impl RegAlloc { } /// Perform register allocation and return the updated MIR. - pub fn allocate(mut self) -> X86Mir { + pub fn allocate(mut self) -> CompileResult { // Phase 1: Assign physical registers (or spill) to virtual registers self.assign_registers(); // Phase 2: Rewrite instructions to use physical registers and insert spill code - self.rewrite_instructions(); + self.rewrite_instructions()?; - self.mir + Ok(self.mir) } /// Perform register allocation and return the MIR, spill count, and used callee-saved registers. - pub fn allocate_with_spills(mut self) -> (X86Mir, u32, Vec) { + pub fn allocate_with_spills(mut self) -> CompileResult<(X86Mir, u32, Vec)> { // Phase 1: Assign physical registers (or spill) to virtual registers self.assign_registers(); // Phase 2: Rewrite instructions to use physical registers and insert spill code - self.rewrite_instructions(); + self.rewrite_instructions()?; let num_spills = self.num_spills; let used_callee_saved = self.used_callee_saved; - (self.mir, num_spills, used_callee_saved) + Ok((self.mir, num_spills, used_callee_saved)) } /// Assign physical registers to all virtual registers using linear scan. @@ -196,21 +198,22 @@ impl RegAlloc { } /// Rewrite all instructions to use physical registers and handle spills. - fn rewrite_instructions(&mut self) { + fn rewrite_instructions(&mut self) -> CompileResult<()> { // For spilled vregs, we need to insert load/store operations. // This is done by building a new instruction list. let old_instructions = std::mem::take(&mut self.mir).into_instructions(); let mut new_mir = X86Mir::new(); for inst in old_instructions { - self.rewrite_inst(&mut new_mir, inst); + self.rewrite_inst(&mut new_mir, inst)?; } self.mir = new_mir; + Ok(()) } /// Rewrite a single instruction, handling spills. - fn rewrite_inst(&self, mir: &mut X86Mir, inst: X86Inst) { + fn rewrite_inst(&self, mir: &mut X86Mir, inst: X86Inst) -> CompileResult<()> { match inst { X86Inst::MovRI32 { dst, imm } => { match self.get_allocation(dst) { @@ -264,7 +267,7 @@ impl RegAlloc { }, X86Inst::MovRR { dst, src } => { - let src_op = self.load_operand(mir, src, Reg::Rax); + let src_op = self.load_operand(mir, src, Reg::Rax)?; let dst_alloc = self.get_allocation(dst); match dst_alloc { @@ -323,7 +326,7 @@ impl RegAlloc { } X86Inst::MovMR { base, offset, src } => { - let src_op = self.load_operand(mir, src, Reg::Rax); + let src_op = self.load_operand(mir, src, Reg::Rax)?; mir.push(X86Inst::MovMR { base, offset, @@ -332,11 +335,11 @@ impl RegAlloc { } X86Inst::AddRR { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| X86Inst::AddRR { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| X86Inst::AddRR { dst: d, src: s })?; } X86Inst::AddRR64 { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| X86Inst::AddRR64 { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| X86Inst::AddRR64 { dst: d, src: s })?; } X86Inst::AddRI { dst, imm } => { @@ -344,19 +347,19 @@ impl RegAlloc { } X86Inst::SubRR { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| X86Inst::SubRR { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| X86Inst::SubRR { dst: d, src: s })?; } X86Inst::SubRR64 { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| X86Inst::SubRR64 { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| X86Inst::SubRR64 { dst: d, src: s })?; } X86Inst::ImulRR { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| X86Inst::ImulRR { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| X86Inst::ImulRR { dst: d, src: s })?; } X86Inst::ImulRR64 { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| X86Inst::ImulRR64 { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| X86Inst::ImulRR64 { dst: d, src: s })?; } X86Inst::Neg { dst } => { @@ -372,15 +375,15 @@ impl RegAlloc { } X86Inst::AndRR { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| X86Inst::AndRR { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| X86Inst::AndRR { dst: d, src: s })?; } X86Inst::OrRR { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| X86Inst::OrRR { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| X86Inst::OrRR { dst: d, src: s })?; } X86Inst::XorRR { dst, src } => { - self.emit_binop(mir, dst, src, |d, s| X86Inst::XorRR { dst: d, src: s }); + self.emit_binop(mir, dst, src, |d, s| X86Inst::XorRR { dst: d, src: s })?; } X86Inst::NotR { dst } => { @@ -436,13 +439,13 @@ impl RegAlloc { } X86Inst::IdivR { src } => { - let src_op = self.load_operand(mir, src, Reg::R10); + let src_op = self.load_operand(mir, src, Reg::R10)?; mir.push(X86Inst::IdivR { src: src_op }); } X86Inst::TestRR { src1, src2 } => { - let src1_op = self.load_operand(mir, src1, Reg::Rax); - let src2_op = self.load_operand(mir, src2, Reg::R10); + let src1_op = self.load_operand(mir, src1, Reg::Rax)?; + let src2_op = self.load_operand(mir, src2, Reg::R10)?; mir.push(X86Inst::TestRR { src1: src1_op, src2: src2_op, @@ -450,8 +453,8 @@ impl RegAlloc { } X86Inst::CmpRR { src1, src2 } => { - let src1_op = self.load_operand(mir, src1, Reg::Rax); - let src2_op = self.load_operand(mir, src2, Reg::R10); + let src1_op = self.load_operand(mir, src1, Reg::Rax)?; + let src2_op = self.load_operand(mir, src2, Reg::R10)?; mir.push(X86Inst::CmpRR { src1: src1_op, src2: src2_op, @@ -459,8 +462,8 @@ impl RegAlloc { } X86Inst::Cmp64RR { src1, src2 } => { - let src1_op = self.load_operand(mir, src1, Reg::Rax); - let src2_op = self.load_operand(mir, src2, Reg::R10); + let src1_op = self.load_operand(mir, src1, Reg::Rax)?; + let src2_op = self.load_operand(mir, src2, Reg::R10)?; mir.push(X86Inst::Cmp64RR { src1: src1_op, src2: src2_op, @@ -468,7 +471,7 @@ impl RegAlloc { } X86Inst::CmpRI { src, imm } => { - let src_op = self.load_operand(mir, src, Reg::Rax); + let src_op = self.load_operand(mir, src, Reg::Rax)?; mir.push(X86Inst::CmpRI { src: src_op, imm }); } @@ -513,7 +516,7 @@ impl RegAlloc { } X86Inst::Movzx { dst, src } => { - let src_op = self.load_operand(mir, src, Reg::Rax); + let src_op = self.load_operand(mir, src, Reg::Rax)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { mir.push(X86Inst::Movzx { @@ -539,7 +542,7 @@ impl RegAlloc { } X86Inst::Movsx8To64 { dst, src } => { - let src_op = self.load_operand(mir, src, Reg::Rax); + let src_op = self.load_operand(mir, src, Reg::Rax)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { mir.push(X86Inst::Movsx8To64 { @@ -565,7 +568,7 @@ impl RegAlloc { } X86Inst::Movsx16To64 { dst, src } => { - let src_op = self.load_operand(mir, src, Reg::Rax); + let src_op = self.load_operand(mir, src, Reg::Rax)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { mir.push(X86Inst::Movsx16To64 { @@ -591,7 +594,7 @@ impl RegAlloc { } X86Inst::Movsx32To64 { dst, src } => { - let src_op = self.load_operand(mir, src, Reg::Rax); + let src_op = self.load_operand(mir, src, Reg::Rax)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { mir.push(X86Inst::Movsx32To64 { @@ -617,7 +620,7 @@ impl RegAlloc { } X86Inst::Movzx8To64 { dst, src } => { - let src_op = self.load_operand(mir, src, Reg::Rax); + let src_op = self.load_operand(mir, src, Reg::Rax)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { mir.push(X86Inst::Movzx8To64 { @@ -643,7 +646,7 @@ impl RegAlloc { } X86Inst::Movzx16To64 { dst, src } => { - let src_op = self.load_operand(mir, src, Reg::Rax); + let src_op = self.load_operand(mir, src, Reg::Rax)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { mir.push(X86Inst::Movzx16To64 { @@ -690,7 +693,7 @@ impl RegAlloc { }, X86Inst::Push { src } => { - let src_op = self.load_operand(mir, src, Reg::Rax); + let src_op = self.load_operand(mir, src, Reg::Rax)?; mir.push(X86Inst::Push { src: src_op }); } @@ -737,7 +740,7 @@ impl RegAlloc { X86Inst::Shl { dst, count } => { // SHL needs count in RCX - let count_op = self.load_operand(mir, count, Reg::Rcx); + let count_op = self.load_operand(mir, count, Reg::Rcx)?; if count_op != Operand::Physical(Reg::Rcx) { mir.push(X86Inst::MovRR { dst: Operand::Physical(Reg::Rcx), @@ -780,7 +783,7 @@ impl RegAlloc { X86Inst::MovRMIndexed { dst, base, offset } => { // Load base vreg into scratch register let base_op = Operand::Virtual(base); - let base_reg = self.load_operand(mir, base_op, Reg::Rax); + let base_reg = self.load_operand(mir, base_op, Reg::Rax)?; let base_phys = match base_reg { Operand::Physical(r) => r, _ => Reg::Rax, @@ -817,9 +820,9 @@ impl RegAlloc { } X86Inst::MovMRIndexed { base, offset, src } => { - let src_op = self.load_operand(mir, src, Reg::Rdx); + let src_op = self.load_operand(mir, src, Reg::Rdx)?; let base_op = Operand::Virtual(base); - let base_reg = self.load_operand(mir, base_op, Reg::Rax); + let base_reg = self.load_operand(mir, base_op, Reg::Rax)?; let base_phys = match base_reg { Operand::Physical(r) => r, _ => Reg::Rax, @@ -892,6 +895,7 @@ impl RegAlloc { X86Inst::Syscall => mir.push(X86Inst::Syscall), X86Inst::Ret => mir.push(X86Inst::Ret), } + Ok(()) } /// Get the allocation for an operand (returns None for physical registers). @@ -904,31 +908,45 @@ impl RegAlloc { /// Load an operand into a physical register, inserting a load if spilled. /// Returns the operand to use (either the allocated register or the scratch register). - fn load_operand(&self, mir: &mut X86Mir, operand: Operand, scratch: Reg) -> Operand { + fn load_operand( + &self, + mir: &mut X86Mir, + operand: Operand, + scratch: Reg, + ) -> CompileResult { match operand { Operand::Virtual(vreg) => match self.allocation[vreg.index() as usize] { - Some(Allocation::Register(reg)) => Operand::Physical(reg), + Some(Allocation::Register(reg)) => Ok(Operand::Physical(reg)), Some(Allocation::Spill(offset)) => { mir.push(X86Inst::MovRM { dst: Operand::Physical(scratch), base: Reg::Rbp, offset, }); - Operand::Physical(scratch) + Ok(Operand::Physical(scratch)) } - None => panic!("vreg {} not allocated", vreg.index()), + None => Err(CompileError::without_span(ErrorKind::LinkError(format!( + "internal codegen error: virtual register {} was not allocated", + vreg.index() + )))), }, - Operand::Physical(reg) => Operand::Physical(reg), + Operand::Physical(reg) => Ok(Operand::Physical(reg)), } } /// Emit a binary operation (dst = dst op src). - fn emit_binop(&self, mir: &mut X86Mir, dst: Operand, src: Operand, make_inst: F) + fn emit_binop( + &self, + mir: &mut X86Mir, + dst: Operand, + src: Operand, + make_inst: F, + ) -> CompileResult<()> where F: FnOnce(Operand, Operand) -> X86Inst, { // Load src first (use R10 as scratch to avoid clobbering RAX) - let src_op = self.load_operand(mir, src, Reg::R10); + let src_op = self.load_operand(mir, src, Reg::R10)?; match self.get_allocation(dst) { Some(Allocation::Register(reg)) => { @@ -955,6 +973,7 @@ impl RegAlloc { mir.push(make_inst(dst, src_op)); } } + Ok(()) } /// Emit a unary operation (dst = op dst). @@ -1083,12 +1102,12 @@ mod tests { imm: 42, }); - let mir = RegAlloc::new(mir, 0).allocate(); + let mir = RegAlloc::new(mir, 0).allocate().unwrap(); // v0 should be allocated to R12 (first allocatable) match &mir.instructions()[0] { X86Inst::MovRI32 { dst, imm } => { - assert_eq!(*dst, Operand::Physical(Reg::R12)); + assert_eq!(dst, &Operand::Physical(Reg::R12)); assert_eq!(*imm, 42); } _ => panic!("expected MovRI32"), @@ -1105,11 +1124,11 @@ mod tests { imm: 60, }); - let mir = RegAlloc::new(mir, 0).allocate(); + let mir = RegAlloc::new(mir, 0).allocate().unwrap(); match &mir.instructions()[0] { X86Inst::MovRI32 { dst, imm } => { - assert_eq!(*dst, Operand::Physical(Reg::Rdi)); + assert_eq!(dst, &Operand::Physical(Reg::Rdi)); assert_eq!(*imm, 60); } _ => panic!("expected MovRI32"), @@ -1134,14 +1153,14 @@ mod tests { imm: 2, }); - let mir = RegAlloc::new(mir, 0).allocate(); + let mir = RegAlloc::new(mir, 0).allocate().unwrap(); // Both can be allocated to R12 since they don't interfere match (&mir.instructions()[0], &mir.instructions()[1]) { (X86Inst::MovRI32 { dst: d0, .. }, X86Inst::MovRI32 { dst: d1, .. }) => { // They should both get R12 since v0 is dead before v1 is defined - assert_eq!(*d0, Operand::Physical(Reg::R12)); - assert_eq!(*d1, Operand::Physical(Reg::R12)); + assert_eq!(d0, &Operand::Physical(Reg::R12)); + assert_eq!(d1, &Operand::Physical(Reg::R12)); } _ => panic!("expected two MovRI32"), } @@ -1170,15 +1189,15 @@ mod tests { src: Operand::Virtual(v0), }); - let mir = RegAlloc::new(mir, 0).allocate(); + let mir = RegAlloc::new(mir, 0).allocate().unwrap(); // v0 and v1 should get different registers let d0 = match &mir.instructions()[0] { - X86Inst::MovRI32 { dst, .. } => *dst, + X86Inst::MovRI32 { dst, .. } => dst.clone(), _ => panic!("expected MovRI32"), }; let d1 = match &mir.instructions()[1] { - X86Inst::MovRI32 { dst, .. } => *dst, + X86Inst::MovRI32 { dst, .. } => dst.clone(), _ => panic!("expected MovRI32"), }; diff --git a/crates/rue-compiler/src/lib.rs b/crates/rue-compiler/src/lib.rs index fc7323d7..5dcfc80f 100644 --- a/crates/rue-compiler/src/lib.rs +++ b/crates/rue-compiler/src/lib.rs @@ -6,12 +6,108 @@ //! It re-exports types from the component crates for convenience. use std::io::Write; +use std::path::PathBuf; use std::process::Command; use std::sync::atomic::{AtomicU64, Ordering}; /// Counter for generating unique temp directory names. static TEMP_DIR_COUNTER: AtomicU64 = AtomicU64::new(0); +/// A temporary directory for linking that automatically cleans up on drop. +/// +/// This struct manages the creation of a unique temporary directory for the +/// linking process and automatically removes it when dropped (whether via +/// normal completion or early error return). +struct TempLinkDir { + /// Path to the temporary directory. + path: PathBuf, + /// Paths to the object files written to the directory. + obj_paths: Vec, + /// Path to the runtime archive in the directory. + runtime_path: PathBuf, + /// Path where the linked executable will be written. + output_path: PathBuf, +} + +impl TempLinkDir { + /// Create a new temporary directory for linking. + /// + /// Creates a unique directory in the system temp directory with the + /// format `rue--` to ensure uniqueness even in parallel + /// test execution. + fn new() -> CompileResult { + let unique_id = TEMP_DIR_COUNTER.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!("rue-{}-{}", std::process::id(), unique_id)); + std::fs::create_dir_all(&path).map_err(|e| { + CompileError::without_span(ErrorKind::LinkError(format!( + "failed to create temp directory: {}", + e + ))) + })?; + + let runtime_path = path.join("librue_runtime.a"); + let output_path = path.join("output"); + + Ok(Self { + path, + obj_paths: Vec::new(), + runtime_path, + output_path, + }) + } + + /// Write object files to the temporary directory. + /// + /// Each object file is written to a file named `obj{N}.o` where N is + /// the index. The paths are stored in `self.obj_paths`. + fn write_object_files(&mut self, object_files: &[Vec]) -> CompileResult<()> { + for (i, obj_bytes) in object_files.iter().enumerate() { + let obj_path = self.path.join(format!("obj{}.o", i)); + let mut file = std::fs::File::create(&obj_path).map_err(|e| { + CompileError::without_span(ErrorKind::LinkError(format!( + "failed to create temp object file: {}", + e + ))) + })?; + file.write_all(obj_bytes).map_err(|e| { + CompileError::without_span(ErrorKind::LinkError(format!( + "failed to write temp object file: {}", + e + ))) + })?; + self.obj_paths.push(obj_path); + } + Ok(()) + } + + /// Write the runtime archive to the temporary directory. + fn write_runtime(&self, runtime_bytes: &[u8]) -> CompileResult<()> { + std::fs::write(&self.runtime_path, runtime_bytes).map_err(|e| { + CompileError::without_span(ErrorKind::LinkError(format!( + "failed to write runtime archive: {}", + e + ))) + }) + } + + /// Read the linked executable from the output path. + fn read_output(&self) -> CompileResult> { + std::fs::read(&self.output_path).map_err(|e| { + CompileError::without_span(ErrorKind::LinkError(format!( + "failed to read linked executable: {}", + e + ))) + }) + } +} + +impl Drop for TempLinkDir { + fn drop(&mut self) { + // Best-effort cleanup; ignore errors + let _ = std::fs::remove_dir_all(&self.path); + } +} + /// The rue-runtime staticlib archive bytes, embedded at compile time. /// This is linked into every Rue executable. static RUNTIME_BYTES: &[u8] = include_bytes!("librue_runtime.a"); @@ -155,8 +251,14 @@ pub fn compile_frontend_from_ast(ast: Ast) -> CompileResult { let mut warnings = sema_output.warnings; for func in sema_output.functions { - let cfg_output = - CfgBuilder::build(&func.air, func.num_locals, func.num_param_slots, &func.name); + let cfg_output = CfgBuilder::build( + &func.air, + func.num_locals, + func.num_param_slots, + &func.name, + &sema_output.struct_defs, + &sema_output.array_types, + ); warnings.extend(cfg_output.warnings); functions.push(FunctionWithCfg { analyzed: func, @@ -218,7 +320,7 @@ fn compile_x86_64(state: &CompileState, options: &CompileOptions) -> CompileResu &state.struct_defs, &state.array_types, &state.strings, - ); + )?; // Build object file for this function let mut obj_builder = ObjectBuilder::new(options.target, &func.analyzed.name) @@ -303,47 +405,10 @@ fn link_system( object_files: &[Vec], linker_cmd: &str, ) -> CompileResult { - // Create a temporary directory for object files. - // Use pid + atomic counter to ensure uniqueness even in parallel test execution. - let unique_id = TEMP_DIR_COUNTER.fetch_add(1, Ordering::Relaxed); - let temp_dir = std::env::temp_dir().join(format!("rue-{}-{}", std::process::id(), unique_id)); - std::fs::create_dir_all(&temp_dir).map_err(|e| { - CompileError::without_span(ErrorKind::LinkError(format!( - "failed to create temp directory: {}", - e - ))) - })?; - - // Write object files to temp directory - let mut obj_paths = Vec::new(); - for (i, obj_bytes) in object_files.iter().enumerate() { - let path = temp_dir.join(format!("obj{}.o", i)); - let mut file = std::fs::File::create(&path).map_err(|e| { - CompileError::without_span(ErrorKind::LinkError(format!( - "failed to create temp object file: {}", - e - ))) - })?; - file.write_all(obj_bytes).map_err(|e| { - CompileError::without_span(ErrorKind::LinkError(format!( - "failed to write temp object file: {}", - e - ))) - })?; - obj_paths.push(path); - } - - // Write the runtime archive to temp directory - let runtime_path = temp_dir.join("librue_runtime.a"); - std::fs::write(&runtime_path, RUNTIME_BYTES).map_err(|e| { - CompileError::without_span(ErrorKind::LinkError(format!( - "failed to write runtime archive: {}", - e - ))) - })?; - - // Output path for linked executable - let output_path = temp_dir.join("output"); + // Set up temporary directory with object files and runtime + let mut temp_dir = TempLinkDir::new()?; + temp_dir.write_object_files(object_files)?; + temp_dir.write_runtime(RUNTIME_BYTES)?; // Build the linker command let mut cmd = Command::new(linker_cmd); @@ -352,15 +417,15 @@ fn link_system( cmd.arg("-static"); cmd.arg("-nostdlib"); cmd.arg("-o"); - cmd.arg(&output_path); + cmd.arg(&temp_dir.output_path); // Add object files - for path in &obj_paths { + for path in &temp_dir.obj_paths { cmd.arg(path); } // Add the runtime library - cmd.arg(&runtime_path); + cmd.arg(&temp_dir.runtime_path); // Run the linker let output = cmd.output().map_err(|e| { @@ -372,7 +437,7 @@ fn link_system( if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); - let _ = std::fs::remove_dir_all(&temp_dir); + // temp_dir is dropped here, cleaning up automatically return Err(CompileError::without_span(ErrorKind::LinkError(format!( "linker '{}' failed: {}", linker_cmd, stderr @@ -380,16 +445,9 @@ fn link_system( } // Read the resulting executable - let elf = std::fs::read(&output_path).map_err(|e| { - CompileError::without_span(ErrorKind::LinkError(format!( - "failed to read linked executable: {}", - e - ))) - })?; - - // Clean up temp directory - let _ = std::fs::remove_dir_all(&temp_dir); + let elf = temp_dir.read_output()?; + // temp_dir is dropped here, cleaning up automatically Ok(CompileOutput { elf, warnings: state.warnings.clone(), @@ -407,7 +465,7 @@ fn compile_aarch64(state: &CompileState, options: &CompileOptions) -> CompileRes &state.struct_defs, &state.array_types, &state.strings, - ); + )?; let mut obj_builder = ObjectBuilder::new(options.target, &func.analyzed.name) .code(machine_code.code) @@ -451,46 +509,10 @@ fn link_system_macos( _options: &CompileOptions, object_files: &[Vec], ) -> CompileResult { - // Create a temporary directory for object files. - let unique_id = TEMP_DIR_COUNTER.fetch_add(1, Ordering::Relaxed); - let temp_dir = std::env::temp_dir().join(format!("rue-{}-{}", std::process::id(), unique_id)); - std::fs::create_dir_all(&temp_dir).map_err(|e| { - CompileError::without_span(ErrorKind::LinkError(format!( - "failed to create temp directory: {}", - e - ))) - })?; - - // Write object files to temp directory - let mut obj_paths = Vec::new(); - for (i, obj_bytes) in object_files.iter().enumerate() { - let path = temp_dir.join(format!("obj{}.o", i)); - let mut file = std::fs::File::create(&path).map_err(|e| { - CompileError::without_span(ErrorKind::LinkError(format!( - "failed to create temp object file: {}", - e - ))) - })?; - file.write_all(obj_bytes).map_err(|e| { - CompileError::without_span(ErrorKind::LinkError(format!( - "failed to write temp object file: {}", - e - ))) - })?; - obj_paths.push(path); - } - - // Write the runtime archive to temp directory - let runtime_path = temp_dir.join("librue_runtime.a"); - std::fs::write(&runtime_path, RUNTIME_BYTES).map_err(|e| { - CompileError::without_span(ErrorKind::LinkError(format!( - "failed to write runtime archive: {}", - e - ))) - })?; - - // Output path for linked executable - let output_path = temp_dir.join("output"); + // Set up temporary directory with object files and runtime + let mut temp_dir = TempLinkDir::new()?; + temp_dir.write_object_files(object_files)?; + temp_dir.write_runtime(RUNTIME_BYTES)?; // Use clang as the linker on macOS let mut cmd = Command::new("clang"); @@ -500,15 +522,15 @@ fn link_system_macos( cmd.arg("-arch").arg("arm64"); cmd.arg("-e").arg("__main"); cmd.arg("-o"); - cmd.arg(&output_path); + cmd.arg(&temp_dir.output_path); // Add object files - for path in &obj_paths { + for path in &temp_dir.obj_paths { cmd.arg(path); } // Add the runtime library - cmd.arg(&runtime_path); + cmd.arg(&temp_dir.runtime_path); // Link with libSystem for syscalls on macOS cmd.arg("-lSystem"); @@ -523,7 +545,7 @@ fn link_system_macos( if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); - let _ = std::fs::remove_dir_all(&temp_dir); + // temp_dir is dropped here, cleaning up automatically return Err(CompileError::without_span(ErrorKind::LinkError(format!( "linker failed: {}", stderr @@ -531,16 +553,9 @@ fn link_system_macos( } // Read the resulting executable - let elf = std::fs::read(&output_path).map_err(|e| { - CompileError::without_span(ErrorKind::LinkError(format!( - "failed to read linked executable: {}", - e - ))) - })?; - - // Clean up temp directory - let _ = std::fs::remove_dir_all(&temp_dir); + let elf = temp_dir.read_output()?; + // temp_dir is dropped here, cleaning up automatically Ok(CompileOutput { elf, warnings: state.warnings.clone(), @@ -601,7 +616,7 @@ pub fn generate_allocated_mir( array_types: &[ArrayTypeDef], strings: &[String], target: Target, -) -> Mir { +) -> CompileResult { let num_locals = cfg.num_locals(); let num_params = cfg.num_params(); let existing_slots = num_locals + num_params; @@ -614,9 +629,9 @@ pub fn generate_allocated_mir( // Allocate physical registers let (mir, _num_spills, _used_callee_saved) = - rue_codegen::x86_64::RegAlloc::new(mir, existing_slots).allocate_with_spills(); + rue_codegen::x86_64::RegAlloc::new(mir, existing_slots).allocate_with_spills()?; - Mir::X86_64(mir) + Ok(Mir::X86_64(mir)) } Arch::Aarch64 => { // Lower CFG to Aarch64Mir with virtual registers @@ -625,9 +640,9 @@ pub fn generate_allocated_mir( // Allocate physical registers let (mir, _num_spills, _used_callee_saved) = - rue_codegen::aarch64::RegAlloc::new(mir, existing_slots).allocate_with_spills(); + rue_codegen::aarch64::RegAlloc::new(mir, existing_slots).allocate_with_spills()?; - Mir::Aarch64(mir) + Ok(Mir::Aarch64(mir)) } } } diff --git a/crates/rue-error/src/lib.rs b/crates/rue-error/src/lib.rs index baeee320..cf0188c6 100644 --- a/crates/rue-error/src/lib.rs +++ b/crates/rue-error/src/lib.rs @@ -41,6 +41,8 @@ pub enum PreviewFeature { HmInference, /// Struct methods and impl blocks (ADR-0009). Methods, + /// Destructors for automatic cleanup (ADR-0010). + Destructors, } impl PreviewFeature { @@ -50,6 +52,7 @@ impl PreviewFeature { PreviewFeature::MutableStrings => "mutable_strings", PreviewFeature::HmInference => "hm_inference", PreviewFeature::Methods => "methods", + PreviewFeature::Destructors => "destructors", } } @@ -59,6 +62,7 @@ impl PreviewFeature { "mutable_strings" => Some(PreviewFeature::MutableStrings), "hm_inference" => Some(PreviewFeature::HmInference), "methods" => Some(PreviewFeature::Methods), + "destructors" => Some(PreviewFeature::Destructors), _ => None, } } @@ -69,6 +73,7 @@ impl PreviewFeature { PreviewFeature::MutableStrings => "ADR-019", PreviewFeature::HmInference => "ADR-0007", PreviewFeature::Methods => "ADR-0009", + PreviewFeature::Destructors => "ADR-0010", } } @@ -78,6 +83,7 @@ impl PreviewFeature { PreviewFeature::MutableStrings, PreviewFeature::HmInference, PreviewFeature::Methods, + PreviewFeature::Destructors, ] } diff --git a/crates/rue-intern/src/lib.rs b/crates/rue-intern/src/lib.rs index 76dd02c7..49c76835 100644 --- a/crates/rue-intern/src/lib.rs +++ b/crates/rue-intern/src/lib.rs @@ -262,4 +262,27 @@ mod tests { interner.intern("a"); // duplicate assert_eq!(interner.len(), initial_len + 2); } + + #[test] + fn test_try_get_returns_none_for_invalid_symbol() { + let interner = Interner::new(); + // Create an invalid symbol with an index beyond the interner's capacity + let invalid_sym = Symbol::from_raw(9999); + assert!(interner.try_get(invalid_sym).is_none()); + } + + #[test] + fn test_try_get_returns_some_for_valid_symbol() { + let mut interner = Interner::new(); + let sym = interner.intern("test_string"); + assert_eq!(interner.try_get(sym), Some("test_string")); + } + + #[test] + #[should_panic] + fn test_get_panics_for_invalid_symbol() { + let interner = Interner::new(); + let invalid_sym = Symbol::from_raw(9999); + let _ = interner.get(invalid_sym); // Should panic + } } diff --git a/crates/rue-lexer/src/lib.rs b/crates/rue-lexer/src/lib.rs index 58c7a52c..7d398154 100644 --- a/crates/rue-lexer/src/lib.rs +++ b/crates/rue-lexer/src/lib.rs @@ -27,6 +27,8 @@ pub enum TokenKind { False, Struct, Enum, + Impl, + SelfValue, // self (value, not type) // Type keywords I8, @@ -111,6 +113,8 @@ impl TokenKind { TokenKind::False => "'false'", TokenKind::Struct => "'struct'", TokenKind::Enum => "'enum'", + TokenKind::Impl => "'impl'", + TokenKind::SelfValue => "'self'", TokenKind::I8 => "type 'i8'", TokenKind::I16 => "type 'i16'", TokenKind::I32 => "type 'i32'", @@ -199,6 +203,8 @@ impl std::fmt::Display for TokenKind { TokenKind::False => write!(f, "FALSE"), TokenKind::Struct => write!(f, "STRUCT"), TokenKind::Enum => write!(f, "ENUM"), + TokenKind::Impl => write!(f, "IMPL"), + TokenKind::SelfValue => write!(f, "SELF"), TokenKind::I8 => write!(f, "TYPE(i8)"), TokenKind::I16 => write!(f, "TYPE(i16)"), TokenKind::I32 => write!(f, "TYPE(i32)"), diff --git a/crates/rue-lexer/src/logos_lexer.rs b/crates/rue-lexer/src/logos_lexer.rs index 9a134edc..3bfc6dcb 100644 --- a/crates/rue-lexer/src/logos_lexer.rs +++ b/crates/rue-lexer/src/logos_lexer.rs @@ -79,6 +79,10 @@ pub enum LogosTokenKind { Struct, #[token("enum")] Enum, + #[token("impl")] + Impl, + #[token("self")] + SelfValue, // Type keywords #[token("i8")] @@ -213,6 +217,8 @@ impl From for TokenKind { LogosTokenKind::False => TokenKind::False, LogosTokenKind::Struct => TokenKind::Struct, LogosTokenKind::Enum => TokenKind::Enum, + LogosTokenKind::Impl => TokenKind::Impl, + LogosTokenKind::SelfValue => TokenKind::SelfValue, LogosTokenKind::I8 => TokenKind::I8, LogosTokenKind::I16 => TokenKind::I16, LogosTokenKind::I32 => TokenKind::I32, diff --git a/crates/rue-linker/src/elf.rs b/crates/rue-linker/src/elf.rs index d10020e0..343d77a5 100644 --- a/crates/rue-linker/src/elf.rs +++ b/crates/rue-linker/src/elf.rs @@ -186,6 +186,15 @@ pub enum RelocationType { Pc32, /// R_X86_64_PLT32: 32-bit PLT-relative (treated as PC32 for static linking). Plt32, + /// R_X86_64_GOTPCREL: 32-bit PC-relative GOT offset. + /// For static linking, we relax this to a direct PC-relative reference. + GotPcRel, + /// R_X86_64_REX_GOTPCRELX: Relaxable 32-bit PC-relative GOT offset (with REX prefix). + /// For static linking, we relax this to a direct PC-relative reference. + RexGotPcRelX, + /// R_X86_64_GOTPCRELX: Relaxable 32-bit PC-relative GOT offset. + /// For static linking, we relax this to a direct PC-relative reference. + GotPcRelX, /// R_X86_64_32: 32-bit absolute address. Abs32, /// R_X86_64_32S: 32-bit signed absolute address. @@ -211,8 +220,11 @@ impl RelocationType { 1 => RelocationType::Abs64, 2 => RelocationType::Pc32, 4 => RelocationType::Plt32, + 9 => RelocationType::GotPcRel, // R_X86_64_GOTPCREL 10 => RelocationType::Abs32, 11 => RelocationType::Abs32S, + 41 => RelocationType::GotPcRelX, // R_X86_64_GOTPCRELX + 42 => RelocationType::RexGotPcRelX, // R_X86_64_REX_GOTPCRELX _ => RelocationType::Unknown(r_type), }, ElfMachine::Aarch64 => match r_type { @@ -599,6 +611,11 @@ impl ObjectFile { let r_sym = (r_info >> 32) as usize; let r_type = (r_info & 0xffffffff) as u32; + // Skip R_*_NONE relocations (type 0) - these are no-ops used for padding + if r_type == 0 { + continue; + } + sections[target_section].relocations.push(Relocation { offset: r_offset, symbol_index: r_sym, diff --git a/crates/rue-linker/src/emit.rs b/crates/rue-linker/src/emit.rs index 5e3e080f..9b14047b 100644 --- a/crates/rue-linker/src/emit.rs +++ b/crates/rue-linker/src/emit.rs @@ -247,13 +247,16 @@ impl ObjectBuilder { RelocationType::Abs64 => 1, RelocationType::Pc32 => 2, RelocationType::Plt32 => 4, + RelocationType::GotPcRel => 9, // R_X86_64_GOTPCREL RelocationType::Abs32 => 10, RelocationType::Abs32S => 11, - RelocationType::Jump26 => 282, // R_AARCH64_JUMP26 - RelocationType::Call26 => 283, // R_AARCH64_CALL26 + RelocationType::GotPcRelX => 41, // R_X86_64_GOTPCRELX + RelocationType::RexGotPcRelX => 42, // R_X86_64_REX_GOTPCRELX + RelocationType::Jump26 => 282, // R_AARCH64_JUMP26 + RelocationType::Call26 => 283, // R_AARCH64_CALL26 RelocationType::Aarch64Abs64 => 257, // R_AARCH64_ABS64 RelocationType::AdrpPage21 => 275, // R_AARCH64_ADR_PREL_PG_HI21 - RelocationType::AddLo12 => 277, // R_AARCH64_ADD_ABS_LO12_NC + RelocationType::AddLo12 => 277, // R_AARCH64_ADD_ABS_LO12_NC RelocationType::Unknown(t) => t, }; let r_info = ((sym_idx as u64) << 32) | (r_type as u64); diff --git a/crates/rue-linker/src/linker.rs b/crates/rue-linker/src/linker.rs index 64a2f84b..8949a9e8 100644 --- a/crates/rue-linker/src/linker.rs +++ b/crates/rue-linker/src/linker.rs @@ -265,7 +265,28 @@ impl Linker { // Collect relocations for reloc in §ion.relocations { + // Skip relocations that reference the null symbol (index 0) + // These are typically R_*_NONE relocations that slipped through + if reloc.symbol_index == 0 { + continue; + } let sym = &obj.symbols[reloc.symbol_index]; + + // Skip relocations to symbols in sections we don't handle + // (e.g., .bss, .data, debug sections, etc.) + if let Some(sec_idx) = sym.section_index { + if sec_idx < obj.sections.len() { + let target_sec = &obj.sections[sec_idx]; + if !target_sec.name.starts_with(".text") + && !target_sec.name.starts_with(".rodata") + { + // Symbol is in a section we don't link (e.g., .bss) + // Skip this relocation - it's likely for internal use + continue; + } + } + } + pending_relocations.push(( offset + reloc.offset, sym.name.clone(), @@ -371,23 +392,53 @@ impl Linker { code_vaddr } else if section.name.starts_with(".rodata") { rodata_vaddr + } else if section.name.starts_with(".bss") || section.name.starts_with(".data") + { + // .bss and .data sections need to be placed after rodata + // For now, we don't support them - skip the relocation + // TODO: Add proper support for .bss/.data sections + continue; } else { - return Err(LinkError::UndefinedSymbol(sym_name)); + return Err(LinkError::UndefinedSymbol(format!( + "{} (in section '{}')", + sym_name, section.name + ))); }; base + sec_offset } else { - return Err(LinkError::UndefinedSymbol(sym_name)); + return Err(LinkError::UndefinedSymbol(format!( + "{} (section {} not in section_offsets)", + sym_name, sec_idx + ))); } } else { - return Err(LinkError::UndefinedSymbol(sym_name.clone())); + return Err(LinkError::UndefinedSymbol(format!( + "{} (no section, rel_type={:?})", + if sym_name.is_empty() { + "" + } else { + &sym_name + }, + rel_type + ))); }; let pc = code_vaddr + offset; let patch_offset = offset as usize; match rel_type { - RelocationType::Pc32 | RelocationType::Plt32 => { + RelocationType::Pc32 + | RelocationType::Plt32 + | RelocationType::GotPcRel + | RelocationType::GotPcRelX + | RelocationType::RexGotPcRelX => { // S + A - P, where S is symbol address, A is addend, P is place + // + // For GOT relocations (GotPcRel, GotPcRelX, RexGotPcRelX), we perform + // "GOT relaxation": instead of computing the address through the GOT, + // we directly compute the PC-relative offset to the symbol. + // This works because we're doing static linking and all symbols + // have known addresses at link time. let value = target_addr as i64 + addend - pc as i64; // Check for overflow: value must fit in i32 if value < i32::MIN as i64 || value > i32::MAX as i64 { diff --git a/crates/rue-parser/src/ast.rs b/crates/rue-parser/src/ast.rs index 0690c080..7147537a 100644 --- a/crates/rue-parser/src/ast.rs +++ b/crates/rue-parser/src/ast.rs @@ -41,6 +41,7 @@ pub enum Item { Function(Function), Struct(StructDecl), Enum(EnumDecl), + Impl(ImplBlock), } /// A struct declaration. @@ -85,6 +86,43 @@ pub struct EnumVariant { pub span: Span, } +/// An impl block containing methods for a type. +#[derive(Debug, Clone)] +pub struct ImplBlock { + /// The type this impl block is for + pub type_name: Ident, + /// Methods in this impl block + pub methods: Vec, + /// Span covering the entire impl block + pub span: Span, +} + +/// A method definition in an impl block. +#[derive(Debug, Clone)] +pub struct Method { + /// Directives applied to this method + pub directives: Vec, + /// Method name + pub name: Ident, + /// Whether this method takes self (None = associated function, Some = method with receiver) + pub receiver: Option, + /// Method parameters (excluding self) + pub params: Vec, + /// Return type (None means implicit unit `()`) + pub return_type: Option, + /// Method body + pub body: Expr, + /// Span covering the entire method + pub span: Span, +} + +/// A self parameter in a method. +#[derive(Debug, Clone)] +pub struct SelfParam { + /// Span covering the `self` keyword + pub span: Span, +} + /// A function definition. #[derive(Debug, Clone)] pub struct Function { @@ -209,6 +247,8 @@ pub enum Expr { StructLit(StructLitExpr), /// Field access (e.g., `point.x`) Field(FieldExpr), + /// Method call (e.g., `point.distance()`) + MethodCall(MethodCallExpr), /// Intrinsic call (e.g., `@dbg(42)`) IntrinsicCall(IntrinsicCallExpr), /// Array literal (e.g., `[1, 2, 3]`) @@ -217,6 +257,8 @@ pub enum Expr { Index(IndexExpr), /// Path expression (e.g., `Color::Red`) Path(PathExpr), + /// Associated function call (e.g., `Point::origin()`) + AssocFnCall(AssocFnCallExpr), } /// An integer literal. @@ -447,6 +489,18 @@ pub struct FieldExpr { pub span: Span, } +/// A method call expression (e.g., `point.distance()`). +#[derive(Debug, Clone)] +pub struct MethodCallExpr { + /// Base expression (the receiver) + pub receiver: Box, + /// Method name + pub method: Ident, + /// Arguments (excluding self) + pub args: Vec, + pub span: Span, +} + /// An array literal expression (e.g., `[1, 2, 3]`). #[derive(Debug, Clone)] pub struct ArrayLitExpr { @@ -475,6 +529,18 @@ pub struct PathExpr { pub span: Span, } +/// An associated function call expression (e.g., `Point::origin()`). +#[derive(Debug, Clone)] +pub struct AssocFnCallExpr { + /// The type name (e.g., `Point`) + pub type_name: Ident, + /// The function name (e.g., `origin`) + pub function: Ident, + /// Arguments + pub args: Vec, + pub span: Span, +} + /// A statement (does not produce a value). #[derive(Debug, Clone)] pub enum Statement { @@ -603,10 +669,12 @@ impl Expr { Expr::Return(return_expr) => return_expr.span, Expr::StructLit(struct_lit) => struct_lit.span, Expr::Field(field_expr) => field_expr.span, + Expr::MethodCall(method_call) => method_call.span, Expr::IntrinsicCall(intrinsic) => intrinsic.span, Expr::ArrayLit(array_lit) => array_lit.span, Expr::Index(index_expr) => index_expr.span, Expr::Path(path_expr) => path_expr.span, + Expr::AssocFnCall(assoc_fn_call) => assoc_fn_call.span, } } } @@ -632,6 +700,7 @@ impl<'a> fmt::Display for AstPrinter<'a> { Item::Function(func) => fmt_function(f, func, 0)?, Item::Struct(s) => fmt_struct(f, s, 0)?, Item::Enum(e) => fmt_enum(f, e, 0)?, + Item::Impl(impl_block) => fmt_impl_block(f, impl_block, 0)?, } } Ok(()) @@ -665,6 +734,40 @@ fn fmt_enum(f: &mut fmt::Formatter<'_>, e: &EnumDecl, level: usize) -> fmt::Resu Ok(()) } +fn fmt_impl_block(f: &mut fmt::Formatter<'_>, impl_block: &ImplBlock, level: usize) -> fmt::Result { + indent(f, level)?; + writeln!(f, "Impl {}", impl_block.type_name.name)?; + for method in &impl_block.methods { + fmt_method(f, method, level + 1)?; + } + Ok(()) +} + +fn fmt_method(f: &mut fmt::Formatter<'_>, method: &Method, level: usize) -> fmt::Result { + indent(f, level)?; + write!(f, "Method {}", method.name.name)?; + write!(f, "(")?; + if method.receiver.is_some() { + write!(f, "self")?; + if !method.params.is_empty() { + write!(f, ", ")?; + } + } + for (i, param) in method.params.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}: {}", param.name.name, param.ty)?; + } + write!(f, ")")?; + if let Some(ref ret) = method.return_type { + write!(f, " -> {}", ret)?; + } + writeln!(f)?; + fmt_expr(f, &method.body, level + 1)?; + Ok(()) +} + fn fmt_function(f: &mut fmt::Formatter<'_>, func: &Function, level: usize) -> fmt::Result { indent(f, level)?; write!(f, "Function {}", func.name.name)?; @@ -797,6 +900,20 @@ fn fmt_expr(f: &mut fmt::Formatter<'_>, expr: &Expr, level: usize) -> fmt::Resul writeln!(f, "Field .{}", field.field.name)?; fmt_expr(f, &field.base, level + 1) } + Expr::MethodCall(method_call) => { + writeln!(f, "MethodCall .{}", method_call.method.name)?; + indent(f, level + 1)?; + writeln!(f, "Receiver:")?; + fmt_expr(f, &method_call.receiver, level + 2)?; + if !method_call.args.is_empty() { + indent(f, level + 1)?; + writeln!(f, "Args:")?; + for arg in &method_call.args { + fmt_expr(f, arg, level + 2)?; + } + } + Ok(()) + } Expr::ArrayLit(array) => { writeln!(f, "ArrayLit")?; for elem in &array.elements { @@ -816,6 +933,18 @@ fn fmt_expr(f: &mut fmt::Formatter<'_>, expr: &Expr, level: usize) -> fmt::Resul Expr::Path(path) => { writeln!(f, "Path {}::{}", path.type_name.name, path.variant.name) } + Expr::AssocFnCall(assoc_fn_call) => { + writeln!( + f, + "AssocFnCall {}::{}", + assoc_fn_call.type_name.name, assoc_fn_call.function.name + )?; + for arg in &assoc_fn_call.args { + indent(f, level + 1)?; + fmt_expr(f, arg, level + 1)?; + } + Ok(()) + } } } diff --git a/crates/rue-parser/src/chumsky_parser.rs b/crates/rue-parser/src/chumsky_parser.rs index 34ccea44..872b7821 100644 --- a/crates/rue-parser/src/chumsky_parser.rs +++ b/crates/rue-parser/src/chumsky_parser.rs @@ -4,12 +4,13 @@ //! with Pratt parsing for expression precedence. use crate::ast::{ - ArrayLitExpr, AssignStatement, AssignTarget, Ast, BinaryExpr, BinaryOp, BlockExpr, BoolLit, - BreakExpr, CallExpr, ContinueExpr, Directive, DirectiveArg, EnumDecl, EnumVariant, Expr, - FieldDecl, FieldExpr, FieldInit, Function, Ident, IfExpr, IndexExpr, IntLit, IntrinsicArg, - IntrinsicCallExpr, Item, LetPattern, LetStatement, LoopExpr, MatchArm, MatchExpr, NegIntLit, - Param, ParenExpr, PathExpr, PathPattern, Pattern, ReturnExpr, Statement, StringLit, StructDecl, - StructLitExpr, TypeExpr, UnaryExpr, UnaryOp, UnitLit, WhileExpr, + ArrayLitExpr, AssignStatement, AssignTarget, AssocFnCallExpr, Ast, BinaryExpr, BinaryOp, + BlockExpr, BoolLit, BreakExpr, CallExpr, ContinueExpr, Directive, DirectiveArg, EnumDecl, + EnumVariant, Expr, FieldDecl, FieldExpr, FieldInit, Function, Ident, IfExpr, ImplBlock, + IndexExpr, IntLit, IntrinsicArg, IntrinsicCallExpr, Item, LetPattern, LetStatement, LoopExpr, + MatchArm, MatchExpr, Method, MethodCallExpr, NegIntLit, Param, ParenExpr, PathExpr, + PathPattern, Pattern, ReturnExpr, SelfParam, Statement, StringLit, StructDecl, StructLitExpr, + TypeExpr, UnaryExpr, UnaryOp, UnitLit, WhileExpr, }; use chumsky::input::{Input as ChumskyInput, Stream, ValueInput}; use chumsky::pratt::{infix, left, prefix}; @@ -36,6 +37,26 @@ where } } +/// Parser for primitive type keywords: i8, i16, i32, i64, u8, u16, u32, u64, bool +/// These are reserved keywords that cannot be used as identifiers. +fn primitive_type_parser<'src, I>() +-> impl Parser<'src, I, TypeExpr, extra::Err>> + Clone +where + I: ValueInput<'src, Token = TokenKind, Span = SimpleSpan>, +{ + select! { + TokenKind::I8 = e => TypeExpr::Named(Ident { name: "i8".to_string(), span: to_rue_span(e.span()) }), + TokenKind::I16 = e => TypeExpr::Named(Ident { name: "i16".to_string(), span: to_rue_span(e.span()) }), + TokenKind::I32 = e => TypeExpr::Named(Ident { name: "i32".to_string(), span: to_rue_span(e.span()) }), + TokenKind::I64 = e => TypeExpr::Named(Ident { name: "i64".to_string(), span: to_rue_span(e.span()) }), + TokenKind::U8 = e => TypeExpr::Named(Ident { name: "u8".to_string(), span: to_rue_span(e.span()) }), + TokenKind::U16 = e => TypeExpr::Named(Ident { name: "u16".to_string(), span: to_rue_span(e.span()) }), + TokenKind::U32 = e => TypeExpr::Named(Ident { name: "u32".to_string(), span: to_rue_span(e.span()) }), + TokenKind::U64 = e => TypeExpr::Named(Ident { name: "u64".to_string(), span: to_rue_span(e.span()) }), + TokenKind::Bool = e => TypeExpr::Named(Ident { name: "bool".to_string(), span: to_rue_span(e.span()) }), + } +} + /// Parser for type expressions: primitive types (i32, bool, etc.), () for unit, ! for never, or [T; N] for arrays fn type_parser<'src, I>() -> impl Parser<'src, I, TypeExpr, extra::Err>> + Clone @@ -66,19 +87,6 @@ where span: to_rue_span(e.span()), }); - // Primitive type keywords: i8, i16, i32, i64, u8, u16, u32, u64, bool - let primitive_type = select! { - TokenKind::I8 = e => TypeExpr::Named(Ident { name: "i8".to_string(), span: to_rue_span(e.span()) }), - TokenKind::I16 = e => TypeExpr::Named(Ident { name: "i16".to_string(), span: to_rue_span(e.span()) }), - TokenKind::I32 = e => TypeExpr::Named(Ident { name: "i32".to_string(), span: to_rue_span(e.span()) }), - TokenKind::I64 = e => TypeExpr::Named(Ident { name: "i64".to_string(), span: to_rue_span(e.span()) }), - TokenKind::U8 = e => TypeExpr::Named(Ident { name: "u8".to_string(), span: to_rue_span(e.span()) }), - TokenKind::U16 = e => TypeExpr::Named(Ident { name: "u16".to_string(), span: to_rue_span(e.span()) }), - TokenKind::U32 = e => TypeExpr::Named(Ident { name: "u32".to_string(), span: to_rue_span(e.span()) }), - TokenKind::U64 = e => TypeExpr::Named(Ident { name: "u64".to_string(), span: to_rue_span(e.span()) }), - TokenKind::Bool = e => TypeExpr::Named(Ident { name: "bool".to_string(), span: to_rue_span(e.span()) }), - }; - // Named type: user-defined types like MyStruct let named_type = ident_parser().map(TypeExpr::Named); @@ -86,7 +94,7 @@ where unit_type, never_type, array_type, - primitive_type, + primitive_type_parser(), named_type, )) }) @@ -569,12 +577,13 @@ where }) .boxed(); - // What can follow an identifier: call args, struct fields, path (::variant), or nothing + // What can follow an identifier: call args, struct fields, path (::variant), path call (::fn()), or nothing #[derive(Clone)] enum IdentSuffix { Call(Vec), StructLit(Vec), - Path(Ident), // ::Variant + Path(Ident), // ::Variant (for enum variants) + PathCall(Ident, Vec), // ::function() (for associated functions) None, } @@ -590,6 +599,14 @@ where field_inits_parser(expr.clone()) .delimited_by(just(TokenKind::LBrace), just(TokenKind::RBrace)) .map(IdentSuffix::StructLit), + // Associated function call: ::function(args) + just(TokenKind::ColonColon) + .ignore_then(ident_parser()) + .then( + args_parser(expr.clone()) + .delimited_by(just(TokenKind::LParen), just(TokenKind::RParen)), + ) + .map(|(func, args)| IdentSuffix::PathCall(func, args)), // Path: ::Variant (for enum variants) just(TokenKind::ColonColon) .ignore_then(ident_parser()) @@ -609,6 +626,12 @@ where fields, span: to_rue_span(e.span()), }), + IdentSuffix::PathCall(function, args) => Expr::AssocFnCall(AssocFnCallExpr { + type_name: name, + function, + args, + span: to_rue_span(e.span()), + }), IdentSuffix::Path(variant) => Expr::Path(PathExpr { type_name: name, variant, @@ -661,17 +684,7 @@ where }); // Primitive type keywords (these can't be variable names) - let primitive_type = select! { - TokenKind::I8 = e => IntrinsicArg::Type(TypeExpr::Named(Ident { name: "i8".to_string(), span: to_rue_span(e.span()) })), - TokenKind::I16 = e => IntrinsicArg::Type(TypeExpr::Named(Ident { name: "i16".to_string(), span: to_rue_span(e.span()) })), - TokenKind::I32 = e => IntrinsicArg::Type(TypeExpr::Named(Ident { name: "i32".to_string(), span: to_rue_span(e.span()) })), - TokenKind::I64 = e => IntrinsicArg::Type(TypeExpr::Named(Ident { name: "i64".to_string(), span: to_rue_span(e.span()) })), - TokenKind::U8 = e => IntrinsicArg::Type(TypeExpr::Named(Ident { name: "u8".to_string(), span: to_rue_span(e.span()) })), - TokenKind::U16 = e => IntrinsicArg::Type(TypeExpr::Named(Ident { name: "u16".to_string(), span: to_rue_span(e.span()) })), - TokenKind::U32 = e => IntrinsicArg::Type(TypeExpr::Named(Ident { name: "u32".to_string(), span: to_rue_span(e.span()) })), - TokenKind::U64 = e => IntrinsicArg::Type(TypeExpr::Named(Ident { name: "u64".to_string(), span: to_rue_span(e.span()) })), - TokenKind::Bool = e => IntrinsicArg::Type(TypeExpr::Named(Ident { name: "bool".to_string(), span: to_rue_span(e.span()) })), - }; + let primitive_type = primitive_type_parser().map(IntrinsicArg::Type); choice((unit_type, never_type, array_type, primitive_type)) }; @@ -729,25 +742,38 @@ where block_expr, )); - // Suffix for field access (.field) or indexing ([expr]) + // Suffix for field access (.field), method call (.method(args)), or indexing ([expr]) #[derive(Clone)] enum Suffix { Field(Ident), + MethodCall(Ident, Vec), Index(Expr), } + // Method call: .ident(args) + let method_call_suffix = just(TokenKind::Dot) + .ignore_then(ident_parser()) + .then( + args_parser(expr.clone()) + .delimited_by(just(TokenKind::LParen), just(TokenKind::RParen)), + ) + .map(|(method, args)| Suffix::MethodCall(method, args)); + + // Field access: .ident (but NOT followed by () let field_suffix = just(TokenKind::Dot) .ignore_then(ident_parser()) + .then_ignore(none_of([TokenKind::LParen]).rewind()) .map(Suffix::Field); let index_suffix = expr .delimited_by(just(TokenKind::LBracket), just(TokenKind::RBracket)) .map(Suffix::Index); - // Field access and indexing suffix: .field or [expr] - // Handles chains like a.b.c or a[0][1] or a[0].field + // Field access, method call, and indexing suffix: .field, .method(), or [expr] + // Method call must come before field access to catch .method(args) before .field + // Handles chains like a.b.c or a[0][1] or a[0].field or a.method().field primary.foldl( - choice((field_suffix, index_suffix)).repeated(), + choice((method_call_suffix, field_suffix, index_suffix)).repeated(), |base, suffix| match suffix { Suffix::Field(field) => { let span = Span::new(base.span().start, field.span.end); @@ -757,6 +783,22 @@ where span, }) } + Suffix::MethodCall(method, args) => { + let end = if args.is_empty() { + // Span ends after the closing paren, but we don't have that span + // We'll use the method name span end + 2 for "()" as approximation + method.span.end + 2 + } else { + args.last().unwrap().span().end + 1 + }; + let span = Span::new(base.span().start, end); + Expr::MethodCall(MethodCallExpr { + receiver: Box::new(base), + method, + args, + span, + }) + } Suffix::Index(index) => { let span = Span::new(base.span().start, index.span().end); Expr::Index(IndexExpr { @@ -1166,7 +1208,79 @@ where }) } -/// Parser for top-level items (functions, structs, and enums) +/// Parser for method definitions: [@directive]* fn name(self, params) -> Type { body } +/// Methods differ from functions in that they can have `self` as the first parameter. +fn method_parser<'src, I>() +-> impl Parser<'src, I, Method, extra::Err>> + Clone +where + I: ValueInput<'src, Token = TokenKind, Span = SimpleSpan>, +{ + let expr = expr_parser(); + + // Parse optional self parameter + let self_param = just(TokenKind::SelfValue).map_with(|_, e| SelfParam { + span: to_rue_span(e.span()), + }); + + // Parse self followed by optional regular params + let self_then_params = self_param + .then( + just(TokenKind::Comma) + .ignore_then(params_parser()) + .or_not() + .map(|opt| opt.unwrap_or_default()), + ) + .map(|(self_param, params)| (Some(self_param), params)); + + // Parse just regular params (no self) - this is an associated function + let just_params = params_parser().map(|params| (None, params)); + + // Try self first, then fall back to regular params + let params_with_optional_self = self_then_params.or(just_params); + + directives_parser() + .then(just(TokenKind::Fn).ignore_then(ident_parser())) + .then( + params_with_optional_self + .delimited_by(just(TokenKind::LParen), just(TokenKind::RParen)), + ) + .then(just(TokenKind::Arrow).ignore_then(type_parser()).or_not()) + .then(block_parser(expr)) + .map_with( + |((((directives, name), (receiver, params)), return_type), body), e| Method { + directives, + name, + receiver, + params, + return_type, + body, + span: to_rue_span(e.span()), + }, + ) +} + +/// Parser for impl blocks: impl Type { fn... } +fn impl_parser<'src, I>() +-> impl Parser<'src, I, ImplBlock, extra::Err>> + Clone +where + I: ValueInput<'src, Token = TokenKind, Span = SimpleSpan>, +{ + just(TokenKind::Impl) + .ignore_then(ident_parser()) + .then( + method_parser() + .repeated() + .collect::>() + .delimited_by(just(TokenKind::LBrace), just(TokenKind::RBrace)), + ) + .map_with(|(type_name, methods), e| ImplBlock { + type_name, + methods, + span: to_rue_span(e.span()), + }) +} + +/// Parser for top-level items (functions, structs, enums, and impl blocks) fn item_parser<'src, I>() -> impl Parser<'src, I, Item, extra::Err>> + Clone where I: ValueInput<'src, Token = TokenKind, Span = SimpleSpan>, @@ -1175,6 +1289,7 @@ where function_parser().map(Item::Function), struct_parser().map(Item::Struct), enum_parser().map(Item::Enum), + impl_parser().map(Item::Impl), )) } @@ -1296,6 +1411,7 @@ mod tests { }, Item::Struct(_) => panic!("parse_expr helper should only be used with functions"), Item::Enum(_) => panic!("parse_expr helper should only be used with functions"), + Item::Impl(_) => panic!("parse_expr helper should only be used with functions"), } } @@ -1321,6 +1437,7 @@ mod tests { } Item::Struct(_) => panic!("expected Function"), Item::Enum(_) => panic!("expected Function"), + Item::Impl(_) => panic!("expected Function"), } } @@ -1386,6 +1503,7 @@ mod tests { }, Item::Struct(_) => panic!("expected Function"), Item::Enum(_) => panic!("expected Function"), + Item::Impl(_) => panic!("expected Function"), } } diff --git a/crates/rue-parser/src/lib.rs b/crates/rue-parser/src/lib.rs index f7037978..7f34b5b5 100644 --- a/crates/rue-parser/src/lib.rs +++ b/crates/rue-parser/src/lib.rs @@ -8,8 +8,9 @@ mod chumsky_parser; pub use ast::{ ArrayLitExpr, AssignStatement, AssignTarget, Ast, BinaryExpr, BinaryOp, BlockExpr, CallExpr, Directive, DirectiveArg, EnumDecl, EnumVariant, Expr, FieldDecl, FieldExpr, FieldInit, - Function, Ident, IndexExpr, IntLit, IntrinsicArg, IntrinsicCallExpr, Item, LetPattern, - LetStatement, MatchArm, MatchExpr, Param, ParenExpr, PathExpr, PathPattern, Pattern, - ReturnExpr, Statement, StructDecl, StructLitExpr, TypeExpr, UnaryExpr, UnaryOp, WhileExpr, + Function, Ident, ImplBlock, IndexExpr, IntLit, IntrinsicArg, IntrinsicCallExpr, Item, + LetPattern, LetStatement, MatchArm, MatchExpr, Method, MethodCallExpr, Param, ParenExpr, + PathExpr, PathPattern, Pattern, ReturnExpr, SelfParam, Statement, StructDecl, StructLitExpr, + TypeExpr, UnaryExpr, UnaryOp, WhileExpr, }; pub use chumsky_parser::ChumskyParser as Parser; diff --git a/crates/rue-rir/src/astgen.rs b/crates/rue-rir/src/astgen.rs index 82e598de..65956a2b 100644 --- a/crates/rue-rir/src/astgen.rs +++ b/crates/rue-rir/src/astgen.rs @@ -9,8 +9,8 @@ use rue_intern::Interner; /// These intrinsics operate on types at compile time (e.g., @size_of(i32)). const TYPE_INTRINSICS: &[&str] = &["size_of", "align_of"]; use rue_parser::{ - AssignTarget, Ast, BinaryOp, Directive, DirectiveArg, EnumDecl, Expr, Function, IntrinsicArg, - Item, LetPattern, Pattern, Statement, StructDecl, TypeExpr, UnaryOp, + AssignTarget, Ast, BinaryOp, Directive, DirectiveArg, EnumDecl, Expr, Function, ImplBlock, + IntrinsicArg, Item, LetPattern, Method, Pattern, Statement, StructDecl, TypeExpr, UnaryOp, }; use crate::inst::{Inst, InstData, InstRef, Rir, RirDirective, RirPattern}; @@ -54,6 +54,11 @@ impl<'a> AstGen<'a> { Item::Enum(enum_decl) => { self.gen_enum(enum_decl); } + Item::Impl(impl_block) => { + // Impl blocks are handled in Phase 2 (RIR Generation) + // For now, store them for later processing by sema + self.gen_impl_block(impl_block); + } } } @@ -110,6 +115,61 @@ impl<'a> AstGen<'a> { }) } + fn gen_impl_block(&mut self, impl_block: &ImplBlock) -> InstRef { + let type_name = self.interner.intern(&impl_block.type_name.name); + + // Generate each method in the impl block + let methods: Vec<_> = impl_block + .methods + .iter() + .map(|m| self.gen_method(m)) + .collect(); + + self.rir.add_inst(Inst { + data: InstData::ImplDecl { type_name, methods }, + span: impl_block.span, + }) + } + + fn gen_method(&mut self, method: &Method) -> InstRef { + // Convert directives + let directives = self.convert_directives(&method.directives); + + // Intern the method name and return type + let name = self.interner.intern(&method.name.name); + let return_type = match &method.return_type { + Some(ty) => self.intern_type(ty), + None => self.interner.intern("()"), // Default to unit type + }; + + // Intern parameters (excluding self, which is handled specially by sema) + let params: Vec<_> = method + .params + .iter() + .map(|p| { + let param_name = self.interner.intern(&p.name.name); + let param_type = self.intern_type(&p.ty); + (param_name, param_type) + }) + .collect(); + + // Generate body expression + let body = self.gen_expr(&method.body); + + // For now, we emit methods as regular FnDecl instructions. + // Sema will handle the method resolution and self parameter. + self.rir.add_inst(Inst { + data: InstData::FnDecl { + directives, + name, + params, + return_type, + body, + }, + span: method.span, + }) + } + /// Convert AST directives to RIR directives fn convert_directives(&mut self, directives: &[Directive]) -> Vec { directives @@ -407,6 +467,38 @@ impl<'a> AstGen<'a> { span: path_expr.span, }) } + Expr::MethodCall(method_call) => { + let receiver = self.gen_expr(&method_call.receiver); + let method = self.interner.intern(&method_call.method.name); + let args: Vec<_> = method_call.args.iter().map(|a| self.gen_expr(a)).collect(); + + self.rir.add_inst(Inst { + data: InstData::MethodCall { + receiver, + method, + args, + }, + span: method_call.span, + }) + } + Expr::AssocFnCall(assoc_fn_call) => { + let type_name = self.interner.intern(&assoc_fn_call.type_name.name); + let function = self.interner.intern(&assoc_fn_call.function.name); + let args: Vec<_> = assoc_fn_call + .args + .iter() + .map(|a| self.gen_expr(a)) + .collect(); + + self.rir.add_inst(Inst { + data: InstData::AssocFnCall { + type_name, + function, + args, + }, + span: assoc_fn_call.span, + }) + } } } diff --git a/crates/rue-rir/src/inst.rs b/crates/rue-rir/src/inst.rs index e989d18a..b83166a8 100644 --- a/crates/rue-rir/src/inst.rs +++ b/crates/rue-rir/src/inst.rs @@ -405,6 +405,35 @@ pub enum InstData { /// Value to store value: InstRef, }, + + // Method operations (preview: methods) + /// Impl block declaration + ImplDecl { + /// Type name this impl block is for + type_name: Symbol, + /// Methods defined in this impl block (references to FnDecl instructions) + methods: Vec, + }, + + /// Method call: receiver.method(args) + MethodCall { + /// Receiver expression (the struct value) + receiver: InstRef, + /// Method name + method: Symbol, + /// Argument instruction refs + args: Vec, + }, + + /// Associated function call: Type::function(args) + AssocFnCall { + /// Type name (e.g., Point) + type_name: Symbol, + /// Function name (e.g., origin) + function: Symbol, + /// Argument instruction refs + args: Vec, + }, } impl fmt::Display for InstRef { @@ -710,6 +739,45 @@ impl<'a, 'b> RirPrinter<'a, 'b> { InstData::IndexSet { base, index, value } => { out.push_str(&format!("index_set {}[{}] = {}\n", base, index, value)); } + InstData::ImplDecl { type_name, methods } => { + let type_str = self.interner.get(*type_name); + let methods_str: Vec = + methods.iter().map(|m| format!("{}", m)).collect(); + out.push_str(&format!( + "impl {} {{ {} }}\n", + type_str, + methods_str.join(", ") + )); + } + InstData::MethodCall { + receiver, + method, + args, + } => { + let method_str = self.interner.get(*method); + let args_str: Vec = args.iter().map(|a| format!("{}", a)).collect(); + out.push_str(&format!( + "method_call {}.{}({})\n", + receiver, + method_str, + args_str.join(", ") + )); + } + InstData::AssocFnCall { + type_name, + function, + args, + } => { + let type_str = self.interner.get(*type_name); + let func_str = self.interner.get(*function); + let args_str: Vec = args.iter().map(|a| format!("{}", a)).collect(); + out.push_str(&format!( + "assoc_fn_call {}::{}({})\n", + type_str, + func_str, + args_str.join(", ") + )); + } } } out diff --git a/crates/rue-runtime/BUCK b/crates/rue-runtime/BUCK index 447ad05c..ff18f882 100644 --- a/crates/rue-runtime/BUCK +++ b/crates/rue-runtime/BUCK @@ -9,6 +9,13 @@ rust_library( "-Cpanic=abort", "-Copt-level=z", "-Clto=true", + # Use static relocation model to avoid GOT-relative relocations + # that our simple linker doesn't support + "-Crelocation-model=static", + # Disable LSE atomics on aarch64 to avoid __aarch64_have_lse_atomics + # runtime detection symbol from compiler-rt that we don't have. + # We need both -lse (the feature) and -lse2 (newer extension). + "-Ctarget-feature=-lse,-lse2,-outline-atomics", ], visibility = ["PUBLIC"], ) diff --git a/crates/rue-runtime/src/aarch64_linux.rs b/crates/rue-runtime/src/aarch64_linux.rs index 3e73f1c5..9e7b7300 100644 --- a/crates/rue-runtime/src/aarch64_linux.rs +++ b/crates/rue-runtime/src/aarch64_linux.rs @@ -33,6 +33,12 @@ const SYS_EXIT: u64 = 93; /// Linux aarch64 syscall number for write (from asm-generic/unistd.h). const SYS_WRITE: u64 = 64; +/// Linux aarch64 syscall number for mmap (from asm-generic/unistd.h). +const SYS_MMAP: u64 = 222; + +/// Linux aarch64 syscall number for munmap (from asm-generic/unistd.h). +const SYS_MUNMAP: u64 = 215; + /// Standard error file descriptor. const STDERR: u64 = 2; @@ -201,6 +207,92 @@ pub fn print_bool(value: bool) { } } +/// Map anonymous memory pages. +/// +/// This is a wrapper around the Linux `mmap(2)` syscall configured for +/// anonymous private memory allocation (no file backing). +/// +/// # Arguments +/// +/// * `size` - Number of bytes to allocate. Will be rounded up to page size by the kernel. +/// +/// # Returns +/// +/// On success, returns a pointer to the mapped memory region. +/// On error, returns a null pointer. +/// +/// # Memory Protection +/// +/// The mapped region is readable and writable (PROT_READ | PROT_WRITE). +/// +/// # Safety +/// +/// The returned pointer (if non-null) points to valid, zero-initialized memory. +/// The caller is responsible for calling `munmap` when done. +pub fn mmap(size: usize) -> *mut u8 { + // mmap flags + const PROT_READ: u64 = 0x1; + const PROT_WRITE: u64 = 0x2; + const MAP_PRIVATE: u64 = 0x02; + const MAP_ANONYMOUS: u64 = 0x20; + + let result: i64; + // SAFETY: mmap syscall with anonymous mapping is safe. + // We're requesting private anonymous memory with read/write permissions. + unsafe { + asm!( + "svc #0", + in("x8") SYS_MMAP, + inlateout("x0") 0u64 => result, // addr: NULL (let kernel choose) + in("x1") size, // length + in("x2") PROT_READ | PROT_WRITE, // prot + in("x3") MAP_PRIVATE | MAP_ANONYMOUS, // flags + in("x4") -1i64 as u64, // fd: -1 for anonymous + in("x5") 0u64, // offset: 0 + ); + } + + // mmap returns MAP_FAILED (-1 as usize) on error + if result < 0 { + core::ptr::null_mut() + } else { + result as *mut u8 + } +} + +/// Unmap memory pages previously mapped with `mmap`. +/// +/// This is a wrapper around the Linux `munmap(2)` syscall. +/// +/// # Arguments +/// +/// * `addr` - Pointer to the start of the mapped region (must be page-aligned) +/// * `size` - Size of the region to unmap (will be rounded up to page size) +/// +/// # Returns +/// +/// Returns 0 on success, or a negative errno on failure. +/// +/// # Safety +/// +/// The caller must ensure: +/// - `addr` was returned by a previous `mmap` call +/// - `size` matches the size used in the `mmap` call +/// - The memory is not accessed after this call +pub fn munmap(addr: *mut u8, size: usize) -> i64 { + let result: i64; + // SAFETY: munmap is safe if addr/size are valid from a previous mmap. + unsafe { + asm!( + "svc #0", + in("x8") SYS_MUNMAP, + inlateout("x0") addr => result, + in("x1") size, + ); + } + result +} + /// Exit the process with the given status code. /// /// This performs a direct syscall to `exit(2)` and never returns. @@ -268,7 +360,77 @@ mod tests { // Verify our syscall numbers match Linux aarch64 assert_eq!(SYS_EXIT, 93); assert_eq!(SYS_WRITE, 64); + assert_eq!(SYS_MMAP, 222); + assert_eq!(SYS_MUNMAP, 215); assert_eq!(STDERR, 2); assert_eq!(STDOUT, 1); } + + #[test] + fn test_mmap_basic() { + // Allocate a page of memory + let size = 4096; + let ptr = mmap(size); + assert!(!ptr.is_null()); + + // Memory should be zero-initialized and writable + unsafe { + assert_eq!(*ptr, 0); + *ptr = 42; + assert_eq!(*ptr, 42); + } + + // Clean up + let result = munmap(ptr, size); + assert_eq!(result, 0); + } + + #[test] + fn test_mmap_large() { + // Allocate 1 MB + let size = 1024 * 1024; + let ptr = mmap(size); + assert!(!ptr.is_null()); + + // Write to first and last bytes + unsafe { + *ptr = 1; + *ptr.add(size - 1) = 2; + assert_eq!(*ptr, 1); + assert_eq!(*ptr.add(size - 1), 2); + } + + let result = munmap(ptr, size); + assert_eq!(result, 0); + } + + #[test] + fn test_mmap_multiple() { + // Allocate multiple regions + let size = 4096; + let ptr1 = mmap(size); + let ptr2 = mmap(size); + let ptr3 = mmap(size); + + assert!(!ptr1.is_null()); + assert!(!ptr2.is_null()); + assert!(!ptr3.is_null()); + + // They should be different addresses + assert_ne!(ptr1, ptr2); + assert_ne!(ptr2, ptr3); + assert_ne!(ptr1, ptr3); + + // Clean up all + assert_eq!(munmap(ptr1, size), 0); + assert_eq!(munmap(ptr2, size), 0); + assert_eq!(munmap(ptr3, size), 0); + } + + #[test] + fn test_mmap_zero_size() { + // Zero-size mmap should fail (returns EINVAL on Linux) + let ptr = mmap(0); + assert!(ptr.is_null()); + } } diff --git a/crates/rue-runtime/src/aarch64_macos.rs b/crates/rue-runtime/src/aarch64_macos.rs index dda5dcc0..7285d392 100644 --- a/crates/rue-runtime/src/aarch64_macos.rs +++ b/crates/rue-runtime/src/aarch64_macos.rs @@ -34,6 +34,12 @@ const SYS_EXIT: u64 = 1; /// macOS syscall number for write (SYS_write). const SYS_WRITE: u64 = 4; +/// macOS syscall number for mmap (SYS_mmap). +const SYS_MMAP: u64 = 197; + +/// macOS syscall number for munmap (SYS_munmap). +const SYS_MUNMAP: u64 = 73; + /// Standard error file descriptor. const STDERR: u64 = 2; @@ -210,6 +216,107 @@ pub fn print_bool(value: bool) { } } +/// Map anonymous memory pages. +/// +/// This is a wrapper around the macOS `mmap(2)` syscall configured for +/// anonymous private memory allocation (no file backing). +/// +/// # Arguments +/// +/// * `size` - Number of bytes to allocate. Will be rounded up to page size by the kernel. +/// +/// # Returns +/// +/// On success, returns a pointer to the mapped memory region. +/// On error, returns a null pointer. +/// +/// # Memory Protection +/// +/// The mapped region is readable and writable (PROT_READ | PROT_WRITE). +/// +/// # Safety +/// +/// The returned pointer (if non-null) points to valid, zero-initialized memory. +/// The caller is responsible for calling `munmap` when done. +pub fn mmap(size: usize) -> *mut u8 { + // mmap flags (same values as Linux/BSD) + const PROT_READ: u64 = 0x1; + const PROT_WRITE: u64 = 0x2; + const MAP_PRIVATE: u64 = 0x02; + const MAP_ANONYMOUS: u64 = 0x1000; // Note: macOS uses 0x1000, not 0x20 like Linux + + let result: i64; + let err_flag: u64; + + // SAFETY: mmap syscall with anonymous mapping is safe. + // We're requesting private anonymous memory with read/write permissions. + unsafe { + asm!( + "svc #0x80", + // Check carry flag for error + "cset {err}, cs", + inlateout("x16") SYS_MMAP => _, + in("x0") 0u64, // addr: NULL (let kernel choose) + in("x1") size, // length + in("x2") PROT_READ | PROT_WRITE, // prot + in("x3") MAP_PRIVATE | MAP_ANONYMOUS, // flags + in("x4") -1i64 as u64, // fd: -1 for anonymous + in("x5") 0u64, // offset: 0 + lateout("x0") result, + err = out(reg) err_flag, + out("x17") _, + ); + } + + // If carry flag was set, syscall failed + if err_flag != 0 { + core::ptr::null_mut() + } else { + result as *mut u8 + } +} + +/// Unmap memory pages previously mapped with `mmap`. +/// +/// This is a wrapper around the macOS `munmap(2)` syscall. +/// +/// # Arguments +/// +/// * `addr` - Pointer to the start of the mapped region (must be page-aligned) +/// * `size` - Size of the region to unmap (will be rounded up to page size) +/// +/// # Returns +/// +/// Returns 0 on success, or a negative errno on failure. +/// +/// # Safety +/// +/// The caller must ensure: +/// - `addr` was returned by a previous `mmap` call +/// - `size` matches the size used in the `mmap` call +/// - The memory is not accessed after this call +pub fn munmap(addr: *mut u8, size: usize) -> i64 { + let result: i64; + let err_flag: u64; + + // SAFETY: munmap is safe if addr/size are valid from a previous mmap. + unsafe { + asm!( + "svc #0x80", + "cset {err}, cs", + inlateout("x16") SYS_MUNMAP => _, + in("x0") addr, + in("x1") size, + lateout("x0") result, + err = out(reg) err_flag, + out("x17") _, + ); + } + + // If carry flag was set, result is errno (positive), negate it + if err_flag != 0 { -result } else { result } +} + /// Exit the process with the given status code. /// /// This performs a direct syscall to `exit(2)` and never returns. @@ -277,7 +384,77 @@ mod tests { // Verify our syscall numbers match macOS assert_eq!(SYS_EXIT, 1); assert_eq!(SYS_WRITE, 4); + assert_eq!(SYS_MMAP, 197); + assert_eq!(SYS_MUNMAP, 73); assert_eq!(STDERR, 2); assert_eq!(STDOUT, 1); } + + #[test] + fn test_mmap_basic() { + // Allocate a page of memory + let size = 4096; + let ptr = mmap(size); + assert!(!ptr.is_null()); + + // Memory should be zero-initialized and writable + unsafe { + assert_eq!(*ptr, 0); + *ptr = 42; + assert_eq!(*ptr, 42); + } + + // Clean up + let result = munmap(ptr, size); + assert_eq!(result, 0); + } + + #[test] + fn test_mmap_large() { + // Allocate 1 MB + let size = 1024 * 1024; + let ptr = mmap(size); + assert!(!ptr.is_null()); + + // Write to first and last bytes + unsafe { + *ptr = 1; + *ptr.add(size - 1) = 2; + assert_eq!(*ptr, 1); + assert_eq!(*ptr.add(size - 1), 2); + } + + let result = munmap(ptr, size); + assert_eq!(result, 0); + } + + #[test] + fn test_mmap_multiple() { + // Allocate multiple regions + let size = 4096; + let ptr1 = mmap(size); + let ptr2 = mmap(size); + let ptr3 = mmap(size); + + assert!(!ptr1.is_null()); + assert!(!ptr2.is_null()); + assert!(!ptr3.is_null()); + + // They should be different addresses + assert_ne!(ptr1, ptr2); + assert_ne!(ptr2, ptr3); + assert_ne!(ptr1, ptr3); + + // Clean up all + assert_eq!(munmap(ptr1, size), 0); + assert_eq!(munmap(ptr2, size), 0); + assert_eq!(munmap(ptr3, size), 0); + } + + #[test] + fn test_mmap_zero_size() { + // Zero-size mmap should fail (returns EINVAL on macOS) + let ptr = mmap(0); + assert!(ptr.is_null()); + } } diff --git a/crates/rue-runtime/src/heap.rs b/crates/rue-runtime/src/heap.rs new file mode 100644 index 00000000..8516969e --- /dev/null +++ b/crates/rue-runtime/src/heap.rs @@ -0,0 +1,644 @@ +//! Heap allocation for Rue programs. +//! +//! This module provides a simple bump allocator backed by `mmap` for heap +//! allocation. It's designed for simplicity over memory efficiency - memory +//! is only returned to the OS when the program exits. +//! +//! # Design +//! +//! The allocator uses a series of "arenas" - large chunks of memory obtained +//! from the OS via `mmap`. Allocations bump a pointer forward within the +//! current arena. When an arena fills up, a new one is allocated. +//! +//! ```text +//! +------------------+------------------+ +//! | Arena 1 | Arena 2 | +//! | [alloc][alloc] | [alloc][...free] | +//! +------------------+------------------+ +//! ^ +//! bump pointer +//! ``` +//! +//! # Thread Safety +//! +//! This allocator uses atomic operations for the global arena pointer, making +//! it safe to use from multiple threads. However, concurrent allocations may +//! contend on the bump pointer within an arena. For Rue V1 (single-threaded), +//! this is fine. For heavily concurrent workloads, a thread-local allocator +//! would be more efficient. +//! +//! # Memory Efficiency +//! +//! Individual `free()` calls are no-ops. Memory is only reclaimed when: +//! - The program exits (OS reclaims all memory) +//! - A future allocator implementation adds actual freeing +//! +//! This is a deliberate trade-off: simplicity over memory efficiency. +//! Rue programs are typically short-lived, so memory waste is acceptable. + +use crate::platform; +use core::ptr; +use core::sync::atomic::{AtomicPtr, AtomicUsize, Ordering}; + +/// Default arena size: 64 KiB +/// +/// This is chosen to be: +/// - Large enough to reduce mmap syscall overhead +/// - Small enough to not waste too much memory for small programs +/// - A multiple of typical page sizes (4 KiB) +const DEFAULT_ARENA_SIZE: usize = 64 * 1024; + +/// Header at the start of each arena. +/// +/// Arenas are linked together in a singly-linked list. This header +/// sits at the very beginning of each mmap'd region. +/// +/// The `offset` field is atomic to allow lock-free bump allocation. +#[repr(C)] +struct ArenaHeader { + /// Pointer to the next arena in the list (older arenas). + /// Only written during arena creation, so doesn't need to be atomic. + next: *mut ArenaHeader, + /// Total size of this arena (including header). + /// Immutable after creation. + size: usize, + /// Current allocation offset from the start of the arena. + /// Starts after the header, bumps forward with each allocation. + /// Atomic to support lock-free concurrent allocation. + offset: AtomicUsize, +} + +/// Global pointer to the current arena. +/// +/// This is the head of a linked list of arenas. New arenas are prepended +/// to the list. Using `AtomicPtr` makes this safe to access from multiple +/// threads without any unsafe `Sync` implementations. +static CURRENT_ARENA: AtomicPtr = AtomicPtr::new(ptr::null_mut()); + +/// Align a value up to the given alignment. +/// +/// # Panics +/// +/// Alignment must be a power of 2. This is not checked at runtime +/// for performance - callers must ensure valid alignment. +#[inline] +const fn align_up(value: usize, align: usize) -> usize { + (value + align - 1) & !(align - 1) +} + +/// Check if a value is a power of 2. +#[inline] +const fn is_power_of_two(n: u64) -> bool { + n != 0 && (n & (n - 1)) == 0 +} + +/// Allocate a new arena of at least `min_size` bytes. +/// +/// Returns a pointer to the arena header, or null on failure. +fn alloc_arena(min_size: usize) -> *mut ArenaHeader { + // Ensure we have room for the header plus the requested size + let header_size = core::mem::size_of::(); + + // Use checked_add to prevent overflow + let Some(total) = header_size.checked_add(min_size) else { + return ptr::null_mut(); + }; + let total_size = align_up(total, 4096); // Page-align + + // Use at least the default arena size + let arena_size = if total_size < DEFAULT_ARENA_SIZE { + DEFAULT_ARENA_SIZE + } else { + total_size + }; + + // Request memory from the OS + let ptr = platform::mmap(arena_size); + if ptr.is_null() { + return ptr::null_mut(); + } + + // Initialize the header + let header = ptr as *mut ArenaHeader; + // SAFETY: ptr is valid and properly aligned (mmap returns page-aligned memory) + unsafe { + (*header).next = ptr::null_mut(); + (*header).size = arena_size; + (*header).offset = AtomicUsize::new(header_size); // Start allocations after header + } + + header +} + +/// Allocate memory from the heap. +/// +/// # Arguments +/// +/// * `size` - Number of bytes to allocate +/// * `align` - Required alignment (must be a power of 2) +/// +/// # Returns +/// +/// A pointer to the allocated memory, or null on failure. +/// The memory is zero-initialized (from mmap). +/// +/// # Failure Conditions +/// +/// Returns null if: +/// - `size` is 0 +/// - `align` is 0 or not a power of 2 +/// - Out of memory (mmap fails) +/// +/// # Safety +/// +/// The returned pointer (if non-null) is valid and properly aligned. +/// The memory remains valid until the program exits. +pub fn alloc(size: u64, align: u64) -> *mut u8 { + // Validate arguments + if size == 0 || align == 0 || !is_power_of_two(align) { + return ptr::null_mut(); + } + + let size = size as usize; + let align = align as usize; + + loop { + // Load current arena + let arena = CURRENT_ARENA.load(Ordering::Acquire); + + if arena.is_null() { + // No arena yet - try to create one + let new_arena = alloc_arena(size); + if new_arena.is_null() { + return ptr::null_mut(); + } + + // Try to install our new arena as the current one + match CURRENT_ARENA.compare_exchange( + ptr::null_mut(), + new_arena, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => { + // We installed the arena, now allocate from it + return alloc_from_arena(new_arena, size, align); + } + Err(_) => { + // Someone else beat us - free our arena and retry + // SAFETY: new_arena was just created by us and not shared + unsafe { + platform::munmap(new_arena as *mut u8, (*new_arena).size); + } + continue; + } + } + } + + // Try to allocate from the current arena + // SAFETY: arena is non-null and points to valid memory from mmap + unsafe { + let arena_size = (*arena).size; + + // Use compare-and-swap loop to atomically bump the offset + loop { + let current_offset = (*arena).offset.load(Ordering::Relaxed); + + // Calculate aligned offset with overflow check + let aligned_offset = align_up(current_offset, align); + let Some(new_offset) = aligned_offset.checked_add(size) else { + return ptr::null_mut(); + }; + + if new_offset > arena_size { + // Doesn't fit - need a new arena + break; + } + + // Try to claim this space + match (*arena).offset.compare_exchange_weak( + current_offset, + new_offset, + Ordering::Relaxed, + Ordering::Relaxed, + ) { + Ok(_) => { + // Success! Return the allocated memory + return (arena as *mut u8).add(aligned_offset); + } + Err(_) => { + // Someone else modified the offset, retry + continue; + } + } + } + + // Allocation doesn't fit in current arena - create a new one + let new_arena = alloc_arena(size); + if new_arena.is_null() { + return ptr::null_mut(); + } + + // Link new arena to the old one + (*new_arena).next = arena; + + // Try to install our new arena as the current one + match CURRENT_ARENA.compare_exchange( + arena, + new_arena, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => { + // We installed the arena, now allocate from it + return alloc_from_arena(new_arena, size, align); + } + Err(_) => { + // Someone else changed the arena - free ours and retry + platform::munmap(new_arena as *mut u8, (*new_arena).size); + continue; + } + } + } + } +} + +/// Allocate from a freshly-created arena (no contention possible). +/// +/// This is a helper for the common case where we just created an arena +/// and know we're the only thread using it. +fn alloc_from_arena(arena: *mut ArenaHeader, size: usize, align: usize) -> *mut u8 { + // SAFETY: arena is valid and we have exclusive access (just created it) + unsafe { + let header_size = core::mem::size_of::(); + let aligned_offset = align_up(header_size, align); + // Note: overflow is impossible here because alloc_arena already ensured + // the arena is large enough for header_size + size (with alignment padding) + let new_offset = aligned_offset + size; + (*arena).offset.store(new_offset, Ordering::Relaxed); + (arena as *mut u8).add(aligned_offset) + } +} + +/// Free memory previously allocated by `alloc`. +/// +/// # Arguments +/// +/// * `ptr` - Pointer to the memory to free +/// * `size` - Size of the allocation (for future compatibility) +/// * `align` - Alignment of the allocation (for future compatibility) +/// +/// # Current Implementation +/// +/// This is a **no-op** in the current bump allocator. Memory is only +/// reclaimed when the program exits. The `size` and `align` parameters +/// are accepted for API compatibility with future allocators that may +/// actually free memory. +/// +/// # Safety +/// +/// The caller should ensure `ptr` was returned by a previous `alloc` call, +/// but since this is a no-op, invalid pointers are harmless. +#[allow(unused_variables)] +pub fn free(ptr: *mut u8, size: u64, align: u64) { + // No-op for bump allocator. + // Memory is reclaimed when the program exits. + // + // Future allocators may implement actual freeing by: + // 1. Finding which arena the pointer belongs to + // 2. Marking the region as free in a freelist + // 3. Potentially unmapping arenas when fully free +} + +/// Reallocate memory to a new size. +/// +/// # Arguments +/// +/// * `ptr` - Pointer to the existing allocation (or null for new allocation) +/// * `old_size` - Size of the existing allocation (ignored if ptr is null) +/// * `new_size` - Desired new size +/// * `align` - Required alignment (must be a power of 2) +/// +/// # Returns +/// +/// A pointer to the reallocated memory, or null on failure. +/// +/// # Behavior +/// +/// - If `ptr` is null: behaves like `alloc(new_size, align)` +/// - If `new_size` is 0: behaves like `free(ptr, old_size, align)`, returns null +/// - If `new_size <= old_size`: returns `ptr` unchanged (no shrinking needed) +/// - If `new_size > old_size`: allocates new block, copies old data, returns new pointer +/// +/// # Memory Contents +/// +/// When growing, the contents up to `min(old_size, new_size)` are preserved. +/// Any additional bytes are zero-initialized (from mmap). +/// +/// # Safety +/// +/// The old pointer becomes invalid after a successful realloc that changes +/// the address. The caller must use the returned pointer. +pub fn realloc(ptr: *mut u8, old_size: u64, new_size: u64, align: u64) -> *mut u8 { + // Handle null pointer case - just allocate + if ptr.is_null() { + return alloc(new_size, align); + } + + // Handle zero new_size - just free + if new_size == 0 { + free(ptr, old_size, align); + return ptr::null_mut(); + } + + // Validate alignment + if align == 0 || !is_power_of_two(align) { + return ptr::null_mut(); + } + + // If shrinking or same size, just return the original pointer + // (bump allocator can't reclaim the extra space anyway) + if new_size <= old_size { + return ptr; + } + + // Need to grow - allocate new block and copy + let new_ptr = alloc(new_size, align); + if new_ptr.is_null() { + return ptr::null_mut(); + } + + // Copy old data to new location + // SAFETY: Both pointers are valid, and we're copying old_size bytes + // which is guaranteed to be <= both allocation sizes + unsafe { + ptr::copy_nonoverlapping(ptr, new_ptr, old_size as usize); + } + + // Note: We don't free the old pointer since free is a no-op. + // The old memory is "wasted" until program exit. + + new_ptr +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_align_up() { + assert_eq!(align_up(0, 8), 0); + assert_eq!(align_up(1, 8), 8); + assert_eq!(align_up(7, 8), 8); + assert_eq!(align_up(8, 8), 8); + assert_eq!(align_up(9, 8), 16); + assert_eq!(align_up(15, 16), 16); + assert_eq!(align_up(16, 16), 16); + assert_eq!(align_up(17, 16), 32); + } + + #[test] + fn test_is_power_of_two() { + assert!(!is_power_of_two(0)); + assert!(is_power_of_two(1)); + assert!(is_power_of_two(2)); + assert!(!is_power_of_two(3)); + assert!(is_power_of_two(4)); + assert!(!is_power_of_two(5)); + assert!(is_power_of_two(8)); + assert!(is_power_of_two(16)); + assert!(is_power_of_two(4096)); + assert!(!is_power_of_two(4097)); + } + + #[test] + fn test_alloc_zero_size() { + let ptr = alloc(0, 8); + assert!(ptr.is_null()); + } + + #[test] + fn test_alloc_zero_align() { + let ptr = alloc(16, 0); + assert!(ptr.is_null()); + } + + #[test] + fn test_alloc_bad_align() { + // 3 is not a power of 2 + let ptr = alloc(16, 3); + assert!(ptr.is_null()); + } + + #[test] + fn test_alloc_basic() { + let ptr = alloc(64, 8); + assert!(!ptr.is_null()); + assert_eq!(ptr as usize % 8, 0); // Check alignment + + // Memory should be usable + unsafe { + *ptr = 42; + assert_eq!(*ptr, 42); + } + } + + #[test] + fn test_alloc_alignment() { + // Test various alignments + for align in [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 4096] { + let ptr = alloc(64, align); + assert!(!ptr.is_null(), "alloc failed for align={}", align); + assert_eq!( + ptr as usize % align as usize, + 0, + "bad alignment for align={}", + align + ); + } + } + + #[test] + fn test_alloc_multiple() { + // Allocate multiple blocks + let mut ptrs = [ptr::null_mut(); 10]; + for i in 0..10 { + ptrs[i] = alloc(128, 8); + assert!(!ptrs[i].is_null()); + } + + // All pointers should be different + for i in 0..10 { + for j in (i + 1)..10 { + assert_ne!(ptrs[i], ptrs[j]); + } + } + + // All should be usable + for (i, ptr) in ptrs.iter().enumerate() { + unsafe { + **ptr = i as u8; + } + } + for (i, ptr) in ptrs.iter().enumerate() { + unsafe { + assert_eq!(**ptr, i as u8); + } + } + } + + #[test] + fn test_alloc_large() { + // Allocate more than the default arena size + let large_size = DEFAULT_ARENA_SIZE as u64 * 2; + let ptr = alloc(large_size, 8); + assert!(!ptr.is_null()); + + // Should be usable across the entire range + unsafe { + *ptr = 1; + *ptr.add(large_size as usize - 1) = 2; + assert_eq!(*ptr, 1); + assert_eq!(*ptr.add(large_size as usize - 1), 2); + } + } + + #[test] + fn test_free_is_noop() { + // Free should not crash even with various inputs + let ptr = alloc(64, 8); + assert!(!ptr.is_null()); + + // Free multiple times (should be fine since it's a no-op) + free(ptr, 64, 8); + free(ptr, 64, 8); + free(ptr::null_mut(), 0, 0); + } + + #[test] + fn test_realloc_null_ptr() { + // realloc with null ptr should behave like alloc + let ptr = realloc(ptr::null_mut(), 0, 64, 8); + assert!(!ptr.is_null()); + assert_eq!(ptr as usize % 8, 0); + } + + #[test] + fn test_realloc_zero_size() { + // realloc with zero new_size should behave like free + let ptr = alloc(64, 8); + assert!(!ptr.is_null()); + + let result = realloc(ptr, 64, 0, 8); + assert!(result.is_null()); + } + + #[test] + fn test_realloc_shrink() { + // Shrinking should return the same pointer + let ptr = alloc(128, 8); + assert!(!ptr.is_null()); + + // Write some data + unsafe { + *ptr = 42; + } + + // Shrink - should return same pointer + let new_ptr = realloc(ptr, 128, 64, 8); + assert_eq!(new_ptr, ptr); + + // Data should still be there + unsafe { + assert_eq!(*new_ptr, 42); + } + } + + #[test] + fn test_realloc_same_size() { + // Same size should return same pointer + let ptr = alloc(64, 8); + assert!(!ptr.is_null()); + + unsafe { + *ptr = 99; + } + + let new_ptr = realloc(ptr, 64, 64, 8); + assert_eq!(new_ptr, ptr); + + unsafe { + assert_eq!(*new_ptr, 99); + } + } + + #[test] + fn test_realloc_grow() { + // Growing should return new pointer with copied data + let ptr = alloc(32, 8); + assert!(!ptr.is_null()); + + // Write pattern to original + unsafe { + for i in 0..32 { + *ptr.add(i) = i as u8; + } + } + + // Grow + let new_ptr = realloc(ptr, 32, 128, 8); + assert!(!new_ptr.is_null()); + assert_eq!(new_ptr as usize % 8, 0); + + // Verify original data was copied + unsafe { + for i in 0..32 { + assert_eq!(*new_ptr.add(i), i as u8, "byte {} not copied", i); + } + } + + // New area should be writable + unsafe { + *new_ptr.add(100) = 0xFF; + assert_eq!(*new_ptr.add(100), 0xFF); + } + } + + #[test] + fn test_realloc_bad_align() { + let ptr = alloc(64, 8); + assert!(!ptr.is_null()); + + // Bad alignment should fail + let result = realloc(ptr, 64, 128, 3); + assert!(result.is_null()); + + // Zero alignment should fail + let result = realloc(ptr, 64, 128, 0); + assert!(result.is_null()); + } + + #[test] + fn test_realloc_grow_large() { + // Test growing to a large size + let ptr = alloc(64, 8); + assert!(!ptr.is_null()); + + unsafe { + *ptr = 0xAB; + } + + // Grow to larger than default arena + let large_size = DEFAULT_ARENA_SIZE as u64 * 2; + let new_ptr = realloc(ptr, 64, large_size, 8); + assert!(!new_ptr.is_null()); + + // Original data preserved + unsafe { + assert_eq!(*new_ptr, 0xAB); + // Can write to end of new allocation + *new_ptr.add(large_size as usize - 1) = 0xCD; + assert_eq!(*new_ptr.add(large_size as usize - 1), 0xCD); + } + } +} diff --git a/crates/rue-runtime/src/lib.rs b/crates/rue-runtime/src/lib.rs index 4c128220..0882aee3 100644 --- a/crates/rue-runtime/src/lib.rs +++ b/crates/rue-runtime/src/lib.rs @@ -67,6 +67,14 @@ mod aarch64_macos; #[cfg(all(target_arch = "aarch64", target_os = "linux"))] mod aarch64_linux; +// Heap allocation (available on all supported platforms) +#[cfg(any( + all(target_arch = "x86_64", target_os = "linux"), + all(target_arch = "aarch64", target_os = "macos"), + all(target_arch = "aarch64", target_os = "linux") +))] +mod heap; + #[cfg(all(target_arch = "x86_64", target_os = "linux"))] use x86_64_linux as platform; @@ -87,6 +95,95 @@ compile_error!( Other platforms are not currently supported." ); +// ============================================================================ +// Memory intrinsics +// ============================================================================ +// +// These functions are required by LLVM/rustc when using ptr::copy_nonoverlapping, +// ptr::write_bytes, etc. in no_std environments. They provide the same functionality +// as the libc functions but are implemented in pure Rust. + +/// Copy `n` bytes from `src` to `dst`. The memory regions must not overlap. +/// +/// # Safety +/// +/// - `dst` must be valid for writes of `n` bytes +/// - `src` must be valid for reads of `n` bytes +/// - The memory regions must not overlap +#[unsafe(no_mangle)] +pub unsafe extern "C" fn memcpy(dst: *mut u8, src: *const u8, n: usize) -> *mut u8 { + let mut i = 0; + while i < n { + *dst.add(i) = *src.add(i); + i += 1; + } + dst +} + +/// Copy `n` bytes from `src` to `dst`. The memory regions may overlap. +/// +/// # Safety +/// +/// - `dst` must be valid for writes of `n` bytes +/// - `src` must be valid for reads of `n` bytes +#[unsafe(no_mangle)] +pub unsafe extern "C" fn memmove(dst: *mut u8, src: *const u8, n: usize) -> *mut u8 { + if (dst as usize) < (src as usize) { + // Copy forwards + let mut i = 0; + while i < n { + *dst.add(i) = *src.add(i); + i += 1; + } + } else { + // Copy backwards to handle overlap + let mut i = n; + while i > 0 { + i -= 1; + *dst.add(i) = *src.add(i); + } + } + dst +} + +/// Fill `n` bytes of memory at `dst` with the byte `c`. +/// +/// # Safety +/// +/// - `dst` must be valid for writes of `n` bytes +#[unsafe(no_mangle)] +pub unsafe extern "C" fn memset(dst: *mut u8, c: i32, n: usize) -> *mut u8 { + let byte = c as u8; + let mut i = 0; + while i < n { + *dst.add(i) = byte; + i += 1; + } + dst +} + +/// Compare `n` bytes of memory at `s1` and `s2`. +/// +/// Returns 0 if equal, negative if s1 < s2, positive if s1 > s2. +/// +/// # Safety +/// +/// - `s1` must be valid for reads of `n` bytes +/// - `s2` must be valid for reads of `n` bytes +#[unsafe(no_mangle)] +pub unsafe extern "C" fn memcmp(s1: *const u8, s2: *const u8, n: usize) -> i32 { + let mut i = 0; + while i < n { + let a = *s1.add(i); + let b = *s2.add(i); + if a != b { + return (a as i32) - (b as i32); + } + i += 1; + } + 0 +} + /// Panic handler for `#![no_std]` environments. /// /// This handler is only active when the crate is compiled as a library (not @@ -624,6 +721,132 @@ pub extern "C" fn __rue_str_eq(ptr1: *const u8, len1: u64, ptr2: *const u8, len2 1 } +// ============================================================================= +// Heap Allocation +// ============================================================================= + +/// Allocate memory from the heap. +/// +/// This is the main allocation function for Rue programs. Memory is allocated +/// from a bump allocator backed by `mmap`. +/// +/// # Arguments +/// +/// * `size` - Number of bytes to allocate +/// * `align` - Required alignment (must be a power of 2) +/// +/// # Returns +/// +/// A pointer to the allocated memory, or null on failure. +/// The memory is zero-initialized. +/// +/// # ABI +/// +/// ```text +/// extern "C" fn __rue_alloc(size: u64, align: u64) -> *mut u8 +/// ``` +/// +/// - `size` is passed in the first argument register (rdi on x86_64, x0 on aarch64) +/// - `align` is passed in the second argument register (rsi on x86_64, x1 on aarch64) +/// - Returns pointer in rax (x86_64) or x0 (aarch64) +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +#[unsafe(no_mangle)] +pub extern "C" fn __rue_alloc(size: u64, align: u64) -> *mut u8 { + heap::alloc(size, align) +} + +#[cfg(all(target_arch = "aarch64", target_os = "macos"))] +#[unsafe(no_mangle)] +pub extern "C" fn __rue_alloc(size: u64, align: u64) -> *mut u8 { + heap::alloc(size, align) +} + +#[cfg(all(target_arch = "aarch64", target_os = "linux"))] +#[unsafe(no_mangle)] +pub extern "C" fn __rue_alloc(size: u64, align: u64) -> *mut u8 { + heap::alloc(size, align) +} + +/// Free memory previously allocated by `__rue_alloc`. +/// +/// # Arguments +/// +/// * `ptr` - Pointer to the memory to free +/// * `size` - Size of the allocation (for future compatibility) +/// * `align` - Alignment of the allocation (for future compatibility) +/// +/// # Current Implementation +/// +/// This is a **no-op** in the current bump allocator. Memory is only +/// reclaimed when the program exits. The size and align parameters are +/// accepted for API compatibility with future allocators. +/// +/// # ABI +/// +/// ```text +/// extern "C" fn __rue_free(ptr: *mut u8, size: u64, align: u64) +/// ``` +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +#[unsafe(no_mangle)] +pub extern "C" fn __rue_free(ptr: *mut u8, size: u64, align: u64) { + heap::free(ptr, size, align) +} + +#[cfg(all(target_arch = "aarch64", target_os = "macos"))] +#[unsafe(no_mangle)] +pub extern "C" fn __rue_free(ptr: *mut u8, size: u64, align: u64) { + heap::free(ptr, size, align) +} + +#[cfg(all(target_arch = "aarch64", target_os = "linux"))] +#[unsafe(no_mangle)] +pub extern "C" fn __rue_free(ptr: *mut u8, size: u64, align: u64) { + heap::free(ptr, size, align) +} + +/// Reallocate memory to a new size. +/// +/// # Arguments +/// +/// * `ptr` - Pointer to the existing allocation (or null for new allocation) +/// * `old_size` - Size of the existing allocation (ignored if ptr is null) +/// * `new_size` - Desired new size +/// * `align` - Required alignment (must be a power of 2) +/// +/// # Returns +/// +/// A pointer to the reallocated memory, or null on failure. +/// +/// # Behavior +/// +/// - If `ptr` is null: behaves like `__rue_alloc(new_size, align)` +/// - If `new_size` is 0: frees the memory and returns null +/// - If `new_size <= old_size`: returns `ptr` unchanged +/// - If `new_size > old_size`: allocates new block, copies data, returns new pointer +/// +/// # ABI +/// +/// ```text +/// extern "C" fn __rue_realloc(ptr: *mut u8, old_size: u64, new_size: u64, align: u64) -> *mut u8 +/// ``` +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +#[unsafe(no_mangle)] +pub extern "C" fn __rue_realloc(ptr: *mut u8, old_size: u64, new_size: u64, align: u64) -> *mut u8 { + heap::realloc(ptr, old_size, new_size, align) +} + +#[cfg(all(target_arch = "aarch64", target_os = "macos"))] +#[unsafe(no_mangle)] +pub extern "C" fn __rue_realloc(ptr: *mut u8, old_size: u64, new_size: u64, align: u64) -> *mut u8 { + heap::realloc(ptr, old_size, new_size, align) +} + +#[cfg(all(target_arch = "aarch64", target_os = "linux"))] +#[unsafe(no_mangle)] +pub extern "C" fn __rue_realloc(ptr: *mut u8, old_size: u64, new_size: u64, align: u64) -> *mut u8 { + heap::realloc(ptr, old_size, new_size, align) +} + // Re-export platform functions for tests #[cfg(all(test, target_arch = "x86_64", target_os = "linux"))] pub use x86_64_linux::{exit, write, write_all, write_stderr}; diff --git a/crates/rue-runtime/src/x86_64_linux.rs b/crates/rue-runtime/src/x86_64_linux.rs index 611339f5..6ba2a135 100644 --- a/crates/rue-runtime/src/x86_64_linux.rs +++ b/crates/rue-runtime/src/x86_64_linux.rs @@ -30,6 +30,12 @@ use core::arch::asm; /// Linux syscall number for write (see `man 2 write`). const SYS_WRITE: i64 = 1; +/// Linux syscall number for mmap (see `man 2 mmap`). +const SYS_MMAP: i64 = 9; + +/// Linux syscall number for munmap (see `man 2 munmap`). +const SYS_MUNMAP: i64 = 11; + /// Linux syscall number for exit (see `man 2 exit`). const SYS_EXIT: i64 = 60; @@ -243,6 +249,98 @@ pub fn print_bool(value: bool) { } } +/// Map anonymous memory pages. +/// +/// This is a wrapper around the Linux `mmap(2)` syscall configured for +/// anonymous private memory allocation (no file backing). +/// +/// # Arguments +/// +/// * `size` - Number of bytes to allocate. Will be rounded up to page size by the kernel. +/// +/// # Returns +/// +/// On success, returns a pointer to the mapped memory region. +/// On error, returns a null pointer. +/// +/// # Memory Protection +/// +/// The mapped region is readable and writable (PROT_READ | PROT_WRITE). +/// +/// # Safety +/// +/// The returned pointer (if non-null) points to valid, zero-initialized memory. +/// The caller is responsible for calling `munmap` when done. +pub fn mmap(size: usize) -> *mut u8 { + // mmap flags + const PROT_READ: i64 = 0x1; + const PROT_WRITE: i64 = 0x2; + const MAP_PRIVATE: i64 = 0x02; + const MAP_ANONYMOUS: i64 = 0x20; + + let result: i64; + // SAFETY: mmap syscall with anonymous mapping is safe. + // We're requesting private anonymous memory with read/write permissions. + unsafe { + asm!( + "syscall", + in("rax") SYS_MMAP, + in("rdi") 0i64, // addr: NULL (let kernel choose) + in("rsi") size, // length + in("rdx") PROT_READ | PROT_WRITE, // prot + in("r10") MAP_PRIVATE | MAP_ANONYMOUS, // flags + in("r8") -1i64, // fd: -1 for anonymous + in("r9") 0i64, // offset: 0 + lateout("rax") result, + out("rcx") _, + out("r11") _, + ); + } + + // mmap returns MAP_FAILED (-1 as usize) on error + if result < 0 { + core::ptr::null_mut() + } else { + result as *mut u8 + } +} + +/// Unmap memory pages previously mapped with `mmap`. +/// +/// This is a wrapper around the Linux `munmap(2)` syscall. +/// +/// # Arguments +/// +/// * `addr` - Pointer to the start of the mapped region (must be page-aligned) +/// * `size` - Size of the region to unmap (will be rounded up to page size) +/// +/// # Returns +/// +/// Returns 0 on success, or a negative errno on failure. +/// +/// # Safety +/// +/// The caller must ensure: +/// - `addr` was returned by a previous `mmap` call +/// - `size` matches the size used in the `mmap` call +/// - The memory is not accessed after this call +pub fn munmap(addr: *mut u8, size: usize) -> i64 { + let result: i64; + // SAFETY: munmap is safe if addr/size are valid from a previous mmap. + unsafe { + asm!( + "syscall", + in("rax") SYS_MUNMAP, + in("rdi") addr, + in("rsi") size, + lateout("rax") result, + out("rcx") _, + out("r11") _, + ); + } + result +} + /// Exit the process with the given status code. /// /// This performs a direct syscall to `exit(2)` and never returns. @@ -347,7 +445,77 @@ mod tests { fn test_syscall_constants() { // Verify our syscall numbers match Linux x86-64 ABI assert_eq!(SYS_WRITE, 1); + assert_eq!(SYS_MMAP, 9); + assert_eq!(SYS_MUNMAP, 11); assert_eq!(SYS_EXIT, 60); assert_eq!(STDERR, 2); } + + #[test] + fn test_mmap_basic() { + // Allocate a page of memory + let size = 4096; + let ptr = mmap(size); + assert!(!ptr.is_null()); + + // Memory should be zero-initialized and writable + unsafe { + assert_eq!(*ptr, 0); + *ptr = 42; + assert_eq!(*ptr, 42); + } + + // Clean up + let result = munmap(ptr, size); + assert_eq!(result, 0); + } + + #[test] + fn test_mmap_large() { + // Allocate 1 MB + let size = 1024 * 1024; + let ptr = mmap(size); + assert!(!ptr.is_null()); + + // Write to first and last bytes + unsafe { + *ptr = 1; + *ptr.add(size - 1) = 2; + assert_eq!(*ptr, 1); + assert_eq!(*ptr.add(size - 1), 2); + } + + let result = munmap(ptr, size); + assert_eq!(result, 0); + } + + #[test] + fn test_mmap_multiple() { + // Allocate multiple regions + let size = 4096; + let ptr1 = mmap(size); + let ptr2 = mmap(size); + let ptr3 = mmap(size); + + assert!(!ptr1.is_null()); + assert!(!ptr2.is_null()); + assert!(!ptr3.is_null()); + + // They should be different addresses + assert_ne!(ptr1, ptr2); + assert_ne!(ptr2, ptr3); + assert_ne!(ptr1, ptr3); + + // Clean up all + assert_eq!(munmap(ptr1, size), 0); + assert_eq!(munmap(ptr2, size), 0); + assert_eq!(munmap(ptr3, size), 0); + } + + #[test] + fn test_mmap_zero_size() { + // Zero-size mmap should fail (returns EINVAL on Linux) + let ptr = mmap(0); + assert!(ptr.is_null()); + } } diff --git a/crates/rue-spec/cases/items/impl-blocks.toml b/crates/rue-spec/cases/items/impl-blocks.toml new file mode 100644 index 00000000..c3d9ecb5 --- /dev/null +++ b/crates/rue-spec/cases/items/impl-blocks.toml @@ -0,0 +1,114 @@ +[section] +id = "items.impl-blocks" +spec_chapter = "6.4" +name = "Impl Blocks and Methods" +description = "Impl block definitions and method calls." + +# ============================================================================= +# Basic Impl Block Syntax (Parsing Only - Phase 1) +# ============================================================================= + +# These tests verify that impl blocks and method calls parse correctly. +# They are expected to fail at later compilation stages until Phase 2-4 are complete. + +[[case]] +name = "impl_block_basic_parse" +preview = "methods" +source = """ +struct Point { x: i32, y: i32 } + +impl Point { + fn get_x(self) -> i32 { + self.x + } +} + +fn main() -> i32 { + let p = Point { x: 42, y: 10 }; + p.get_x() +} +""" +exit_code = 42 + +[[case]] +name = "impl_block_associated_function" +preview = "methods" +source = """ +struct Point { x: i32, y: i32 } + +impl Point { + fn origin() -> Point { + Point { x: 0, y: 0 } + } +} + +fn main() -> i32 { + let p = Point::origin(); + p.x +} +""" +exit_code = 0 + +[[case]] +name = "impl_block_method_with_args" +preview = "methods" +source = """ +struct Point { x: i32, y: i32 } + +impl Point { + fn add(self, dx: i32, dy: i32) -> Point { + Point { x: self.x + dx, y: self.y + dy } + } +} + +fn main() -> i32 { + let p = Point { x: 10, y: 20 }; + let p2 = p.add(32, 0); + p2.x +} +""" +exit_code = 42 + +[[case]] +name = "method_call_chain" +preview = "methods" +source = """ +struct Counter { value: i32 } + +impl Counter { + fn inc(self) -> Counter { + Counter { value: self.value + 1 } + } +} + +fn main() -> i32 { + let c = Counter { value: 39 }; + c.inc().inc().inc().value +} +""" +exit_code = 42 + +[[case]] +name = "multiple_impl_blocks" +preview = "methods" +source = """ +struct Point { x: i32, y: i32 } + +impl Point { + fn get_x(self) -> i32 { + self.x + } +} + +impl Point { + fn get_y(self) -> i32 { + self.y + } +} + +fn main() -> i32 { + let p = Point { x: 42, y: 10 }; + p.get_x() +} +""" +exit_code = 42 diff --git a/crates/rue-spec/cases/types/destructors.toml b/crates/rue-spec/cases/types/destructors.toml new file mode 100644 index 00000000..7dcae453 --- /dev/null +++ b/crates/rue-spec/cases/types/destructors.toml @@ -0,0 +1,239 @@ +[section] +id = "types.destructors" +spec_chapter = "3.9" +name = "Destructors" +description = "Drop semantics and destructor behavior." + +# ============================================================================= +# Drop Semantics +# ============================================================================= + +[[case]] +name = "value_dropped_at_scope_exit" +spec = ["3.9:1", "3.9:2"] +preview = "destructors" +description = "Values are dropped when their binding goes out of scope" +source = """ +struct Data { value: i32 } + +fn main() -> i32 { + let d = Data { value: 42 }; + d.value +} // d is dropped here +""" +exit_code = 42 + +[[case]] +name = "moved_value_not_dropped_at_original_site" +spec = ["3.9:2", "3.9:3"] +preview = "destructors" +description = "Moved values are not dropped at their original binding" +source = """ +struct Data { value: i32 } + +fn consume(d: Data) -> i32 { d.value } + +fn main() -> i32 { + let d = Data { value: 42 }; + consume(d) // d is moved, dropped inside consume() +} // d is NOT dropped here (was moved) +""" +exit_code = 42 + +# ============================================================================= +# Drop Order +# ============================================================================= + +[[case]] +name = "drop_order_reverse_declaration" +spec = ["3.9:4", "3.9:5", "3.9:6"] +preview = "destructors" +description = "Multiple values dropped in reverse declaration order (LIFO)" +source = """ +struct Data { value: i32 } + +fn main() -> i32 { + let a = Data { value: 1 }; // declared first + let b = Data { value: 2 }; // declared second + a.value + b.value +} // b dropped first, then a dropped +""" +exit_code = 3 + +# ============================================================================= +# Trivially Droppable Types +# ============================================================================= + +[[case]] +name = "primitives_trivially_droppable" +spec = ["3.9:7", "3.9:8"] +preview = "destructors" +description = "Primitive types are trivially droppable (no destructor)" +source = """ +fn main() -> i32 { + let x: i32 = 42; + let b: bool = true; + let u: () = (); + x +} +""" +exit_code = 42 + +[[case]] +name = "struct_with_trivial_fields_is_trivial" +spec = ["3.9:9", "3.9:10"] +preview = "destructors" +description = "Struct with only trivially droppable fields is trivially droppable" +source = """ +struct Point { x: i32, y: i32 } + +fn main() -> i32 { + let p = Point { x: 10, y: 20 }; + p.x + p.y +} +""" +exit_code = 30 + +[[case]] +name = "enum_trivially_droppable" +spec = ["3.9:8"] +preview = "destructors" +description = "Enum types are trivially droppable" +source = """ +enum Color { Red, Green, Blue } + +fn main() -> i32 { + let c = Color::Green; + match c { + Color::Red => 1, + Color::Green => 2, + Color::Blue => 3, + } +} +""" +exit_code = 2 + +[[case]] +name = "array_of_trivial_is_trivial" +spec = ["3.9:8"] +preview = "destructors" +description = "Arrays of trivially droppable types are trivially droppable" +source = """ +fn main() -> i32 { + let arr = [1, 2, 3, 4, 5]; + arr[0] + arr[4] +} +""" +exit_code = 6 + +# ============================================================================= +# Drop Placement +# ============================================================================= + +[[case]] +name = "drop_at_block_scope_end" +spec = ["3.9:15"] +preview = "destructors" +description = "Values are dropped at end of block scope" +source = """ +struct Data { value: i32 } + +fn main() -> i32 { + let result = { + let d = Data { value: 42 }; + d.value + }; // d dropped here, at end of block + result +} +""" +exit_code = 42 + +[[case]] +name = "drop_before_early_return" +spec = ["3.9:15", "3.9:17"] +preview = "destructors" +description = "Values are dropped before early return" +source = """ +struct Data { value: i32 } + +fn example(condition: bool) -> i32 { + let a = Data { value: 1 }; + if condition { + return 42; // a dropped before return + } + a.value +} + +fn main() -> i32 { + example(true) +} +""" +exit_code = 42 + +[[case]] +name = "drop_in_each_branch" +spec = ["3.9:16", "3.9:17"] +preview = "destructors" +description = "Each branch independently drops its bindings" +source = """ +struct Data { value: i32 } + +fn example(condition: bool) -> i32 { + let a = Data { value: 1 }; + if condition { + let b = Data { value: 2 }; + return a.value + b.value; // b dropped, then a dropped, then return + } + let c = Data { value: 3 }; + a.value + c.value // c dropped, then a dropped +} + +fn main() -> i32 { + example(true) +} +""" +exit_code = 3 + +# ============================================================================= +# Types with Destructors (Phase 2+) +# ============================================================================= + +# These tests are placeholders for future phases when we have types with +# actual destructors (e.g., mutable strings). For now they just verify +# the spec paragraphs exist and will be covered. + +[[case]] +name = "type_with_destructor" +spec = ["3.9:11"] +preview = "destructors" +description = "A type has a destructor if dropping requires cleanup" +skip = true +source = """ +// Placeholder: will test with mutable String when available +fn main() -> i32 { 0 } +""" +exit_code = 0 + +[[case]] +name = "struct_with_destructor_field" +spec = ["3.9:12"] +preview = "destructors" +description = "Struct has destructor if any field has destructor" +skip = true +source = """ +// Placeholder: will test with struct containing String when available +fn main() -> i32 { 0 } +""" +exit_code = 0 + +[[case]] +name = "field_drop_order" +spec = ["3.9:13"] +preview = "destructors" +description = "Fields dropped in declaration order" +skip = true +source = """ +// Placeholder: will test with observable drop order when available +fn main() -> i32 { 0 } +""" +exit_code = 0 diff --git a/crates/rue-spec/cases/types/move-semantics.toml b/crates/rue-spec/cases/types/move-semantics.toml index 28189d41..110056fb 100644 --- a/crates/rue-spec/cases/types/move-semantics.toml +++ b/crates/rue-spec/cases/types/move-semantics.toml @@ -279,3 +279,131 @@ fn main() -> i32 { } """ exit_code = 1 + +# ============================================================================= +# Return Moves +# ============================================================================= + +[[case]] +name = "struct_returned_from_function" +spec = ["3.8:7"] +description = "Returning a struct from a function transfers ownership to the caller" +source = """ +struct Point { x: i32, y: i32 } +fn make_point() -> Point { Point { x: 1, y: 2 } } +fn main() -> i32 { + let p = make_point(); + p.x + p.y +} +""" +exit_code = 3 + +[[case]] +name = "struct_returned_then_moved" +spec = ["3.8:7"] +description = "Struct returned from function can be moved like any other owned value" +source = """ +struct Data { value: i32 } +fn create() -> Data { Data { value: 42 } } +fn consume(d: Data) -> i32 { d.value } +fn main() -> i32 { + let d = create(); + consume(d) +} +""" +exit_code = 42 + +[[case]] +name = "struct_returned_use_after_move_error" +spec = ["3.8:5", "3.8:7"] +description = "Struct returned from function follows normal move rules" +source = """ +struct Data { value: i32 } +fn create() -> Data { Data { value: 42 } } +fn main() -> i32 { + let d = create(); + let e = d; + d.value +} +""" +compile_fail = true +error_contains = "use of moved value" + +[[case]] +name = "return_moves_local_variable" +spec = ["3.8:7"] +description = "Returning a local variable moves it out of the function" +source = """ +struct Point { x: i32, y: i32 } +fn make_point() -> Point { + let p = Point { x: 10, y: 20 }; + p +} +fn main() -> i32 { + let result = make_point(); + result.x + result.y +} +""" +exit_code = 30 + +[[case]] +name = "return_expression_creates_value" +spec = ["3.8:7"] +description = "Struct literal in return position transfers ownership directly" +source = """ +struct Pair { a: i32, b: i32 } +fn make_pair(x: i32, y: i32) -> Pair { + Pair { a: x, b: y } +} +fn main() -> i32 { + let p = make_pair(3, 4); + p.a * p.b +} +""" +exit_code = 12 + +[[case]] +name = "chained_returns" +spec = ["3.8:7"] +description = "Ownership can transfer through multiple function returns" +source = """ +struct Value { n: i32 } +fn inner() -> Value { Value { n: 5 } } +fn outer() -> Value { inner() } +fn main() -> i32 { + let v = outer(); + v.n +} +""" +exit_code = 5 + +[[case]] +name = "return_parameter" +spec = ["3.8:7", "3.8:11"] +description = "Function can return its parameter, transferring ownership back" +source = """ +struct Data { value: i32 } +fn identity(d: Data) -> Data { d } +fn main() -> i32 { + let d = Data { value: 100 }; + let e = identity(d); + e.value +} +""" +exit_code = 100 + +[[case]] +name = "return_parameter_original_invalid" +spec = ["3.8:5", "3.8:7", "3.8:11"] +description = "After passing to function that returns it, original binding is still moved" +source = """ +struct Data { value: i32 } +fn identity(d: Data) -> Data { d } +fn main() -> i32 { + let d = Data { value: 100 }; + let _e = identity(d); + d.value +} +""" +compile_fail = true +error_contains = "use of moved value" \ No newline at end of file diff --git a/crates/rue/src/main.rs b/crates/rue/src/main.rs index 31a31f2e..ca968df4 100644 --- a/crates/rue/src/main.rs +++ b/crates/rue/src/main.rs @@ -402,13 +402,19 @@ fn handle_emit(source: &str, options: &Options) -> Result<(), ()> { for func in &state.functions { println!(".globl {}", func.analyzed.name); println!("{}:", func.analyzed.name); - let mir = generate_allocated_mir( + let mir = match generate_allocated_mir( &func.cfg, &state.struct_defs, &state.array_types, &state.strings, options.target, - ); + ) { + Ok(mir) => mir, + Err(e) => { + print_error(&e, source, &options.source_path); + return Err(()); + } + }; print_assembly(&mir); println!(); } diff --git a/docs/designs/0009-struct-methods.md b/docs/designs/0009-struct-methods.md index 1e2b0799..8e7e7ea2 100644 --- a/docs/designs/0009-struct-methods.md +++ b/docs/designs/0009-struct-methods.md @@ -132,22 +132,28 @@ Methods can only be defined for structs in the same compilation unit. (This is a ## Implementation Phases -- [ ] **Phase 1: Parsing** - rue-qs3z.1 +- [x] **Phase 1: Parsing** - rue-qs3z.1 - Add `impl` keyword to lexer - Parse `impl Type { fn... }` blocks - Add `Item::ImplBlock` to AST - Parse method calls as a variant of field access -- [ ] **Phase 2: RIR Generation** - rue-qs3z.2 - - Add method info to RIR +- [x] **Phase 2: RIR Generation** - rue-qs3z.2 + - Add method info to RIR (ImplDecl, MethodCall, AssocFnCall instructions) - Generate RIR for impl blocks - Handle method calls in expression generation + - Parse associated function calls (Type::fn() syntax) - [ ] **Phase 3: Type Checking** - rue-qs3z.3 - Add method registry to struct definitions - Type check impl blocks - Resolve method calls to their definitions - Handle `self` parameter binding + - Method resolution requires: + 1. Looking up the receiver type + 2. Finding the impl block for that type + 3. Resolving the method name + 4. Type checking the arguments against method signature - [ ] **Phase 4: Code Generation** - rue-qs3z.4 - Lower method calls to regular calls with receiver as first argument diff --git a/docs/designs/0010-destructors.md b/docs/designs/0010-destructors.md new file mode 100644 index 00000000..323350a6 --- /dev/null +++ b/docs/designs/0010-destructors.md @@ -0,0 +1,403 @@ +--- +id: 0010 +title: Destructors +status: proposal +tags: [types, semantics, ownership, memory] +feature-flag: destructors +created: 2025-12-24 +accepted: +implemented: +spec-sections: ["3.9"] +superseded-by: +--- + +# ADR-0010: Destructors + +## Status + +Proposal + +## Summary + +Add destructor support to Rue, enabling types to define cleanup logic that runs automatically when values go out of scope. Destructors are required for heap-allocated types like mutable strings where `free()` must be called. We begin with compiler-synthesized destructors for built-in types, with a path to user-defined destructors via the `drop` keyword. + +## Context + +### Why Destructors Now? + +Rue's affine type system (ADR-0008) establishes that types are affine by default: they can be dropped. But currently "dropped" just means "memory is deallocated"—there's no mechanism to run cleanup code. + +Mutable strings are the immediate motivator. A mutable string owns a heap-allocated buffer: + +```rue +struct String { + ptr: RawPtr, // Points to heap allocation + len: u64, + capacity: u64, +} +``` + +When a string goes out of scope, we must call `free(ptr)`. Without destructors, this memory leaks. + +### What Exists Today + +The compiler already has most infrastructure for destructors: + +1. **Affine type tracking**: We know which values are live and when they're consumed +2. **Scope management**: `push_scope()`/`pop_scope()` track variable lifetimes +3. **CFG representation**: Designed for "drop elaboration" (noted in rue-cfg comments) +4. **Move tracking**: We know when values transfer ownership + +What's missing: +- Drop instructions in the IR +- CFG pass to insert drops at scope exits +- Codegen to emit drop calls +- Runtime support for built-in type cleanup + +### Design Philosophy + +We want a path from "built-in types have compiler-synthesized destructors" to "users can define their own destructors." This mirrors the progression: + +1. **V1**: Built-in types (String, Vec) have hardcoded cleanup +2. **V2**: User-defined `drop()` method support (provisional syntax) +3. **V3**: Finalize drop API (future ADR, may integrate with traits or other mechanisms) + +## Decision + +### Drop Semantics + +When a value's owning binding goes out of scope for the last time (not moved elsewhere), its destructor runs exactly once: + +```rue +fn example() { + let s = String::new("hello"); // String is allocated + // ... use s ... +} // s goes out of scope, destructor runs, memory freed +``` + +Drop order: **reverse declaration order** (last declared, first dropped): + +```rue +fn example() { + let a = String::new("first"); + let b = String::new("second"); +} // b dropped, then a dropped +``` + +This matches Rust and C++ (LIFO). It's required for correctness when values may reference each other. + +### Types That Need Destructors + +**Built-in types that will need destructors (when added):** +- `String` (mutable strings, planned) — frees heap buffer +- `Vec` (future) — drops elements, then frees buffer +- `Box` (future) — drops pointee, then frees memory + +Note: None of these types exist yet. This ADR provides the infrastructure they'll need. + +**Types without destructors (trivially droppable):** +- All primitives: `i8`..`i64`, `u8`..`u64`, `bool`, `()` +- Arrays of trivially droppable types +- `@copy` structs (implicitly trivially droppable) + +**User-defined types:** +- Structs with fields that have destructors need synthesized destructors +- Structs can opt-in to custom destructors (Phase 3) + +### Synthesized Destructors + +For structs containing non-trivially-droppable fields, the compiler synthesizes a destructor: + +```rue +struct Person { + name: String, // needs drop + age: i32, // trivial +} + +// Compiler synthesizes (conceptually): +fn drop_Person(self: Person) { + drop(self.name); // String destructor + // age is trivial, no drop needed +} +``` + +Fields are dropped in **declaration order** (matches C++, Rust). + +### Explicit Drop (V2) + +In Phase 3, users can define custom destructors: + +```rue +struct FileHandle { + fd: i32, +} + +drop fn FileHandle(self) { + close(self.fd); +} +``` + +The syntax `drop fn TypeName(self)` is chosen because: +- `drop` as keyword clearly indicates purpose +- `fn` signals it's a function definition +- Takes `self` by value (consuming) +- No return type (implicitly `()`) + +Alternative considered: `impl Drop for T` — deferred pending decisions on traits or other abstraction mechanisms. + +### IR Representation + +#### AIR: Drop Instruction + +Add a `Drop` instruction to AIR: + +```rust +enum Inst { + // ... existing ... + + /// Drop a value, running its destructor if any + Drop { + value: InstRef, + ty: Type, + }, +} +``` + +The type is needed because we may drop a polymorphic value (in the future). + +#### CFG: Drop Placement + +The CFG builder inserts `Drop` instructions: + +1. **Scope exits**: When leaving a block, drop all live bindings in reverse order +2. **Early returns**: Before each `return`, drop all live bindings in all enclosing scopes +3. **Loops with break**: Before `break`, drop bindings declared inside the loop +4. **Conditionals**: Each branch independently drops its bindings + +Example CFG transformation: + +```rue +fn example() -> i32 { + let s = String::new("hello"); + if condition { + return 42; // Must drop s here + } + let t = String::new("world"); + 0 +} // Must drop t then s here +``` + +Becomes: + +``` +entry: + s = String::new("hello") + branch condition -> then, else + +then: + drop(s) + return 42 + +else: + t = String::new("world") + drop(t) + drop(s) + return 0 +``` + +### Codegen + +Drop instructions lower to function calls: + +```asm +; drop(s) where s: String +mov rdi, [rbp-8] ; load s.ptr +call __rue_drop_String +``` + +For trivially droppable types, the drop is a no-op (elided). + +### Runtime Support + +Drop functions for built-in types will be added to `rue-runtime` as those types are implemented. The naming convention is `__rue_drop_`. For example, when mutable strings land: + +```rust +// Example: what a String drop might look like (when String is added) +#[no_mangle] +pub extern "C" fn __rue_drop_String(s: RawString) { + if !s.ptr.is_null() && s.capacity > 0 { + __rue_free(s.ptr, s.capacity); + } +} +``` + +Until heap-allocated types exist, the runtime won't need any drop functions. + +### Copy Types and Drop + +**Critical constraint**: `@copy` types cannot have destructors. + +If a type is Copy, it can be duplicated via bitwise copy. If it also had a destructor, the destructor would run multiple times (double-free). Therefore: + +```rue +@copy +struct Bad { + ptr: RawPtr, // ERROR: @copy type with pointer? dangerous +} +``` + +We enforce: `@copy` structs can only contain `@copy` fields (already in ADR-0008), and `@copy` types are trivially droppable (no destructor). + +### Linear Types and Drop + +Linear types (from ADR-0008) **must be explicitly consumed**—they cannot be implicitly dropped: + +```rue +linear struct MustConsume { ... } + +fn bad() { + let m = MustConsume { ... }; +} // ERROR: linear value dropped without consumption +``` + +The destructor mechanism skips linear types; they error at implicit drop points instead. + +**Open question**: How does consumption ultimately happen? At the end of the chain, something must run the linear value's destructor. Options include special "sink" functions, an explicit `consume` keyword, or allowing destructors on linear types that run when ownership is transferred to a consuming function. See Open Questions. + +### Panic Safety + +Rue currently aborts on panic rather than unwinding. This means: +- Destructors do not run on panic (no stack unwinding) +- A panic in a destructor simply aborts + +If unwinding is added in the future, destructor behavior during unwinding will need a separate ADR. + +## Implementation Phases + +Epic: rue-wjha + +Following spec-first, test-driven development: each phase writes spec paragraphs, then tests that reference them, then implementation to make tests pass. + +### Phase 1: Spec and Infrastructure (rue-wjha.7) + +**Spec**: Add destructor chapter to specification (section 3.9 or similar): +- When destructors run (scope exit, not moved) +- Drop order (reverse declaration order) +- Trivially droppable types (primitives, `@copy` structs) +- Types with destructors (structs containing non-trivial fields) + +**Tests**: Add spec tests with `preview = "destructors"`: +- Trivially droppable types compile and run (no behavioral change) +- Golden tests showing Drop instructions in AIR/CFG output + +**Implementation**: +- Add `Drop` instruction to AIR +- Add `type_needs_drop()` method to `CfgBuilder` (requires access to struct/array type definitions) +- Add drop elaboration pass stub in CFG builder + +### Phase 2: Drop Elaboration (rue-wjha.8) + +**Spec**: Add paragraphs for: +- Drop at end of block scope +- Drop before early return +- Drop in each branch of conditionals +- Drop before break/continue in loops + +**Tests**: Golden tests showing correct Drop placement: +- Simple scope exit +- Early return drops all live bindings +- If/else branches drop their own bindings +- Loop with break drops loop-local bindings + +**Implementation**: +- CFG builder inserts `Drop` at scope exits +- Handle early returns, conditionals, loops + +### Phase 3: Codegen (rue-wjha.9) + +**Spec**: Add paragraphs for: +- Drop calls generated for non-trivial types +- Trivially droppable types elide drop calls + +**Tests**: +- Golden tests showing generated assembly calls `__rue_drop_*` +- Tests confirming no drop calls for trivially droppable types + +**Implementation**: +- x86_64 backend: emit calls to `__rue_drop_*` +- aarch64 backend: emit calls to `__rue_drop_*` +- Elide drops for trivially droppable types +- Register allocation around drop calls + +### Phase 4: User-Defined Destructors (rue-wjha.10) + +**Spec**: Add paragraphs for: +- `drop fn TypeName(self)` syntax +- One destructor per type +- Destructor runs after field destructors (or before? decide) + +**Tests**: +- Parse `drop fn` syntax +- Error on duplicate destructors +- Error on wrong signature +- User destructor runs at scope exit + +**Implementation**: +- Parse `drop fn TypeName(self) { ... }` +- Semantic analysis: validate signature +- Generate drop function, wire into type's destructor + +### Phase 5: Integration with Built-in Types (rue-wjha.11) + +This phase is deferred until mutable strings or other heap-allocated types land. It will: +- Add `__rue_drop_String` to runtime +- Wire String type to use it +- Verify no memory leaks (valgrind clean) + +## Consequences + +### Positive + +- **Memory safety**: Heap-allocated types clean up automatically +- **RAII pattern**: Enables safe resource management (files, locks, etc.) +- **Path forward**: Built-in to user-defined to trait-based is incremental +- **Predictable**: Drop order is deterministic and documented + +### Negative + +- **Complexity**: Drop elaboration touches many parts of the compiler +- **Performance**: Drop calls have overhead (mitigated by elision for trivial types) +- **Multi-backend**: Must implement in both x86_64 and aarch64 backends + +### Neutral + +- **Different from Rust**: We use `drop fn` syntax instead of `impl Drop` +- **Simpler than Rust**: No `Drop` trait until we have traits + +## Open Questions + +1. **Allocator story**: How do we hook into malloc/free? System allocator? Custom? + +2. **Generic drops**: When we have generics, how do we call drop on `T`? Monomorphization? vtable? + +3. **Drop during assignment**: Does `x = new_value` drop the old value? (Probably yes.) + +4. **Partial initialization**: What if struct construction panics mid-way? (All constructed fields should drop.) + +5. **Arrays with destructors**: `[String; 10]` needs to drop 10 strings. How do we track which elements were initialized? + +6. **Linear type consumption**: How does consumption ultimately happen for linear types? At the end of the ownership chain, something must finalize the value. Options: (a) special "sink" functions that are allowed to drop linear values, (b) explicit `consume t;` statement, (c) linear types can have destructors that run when passed to a consuming function. + +## Future Work + +- **Finalize drop API**: The `drop fn` syntax is provisional. A future ADR will decide the final API, potentially integrating with traits or other abstraction mechanisms if they land. +- **Drop flags**: Runtime tracking of whether a value needs drop (for conditional moves) +- **Async drop**: When we have async, dropping across await points +- **Copy types with Drop**: With MVS (no aliasing), maybe possible? Needs research. + +## References + +- [ADR-0008: Affine Types and MVS](0008-affine-types-mvs.md) — Ownership foundation +- [Rust Drop trait](https://doc.rust-lang.org/std/ops/trait.Drop.html) — Inspiration +- [C++ Destructors](https://en.cppreference.com/w/cpp/language/destructor) — Drop order rules +- [Hylo Deinitialization](https://github.com/hylo-lang/hylo) — MVS approach to cleanup diff --git a/docs/designs/0011-runtime-heap.md b/docs/designs/0011-runtime-heap.md new file mode 100644 index 00000000..583bd61c --- /dev/null +++ b/docs/designs/0011-runtime-heap.md @@ -0,0 +1,244 @@ +--- +id: 0011 +title: Runtime Heap +status: proposal +tags: [runtime, memory, allocator] +feature-flag: runtime-heap +created: 2025-12-25 +accepted: +implemented: +spec-sections: [] +superseded-by: +--- + +# ADR-0011: Runtime Heap + +## Status + +Proposal + +## Summary + +Add heap allocation support to the Rue runtime via a simple bump allocator backed by `mmap`/`munmap` syscalls. This provides the foundation for heap-allocated types like `String`, `Vec`, and `Box` without depending on libc. The implementation exposes `__rue_alloc`, `__rue_realloc`, and `__rue_free` functions that can be called from generated code. + +## Context + +### Why a Custom Allocator? + +Rue compiles to standalone executables with no libc dependency. The runtime uses direct syscalls for all system interactions (exit, write). For heap allocation, we need to continue this pattern: + +1. **No libc dependency**: Keep executables minimal and self-contained +2. **Control**: Understand exactly what's happening with memory +3. **Simplicity**: A bump allocator is trivial to implement and debug +4. **Foundation**: Enables String, Vec, Box, and other heap types + +### What's Needed + +The destructor ADR (0010) identifies the missing piece: + +> "Open question: How do we hook into malloc/free? System allocator? Custom?" + +This ADR answers: custom allocator using mmap/munmap directly. + +### Design Constraints + +1. **Platform support**: Must work on x86-64 Linux and AArch64 macOS +2. **No global state initialization**: The allocator must work without explicit init +3. **Thread safety**: Not required initially (Rue is single-threaded) +4. **Simplicity over performance**: This is V1; optimize later if needed + +## Decision + +### Allocator Design: Bump Allocator with Arenas + +We use a **bump allocator** - the simplest possible design: + +1. Request large chunks ("arenas") from the OS via `mmap` +2. Allocations bump a pointer forward within the current arena +3. When an arena fills, request a new one +4. Individual `free()` is a no-op; memory is only returned when all arenas are freed + +This trades memory efficiency for simplicity. It's appropriate because: +- Rue programs are typically short-lived (compile, run, exit) +- Memory is reclaimed by the OS on exit anyway +- Future optimization can add a more sophisticated allocator + +### API + +Three functions exported from the runtime: + +```rust +/// Allocate `size` bytes aligned to `align`. +/// Returns null on failure (OOM or invalid arguments). +#[no_mangle] +pub extern "C" fn __rue_alloc(size: u64, align: u64) -> *mut u8 + +/// Reallocate `ptr` from `old_size` to `new_size` bytes. +/// Returns null on failure. Old data is copied to new location. +/// If ptr is null, behaves like alloc. If new_size is 0, behaves like free. +#[no_mangle] +pub extern "C" fn __rue_realloc(ptr: *mut u8, old_size: u64, new_size: u64, align: u64) -> *mut u8 + +/// Free memory at `ptr`. +/// For the bump allocator, this is a no-op (memory reclaimed on exit). +#[no_mangle] +pub extern "C" fn __rue_free(ptr: *mut u8, size: u64, align: u64) +``` + +The API includes `size` and `align` parameters even for `free` to enable future allocator upgrades without API changes. + +### Arena Management + +``` ++------------------+------------------+------------------+ +| Arena 1 | Arena 2 | Arena 3 | +| [alloc][alloc] | [alloc][...free] | [unused] | ++------------------+------------------+------------------+ + ^ + bump pointer +``` + +- Default arena size: 64 KiB (one large page, adjustable) +- Arenas are linked via a header at the start of each arena +- Large allocations (> arena size / 2) get their own dedicated arena + +### Implementation Details + +#### Global State + +```rust +struct ArenaHeader { + next: *mut ArenaHeader, // linked list of arenas + size: usize, // arena size (excluding header) + used: usize, // bytes allocated in this arena +} + +static mut ARENA_HEAD: *mut ArenaHeader = null_mut(); +static mut CURRENT_ARENA: *mut ArenaHeader = null_mut(); +``` + +Global state is initialized lazily on first allocation. + +#### Alignment + +Allocations are aligned by bumping the pointer to the next aligned address: + +```rust +fn align_up(addr: usize, align: usize) -> usize { + (addr + align - 1) & !(align - 1) +} +``` + +#### Platform Syscalls + +**Linux (x86-64 and AArch64)**: +```rust +// mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) +// syscall number: 9 (x86-64), 222 (aarch64) + +// munmap(addr, size) +// syscall number: 11 (x86-64), 215 (aarch64) +``` + +**macOS (AArch64)**: +```rust +// mmap: syscall 197 +// munmap: syscall 73 +``` + +### Error Handling + +- OOM: Return null pointer (caller must check) +- Invalid alignment (not power of 2): Return null pointer +- Zero-size allocation: Return null pointer + +No panics in the allocator - it just returns null on failure. Higher-level code (String, Vec) can panic with a useful message. + +### Testing Strategy + +1. **Unit tests in runtime**: Test allocator directly with various sizes/alignments +2. **Integration tests**: Allocate from Rue code, verify memory is usable +3. **Valgrind/ASan**: Verify no memory corruption (on Linux) + +## Implementation Phases + +Epic: rue-n50n + +### Phase 1: Syscall Wrappers (rue-n50n.1) + +Add mmap/munmap wrappers to each platform module: +- `x86_64_linux.rs`: `mmap()`, `munmap()` +- `aarch64_linux.rs`: `mmap()`, `munmap()` +- `aarch64_macos.rs`: `mmap()`, `munmap()` + +**Testable**: Call mmap, write to memory, munmap without crashing. + +### Phase 2: Bump Allocator Core (rue-n50n.2) + +Implement the allocator logic: +- Arena header structure +- `__rue_alloc` with alignment support +- `__rue_free` (no-op for now) +- Lazy initialization of first arena + +**Testable**: Allocate various sizes, verify returned pointers are aligned. + +### Phase 3: Realloc and Large Allocations (rue-n50n.3) + +- `__rue_realloc` implementation +- Large allocation handling (dedicated arenas) +- Edge cases (null ptr, zero size) + +**Testable**: Realloc growing and shrinking, large allocations. + +### Phase 4: Integration with Compiler (rue-n50n.4) + +- Add codegen support for calling `__rue_alloc`/`__rue_free` +- Wire up for future String/Vec types +- Document calling convention + +**Testable**: Rue code can call allocation intrinsics. + +## Consequences + +### Positive + +- **No libc dependency**: Maintains Rue's minimal runtime philosophy +- **Simplicity**: Bump allocator is ~100 lines of code +- **Cross-platform**: Same API on Linux and macOS +- **Foundation**: Enables all heap-allocated types + +### Negative + +- **Memory waste**: Bump allocator never frees until program exit +- **No thread safety**: Single-threaded only (acceptable for V1) +- **Large allocations**: Each gets a whole arena (wasteful) + +### Neutral + +- **API stability**: Include size/align in free for future compatibility +- **Performance**: Bump allocation is O(1), realloc is O(n) copy + +## Open Questions + +1. **Arena size**: 64 KiB reasonable? Should it grow dynamically? + +2. **Thread safety**: When Rue adds threading, need mutex or thread-local arenas? + +3. **Debug mode**: Should we poison freed memory in debug builds? + +4. **Metrics**: Should we track total allocated bytes for debugging? + +## Future Work + +- **Freelist allocator**: Add actual freeing for long-running programs +- **Size classes**: Like jemalloc, for better memory reuse +- **Thread-local arenas**: For multithreaded programs +- **Custom allocators**: Let users provide their own allocator + +## References + +- [ADR-0010: Destructors](0010-destructors.md) - Consumer of heap allocation +- [ADR-0008: Affine Types](0008-affine-types-mvs.md) - Ownership model +- [mmap(2)](https://man7.org/linux/man-pages/man2/mmap.2.html) - Linux syscall +- [Bump Allocation](https://fitzgeraldnick.com/2019/11/01/always-bump-downwards.html) - Design reference diff --git a/docs/spec/src/03-types/09-destructors.md b/docs/spec/src/03-types/09-destructors.md new file mode 100644 index 00000000..c486e9c6 --- /dev/null +++ b/docs/spec/src/03-types/09-destructors.md @@ -0,0 +1,128 @@ ++++ +title = "Destructors" +weight = 9 ++++ + +# Destructors + +This section describes when and how values are cleaned up in Rue. + +## Drop Semantics + +{{ rule(id="3.9:1", cat="normative") }} + +When a value's owning binding goes out of scope and the value has not been moved elsewhere, the value is *dropped*. Dropping a value runs its destructor, if it has one. + +{{ rule(id="3.9:2", cat="normative") }} + +A value is dropped exactly once. Values that are moved are not dropped at their original binding site; they are dropped at their final destination. + +{{ rule(id="3.9:3", cat="example") }} + +```rue +struct Data { value: i32 } + +fn consume(d: Data) -> i32 { d.value } + +fn main() -> i32 { + let d = Data { value: 42 }; + consume(d) // d is moved, dropped inside consume() +} // d is NOT dropped here (was moved) +``` + +## Drop Order + +{{ rule(id="3.9:4", cat="normative") }} + +When multiple values go out of scope at the same point, they are dropped in reverse declaration order (last declared, first dropped). + +{{ rule(id="3.9:5", cat="example") }} + +```rue +fn main() -> i32 { + let a = Data { value: 1 }; // declared first + let b = Data { value: 2 }; // declared second + 0 +} // b dropped first, then a +``` + +{{ rule(id="3.9:6", cat="informative") }} + +Reverse declaration order (LIFO) ensures that values declared later, which may depend on earlier values, are cleaned up first. + +## Trivially Droppable Types + +{{ rule(id="3.9:7", cat="normative") }} + +A type is *trivially droppable* if dropping it requires no action. Trivially droppable types have no destructor. + +{{ rule(id="3.9:8", cat="normative") }} + +The following types are trivially droppable: +- All integer types (`i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`) +- The boolean type (`bool`) +- The unit type (`()`) +- The never type (`!`) +- Enum types +- Arrays of trivially droppable types + +{{ rule(id="3.9:9", cat="normative") }} + +A struct type is trivially droppable if all of its fields are trivially droppable. + +{{ rule(id="3.9:10", cat="example") }} + +```rue +// Trivially droppable: all fields are trivially droppable +struct Point { x: i32, y: i32 } + +fn main() -> i32 { + let p = Point { x: 1, y: 2 }; + p.x // p is trivially dropped (no-op) +} +``` + +## Types with Destructors + +{{ rule(id="3.9:11", cat="normative") }} + +A type has a destructor if dropping it requires cleanup actions. When such a type is dropped, its destructor is invoked. + +{{ rule(id="3.9:12", cat="normative") }} + +A struct has a destructor if any of its fields has a destructor, or if the struct has a user-defined destructor. + +{{ rule(id="3.9:13", cat="normative") }} + +For a struct with a destructor, fields are dropped in declaration order (first declared, first dropped). + +{{ rule(id="3.9:14", cat="informative") }} + +The distinction between "drop order of bindings" (reverse declaration) and "drop order of fields" (declaration order) matches C++ and Rust behavior. Bindings use LIFO for dependency correctness; fields use declaration order for consistency with construction order. + +## Drop Placement + +{{ rule(id="3.9:15", cat="dynamic-semantics") }} + +Drops are inserted at the following points: +- At the end of a block scope, for all live bindings declared in that scope +- Before a `return` statement, for all live bindings in all enclosing scopes +- Before a `break` statement, for all live bindings declared inside the loop + +{{ rule(id="3.9:16", cat="dynamic-semantics") }} + +Each branch of a conditional independently drops bindings declared within that branch. + +{{ rule(id="3.9:17", cat="example") }} + +```rue +fn example(condition: bool) -> i32 { + let a = Data { value: 1 }; + if condition { + let b = Data { value: 2 }; + return 42; // b dropped, then a dropped, then return + } + let c = Data { value: 3 }; + 0 // c dropped, then a dropped +} +```