diff --git a/crates/nera-codegen/src/func_translator/ffi_emit.rs b/crates/nera-codegen/src/func_translator/ffi_emit.rs index 60620cd..300ef95 100644 --- a/crates/nera-codegen/src/func_translator/ffi_emit.rs +++ b/crates/nera-codegen/src/func_translator/ffi_emit.rs @@ -39,6 +39,67 @@ impl FuncTranslator { Ok(builder.inst_results(call)[0]) } + /// All migrated runtime-helper and inline slow paths share this check. + pub(super) fn emit_checked_arithmetic_call( + &mut self, + module: &mut ObjectModule, + builder: &mut FunctionBuilder, + name: &str, + ctx_var: Variable, + operands: &[Value], + failure: Option, + ) -> Result { + let value = self.emit_ffi_call(module, builder, name, ctx_var, operands)?; + let ctx = builder.use_var(ctx_var); + let status = builder.ins().load(types::I32, MemFlags::trusted(), ctx, CTX_OFFSET_STATUS); + let failed = builder.ins().icmp_imm(IntCC::Equal, status, STATUS_EXCEPTION as i64); + let next = builder.create_block(); + match failure { + Some(target) => { builder.ins().brif(failed, target, &[], next, &[]); } + None => { + let leave = builder.create_block(); + builder.ins().brif(failed, leave, &[], next, &[]); + builder.switch_to_block(leave); + let nil = builder.ins().iconst(types::I64, NIL_VALUE as i64); + self.emit_stack_pop_and_return(module, builder, ctx_var, nil)?; + } + } + builder.switch_to_block(next); + Ok(value) + } + + pub(super) fn emit_inline_not( + &mut self, + module: &mut ObjectModule, + builder: &mut FunctionBuilder, + ctx_var: Variable, + operand: Value, + failure: Option, + ) -> Result { + let true_atom = self.load_true_atom(module, builder)?; + let false_atom = self.load_false_atom(module, builder)?; + let is_true = builder.ins().icmp(IntCC::Equal, operand, true_atom); + let is_false = builder.ins().icmp(IntCC::Equal, operand, false_atom); + let is_bool = builder.ins().bor(is_true, is_false); + let fast = builder.create_block(); + let slow = builder.create_block(); + builder.set_cold_block(slow); + let merge = builder.create_block(); + builder.append_block_param(merge, types::I64); + builder.ins().brif(is_bool, fast, &[], slow, &[]); + builder.switch_to_block(fast); + builder.seal_block(fast); + let flipped = builder.ins().select(is_true, false_atom, true_atom); + builder.ins().jump(merge, &[BlockArg::Value(flipped)]); + builder.switch_to_block(slow); + builder.seal_block(slow); + let value = self.emit_checked_arithmetic_call(module, builder, "nera_rt_bool_not", ctx_var, &[operand], failure)?; + builder.ins().jump(merge, &[BlockArg::Value(value)]); + builder.switch_to_block(merge); + builder.seal_block(merge); + Ok(builder.block_params(merge)[0]) + } + /// Emit a call to a void FFI function (no return value used). pub(super) fn emit_ffi_call_void( &mut self, @@ -176,7 +237,7 @@ impl FuncTranslator { lhs: Value, rhs: Value, operation: InlineArithmetic, - check_exception_in_slow_path: bool, + failure: Option, ) -> Result { @@ -244,23 +305,7 @@ impl FuncTranslator { // Slow path: FFI fallback builder.switch_to_block(slow_block); builder.seal_block(slow_block); - let slow_ret = self.emit_ffi_call(module, builder, ffi_name, ctx_var, &[lhs, rhs])?; - if check_exception_in_slow_path { - // Check for exception here so the fast path (which never raises) avoids the cost. - let ctx_val = builder.use_var(ctx_var); - let status = builder.ins().load(types::I32, MemFlags::trusted(), ctx_val, CTX_OFFSET_STATUS); - let exc_const = builder.ins().iconst(types::I32, STATUS_EXCEPTION as i64); - let is_exc = builder.ins().icmp(IntCC::Equal, status, exc_const); - let exc_block = builder.create_block(); - let slow_ok_block = builder.create_block(); - builder.ins().brif(is_exc, exc_block, &[], slow_ok_block, &[]); - - builder.switch_to_block(exc_block); - let nil = builder.ins().iconst(types::I64, NIL_VALUE as i64); - self.emit_stack_pop_and_return(module, builder, ctx_var, nil)?; - - builder.switch_to_block(slow_ok_block); - } + let slow_ret = self.emit_checked_arithmetic_call(module, builder, ffi_name, ctx_var, &[lhs, rhs], failure)?; builder.ins().jump(merge_block, &[BlockArg::Value(slow_ret)]); // Merge diff --git a/crates/nera-codegen/src/func_translator/instructions.rs b/crates/nera-codegen/src/func_translator/instructions.rs index 2128bf9..9e2eff3 100644 --- a/crates/nera-codegen/src/func_translator/instructions.rs +++ b/crates/nera-codegen/src/func_translator/instructions.rs @@ -8,7 +8,7 @@ use cranelift_frontend::{FunctionBuilder, Variable}; use cranelift_module::Module; use cranelift_object::ObjectModule; -use nera_ir::ir::{CallTarget, Instruction, Literal, VarId}; +use nera_ir::ir::{ArithmeticOp, FailureDestination, CallTarget, Instruction, Literal, VarId}; use crate::term_layout::*; use super::ffi_emit::SegmentSizeKind; use super::{ModuleContext, declare_runtime_func, @@ -42,6 +42,39 @@ impl FuncTranslator { }; let is_try_protected = protected_result.is_some_and(|v| self.current_try_protected_values.contains(v)); match inst { + Instruction::Arithmetic(site) => { + let failure = match site.failure { + FailureDestination::Propagate => None, + FailureDestination::Block(target) => Some(*self.current_blocks.get(&target) + .ok_or_else(|| TranslateError::Module("missing arithmetic failure destination".into()))?), + }; + let operands: Vec<_> = site.operation.operands().iter() + .map(|v| builder.use_var(var_map[v])).collect(); + let value = match site.operation { + ArithmeticOp::Add(..) | ArithmeticOp::Subtract(..) | ArithmeticOp::Multiply(..) => { + use super::ffi_emit::InlineArithmetic; + let operation = match site.operation { + ArithmeticOp::Add(..) => InlineArithmetic::Add, + ArithmeticOp::Subtract(..) => InlineArithmetic::Subtract, + _ => InlineArithmetic::Multiply, + }; + self.emit_inline_arith(module, builder, ctx_var, operands[0], operands[1], operation, failure)? + } + ArithmeticOp::Not(_) => self.emit_inline_not(module, builder, ctx_var, operands[0], failure)?, + _ => { + let name = match site.operation { + ArithmeticOp::Divide(..) => "nera_rt_fdiv", + ArithmeticOp::Negate(_) => "nera_rt_negate", + ArithmeticOp::BitNot(_) => "nera_rt_bnot", + _ => unreachable!(), + }; + self.emit_checked_arithmetic_call(module, builder, name, ctx_var, &operands, failure)? + } + }; + if let Some(var) = site.result.and_then(|v| var_map.get(&v)) { + builder.def_var(*var, value); + } + } Instruction::Const(result, lit) => { let val = match lit { Literal::Integer(n) => { @@ -91,16 +124,9 @@ impl FuncTranslator { let check_exc_in_slow = !is_try_protected; let val = match op { - // --- Inline small-int arithmetic fast paths --- - nera_syntax::ast::BinOp::Add | nera_syntax::ast::BinOp::Sub | nera_syntax::ast::BinOp::Mul => { - use super::ffi_emit::InlineArithmetic; - let operation = match op { - nera_syntax::ast::BinOp::Add => InlineArithmetic::Add, - nera_syntax::ast::BinOp::Sub => InlineArithmetic::Subtract, - _ => InlineArithmetic::Multiply, - }; - self.emit_inline_arith(module, builder, ctx_var, lhs_val, rhs_val, - operation, check_exc_in_slow)? + nera_syntax::ast::BinOp::Add | nera_syntax::ast::BinOp::Sub + | nera_syntax::ast::BinOp::Mul | nera_syntax::ast::BinOp::Div => { + return Err(TranslateError::Module("arithmetic requires an explicit operation site".into())); } nera_syntax::ast::BinOp::IntDiv => { self.emit_inline_div_rem(module, builder, ctx_var, lhs_val, rhs_val, "nera_rt_int_div", false, check_exc_in_slow)? @@ -230,10 +256,6 @@ impl FuncTranslator { builder.seal_block(merge_block); builder.block_params(merge_block)[0] } - nera_syntax::ast::BinOp::Div => { - needs_exception_check = true; - self.emit_ffi_call(module, builder, "nera_rt_fdiv", ctx_var, &[lhs_val, rhs_val])? - } nera_syntax::ast::BinOp::ListAppend => { needs_exception_check = true; self.emit_ffi_call(module, builder, "nera_rt_list_append", ctx_var, &[lhs_val, rhs_val])? @@ -256,62 +278,15 @@ impl FuncTranslator { } Instruction::UnaryOp(result, op, operand) => { + if !matches!(op, nera_syntax::ast::UnaryOp::Pos) { + return Err(TranslateError::Module("arithmetic requires an explicit operation site".into())); + } let operand_val = builder.use_var(var_map[operand]); - - let needs_exception_check = true; - - let val = match op { - nera_syntax::ast::UnaryOp::Pos => { - self.emit_ffi_call(module, builder, "nera_rt_unary_plus", ctx_var, &[operand_val])? - } - nera_syntax::ast::UnaryOp::Neg => { - self.emit_ffi_call(module, builder, "nera_rt_negate", ctx_var, &[operand_val])? - } - nera_syntax::ast::UnaryOp::Not => { - // Logical not: true -> false, false -> true. - // Raises badarg if operand is not a boolean atom. - let true_atom = self.load_true_atom(module, builder)?; - let false_atom = self.load_false_atom(module, builder)?; - let is_true = builder.ins().icmp(IntCC::Equal, operand_val, true_atom); - let is_false = builder.ins().icmp(IntCC::Equal, operand_val, false_atom); - let is_bool = builder.ins().bor(is_true, is_false); - - let fast_block = builder.create_block(); - let slow_block = builder.create_block(); - builder.set_cold_block(slow_block); - let merge_block = builder.create_block(); - builder.append_block_param(merge_block, types::I64); - - builder.ins().brif(is_bool, fast_block, &[], slow_block, &[]); - - // Fast path: flip true<->false - builder.switch_to_block(fast_block); - builder.seal_block(fast_block); - let fast_result = builder.ins().select(is_true, false_atom, true_atom); - builder.ins().jump(merge_block, &[BlockArg::Value(fast_result)]); - - // Slow path: FFI call that raises badarg - builder.switch_to_block(slow_block); - builder.seal_block(slow_block); - let slow_ret = self.emit_ffi_call(module, builder, "nera_rt_bool_not", ctx_var, &[operand_val])?; - builder.ins().jump(merge_block, &[BlockArg::Value(slow_ret)]); - - builder.switch_to_block(merge_block); - builder.seal_block(merge_block); - builder.block_params(merge_block)[0] - } - nera_syntax::ast::UnaryOp::Bnot => { - self.emit_ffi_call(module, builder, "nera_rt_bnot", ctx_var, &[operand_val])? - } - }; - + let val = self.emit_ffi_call(module, builder, "nera_rt_unary_plus", ctx_var, &[operand_val])?; if let Some(var) = var_map.get(result) { builder.def_var(*var, val); } - - // Check for exception after the unary operation - // (skip if inside a try block — CheckException handles it) - if needs_exception_check && !is_try_protected { + if !is_try_protected { self.emit_ffi_exception_check(module, builder, ctx_var)?; } } diff --git a/crates/nera-codegen/src/func_translator/liveness.rs b/crates/nera-codegen/src/func_translator/liveness.rs index 7e51550..fab71cb 100644 --- a/crates/nera-codegen/src/func_translator/liveness.rs +++ b/crates/nera-codegen/src/func_translator/liveness.rs @@ -99,7 +99,7 @@ pub(super) fn compute_suspend_points( let mut successors: HashMap> = HashMap::new(); for block in &ir_func.blocks { - let succs = terminator_successors(&block.terminator); + let succs = block.successors(); successors.insert(block.id, succs); } @@ -195,6 +195,7 @@ pub(super) fn compute_suspend_points( /// Collect VarIds defined (written) by an instruction. pub(super) fn collect_instruction_defs(inst: &Instruction, defs: &mut HashSet) { match inst { + Instruction::Arithmetic(site) => defs.extend(site.result), Instruction::Const(v, _) | Instruction::BinOp(v, _, _, _) | Instruction::UnaryOp(v, _, _) @@ -286,6 +287,7 @@ pub(super) fn collect_instruction_defs(inst: &Instruction, defs: &mut HashSet) { match inst { + Instruction::Arithmetic(site) => uses.extend(site.operation.operands()), Instruction::Const(_, _) => {} Instruction::BinOp(_, _, a, b) => { uses.insert(*a); uses.insert(*b); } Instruction::UnaryOp(_, _, a) => { uses.insert(*a); } @@ -414,6 +416,7 @@ pub(super) fn collect_terminator_uses(term: &Terminator, uses: &mut HashSet Vec { term.successors() } diff --git a/crates/nera-codegen/src/func_translator/mod.rs b/crates/nera-codegen/src/func_translator/mod.rs index e0b03e4..f432007 100644 --- a/crates/nera-codegen/src/func_translator/mod.rs +++ b/crates/nera-codegen/src/func_translator/mod.rs @@ -144,9 +144,10 @@ pub struct FuncTranslator { pub(super) current_ir_block_id: Option, /// Per-function: the instruction index within the current block. pub(super) current_inst_idx: usize, - /// Per-function: set of block IDs that contain CheckException. - /// BinOp/UnaryOp FFI calls in these blocks should not emit exception - /// checks because try/catch will handle them. + pub(super) current_blocks: HashMap, + /// Per-function: protected result registers for calls and unmigrated + /// operations whose existing IR checks handle exceptions. Arithmetic sites + /// never consult this set; their destinations are instruction-local. pub(super) current_try_protected_values: std::collections::HashSet, /// Per-function: Cranelift Variable that holds the reduction counter. /// Kept in a CPU register across self-tail-call loop iterations; @@ -177,6 +178,7 @@ impl FuncTranslator { current_call_result_vars: Vec::new(), current_ir_block_id: None, current_inst_idx: 0, + current_blocks: HashMap::new(), current_try_protected_values: std::collections::HashSet::new(), reds_var: None, last_branch_predicate: None, @@ -293,10 +295,17 @@ impl FuncTranslator { var_map.entry(var_id).or_insert_with(|| builder.declare_var(types::I64)); }; + self.current_blocks = block_map.clone(); + // Pre-declare all variables used in the function for ir_block in &ir_func.blocks { for inst in &ir_block.instructions { match inst { + Instruction::Arithmetic(site) => { + if let Some(v) = site.result { + declare_var(v, &mut var_map, &mut builder); + } + } Instruction::Const(v, _) | Instruction::BinOp(v, _, _, _) | Instruction::UnaryOp(v, _, _) diff --git a/crates/nera-ir/src/display.rs b/crates/nera-ir/src/display.rs index b06a089..4a82b5e 100644 --- a/crates/nera-ir/src/display.rs +++ b/crates/nera-ir/src/display.rs @@ -55,6 +55,8 @@ impl fmt::Display for BasicBlock { impl fmt::Display for Instruction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Instruction::Arithmetic(site) => write!(f, "site{} {:?} = {:?} [{:?}, failure {:?}]", + site.id.0, site.result, site.operation, site.context, site.failure), Instruction::Const(v, lit) => write!(f, "v{} = const {}", v.0, lit), Instruction::BinOp(v, op, a, b) => { write!(f, "v{} = {:?}(v{}, v{})", v.0, op, a.0, b.0) diff --git a/crates/nera-ir/src/ir.rs b/crates/nera-ir/src/ir.rs index 02af6af..f921838 100644 --- a/crates/nera-ir/src/ir.rs +++ b/crates/nera-ir/src/ir.rs @@ -27,7 +27,8 @@ pub struct IrFunction { pub arity: u32, pub mangled_name: String, pub blocks: Vec, - /// Results of potentially raising operations lexically inside a try body. + /// Legacy protected results for calls and unmigrated operations in try bodies. + /// Arithmetic sites carry their own failure destinations instead. pub protected_values: Vec, pub entry_block: BlockId, pub params: Vec, @@ -41,6 +42,76 @@ pub struct BlockId(pub u32); #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct VarId(pub u32); +/// Function-local instruction identity, independent of mutable result registers. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct OperationId(pub u32); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvaluationContext { + Body, + Try, + Guard, + Cleanup, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FailureDestination { + Propagate, + Block(BlockId), +} + +/// The bounded arithmetic pilot's single raising policy. Every variant checks +/// runtime failure before any following instruction can execute. +#[derive(Debug, Clone, Copy)] +pub enum ArithmeticOp { + Add(VarId, VarId), + Subtract(VarId, VarId), + Multiply(VarId, VarId), + Divide(VarId, VarId), + Negate(VarId), + BitNot(VarId), + Not(VarId), +} + +impl ArithmeticOp { + pub fn operands(self) -> Vec { + match self { + Self::Add(a, b) | Self::Subtract(a, b) | Self::Multiply(a, b) + | Self::Divide(a, b) => vec![a, b], + Self::Negate(a) | Self::BitNot(a) | Self::Not(a) => vec![a], + } + } + + pub fn from_binary(op: BinOp, a: VarId, b: VarId) -> Option { + Some(match op { + BinOp::Add => Self::Add(a, b), + BinOp::Sub => Self::Subtract(a, b), + BinOp::Mul => Self::Multiply(a, b), + BinOp::Div => Self::Divide(a, b), + _ => return None, + }) + } + + pub fn from_unary(op: UnaryOp, a: VarId) -> Option { + Some(match op { + UnaryOp::Neg => Self::Negate(a), + UnaryOp::Bnot => Self::BitNot(a), + UnaryOp::Not => Self::Not(a), + _ => return None, + }) + } +} + +#[derive(Debug, Clone)] +pub struct ArithmeticSite { + pub id: OperationId, + /// None supports an operation whose result is deliberately discarded. + pub result: Option, + pub operation: ArithmeticOp, + pub context: EvaluationContext, + pub failure: FailureDestination, +} + #[derive(Debug, Clone)] pub struct BasicBlock { pub id: BlockId, @@ -50,8 +121,27 @@ pub struct BasicBlock { pub terminator: Terminator, } +impl BasicBlock { + /// Normal CFG edges plus the explicit instruction-level failure edges. + pub fn successors(&self) -> Vec { + let mut successors = self.terminator.successors(); + for inst in &self.instructions { + if let Instruction::Arithmetic(ArithmeticSite { + failure: FailureDestination::Block(target), .. + }) = inst { + if !successors.contains(target) { + successors.push(*target); + } + } + } + successors + } +} + #[derive(Debug, Clone)] pub enum Instruction { + /// On failure, transfer immediately without defining the result. + Arithmetic(ArithmeticSite), /// result = literal value Const(VarId, Literal), diff --git a/crates/nera-ir/src/lower/exception.rs b/crates/nera-ir/src/lower/exception.rs index 76d41f6..a644dd5 100644 --- a/crates/nera-ir/src/lower/exception.rs +++ b/crates/nera-ir/src/lower/exception.rs @@ -186,7 +186,11 @@ impl IrBuilder { *scope = before_bindings; } self.leave_branch(&branch); - let _after_result = self.lower_body(after_exprs, false)?; + let previous_context = self.evaluation_context; + self.evaluation_context = EvaluationContext::Cleanup; + let after_result = self.lower_body(after_exprs, false); + self.evaluation_context = previous_context; + let _after_result = after_result?; // An exception the after body left pending replaces the held one. let after_failed = self.fresh_var(); self.emit(Instruction::CheckException(after_failed)); diff --git a/crates/nera-ir/src/lower/expr.rs b/crates/nera-ir/src/lower/expr.rs index 212f34a..159e5fb 100644 --- a/crates/nera-ir/src/lower/expr.rs +++ b/crates/nera-ir/src/lower/expr.rs @@ -130,8 +130,9 @@ impl IrBuilder { let lhs_var = self.lower_expr(lhs, false)?; let rhs_var = self.lower_expr(rhs, false)?; let result = self.fresh_var(); - self.emit(Instruction::BinOp(result, *op, lhs_var, rhs_var)); - if Self::binop_may_raise(*op) { + self.emit_binop(result, *op, lhs_var, rhs_var); + if Self::binop_may_raise(*op) + && ArithmeticOp::from_binary(*op, lhs_var, rhs_var).is_none() { self.branch_on_pending_exception(); } Ok(result) @@ -142,8 +143,10 @@ impl IrBuilder { ast::Expr::UnaryOp(op, operand, _) => { let operand_var = self.lower_expr(operand, false)?; let result = self.fresh_var(); - self.emit(Instruction::UnaryOp(result, *op, operand_var)); - self.branch_on_pending_exception(); + self.emit_unaryop(result, *op, operand_var); + if matches!(op, ast::UnaryOp::Pos) { + self.branch_on_pending_exception(); + } Ok(result) } @@ -192,7 +195,7 @@ impl IrBuilder { if let Some(remaining_bits) = mc_remaining { let seven = self.emit_const(Literal::Integer(7)); let rounded_bits = self.fresh_var(); - self.emit(Instruction::BinOp(rounded_bits, BinOp::Add, remaining_bits, seven)); + self.emit_binop(rounded_bits, BinOp::Add, remaining_bits, seven); let eight = self.emit_const(Literal::Integer(8)); let result = self.fresh_var(); self.emit(Instruction::BinOp(result, BinOp::IntDiv, rounded_bits, eight)); @@ -1188,7 +1191,7 @@ impl IrBuilder { .unwrap_or_else(|| self.emit_const(Literal::Integer(0))); for si in sizes { let new_sum = self.fresh_var(); - self.emit(Instruction::BinOp(new_sum, BinOp::Add, sum, si.1)); + self.emit_binop(new_sum, BinOp::Add, sum, si.1); sum = new_sum; } sum @@ -1220,7 +1223,7 @@ impl IrBuilder { self.branch_on_pending_exception(); // Advance offset by actual bits written (variable for UTF-8/16) let new_offset = self.fresh_var(); - self.emit(Instruction::BinOp(new_offset, BinOp::Add, bit_offset_var, bits_written)); + self.emit_binop(new_offset, BinOp::Add, bit_offset_var, bits_written); bit_offset_var = new_offset; } else if *is_bin { self.emit(Instruction::BinaryPutBinary(bin_var, bit_offset_var, *val_var, *bits_var)); @@ -1229,7 +1232,7 @@ impl IrBuilder { bit_offset_var = *bits_var; } else { let new_offset = self.fresh_var(); - self.emit(Instruction::BinOp(new_offset, BinOp::Add, bit_offset_var, *bits_var)); + self.emit_binop(new_offset, BinOp::Add, bit_offset_var, *bits_var); bit_offset_var = new_offset; } } else if *is_float { @@ -1239,7 +1242,7 @@ impl IrBuilder { bit_offset_var = *bits_var; } else { let new_offset = self.fresh_var(); - self.emit(Instruction::BinOp(new_offset, BinOp::Add, bit_offset_var, *bits_var)); + self.emit_binop(new_offset, BinOp::Add, bit_offset_var, *bits_var); bit_offset_var = new_offset; } } else { @@ -1249,7 +1252,7 @@ impl IrBuilder { bit_offset_var = *bits_var; } else { let new_offset = self.fresh_var(); - self.emit(Instruction::BinOp(new_offset, BinOp::Add, bit_offset_var, *bits_var)); + self.emit_binop(new_offset, BinOp::Add, bit_offset_var, *bits_var); bit_offset_var = new_offset; } } @@ -1337,10 +1340,13 @@ impl IrBuilder { let branch = self.enter_branch(); // Lower the guard expression + let previous_context = self.evaluation_context; + self.evaluation_context = EvaluationContext::Guard; let expr_val = self.lower_expr(expr, false).unwrap_or_else(|_| { self.emit_const(Literal::Atom(self.false_atom_id)) }); + self.evaluation_context = previous_context; self.try_exception_targets.pop(); // Check if an FFI call raised an exception diff --git a/crates/nera-ir/src/lower/mod.rs b/crates/nera-ir/src/lower/mod.rs index 12a92e4..771db17 100644 --- a/crates/nera-ir/src/lower/mod.rs +++ b/crates/nera-ir/src/lower/mod.rs @@ -109,6 +109,8 @@ pub struct IrBuilder { /// the top of this stack instead of returning from the function. pub(super) try_exception_targets: Vec, pub(super) protected_values: Vec, + pub(super) next_operation: u32, + pub(super) evaluation_context: EvaluationContext, /// Counter for generating unique list comprehension helper names. pub(super) lc_counter: u32, @@ -177,6 +179,8 @@ pub(super) struct BuilderState { pub(super) scopes: Vec>, pub(super) try_exception_targets: Vec, pub(super) protected_values: Vec, + pub(super) next_operation: u32, + pub(super) evaluation_context: EvaluationContext, // VarIds are local to each function, including deferred binary bindings. pub(super) match_contexts: HashMap, pattern_inputs: Option>, @@ -197,12 +201,16 @@ impl IrBuilder { scopes: std::mem::take(&mut self.scopes), try_exception_targets: std::mem::take(&mut self.try_exception_targets), protected_values: std::mem::take(&mut self.protected_values), + next_operation: self.next_operation, + evaluation_context: self.evaluation_context, match_contexts: std::mem::take(&mut self.match_contexts), pattern_inputs: self.pattern_inputs.take(), }; // Reset for the nested function self.current_blocks = Vec::new(); self.protected_values.clear(); + self.next_operation = 0; + self.evaluation_context = EvaluationContext::Body; self.next_var = 0; self.next_block = 0; self.scopes = vec![HashMap::new()]; @@ -221,6 +229,8 @@ impl IrBuilder { self.scopes = state.scopes; self.try_exception_targets = state.try_exception_targets; self.protected_values = state.protected_values; + self.next_operation = state.next_operation; + self.evaluation_context = state.evaluation_context; self.match_contexts = state.match_contexts; self.pattern_inputs = state.pattern_inputs; } @@ -289,6 +299,8 @@ impl IrBuilder { maps_atom_id, try_exception_targets: Vec::new(), protected_values: Vec::new(), + next_operation: 0, + evaluation_context: EvaluationContext::Body, lc_counter: 0, lambda_counter: 0, next_atom_id, @@ -410,6 +422,8 @@ impl IrBuilder { // Reset state for new function self.current_blocks = Vec::new(); self.protected_values.clear(); + self.next_operation = 0; + self.evaluation_context = EvaluationContext::Body; self.next_block = 0; self.next_var = 0; self.scopes = vec![HashMap::new()]; @@ -585,6 +599,36 @@ impl IrBuilder { } } + pub fn emit_binop(&mut self, result: VarId, op: ast::BinOp, lhs: VarId, rhs: VarId) { + if let Some(operation) = ArithmeticOp::from_binary(op, lhs, rhs) { + self.emit_arithmetic(result, operation); + } else { + self.emit(Instruction::BinOp(result, op, lhs, rhs)); + } + } + + pub fn emit_unaryop(&mut self, result: VarId, op: ast::UnaryOp, operand: VarId) { + if let Some(operation) = ArithmeticOp::from_unary(op, operand) { + self.emit_arithmetic(result, operation); + } else { + self.emit(Instruction::UnaryOp(result, op, operand)); + } + } + + fn emit_arithmetic(&mut self, result: VarId, operation: ArithmeticOp) { + let failure = self.try_exception_targets.last().copied() + .map_or(FailureDestination::Propagate, FailureDestination::Block); + let context = match (self.evaluation_context, failure) { + (EvaluationContext::Body, FailureDestination::Block(_)) => EvaluationContext::Try, + (context, _) => context, + }; + let id = OperationId(self.next_operation); + self.next_operation += 1; + self.current_instructions.push(Instruction::Arithmetic(ArithmeticSite { + id, result: Some(result), operation, context, failure, + })); + } + pub fn emit(&mut self, inst: Instruction) { if !self.try_exception_targets.is_empty() { let result = match &inst { diff --git a/crates/nera-ir/src/lower/pattern_lower.rs b/crates/nera-ir/src/lower/pattern_lower.rs index 0f9c743..ba7e36b 100644 --- a/crates/nera-ir/src/lower/pattern_lower.rs +++ b/crates/nera-ir/src/lower/pattern_lower.rs @@ -142,12 +142,12 @@ impl IrBuilder { // Rest segment: compute remaining bits, register match context if let Some(name) = fresh_name(self, seg) { let remaining_bits = self.fresh_var(); - self.emit(Instruction::BinOp( + self.emit_binop( remaining_bits, nera_syntax::ast::BinOp::Sub, bin_total_bits, bit_offset_var, - )); + ); let rest_var = self.fresh_var(); self.bind_var(name, rest_var); self.match_contexts.insert(rest_var, MatchContextInfo { @@ -173,11 +173,11 @@ impl IrBuilder { // UTF raw readers share integer extraction's // containing-byte base for reused cursors. let extraction_offset = self.fresh_var(); - self.emit(Instruction::BinOp(extraction_offset, - ast::BinOp::Add, bit_offset_var, initial_bit_offset)); + self.emit_binop(extraction_offset, + ast::BinOp::Add, bit_offset_var, initial_bit_offset); let extraction_end = self.fresh_var(); - self.emit(Instruction::BinOp(extraction_end, - ast::BinOp::Add, bin_total_bits, initial_bit_offset)); + self.emit_binop(extraction_end, + ast::BinOp::Add, bin_total_bits, initial_bit_offset); match seg.type_spec.typ { ast::BinSegmentKind::Utf8 => { self.emit(Instruction::BsGetUtf8( @@ -202,12 +202,12 @@ impl IrBuilder { } // Advance bit offset by actual bits consumed let new_offset = self.fresh_var(); - self.emit(Instruction::BinOp( + self.emit_binop( new_offset, nera_syntax::ast::BinOp::Add, bit_offset_var, bits_consumed, - )); + ); bit_offset_var = new_offset; } _ => { @@ -231,12 +231,12 @@ impl IrBuilder { } else { let unit_var = self.emit_const(Literal::Integer(unit)); let product = self.fresh_var(); - self.emit(Instruction::BinOp( + self.emit_binop( product, nera_syntax::ast::BinOp::Mul, size_var, unit_var, - )); + ); product } } @@ -278,8 +278,8 @@ impl IrBuilder { } else { // BsGetIntegerInline now takes bit_offset and bit_count let extraction_offset = self.fresh_var(); - self.emit(Instruction::BinOp(extraction_offset, - nera_syntax::ast::BinOp::Add, bit_offset_var, initial_bit_offset)); + self.emit_binop(extraction_offset, + nera_syntax::ast::BinOp::Add, bit_offset_var, initial_bit_offset); self.emit(Instruction::BsGetIntegerInline( extracted, mc_base_ptr, extraction_offset, seg_total_bits, flags, )); @@ -300,12 +300,12 @@ impl IrBuilder { // Advance bit offset let new_offset = self.fresh_var(); - self.emit(Instruction::BinOp( + self.emit_binop( new_offset, nera_syntax::ast::BinOp::Add, bit_offset_var, seg_total_bits, - )); + ); bit_offset_var = new_offset; } } diff --git a/crates/nera-ir/src/lower/tests.rs b/crates/nera-ir/src/lower/tests.rs index 569a1e4..88b3844 100644 --- a/crates/nera-ir/src/lower/tests.rs +++ b/crates/nera-ir/src/lower/tests.rs @@ -114,13 +114,13 @@ add(A, B) -> A + B. "#; let ir = lower_source(source).unwrap(); assert_eq!(ir.functions.len(), 1); - // Check that the IR contains a BinOp instruction + // Check that the IR contains a checked arithmetic instruction let has_binop = ir.functions[0].blocks.iter().any(|block| { block.instructions.iter().any(|inst| { - matches!(inst, Instruction::BinOp(_, BinOp::Add, _, _)) + matches!(inst, Instruction::Arithmetic(ArithmeticSite { operation: ArithmeticOp::Add(..), .. })) }) }); - assert!(has_binop, "Expected Add BinOp in IR"); + assert!(has_binop, "Expected checked Add operation in IR"); } // ── Adversarial pattern matching tests ────────────────────── @@ -1063,11 +1063,11 @@ f() -> 1 + 2. b.instructions.iter().any(|i| matches!(i, Instruction::Const(_, Literal::Integer(3)))) ); assert!(has_const_3, "1 + 2 should be folded to Const(3)"); - // No BinOp::Add should remain for the constant expression + // No checked Add should remain for the constant expression let has_add = ir.functions[0].blocks.iter().any(|b| - b.instructions.iter().any(|i| matches!(i, Instruction::BinOp(_, BinOp::Add, _, _))) + b.instructions.iter().any(|i| matches!(i, Instruction::Arithmetic(ArithmeticSite { operation: ArithmeticOp::Add(..), .. }))) ); - assert!(!has_add, "BinOp::Add should be eliminated by constant folding"); + assert!(!has_add, "Checked Add should be eliminated by constant folding"); } #[test] @@ -1114,11 +1114,11 @@ f() -> 10 / 4. f() -> 10 / 0. "#; let ir = lower_source(source).unwrap(); - // Division by zero should NOT be folded — must remain as BinOp + // Division by zero must remain as checked runtime arithmetic let has_div = ir.functions[0].blocks.iter().any(|b| - b.instructions.iter().any(|i| matches!(i, Instruction::BinOp(_, BinOp::Div, _, _))) + b.instructions.iter().any(|i| matches!(i, Instruction::Arithmetic(ArithmeticSite { operation: ArithmeticOp::Divide(..), .. }))) ); - assert!(has_div, "Division by zero should be preserved as runtime BinOp"); + assert!(has_div, "Division by zero should be preserved as runtime arithmetic operation"); } #[test] @@ -1141,8 +1141,10 @@ f() -> 10 / 0. let ir = lower_source(&source).unwrap(); let instructions = || ir.functions[0].blocks.iter().flat_map(|b| b.instructions.iter()); assert!( - instructions().any(|i| matches!(i, Instruction::BinOp(_, o, _, _) if *o == op)), - "{expr} should be preserved as a runtime BinOp" + instructions().any(|i| matches!(i, Instruction::Arithmetic(site) if matches!((site.operation, op), + (ArithmeticOp::Add(..), BinOp::Add) | (ArithmeticOp::Subtract(..), BinOp::Sub) + | (ArithmeticOp::Multiply(..), BinOp::Mul) | (ArithmeticOp::Divide(..), BinOp::Div)))), + "{expr} should be preserved as a runtime arithmetic operation" ); assert!( !instructions().any(|i| matches!(i, Instruction::Const(_, Literal::Float(f)) if !f.is_finite())), @@ -1180,7 +1182,13 @@ f() -> 4611686018427387903 + 4611686018427387903. instructions: vec![ Instruction::Const(VarId(0), Literal::Integer(i64::MAX)), Instruction::Const(VarId(1), Literal::Integer(1)), - Instruction::BinOp(VarId(2), BinOp::Add, VarId(0), VarId(1)), + Instruction::Arithmetic(ArithmeticSite { + id: OperationId(1), + result: Some(VarId(2)), + operation: ArithmeticOp::Add(VarId(0), VarId(1)), + context: EvaluationContext::Body, + failure: FailureDestination::Propagate, + }), ], terminator: Terminator::Return(VarId(2)), }], @@ -1190,9 +1198,9 @@ f() -> 4611686018427387903 + 4611686018427387903. fold_constants(&mut func, AtomId(100), AtomId(101)); assert_valid(&func, "after standalone pass"); - // Should still be a BinOp (overflow not folded) + // Overflow remains checked runtime arithmetic assert!( - matches!(&func.blocks[0].instructions[2], Instruction::BinOp(_, BinOp::Add, _, _)), + matches!(&func.blocks[0].instructions[2], Instruction::Arithmetic(ArithmeticSite { operation: ArithmeticOp::Add(..), .. })), "i64::MAX + 1 should NOT be folded (overflow)" ); } @@ -1250,7 +1258,7 @@ f() -> 1.5 + 2.5. let expected_atom = atom_result(&ir.functions[1]).unwrap(); assert_eq!(atom_result(&ir.functions[0]), Some(expected_atom), "{expression}"); assert!(!ir.functions[0].blocks.iter().flat_map(|block| &block.instructions) - .any(|instruction| matches!(instruction, Instruction::BinOp(..))), + .any(|instruction| matches!(instruction, Instruction::BinOp(..) | Instruction::Arithmetic(..))), "{expression} must fold to a literal"); println!("folded {expression} -> {expected}"); } @@ -1335,7 +1343,13 @@ f() -> -42. params: vec![], instructions: vec![ Instruction::Const(VarId(0), Literal::Atom(AtomId(100))), // true - Instruction::UnaryOp(VarId(1), nera_syntax::ast::UnaryOp::Not, VarId(0)), + Instruction::Arithmetic(ArithmeticSite { + id: OperationId(2), + result: Some(VarId(1)), + operation: ArithmeticOp::Not(VarId(0)), + context: EvaluationContext::Body, + failure: FailureDestination::Propagate, + }), ], terminator: Terminator::Return(VarId(1)), }], @@ -1367,7 +1381,13 @@ f() -> -42. params: vec![], instructions: vec![ Instruction::Const(VarId(0), Literal::Integer(0)), - Instruction::UnaryOp(VarId(1), nera_syntax::ast::UnaryOp::Bnot, VarId(0)), + Instruction::Arithmetic(ArithmeticSite { + id: OperationId(3), + result: Some(VarId(1)), + operation: ArithmeticOp::BitNot(VarId(0)), + context: EvaluationContext::Body, + failure: FailureDestination::Propagate, + }), ], terminator: Terminator::Return(VarId(1)), }], @@ -1400,9 +1420,21 @@ f() -> -42. instructions: vec![ Instruction::Const(VarId(0), Literal::Integer(1)), Instruction::Const(VarId(1), Literal::Integer(2)), - Instruction::BinOp(VarId(2), BinOp::Add, VarId(0), VarId(1)), + Instruction::Arithmetic(ArithmeticSite { + id: OperationId(4), + result: Some(VarId(2)), + operation: ArithmeticOp::Add(VarId(0), VarId(1)), + context: EvaluationContext::Body, + failure: FailureDestination::Propagate, + }), Instruction::Const(VarId(3), Literal::Integer(3)), - Instruction::BinOp(VarId(4), BinOp::Add, VarId(2), VarId(3)), + Instruction::Arithmetic(ArithmeticSite { + id: OperationId(5), + result: Some(VarId(4)), + operation: ArithmeticOp::Add(VarId(2), VarId(3)), + context: EvaluationContext::Body, + failure: FailureDestination::Propagate, + }), ], terminator: Terminator::Return(VarId(4)), }], @@ -1429,7 +1461,7 @@ f(X) -> X + 1. "#; let ir = lower_source(source).unwrap(); let has_add = ir.functions[0].blocks.iter().any(|b| - b.instructions.iter().any(|i| matches!(i, Instruction::BinOp(_, BinOp::Add, _, _))) + b.instructions.iter().any(|i| matches!(i, Instruction::Arithmetic(ArithmeticSite { operation: ArithmeticOp::Add(..), .. }))) ); assert!(has_add, "X + 1 should NOT be folded (X is unknown)"); } diff --git a/crates/nera-ir/src/passes/constant_folding.rs b/crates/nera-ir/src/passes/constant_folding.rs index 6d83182..c01bd40 100644 --- a/crates/nera-ir/src/passes/constant_folding.rs +++ b/crates/nera-ir/src/passes/constant_folding.rs @@ -1,6 +1,6 @@ //! Existing block-local constant evaluation. -use crate::ir::{Instruction, IrFunction, Literal, VarId}; +use crate::ir::{ArithmeticOp, Instruction, IrFunction, Literal, VarId}; use nera_syntax::ast::{AtomId, BinOp, UnaryOp}; /// Constant folding pass: evaluate compile-time-known arithmetic, bitwise, @@ -14,6 +14,33 @@ pub fn fold_constants(func: &mut IrFunction, true_atom_id: AtomId, false_atom_id for inst in &mut block.instructions { match inst { + Instruction::Arithmetic(site) => { + let folded = match site.operation { + ArithmeticOp::Add(a, b) | ArithmeticOp::Subtract(a, b) + | ArithmeticOp::Multiply(a, b) | ArithmeticOp::Divide(a, b) => { + let op = match site.operation { + ArithmeticOp::Add(..) => BinOp::Add, + ArithmeticOp::Subtract(..) => BinOp::Sub, + ArithmeticOp::Multiply(..) => BinOp::Mul, + _ => BinOp::Div, + }; + known.get(&a).zip(known.get(&b)) + .and_then(|(a, b)| fold_binop(op, a, b, true_atom_id, false_atom_id)) + } + ArithmeticOp::Negate(a) | ArithmeticOp::BitNot(a) | ArithmeticOp::Not(a) => { + let op = match site.operation { + ArithmeticOp::Negate(_) => UnaryOp::Neg, + ArithmeticOp::BitNot(_) => UnaryOp::Bnot, + _ => UnaryOp::Not, + }; + known.get(&a).and_then(|a| fold_unaryop(op, a, true_atom_id, false_atom_id)) + } + }; + if let (Some(result), Some(folded)) = (site.result, folded) { + known.insert(result, folded.clone()); + *inst = Instruction::Const(result, folded); + } + } Instruction::Const(var, lit) => { known.insert(*var, lit.clone()); } diff --git a/crates/nera-ir/src/passes/dead_code.rs b/crates/nera-ir/src/passes/dead_code.rs index 29d428f..95fa96a 100644 --- a/crates/nera-ir/src/passes/dead_code.rs +++ b/crates/nera-ir/src/passes/dead_code.rs @@ -69,7 +69,7 @@ pub fn eliminate_dead_code(func: &mut IrFunction, true_atom_id: AtomId, false_at while let Some(block_id) = queue.pop_front() { if let Some(block) = func.blocks.iter().find(|b| b.id == block_id) { - for succ in block.terminator.successors() { + for succ in block.successors() { if reachable.insert(succ) { queue.push_back(succ); } diff --git a/crates/nera-ir/src/passes/tuple_send.rs b/crates/nera-ir/src/passes/tuple_send.rs index 98ec9b5..4f6b9d5 100644 --- a/crates/nera-ir/src/passes/tuple_send.rs +++ b/crates/nera-ir/src/passes/tuple_send.rs @@ -1,8 +1,7 @@ -//! Tuple/send fusion using the existing per-block use-count heuristic. +//! Tuple/send fusion only when the tuple has exactly one operand read. -use super::uses::{collect_instruction_uses, collect_terminator_uses}; +use super::uses::{instruction_reads, terminator_reads}; use crate::ir::{Instruction, IrFunction, Literal, VarId}; -use std::collections::HashSet; /// Fuse MakeTuple + Send into SendDirect when the tuple is single-use. /// This avoids allocating the tuple on the sender heap; the FFI function @@ -13,13 +12,13 @@ pub fn fuse_make_tuple_send(func: &mut IrFunction) { // Pass 1: count uses of each VarId across all blocks. let mut use_count: HashMap = HashMap::new(); for block in &func.blocks { - let mut uses = HashSet::new(); - for inst in &block.instructions { - collect_instruction_uses(inst, &mut uses); - } - collect_terminator_uses(&block.terminator, &mut uses); - for v in uses { - *use_count.entry(v).or_insert(0) += 1; + for value in block + .instructions + .iter() + .flat_map(instruction_reads) + .chain(terminator_reads(&block.terminator)) + { + *use_count.entry(value).or_insert(0) += 1; } } diff --git a/crates/nera-ir/src/passes/uses.rs b/crates/nera-ir/src/passes/uses.rs index cee37a3..3e576f3 100644 --- a/crates/nera-ir/src/passes/uses.rs +++ b/crates/nera-ir/src/passes/uses.rs @@ -4,46 +4,53 @@ use crate::ir::{CallTarget, Instruction, Terminator, VarId}; use std::collections::HashSet; -/// Collect VarIds used (read) by an instruction. Self-contained version for nera-ir crate. +/// Collect distinct reads for consumers that only ask whether a value is used. pub(super) fn collect_instruction_uses(inst: &Instruction, uses: &mut HashSet) { + uses.extend(instruction_reads(inst)); +} + +/// Preserve every operand occurrence for single-use optimization decisions. +pub(super) fn instruction_reads(inst: &Instruction) -> Vec { + let mut uses = Vec::new(); match inst { + Instruction::Arithmetic(site) => uses.extend(site.operation.operands()), Instruction::Const(_, _) => {} Instruction::BinOp(_, _, a, b) => { - uses.insert(*a); - uses.insert(*b); + uses.push(*a); + uses.push(*b); } Instruction::UnaryOp(_, _, a) => { - uses.insert(*a); + uses.push(*a); } Instruction::MakeTuple(_, args) => { for a in args { - uses.insert(*a); + uses.push(*a); } } Instruction::MakeCons(_, h, t) => { - uses.insert(*h); - uses.insert(*t); + uses.push(*h); + uses.push(*t); } Instruction::TupleElement(_, t, _) => { - uses.insert(*t); + uses.push(*t); } Instruction::ListHead(_, l) => { - uses.insert(*l); + uses.push(*l); } Instruction::ListTail(_, l) => { - uses.insert(*l); + uses.push(*l); } Instruction::Call(_, target, args) => { for a in args { - uses.insert(*a); + uses.push(*a); } match target { CallTarget::Closure(v) => { - uses.insert(*v); + uses.push(*v); } CallTarget::Apply(m, f) => { - uses.insert(*m); - uses.insert(*f); + uses.push(*m); + uses.push(*f); } _ => {} } @@ -51,110 +58,110 @@ pub(super) fn collect_instruction_uses(inst: &Instruction, uses: &mut HashSet {} Instruction::Spawn(_, _, _, args) => { for a in args { - uses.insert(*a); + uses.push(*a); } } Instruction::Send(_, pid, msg) => { - uses.insert(*pid); - uses.insert(*msg); + uses.push(*pid); + uses.push(*msg); } Instruction::SendDirect(_, pid, elems) => { - uses.insert(*pid); + uses.push(*pid); for e in elems { - uses.insert(*e); + uses.push(*e); } } Instruction::TypeTest(_, _, val) => { - uses.insert(*val); + uses.push(*val); } Instruction::Move(_, src) => { - uses.insert(*src); + uses.push(*src); } Instruction::TupleSize(_, t) => { - uses.insert(*t); + uses.push(*t); } Instruction::MakeBinary(_, size) => { - uses.insert(*size); + uses.push(*size); } Instruction::BinaryPutInteger(bin, off, val, size, _) => { - uses.insert(*bin); - uses.insert(*off); - uses.insert(*val); - uses.insert(*size); + uses.push(*bin); + uses.push(*off); + uses.push(*val); + uses.push(*size); } Instruction::BinaryPutFloat(bin, off, val, size, _) => { - uses.insert(*bin); - uses.insert(*off); - uses.insert(*val); - uses.insert(*size); + uses.push(*bin); + uses.push(*off); + uses.push(*val); + uses.push(*size); } Instruction::BinaryPutBinary(dest, off, src, count) => { - uses.insert(*dest); - uses.insert(*off); - uses.insert(*src); - uses.insert(*count); + uses.push(*dest); + uses.push(*off); + uses.push(*src); + uses.push(*count); } Instruction::BinaryGetInteger(_, bin, off, size, _) => { - uses.insert(*bin); - uses.insert(*off); - uses.insert(*size); + uses.push(*bin); + uses.push(*off); + uses.push(*size); } Instruction::BinaryGetFloat(_, bin, off, size, _) => { - uses.insert(*bin); - uses.insert(*off); - uses.insert(*size); + uses.push(*bin); + uses.push(*off); + uses.push(*size); } Instruction::BinaryGetBinary(_, bin, off, size) => { - uses.insert(*bin); - uses.insert(*off); - uses.insert(*size); + uses.push(*bin); + uses.push(*off); + uses.push(*size); } Instruction::BsMatchBitOffset(_, bin) | Instruction::BinarySize(_, bin) => { - uses.insert(*bin); + uses.push(*bin); } Instruction::BsStartMatch(_, _, bin) => { - uses.insert(*bin); + uses.push(*bin); } Instruction::BsMatchStart2(_, _, _, input) => { - uses.insert(*input); + uses.push(*input); } Instruction::BsMatchCtxAdvance(mc, off) => { - uses.insert(*mc); - uses.insert(*off); + uses.push(*mc); + uses.push(*off); } Instruction::BsGetIntegerInline(_, base, off, count, _) => { - uses.insert(*base); - uses.insert(*off); - uses.insert(*count); + uses.push(*base); + uses.push(*off); + uses.push(*count); } Instruction::BsMaterialize(_, orig, off, count) => { - uses.insert(*orig); - uses.insert(*off); - uses.insert(*count); + uses.push(*orig); + uses.push(*off); + uses.push(*count); } Instruction::MakeMap(_, pairs) => { for p in pairs { - uses.insert(*p); + uses.push(*p); } } Instruction::MapGet(_, map, key) => { - uses.insert(*map); - uses.insert(*key); + uses.push(*map); + uses.push(*key); } Instruction::MapPut(_, map, key, val) => { - uses.insert(*map); - uses.insert(*key); - uses.insert(*val); + uses.push(*map); + uses.push(*key); + uses.push(*val); } Instruction::MapUpdateExact(_, map, key, val) => { - uses.insert(*map); - uses.insert(*key); - uses.insert(*val); + uses.push(*map); + uses.push(*key); + uses.push(*val); } Instruction::CheckReductions => {} Instruction::YieldCheck => {} Instruction::RaiseException(_, _, reason) => { - uses.insert(*reason); + uses.push(*reason); } Instruction::GetExceptionClass(_) => {} Instruction::GetExceptionReason(_) => {} @@ -163,81 +170,87 @@ pub(super) fn collect_instruction_uses(inst: &Instruction, uses: &mut HashSet {} Instruction::MakeClosure(_, _, _, env_vars) => { for ev in env_vars { - uses.insert(*ev); + uses.push(*ev); } } Instruction::MakeRemoteFunRef(_, _, _, _) => {} Instruction::MakeDynamicRemoteFunRef(_, m, f, a) => { - uses.insert(*m); - uses.insert(*f); - uses.insert(*a); + uses.push(*m); + uses.push(*f); + uses.push(*a); } Instruction::BinaryPutUtf8(_, bin, off, cp) => { - uses.insert(*bin); - uses.insert(*off); - uses.insert(*cp); + uses.push(*bin); + uses.push(*off); + uses.push(*cp); } Instruction::BinaryPutUtf16(_, bin, off, cp, _) => { - uses.insert(*bin); - uses.insert(*off); - uses.insert(*cp); + uses.push(*bin); + uses.push(*off); + uses.push(*cp); } Instruction::BinaryPutUtf32(_, bin, off, cp, _) => { - uses.insert(*bin); - uses.insert(*off); - uses.insert(*cp); + uses.push(*bin); + uses.push(*off); + uses.push(*cp); } Instruction::BsGetUtf8(_, _, base, off, total) => { - uses.insert(*base); - uses.insert(*off); - uses.insert(*total); + uses.push(*base); + uses.push(*off); + uses.push(*total); } Instruction::BsGetUtf16(_, _, base, off, total, _) => { - uses.insert(*base); - uses.insert(*off); - uses.insert(*total); + uses.push(*base); + uses.push(*off); + uses.push(*total); } Instruction::BsGetUtf32(_, _, base, off, total, _) => { - uses.insert(*base); - uses.insert(*off); - uses.insert(*total); + uses.push(*base); + uses.push(*off); + uses.push(*total); } Instruction::BinaryResize(bin, size) => { - uses.insert(*bin); - uses.insert(*size); + uses.push(*bin); + uses.push(*size); } Instruction::SetLine(_) => {} } + uses } -/// Collect VarIds used (read) by a terminator. Self-contained version for nera-ir crate. +/// Collect distinct terminator reads for set-based consumers. pub(super) fn collect_terminator_uses(term: &Terminator, uses: &mut HashSet) { + uses.extend(terminator_reads(term)); +} + +pub(super) fn terminator_reads(term: &Terminator) -> Vec { + let mut uses = Vec::new(); match term { Terminator::Return(v) => { - uses.insert(*v); + uses.push(*v); } Terminator::Jump(_, args) => { for a in args { - uses.insert(*a); + uses.push(*a); } } Terminator::Branch(v, _, _) => { - uses.insert(*v); + uses.push(*v); } Terminator::Switch { scrutinee, .. } => { - uses.insert(*scrutinee); + uses.push(*scrutinee); } Terminator::TailCall(target, args) => { for a in args { - uses.insert(*a); + uses.push(*a); } match target { CallTarget::Closure(v) => { - uses.insert(*v); + uses.push(*v); } CallTarget::Apply(m, f) => { - uses.insert(*m); - uses.insert(*f); + uses.push(*m); + uses.push(*f); } _ => {} } @@ -246,12 +259,13 @@ pub(super) fn collect_terminator_uses(term: &Terminator, uses: &mut HashSet { for c in clauses { - uses.insert(c.binding); + uses.push(c.binding); } if let Some((t, _)) = timeout { - uses.insert(*t); + uses.push(*t); } } Terminator::Unreachable => {} } + uses } diff --git a/crates/nera-ir/src/passes/yield_checks.rs b/crates/nera-ir/src/passes/yield_checks.rs index 2cf5222..397d67b 100644 --- a/crates/nera-ir/src/passes/yield_checks.rs +++ b/crates/nera-ir/src/passes/yield_checks.rs @@ -1,9 +1,9 @@ //! Scheduling checks on explicit CFG back-edges. -use crate::ir::{BasicBlock, BlockId, Instruction, IrFunction}; +use crate::ir::{BasicBlock, BlockId, FailureDestination, Instruction, IrFunction}; use std::collections::{HashMap, HashSet}; -/// Insert a yield check at each DFS back-edge source lacking a final reduction check. +/// Check reductions before the earliest backedge exit in each source block. pub fn insert_backedge_yield_checks(func: &mut IrFunction) { // Build block index for quick lookup let block_index: HashMap = func @@ -16,7 +16,7 @@ pub fn insert_backedge_yield_checks(func: &mut IrFunction) { // DFS to find back-edges let mut on_stack: HashSet = HashSet::new(); let mut visited: HashSet = HashSet::new(); - let mut backedge_sources: HashSet = HashSet::new(); + let mut backedge_sources: HashMap = HashMap::new(); fn dfs( block_id: BlockId, @@ -24,7 +24,7 @@ pub fn insert_backedge_yield_checks(func: &mut IrFunction) { block_index: &HashMap, visited: &mut HashSet, on_stack: &mut HashSet, - backedge_sources: &mut HashSet, + backedge_sources: &mut HashMap, ) { if !visited.insert(block_id) { return; @@ -32,11 +32,29 @@ pub fn insert_backedge_yield_checks(func: &mut IrFunction) { on_stack.insert(block_id); if let Some(&idx) = block_index.get(&block_id) { - let succs = blocks[idx].terminator.successors(); - for succ in succs { + let block = &blocks[idx]; + // Keep the normal-edge DFS order, but retain the instruction + // position at which each exceptional edge leaves the block. + let mut exits: Vec<_> = block + .terminator + .successors() + .into_iter() + .map(|target| (target, block.instructions.len())) + .collect(); + for (position, inst) in block.instructions.iter().enumerate() { + if let Instruction::Arithmetic(site) = inst { + if let FailureDestination::Block(target) = site.failure { + exits.push((target, position)); + } + } + } + for (succ, position) in exits { if on_stack.contains(&succ) { // Back-edge: block_id → succ (succ is an ancestor) - backedge_sources.insert(block_id); + backedge_sources + .entry(block_id) + .and_modify(|earliest| *earliest = (*earliest).min(position)) + .or_insert(position); } else if !visited.contains(&succ) { dfs( succ, @@ -62,16 +80,78 @@ pub fn insert_backedge_yield_checks(func: &mut IrFunction) { &mut backedge_sources, ); - // Insert YieldCheck at each back-edge source block (before the terminator) + // A landing-pad chain still carries a pending exception. Its backedge + // cannot yield until ClearException has consumed that state. Defer only + // accessor/empty jump prefixes; ordinary source blocks keep their checks. + let mut checkpoints: HashMap = HashMap::new(); + for (source, position) in backedge_sources { + let block = &func.blocks[block_index[&source]]; + let consumed = if position == block.instructions.len() + && block.instructions.iter().all(is_exception_accessor) + { + match &block.terminator { + crate::ir::Terminator::Jump(next, args) if args.is_empty() => { + exception_consumption(*next, &func.blocks, &block_index) + } + _ => None, + } + } else { + None + }; + let (target, position) = consumed.unwrap_or((source, position)); + checkpoints + .entry(target) + .and_modify(|earliest| *earliest = (*earliest).min(position)) + .or_insert(position); + } + + // A failing arithmetic instruction skips its suffix, so a final check + // cannot cover its backedge. At a deferred consumption point a check may + // already follow ClearException; retain it as well as preceding checks. for block in &mut func.blocks { - if backedge_sources.contains(&block.id) { - // Don't insert if the last instruction is already a YieldCheck or CheckReductions - let already_has = block.instructions.last().is_some_and(|inst| { + if let Some(&position) = checkpoints.get(&block.id) { + let is_check = |inst: &Instruction| { matches!(inst, Instruction::YieldCheck | Instruction::CheckReductions) - }); + }; + let already_has = block.instructions[..position].last().is_some_and(is_check) + || block.instructions.get(position).is_some_and(is_check); if !already_has { - block.instructions.push(Instruction::YieldCheck); + block.instructions.insert(position, Instruction::YieldCheck); } } } } + +fn is_exception_accessor(inst: &Instruction) -> bool { + matches!( + inst, + Instruction::GetExceptionClass(_) + | Instruction::GetExceptionReason(_) + | Instruction::GetExceptionStacktrace(_) + ) +} + +/// Follow only the pending-exception prefixes accepted by the IR contract. +fn exception_consumption( + mut target: BlockId, + blocks: &[BasicBlock], + block_index: &HashMap, +) -> Option<(BlockId, usize)> { + let mut visited = HashSet::new(); + while visited.insert(target) { + let block = &blocks[*block_index.get(&target)?]; + for (position, inst) in block.instructions.iter().enumerate() { + if matches!(inst, Instruction::ClearException) { + return Some((target, position + 1)); + } + if !is_exception_accessor(inst) { + return None; + } + } + match &block.terminator { + crate::ir::Terminator::Jump(next, args) if args.is_empty() => target = *next, + _ => return None, + } + } + None +} diff --git a/crates/nera-ir/src/pattern.rs b/crates/nera-ir/src/pattern.rs index 6ddfc81..ae57a47 100644 --- a/crates/nera-ir/src/pattern.rs +++ b/crates/nera-ir/src/pattern.rs @@ -871,22 +871,22 @@ fn compile_single_pattern_impl( } else { let off_const = builder.emit_const(Literal::Integer(off)); let rem = builder.fresh_var(); - builder.emit(Instruction::BinOp( + builder.emit_binop( rem, nera_syntax::ast::BinOp::Sub, total_bits, off_const, - )); + ); rem } } else { let rem = builder.fresh_var(); - builder.emit(Instruction::BinOp( + builder.emit_binop( rem, nera_syntax::ast::BinOp::Sub, total_bits, bit_offset_var, - )); + ); rem }; // An unsized tail must contain a whole number of units. @@ -954,11 +954,11 @@ fn compile_single_pattern_impl( // just like integer extraction. Include a reused cursor's // intra-byte prefix in both the offset and window end. let extraction_offset = builder.fresh_var(); - builder.emit(Instruction::BinOp(extraction_offset, - ast::BinOp::Add, bit_offset_var, initial_bit_offset)); + builder.emit_binop(extraction_offset, + ast::BinOp::Add, bit_offset_var, initial_bit_offset); let extraction_end = builder.fresh_var(); - builder.emit(Instruction::BinOp(extraction_end, - ast::BinOp::Add, total_bits, initial_bit_offset)); + builder.emit_binop(extraction_end, + ast::BinOp::Add, total_bits, initial_bit_offset); match seg.type_spec.typ { BinSegmentKind::Utf8 => { @@ -1018,12 +1018,12 @@ fn compile_single_pattern_impl( // Advance bit offset by actual bits consumed (variable) let new_offset = builder.fresh_var(); - builder.emit(Instruction::BinOp( + builder.emit_binop( new_offset, nera_syntax::ast::BinOp::Add, bit_offset_var, bits_consumed, - )); + ); bit_offset_var = new_offset; known_bit_offset = None; // Variable-width fixed_fields_reserved = false; @@ -1076,20 +1076,20 @@ fn compile_single_pattern_impl( // Runtime bounds check for dynamic-sized segments if known_total_bits.is_none() || (known_bit_offset.is_none() && !fixed_fields_reserved) { let remaining = builder.fresh_var(); - builder.emit(Instruction::BinOp( + builder.emit_binop( remaining, nera_syntax::ast::BinOp::Sub, total_bits, bit_offset_var, - )); + ); // A single later guard is cheaper to keep local. // Reserve a tail only when it replaces multiple guards. let reserve_tail = fixed_tail_fields[segment_index + 1] > 1; let remaining = if reserve_tail { let tail = builder.emit_const(Literal::Integer(fixed_tail[segment_index + 1])); let available = builder.fresh_var(); - builder.emit(Instruction::BinOp(available, - nera_syntax::ast::BinOp::Sub, remaining, tail)); + builder.emit_binop(available, + nera_syntax::ast::BinOp::Sub, remaining, tail); available } else { remaining }; let enough = builder.fresh_var(); @@ -1149,8 +1149,8 @@ fn compile_single_pattern_impl( // The raw base starts at the containing byte; logical // offsets and remaining sizes exclude the prefix bits. let extraction_offset = builder.fresh_var(); - builder.emit(Instruction::BinOp(extraction_offset, - nera_syntax::ast::BinOp::Add, bit_offset_var, initial_bit_offset)); + builder.emit_binop(extraction_offset, + nera_syntax::ast::BinOp::Add, bit_offset_var, initial_bit_offset); builder.emit(Instruction::BsGetIntegerInline( extracted, mc_base_ptr, @@ -1190,12 +1190,12 @@ fn compile_single_pattern_impl( known_bit_offset = Some(new_off); } else { let new_offset = builder.fresh_var(); - builder.emit(Instruction::BinOp( + builder.emit_binop( new_offset, nera_syntax::ast::BinOp::Add, bit_offset_var, seg_bit_count, - )); + ); bit_offset_var = new_offset; known_bit_offset = None; } diff --git a/crates/nera-ir/src/verify.rs b/crates/nera-ir/src/verify.rs index 4dd7e63..97bbea6 100644 --- a/crates/nera-ir/src/verify.rs +++ b/crates/nera-ir/src/verify.rs @@ -5,7 +5,7 @@ use std::collections::{HashMap, HashSet}; -use crate::ir::{BlockId, Instruction, IrFunction, IrModule, Terminator, VarId}; +use crate::ir::{ArithmeticOp, EvaluationContext, FailureDestination, OperationId, BlockId, Instruction, IrFunction, IrModule, Terminator, VarId}; mod operands; use operands::{binary_flags, instruction_operands, terminator_operands}; @@ -30,6 +30,14 @@ pub struct VerificationError { #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum ErrorKind { + #[error("duplicate operation site {0:?}")] + DuplicateOperation(OperationId), + #[error("migrated arithmetic requires an explicit operation site")] + MissingOperationSite, + #[error("guard and try arithmetic require a local failure destination")] + MissingFailureHandler, + #[error("arithmetic failure reaches ordinary code before exception handling")] + UnsafeFailurePath, #[error("arity {arity} does not match {params} parameters")] ArityMismatch { arity: u32, params: usize }, #[error("duplicate parameter {0:?}")] @@ -125,6 +133,7 @@ pub fn verify_function(function: &IrFunction) -> Vec { .iter() .map(|b| terminator_operands(&b.terminator)) .collect(); + let mut operation_ids = HashSet::new(); let mut generated = vec![HashSet::new(); function.blocks.len()]; for (index, block) in function.blocks.iter().enumerate() { if !block.params.is_empty() { @@ -135,6 +144,26 @@ pub fn verify_function(function: &IrFunction) -> Vec { } for (inst_index, inst) in block.instructions.iter().enumerate() { let location = Location::Instruction(block.id, inst_index); + if let Instruction::Arithmetic(site) = inst { + if !operation_ids.insert(site.id) { + report(location, ErrorKind::DuplicateOperation(site.id)); + } + if matches!(site.context, EvaluationContext::Try | EvaluationContext::Guard) + && site.failure == FailureDestination::Propagate { + report(location, ErrorKind::MissingFailureHandler); + } + if let FailureDestination::Block(target) = site.failure { + if indices.contains_key(&target) && !failure_is_handled(function, &indices, target) { + report(location, ErrorKind::UnsafeFailurePath); + } + } + } else if match *inst { + Instruction::BinOp(_, op, a, b) => ArithmeticOp::from_binary(op, a, b).is_some(), + Instruction::UnaryOp(_, op, a) => ArithmeticOp::from_unary(op, a).is_some(), + _ => false, + } { + report(location, ErrorKind::MissingOperationSite); + } let (defs, _) = &operands[index][inst_index]; let mut seen = HashSet::new(); for &var in defs { @@ -173,7 +202,7 @@ pub fn verify_function(function: &IrFunction) -> Vec { } _ => {} } - for successor in block.terminator.successors() { + for successor in block.successors() { if !indices.contains_key(&successor) { report(location, ErrorKind::MissingSuccessor(successor)); graph_valid = false; @@ -182,7 +211,7 @@ pub fn verify_function(function: &IrFunction) -> Vec { } let incoming = - graph_valid.then(|| definite_assignment(function, &indices, &declared, &generated)); + graph_valid.then(|| definite_assignment(function, &indices, &declared, &generated, &operands)); for (index, block) in function.blocks.iter().enumerate() { let mut available = incoming.as_ref().and_then(|sets| sets[index].clone()); let mut check_uses = |uses: &[VarId], location, available: &Option>| { @@ -223,6 +252,30 @@ pub fn verify_function(function: &IrFunction) -> Vec { errors } +/// A declared local failure must enter handling, not an ordinary body suffix. +/// Until ClearException consumes the pending state, only exception accessors +/// and empty landing-pad jumps are permitted. Cycles never consume a failure. +fn failure_is_handled(function: &IrFunction, indices: &HashMap, mut target: BlockId) -> bool { + let mut visited = HashSet::new(); + while visited.insert(target) { + let Some(&index) = indices.get(&target) else { return false; }; + let block = &function.blocks[index]; + for inst in &block.instructions { + match inst { + Instruction::ClearException => return true, + Instruction::GetExceptionClass(_) | Instruction::GetExceptionReason(_) + | Instruction::GetExceptionStacktrace(_) => {} + _ => return false, + } + } + match block.terminator { + Terminator::Jump(next, ref args) if args.is_empty() => target = next, + _ => return false, + } + } + false +} + /// Must-analysis over reachable explicit CFG edges. Start at top and intersect /// predecessor outputs until stable; the synthetic entry edge supplies only /// function parameters, even when a loop jumps back to the entry block. @@ -231,6 +284,7 @@ fn definite_assignment( indices: &HashMap, universe: &HashSet, generated: &[HashSet], + operands: &[Vec<(Vec, Vec)>], ) -> Vec>> { let entry = indices[&function.entry_block]; let mut reachable = vec![false; function.blocks.len()]; @@ -241,18 +295,33 @@ fn definite_assignment( continue; } reachable[index] = true; - for successor in function.blocks[index].terminator.successors() { + for successor in function.blocks[index].successors() { let next = indices[&successor]; - predecessors[next].push(index); pending.push(next); } } + // A failure edge sees only definitions preceding the raising instruction, + // never that instruction's result or the normal suffix of its block. + for (index, block) in function.blocks.iter().enumerate() { + if !reachable[index] { continue; } + for successor in block.terminator.successors() { + predecessors[indices[&successor]].push((index, generated[index].clone())); + } + let mut prefix = HashSet::new(); + for (inst, (defs, _)) in block.instructions.iter().zip(&operands[index]) { + if let Instruction::Arithmetic(site) = inst { + if let FailureDestination::Block(target) = site.failure { + predecessors[indices[&target]].push((index, prefix.clone())); + } + } + prefix.extend(defs); + } + } let params: HashSet<_> = function.params.iter().copied().collect(); let mut incoming: Vec<_> = reachable .iter() .map(|&yes| yes.then(|| universe.clone())) .collect(); - let mut outgoing = incoming.clone(); loop { let mut changed = false; for index in 0..function.blocks.len() { @@ -264,14 +333,11 @@ fn definite_assignment( } else { universe.clone() }; - for &pred in &predecessors[index] { - set.retain(|var| outgoing[pred].as_ref().unwrap().contains(var)); + for (pred, prefix) in &predecessors[index] { + set.retain(|var| incoming[*pred].as_ref().unwrap().contains(var) || prefix.contains(var)); } - let mut out = set.clone(); - out.extend(&generated[index]); - if incoming[index].as_ref() != Some(&set) || outgoing[index].as_ref() != Some(&out) { + if incoming[index].as_ref() != Some(&set) { incoming[index] = Some(set); - outgoing[index] = Some(out); changed = true; } } diff --git a/crates/nera-ir/src/verify/operands.rs b/crates/nera-ir/src/verify/operands.rs index 551b01c..4c50316 100644 --- a/crates/nera-ir/src/verify/operands.rs +++ b/crates/nera-ir/src/verify/operands.rs @@ -6,6 +6,7 @@ use crate::ir::{BinFlags, CallTarget, Instruction, Terminator, VarId}; pub(super) fn instruction_operands(inst: &Instruction) -> (Vec, Vec) { use Instruction::*; match inst { + Arithmetic(site) => (site.result.into_iter().collect(), site.operation.operands()), Const(dst, _) | GetSelf(dst) | GetExceptionClass(dst) @@ -101,7 +102,8 @@ pub(super) fn binary_flags(inst: &Instruction) -> Option { | BinaryPutUtf32(_, _, _, _, flags) | BsGetUtf16(_, _, _, _, _, flags) | BsGetUtf32(_, _, _, _, _, flags) => Some(*flags), - Const(..) + Arithmetic(..) + | Const(..) | BinOp(..) | UnaryOp(..) | MakeTuple(..) diff --git a/crates/nera-ir/tests/passes.rs b/crates/nera-ir/tests/passes.rs index 655306f..09bff3b 100644 --- a/crates/nera-ir/tests/passes.rs +++ b/crates/nera-ir/tests/passes.rs @@ -161,3 +161,159 @@ fn pipeline_preserves_yield_before_folding_and_dead_code() { ]); assert_eq!(format!("{func:#?}"), format!("{expected:#?}")); } + +#[test] +fn exceptional_backedges_check_reductions_before_the_raising_instruction() { + // The explicit arithmetic CFG permits a handler to retry an operation. + // A suffix check is unreachable when that operation fails back to it. + for suffix in [ + None, + Some(Instruction::YieldCheck), + Some(Instruction::CheckReductions), + ] { + let mut body = vec![ + Instruction::Const(VarId(2), Literal::Integer(1)), + Instruction::Arithmetic(ArithmeticSite { + id: OperationId(0), + result: Some(VarId(3)), + operation: ArithmeticOp::Add(VarId(0), VarId(2)), + context: EvaluationContext::Try, + failure: FailureDestination::Block(BlockId(0)), + }), + Instruction::Arithmetic(ArithmeticSite { + id: OperationId(1), + result: None, + operation: ArithmeticOp::Negate(VarId(0)), + context: EvaluationContext::Try, + failure: FailureDestination::Block(BlockId(0)), + }), + ]; + body.extend(suffix); + let mut func = function(vec![ + block( + 0, + vec![Instruction::ClearException], + Terminator::Jump(BlockId(1), vec![]), + ), + block(1, body, Terminator::Return(VarId(0))), + ]); + run_verified(&mut func, insert_backedge_yield_checks); + assert!(matches!( + func.blocks[1].instructions[0], + Instruction::Const(..) + )); + assert!(matches!( + func.blocks[1].instructions[1], + Instruction::YieldCheck + )); + assert!(matches!( + func.blocks[1].instructions[2], + Instruction::Arithmetic(ArithmeticSite { + id: OperationId(0), + .. + }) + )); + assert!(matches!( + func.blocks[1].instructions[3], + Instruction::Arithmetic(ArithmeticSite { + id: OperationId(1), + .. + }) + )); + let once = format!("{func:#?}"); + run_verified(&mut func, insert_backedge_yield_checks); + assert_eq!(format!("{func:#?}"), once); + } +} + +#[test] +fn tuple_send_fusion_counts_each_read_not_each_using_block() { + let original = vec![ + Instruction::MakeTuple(VarId(2), vec![VarId(1)]), + Instruction::BinOp(VarId(3), BinOp::ExactEq, VarId(2), VarId(1)), + Instruction::Send(VarId(4), VarId(0), VarId(2)), + ]; + let mut func = function(vec![block(0, original, Terminator::Return(VarId(3)))]); + let before = format!("{func:#?}"); + run_verified(&mut func, fuse_make_tuple_send); + assert_eq!(format!("{func:#?}"), before); + + // Two reads within Send itself are also not single-use: the tuple may + // not be replaced while its original value is still the recipient. + let mut func = function(vec![block( + 0, + vec![ + Instruction::MakeTuple(VarId(2), vec![VarId(1)]), + Instruction::Send(VarId(3), VarId(2), VarId(2)), + ], + Terminator::Return(VarId(3)), + )]); + let before = format!("{func:#?}"); + run_verified(&mut func, fuse_make_tuple_send); + assert_eq!(format!("{func:#?}"), before); +} + +#[test] +fn exceptional_backedges_preserve_pending_landing_pad_prefixes() { + // A failure can traverse empty pads or capture exception fields before + // reaching ClearException. Yielding inside that pending prefix is invalid. + for prefix in [ + vec![], + vec![Instruction::GetExceptionReason(VarId(4))], + vec![ + Instruction::GetExceptionClass(VarId(4)), + Instruction::GetExceptionStacktrace(VarId(5)), + ], + ] { + let mut func = function(vec![ + block(0, vec![], Terminator::Jump(BlockId(4), vec![])), + block( + 1, + vec![ + Instruction::Arithmetic(ArithmeticSite { + id: OperationId(0), + result: Some(VarId(2)), + operation: ArithmeticOp::Add(VarId(0), VarId(1)), + context: EvaluationContext::Try, + failure: FailureDestination::Block(BlockId(2)), + }), + Instruction::Arithmetic(ArithmeticSite { + id: OperationId(1), + result: None, + operation: ArithmeticOp::Negate(VarId(0)), + context: EvaluationContext::Try, + failure: FailureDestination::Block(BlockId(2)), + }), + ], + Terminator::Return(VarId(0)), + ), + block(2, vec![], Terminator::Jump(BlockId(3), vec![])), + block(3, prefix, Terminator::Jump(BlockId(0), vec![])), + block( + 4, + vec![ + Instruction::GetExceptionClass(VarId(6)), + Instruction::ClearException, + ], + Terminator::Jump(BlockId(1), vec![]), + ), + ]); + let pads_before = format!("{:?}", (&func.blocks[0], &func.blocks[2..4])); + run_verified(&mut func, insert_backedge_yield_checks); + assert_eq!( + format!("{:?}", (&func.blocks[0], &func.blocks[2..4])), + pads_before + ); + assert!(matches!( + func.blocks[4].instructions.as_slice(), + [ + Instruction::GetExceptionClass(_), + Instruction::ClearException, + Instruction::YieldCheck + ] + )); + let once = format!("{func:#?}"); + run_verified(&mut func, insert_backedge_yield_checks); + assert_eq!(format!("{func:#?}"), once); + } +} diff --git a/crates/nera-ir/tests/verify_lowered.rs b/crates/nera-ir/tests/verify_lowered.rs index a81f7d4..59ca5c1 100644 --- a/crates/nera-ir/tests/verify_lowered.rs +++ b/crates/nera-ir/tests/verify_lowered.rs @@ -3,10 +3,14 @@ use std::path::Path; -use nera_ir::{lower::IrBuilder, verify::verify_module}; +use nera_ir::ir::*; +use nera_ir::{ + lower::IrBuilder, + verify::{verify_function, verify_module, ErrorKind}, +}; use nera_syntax::{ast::AtomId, lexer::Lexer, parser::Parser, preproc::Preprocessor}; -fn verify_fixture(relative_path: &str) { +fn lower_fixture_with_atoms(relative_path: &str) -> (IrModule, AtomId, AtomId) { let path = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../..") .join(relative_path); @@ -38,6 +42,15 @@ fn verify_fixture(relative_path: &str) { ); let errors = verify_module(&module); assert!(errors.is_empty(), "{relative_path}: {errors:#?}"); + (module, true_id, false_id) +} + +fn lower_fixture(relative_path: &str) -> IrModule { + lower_fixture_with_atoms(relative_path).0 +} + +fn verify_fixture(relative_path: &str) { + lower_fixture(relative_path); } macro_rules! fixtures { @@ -69,3 +82,267 @@ fixtures! { stdlib_lists => "erlang/stdlib/lists.erl", stdlib_gen_server => "erlang/stdlib/gen_server.erl", } + +// The arithmetic IR contract requires site identities independent of results +// and verified failure boundaries, including generated functions. +// These cases mutate the actual compiler output of the owning runtime fixtures. +#[test] +fn arithmetic_contexts_survive_lowering_and_generated_helpers() { + let mut contexts = Vec::new(); + for fixture in [ + "tests/try_operation_short_circuit.erl", + "tests/try_after_reraise.erl", + ] { + let module = lower_fixture(fixture); + for function in &module.functions { + for inst in function.blocks.iter().flat_map(|b| &b.instructions) { + if let Instruction::Arithmetic(site) = inst { + contexts.push(site.context); + } + } + } + } + for expected in [ + EvaluationContext::Body, + EvaluationContext::Try, + EvaluationContext::Guard, + EvaluationContext::Cleanup, + ] { + assert!(contexts.contains(&expected), "missing {expected:?}"); + } +} + +#[test] +fn arithmetic_site_identity_does_not_depend_on_a_result_register() { + let module = lower_fixture("tests/arith_basic.erl"); + let mut function = module + .functions + .into_iter() + .find(|f| { + f.blocks + .iter() + .flat_map(|b| &b.instructions) + .any(|i| matches!(i, Instruction::Arithmetic(_))) + }) + .unwrap(); + let block = function + .blocks + .iter_mut() + .find(|b| { + b.instructions + .iter() + .any(|i| matches!(i, Instruction::Arithmetic(_))) + }) + .unwrap(); + let index = block + .instructions + .iter() + .position(|i| matches!(i, Instruction::Arithmetic(_))) + .unwrap(); + let Instruction::Arithmetic(mut repeated) = block.instructions[index].clone() else { + unreachable!() + }; + repeated.id = OperationId(u32::MAX); + // Repeated writes to the same mutable register are legal. + block + .instructions + .insert(index, Instruction::Arithmetic(repeated.clone())); + // So is a raising operation with no result at all. + repeated.id = OperationId(u32::MAX - 1); + repeated.result = None; + block + .instructions + .insert(index, Instruction::Arithmetic(repeated)); + assert!(verify_function(&function).is_empty()); + let block = function + .blocks + .iter_mut() + .find(|b| { + b.instructions + .iter() + .any(|i| matches!(i, Instruction::Arithmetic(_))) + }) + .unwrap(); + let mut ids = block.instructions.iter_mut().filter_map(|i| match i { + Instruction::Arithmetic(site) => Some(site), + _ => None, + }); + let first_id = ids.next().unwrap().id; + ids.next().unwrap().id = first_id; + assert!(verify_function(&function) + .iter() + .any(|e| e.kind == ErrorKind::DuplicateOperation(first_id))); +} + +fn protected_arithmetic() -> IrFunction { + lower_fixture("tests/try_operation_short_circuit.erl") + .functions + .into_iter() + .find(|f| { + f.blocks.iter().flat_map(|b| &b.instructions).any(|i| { + matches!( + i, + Instruction::Arithmetic(ArithmeticSite { + context: EvaluationContext::Try, + .. + }) + ) + }) + }) + .unwrap() +} + +#[test] +fn arithmetic_failure_requires_a_real_handling_destination() { + let original = protected_arithmetic(); + for failure in [ + FailureDestination::Propagate, + FailureDestination::Block(BlockId(u32::MAX)), + FailureDestination::Block(original.entry_block), + ] { + let mut function = original.clone(); + let site = function + .blocks + .iter_mut() + .flat_map(|b| &mut b.instructions) + .find_map(|i| match i { + Instruction::Arithmetic(site) => Some(site), + _ => None, + }) + .unwrap(); + site.failure = failure; + let expected = match failure { + FailureDestination::Propagate => ErrorKind::MissingFailureHandler, + FailureDestination::Block(BlockId(u32::MAX)) => { + ErrorKind::MissingSuccessor(BlockId(u32::MAX)) + } + _ => ErrorKind::UnsafeFailurePath, + }; + assert!( + verify_function(&function) + .iter() + .any(|e| e.kind == expected), + "{failure:?}" + ); + } +} + +#[test] +fn arithmetic_failure_cannot_initialize_its_result_in_the_handler() { + let mut function = protected_arithmetic(); + let site = function + .blocks + .iter() + .flat_map(|b| &b.instructions) + .find_map(|i| match i { + Instruction::Arithmetic(site) => Some(site.clone()), + _ => None, + }) + .unwrap(); + let FailureDestination::Block(mut target) = site.failure else { + unreachable!() + }; + loop { + let block = function.blocks.iter_mut().find(|b| b.id == target).unwrap(); + if let Some(index) = block + .instructions + .iter() + .position(|i| matches!(i, Instruction::ClearException)) + { + let result = site.result.unwrap(); + block + .instructions + .insert(index + 1, Instruction::Move(result, result)); + assert!(verify_function(&function) + .iter() + .any(|e| e.kind == ErrorKind::UninitializedVariable(result))); + break; + } + let Terminator::Jump(next, _) = block.terminator else { + unreachable!() + }; + target = next; + } +} + +#[test] +fn migrated_arithmetic_cannot_use_the_unrouted_representation() { + let mut function = protected_arithmetic(); + let instruction = function + .blocks + .iter_mut() + .flat_map(|b| &mut b.instructions) + .find(|i| { + matches!( + i, + Instruction::Arithmetic(ArithmeticSite { + operation: ArithmeticOp::Add(..), + .. + }) + ) + }) + .unwrap(); + let Instruction::Arithmetic(site) = instruction else { + unreachable!() + }; + let ArithmeticOp::Add(a, b) = site.operation else { + unreachable!() + }; + *instruction = Instruction::BinOp(site.result.unwrap(), nera_syntax::ast::BinOp::Add, a, b); + assert!(verify_function(&function) + .iter() + .any(|e| e.kind == ErrorKind::MissingOperationSite)); +} + +#[test] +fn arithmetic_failure_edges_survive_folding_and_cfg_pruning() { + let (module, true_id, false_id) = + lower_fixture_with_atoms("tests/try_operation_short_circuit.erl"); + let mut function = module + .functions + .into_iter() + .find(|f| { + f.blocks.iter().flat_map(|b| &b.instructions).any(|i| { + matches!( + i, + Instruction::Arithmetic(ArithmeticSite { + context: EvaluationContext::Try, + .. + }) + ) + }) + }) + .unwrap(); + let block = function + .blocks + .iter_mut() + .find(|b| { + b.instructions + .iter() + .any(|i| matches!(i, Instruction::Arithmetic(_))) + }) + .unwrap(); + let index = block + .instructions + .iter() + .position(|i| matches!(i, Instruction::Arithmetic(_))) + .unwrap(); + let Instruction::Arithmetic(site) = block.instructions[index].clone() else { + unreachable!() + }; + // Remove the redundant normal-CFG route to the catch block, leaving the + // instruction's declared failure as the only edge to its landing pad. + block.instructions.truncate(index + 1); + block.terminator = Terminator::Return(site.result.unwrap()); + assert!(verify_function(&function).is_empty()); + nera_ir::passes::fold_constants(&mut function, true_id, false_id); + assert!(verify_function(&function).is_empty()); + nera_ir::passes::eliminate_dead_code(&mut function, true_id, false_id); + assert!(verify_function(&function).is_empty()); + let FailureDestination::Block(target) = site.failure else { + unreachable!() + }; + assert!(function.blocks.iter().any(|b| b.id == target)); + assert!(function.blocks.iter().flat_map(|b| &b.instructions).any(|i| + matches!(i, Instruction::Arithmetic(kept) if kept.id == site.id && kept.failure == site.failure && kept.context == site.context))); +} diff --git a/docs/IR_CONTRACT.md b/docs/IR_CONTRACT.md index aa02e0c..7853585 100644 --- a/docs/IR_CONTRACT.md +++ b/docs/IR_CONTRACT.md @@ -54,9 +54,11 @@ in the entry block's reserved parameter list. semantics. Source `receive` lowers to ordinary blocks and runtime calls such as `receive_peek`. The native translator's `Receive` arm is a placeholder. -Vector order is not execution order. Successors determine reachability. The -explicit CFG does not describe all backend-created exits, exception propagation, -or suspend/resume paths. +Vector order is not execution order. Successors determine reachability. +`BasicBlock::successors()` includes the normal terminator successors and the +arithmetic instructions' explicit local failure destinations. Use it for block +reachability; `Terminator::successors()` alone describes only normal edges. +Unmigrated backend-created exits and suspend/resume paths remain implicit. ## Reads, writes and payloads @@ -82,10 +84,31 @@ verification cannot prove operand types, pointer validity, rooting or lifetime. ## Exceptions, scheduling and source information +`Arithmetic` is the single executable representation for binary `+`, `-`, `*`, +`/` and unary minus, `bnot`, `not`. Each instruction carries a function-local +`OperationId`, an optional result register, an evaluation context (body, try, +guard or cleanup), and a failure destination (propagation or a local block). +Each identity names an operation site even when operations rewrite the same +register or discard their results. Raw `BinOp` and +`UnaryOp` instructions remain for unmigrated operators, including unary plus. +The verifier rejects migrated operators in those raw variants. + +On arithmetic failure, the instruction transfers immediately, without defining +its result or executing its block's remaining instructions. Local failure edges +therefore carry only the definitions preceding the operation. Try and guard +contexts require local handlers. A local destination must reach exception +accessors and `ClearException` through landing-pad jumps before ordinary code +can execute; missing destinations, duplicate identities and unsafe paths fail +verification. Native translation uses the same checked helper emission for +inline slow paths and helper-only arithmetic. Immediate-integer arithmetic and +boolean-not fast paths do not load exception status. + `RaiseException` writes a result register and changes runtime exception state; it is an instruction, not a terminator. `CheckException`, exception accessors, `ClearException` and ordinary branches express part of exception handling. -Calls can also trigger backend-generated exception checks and suspension. +Calls and unmigrated operations can also trigger backend-generated exception +checks and suspension. Their existing protected-result metadata and suspension +mechanism are unchanged; arithmetic does not use protected-result routing. `CheckReductions` and `YieldCheck` are distinct scheduling operations, not pure computations. Do not remove or reorder them merely because they have no result. @@ -110,8 +133,8 @@ Unreachable code is checked for undeclared registers but not path-sensitive initialization: there is no entry state for an unreachable component. Success does **not** prove Erlang semantics, static types, call resolution or -call arities, closure target linkage, atom-table validity, exception-handler -balance, suspension safety, GC safety, optimization correctness, or backend +call arities, closure target linkage, atom-table validity, unmigrated +exception-handler balance, suspension safety, GC safety, optimization correctness, or backend support for every representable operation. Those require separate checks. When adding an instruction, update its documented operands/results, exhaustive diff --git a/docs/IR_PASSES.md b/docs/IR_PASSES.md index a952b32..73e5fba 100644 --- a/docs/IR_PASSES.md +++ b/docs/IR_PASSES.md @@ -11,7 +11,7 @@ The source-function pipeline runs once, in this order: | Step | Pass | Existing effect | | --- | --- | --- | -| 1 | `insert_backedge_yield_checks` | Append scheduling checks at DFS back-edge sources when needed. | +| 1 | `insert_backedge_yield_checks` | Insert scheduling checks before each source block's earliest DFS back-edge exit when needed. | | 2 | `fuse_make_tuple_send` | Replace eligible tuple construction and send pairs with `SendDirect`, retaining a nil definition of the tuple register. | | 3 | `eliminate_dead_match_contexts` | Replace `BsMatchStart2` with `BsStartMatch` when the context result has no uses. | | 4 | `fold_constants` | Fold supported operations from constants tracked within each block. | @@ -32,14 +32,28 @@ interactions alongside tuple/send fusion and match-context removal. Each pass mutates one completed `IrFunction` in place. Folding and dead-code elimination additionally receive the module's true/false atom IDs. Passes do not allocate registers or blocks, change function signatures, or resolve calls. -Only dead-code elimination changes the CFG; the other passes change instructions. +Dead-code elimination changes normal CFG edges. Successful arithmetic folding +replaces the entire operation site with a constant, removing its now-impossible +failure edge; failed folds retain the site, context and destination unchanged. +Dead-code elimination and back-edge analysis follow both normal and arithmetic +failure edges through `BasicBlock::successors()`. Each invocation builds its own local maps and sets. There are no cached analyses or invalidation protocol. -`uses.rs` contains the operand collectors shared by tuple/send fusion and -match-context removal. Their existing traversal and counting behavior is -preserved. The structural verifier keeps its own operand handling; extracting -passes does not unify these analyses or alter optimization eligibility. +`uses.rs` owns operand-read enumeration shared by tuple/send fusion and +match-context removal. Fusion counts every read occurrence, including multiple +reads in one instruction or block; a per-block set cannot establish single use. +Match-context removal still asks only whether any read exists and therefore +collects a set. The structural verifier keeps its own operand handling. + +Back-edge analysis retains each exit's instruction position. Normal edges leave +at the terminator; an arithmetic failure leaves at that instruction, bypassing +its suffix. A reduction check must precede the earliest raising back-edge exit, not merely +appear at the end of its block. Landing-pad chains may contain only exception +accessors and jumps before consuming the pending exception. If such a prefix +closes a cycle, its scheduling check runs immediately after the chain's +`ClearException`, never inside the pending prefix. Existing adjacent checks +are retained, and repeating the pass inserts no additional check. ## Verification diff --git a/docs/RUNTIME_CONTRACTS.md b/docs/RUNTIME_CONTRACTS.md index 147b259..3e12641 100644 --- a/docs/RUNTIME_CONTRACTS.md +++ b/docs/RUNTIME_CONTRACTS.md @@ -2,9 +2,9 @@ This index assigns each compatibility fact to an existing Erlang fixture. The focused runner executes those assertions unchanged on native Nera (default and -one scheduler) and Erlang/OTP 29. It changes checking, not product behavior or -language support. This is the first guardrail for incremental compiler/runtime -refactoring, not an exception-routing refactor or a claim of full BEAM parity. +one scheduler) and Erlang/OTP 29. The runner changes checking, not product +behavior or language support. These contracts guard the compiler's bounded arithmetic exception-routing refactor; +they are not a claim of full BEAM parity. ## Contract index @@ -15,7 +15,7 @@ refactoring, not an exception-routing refactor or a claim of full BEAM parity. | [`error_2`](../tests/error_2.erl) | The fixture's stated intent: `error/2` raises the supplied reason through direct, tail, apply, fun and computed-module calls. | Checks exact class/reason, non-returning behavior, catch envelope and list-shaped traces. Does not assert retention of `Args`, file metadata or `error_info`. | | [`float_arith_badarith`](../tests/float_arith_badarith.erl) | The fixture's stated regression: non-finite arithmetic and unrepresentable float operands raise `badarith`; earlier false filters stop evaluation. | Checks zero divisors, overflow, large operands, literal folds, finite results, signed zero/underflow, guard rejection and side effects. Existing uncaught-reason projection does not prove exit-envelope parity or numeric-conversion compatibility. | | [`bin_comp_badarg`](../tests/bin_comp_badarg.erl) | The fixture's stated regression: each binary-comprehension body must be a bitstring at that iteration, not an iolist. | Checks invalid bodies, exact class/reason, preserved body exceptions, valid controls and ordered filter/body effects. The existing monitor assertion accepts two exit-envelope shapes; this runner does not strengthen or normalize them. | -| [`resume_exception`](../tests/resume_exception.erl) | [Suspended exception repair](PROJECT_STATUS.md#p0-1--exception-handlers-lost-across-suspension--repaired): handlers must survive receive suspension. | Checks exit/throw/error, nested callers, absent later dictionary effects, cleanup, message wakeup and normal return. Does not yet own the later arithmetic-context suspension pilot. | +| [`resume_exception`](../tests/resume_exception.erl) | [Suspended exception repair](PROJECT_STATUS.md#p0-1--exception-handlers-lost-across-suspension--repaired): handlers must survive receive suspension. | Checks exit/throw/error, nested callers, absent later dictionary effects, cleanup, message wakeup and normal return. Also checks an arithmetic operand whose callee suspends until a message, then raises: no normal body effect resumes, and cleanup runs exactly once. | Each fact stays in its owning fixture. The runner adds no second implementation of Erlang semantics and performs no textual equality comparison of crash reports, @@ -116,6 +116,10 @@ and shutdown changes follow their own contributing-guide conditions. A passing result proves only the assertions listed above on the recorded engine, artifacts and platform. macOS native execution does not prove Linux or Windows execution; cross-object generation does not prove target-native execution. -No new architecture, performance result or broader language support follows from -these contracts. Refactoring arithmetic exception routing remains a separate -change after these guardrails merge. +No performance result or broader language support follows from these contracts. +The compiler's arithmetic routing representation is documented in +[the IR contract](IR_CONTRACT.md#exceptions-scheduling-and-source-information). +The short-circuit fixture additionally owns ordinary-body failure transfer for +all migrated operators and their guard rejection. The cleanup fixture owns +replacement by each migrated operator's exception, exactly-once cleanup entry +and the absence of subsequent cleanup effects. diff --git a/tests/README.md b/tests/README.md index 976fbaf..b496e3a 100644 --- a/tests/README.md +++ b/tests/README.md @@ -131,6 +131,7 @@ These are automatically skipped by the runner and show up as SKIP in output. | Test File | Feature Category | Assertions | |------------------------|----------------------------------------|------------| | `arith_basic.erl` | Integer arithmetic, bitwise ops | ~20 | +| `arithmetic_tuple_send.erl` | A tuple compared before binary/unary arithmetic and then sent retains its value when arithmetic blocks merge, including folded arithmetic; native and BEAM | 5 paths | | `arith_float.erl` | Float arithmetic | ~10 | | `unary_plus.erl` | Unary plus numeric identity (big integers, float bits and signed zero), non-number badarith, ignored results, guards, try short-circuiting, precedence, operand evaluation, first arguments/elements, maybe bodies, bare binary segments, signed literal patterns and conditional compilation; also run on BEAM | Table-driven | | `float_arith_badarith.erl` | `/` with a zero divisor, signed zeros included, float overflow of `+`, `-`, `*` and `/`, and integers too large for a float raise `badarith`: inside `try` before a later send, in guards and in an uncaught process; literal operands; finite and underflowing results unchanged; a false list or binary comprehension filter stops the later filters; also run on BEAM | Table-driven | @@ -249,11 +250,11 @@ These are automatically skipped by the runner and show up as SKIP in output. | `try_of.erl` | try...of clause | ~6 | | `try_clause.erl` | Unmatched try-of results raise error:{try_clause, Value}: exact unmatched terms, failed guards, local-catch bypass and after cleanup, with matching and body-exception controls; also run on BEAM | Table-driven | | `try_catch_bound_vars.erl` | Bound names in try-of and catch class/reason patterns: exact equality, guards, fresh names, binary tails and re-raise; also run on BEAM | Table-driven | -| `try_after_reraise.erl` | `try ... after` as the only handler in its function: the after body runs once when the body, an `of` clause or a catch clause raises, then the same class, reason and stacktrace reach the caller; a normal return keeps the body's value; an exception from the after body replaces the held one; an after body that waits, times out and collects garbage; an uncaught exit; also run on BEAM | 28 | +| `try_after_reraise.erl` | `try ... after` as the only handler in its function: the after body runs once when the body, an `of` clause or a catch clause raises, then the same class, reason and stacktrace reach the caller; a normal return keeps the body's value; an exception from the after body replaces the held one, including each migrated arithmetic operator before a later side effect with exactly one cleanup entry; an after body that waits, times out and collects garbage; an uncaught exit; also run on BEAM | Table-driven | | `catch_expr.erl` | Catch expression | ~4 | | `error_2.erl` | `error/2` and `erlang:error/2` raise `error:Reason` through the auto-imported and qualified names, a tail call, `apply/3`, a fun and a computed module, whatever `Args` holds; the code after the call does not run; `catch` gives `{'EXIT', {Reason, Stack}}`; also run on BEAM | 16 | | `error_3.erl` | `error/3` and `erlang:error/3` raise `error:Reason` with `error_info` options through direct, tail, apply, fun and computed calls; arbitrary Args/Options, argument evaluation, no continuation after raising, cleanup and catch; also run on BEAM | Table-driven | -| `try_operation_short_circuit.erl` | An operator, unary op, map update, send, spawn, comprehension or `throw` that raises in a `try` body goes to the handler at once: no later send, put, receive or call runs and the first reason is kept, including `catch`, nested `try`, `of`, catch and `after` clauses; a raising guard there still just fails; outside `try`, a failed send or spawn or a `throw` stops the process at once and a map update evaluates every value first; also run on BEAM | Table-driven | +| `try_operation_short_circuit.erl` | An operator, unary op, map update, send, spawn, comprehension or `throw` that raises in a `try` body goes to the handler at once: no later send, put, receive or call runs and the first reason is kept, including `catch`, nested `try`, `of`, catch and `after` clauses; a raising guard there still just fails; outside `try`, each migrated arithmetic operator, a failed send or spawn or a `throw` stops the process at once and a map update evaluates every value first; also run on BEAM | Table-driven | ### Pattern Matching diff --git a/tests/arithmetic_tuple_send.erl b/tests/arithmetic_tuple_send.erl new file mode 100644 index 0000000..be94283 --- /dev/null +++ b/tests/arithmetic_tuple_send.erl @@ -0,0 +1,35 @@ +%% EXPECT: arithmetic_tuple_send_ok +%% Enlarging arithmetic blocks must not let tuple/send fusion replace a +%% tuple that is read by an earlier comparison, even when arithmetic folds. +%% Run the same regression on native Nera and BEAM. +-module(arithmetic_tuple_send). +-export([test/0]). + +test() -> + lists:foreach(fun(F) -> + true = F(self(), 1), + receive {a} -> ok end + end, [fun binary/2, fun negate/2, fun bit_not/2, fun folded/2]), + true = boolean_not(self(), true), + receive {a} -> ok end, + io:format("arithmetic_tuple_send_ok~n"). + +binary(P, X) -> + try T = {a}, Eq = T =:= {a}, _ = X + 1, P ! T, Eq + catch _:_ -> failed end. + +negate(P, X) -> + try T = {a}, Eq = T =:= {a}, _ = -X, P ! T, Eq + catch _:_ -> failed end. + +bit_not(P, X) -> + try T = {a}, Eq = T =:= {a}, _ = bnot X, P ! T, Eq + catch _:_ -> failed end. + +boolean_not(P, X) -> + try T = {a}, Eq = T =:= {a}, _ = not X, P ! T, Eq + catch _:_ -> failed end. + +folded(P, _X) -> + try T = {a}, Eq = T =:= {a}, _ = 1 + 1, P ! T, Eq + catch _:_ -> failed end. diff --git a/tests/resume_exception.erl b/tests/resume_exception.erl index e35151c..e4a5f0c 100644 --- a/tests/resume_exception.erl +++ b/tests/resume_exception.erl @@ -17,6 +17,28 @@ test() -> spawn(fun() -> receive after 5 -> Parent ! go end end), {'EXIT', awakened} = (catch message_exit()), ok = delayed(normal), + %% An arithmetic operand's callee suspends, then raises after receiving. + %% The outer arithmetic context must unwind, not resume its next effect. + Self = self(), + {ArithmeticPid, ArithmeticRef} = spawn_monitor(fun() -> + put(arithmetic_cleanup, 0), + {error, operand_failed} = try + _ = 1 + arithmetic_operand(Self), + put(arithmetic_unreachable, ran), + wrong + catch C:E -> {C, E} + after put(arithmetic_cleanup, get(arithmetic_cleanup) + 1) + end, + undefined = get(arithmetic_unreachable), + 1 = erase(arithmetic_cleanup), + undefined = get(arithmetic_cleanup), + Self ! arithmetic_done + end), + receive {arithmetic_waiting, ArithmeticPid} -> ok end, + await_arithmetic_suspension(ArithmeticPid), + ArithmeticPid ! arithmetic_go, + receive arithmetic_done -> ok end, + receive {'DOWN', ArithmeticRef, process, ArithmeticPid, normal} -> ok end, io:format("resume exceptions ok~n"). outer(Kind) -> @@ -33,5 +55,18 @@ delayed(Kind) -> end end. +arithmetic_operand(Parent) -> + Parent ! {arithmetic_waiting, self()}, + receive arithmetic_go -> error(operand_failed) end. + +%% Observe real waiting before sending: no timing assumption can substitute +%% for the callee actually suspending. The fixture runner bounds completion. +await_arithmetic_suspension(Pid) -> + case process_info(Pid, status) of + {status, waiting} -> ok; + undefined -> error(arithmetic_exited_before_suspension); + _ -> await_arithmetic_suspension(Pid) + end. + message_exit() -> receive go -> exit(awakened) end. diff --git a/tests/try_after_reraise.erl b/tests/try_after_reraise.erl index f9f1af8..655fa25 100644 --- a/tests/try_after_reraise.erl +++ b/tests/try_after_reraise.erl @@ -44,6 +44,26 @@ test() -> try error(first) after receive never -> no after id(-1) -> ok end end end), [] = drain(), + %% Each migrated operator in cleanup replaces the held exception, + %% runs that cleanup once, and cannot reach its next side effect. + lists:foreach(fun({Reason, Cleanup}) -> + {error, Reason} = outcome(Cleanup), + [cleanup_entered] = drain() + end, [{badarith, fun() -> try error(first) after + self() ! cleanup_entered, _ = 1 + id(a), self() ! unreachable end end}, + {badarith, fun() -> try error(first) after + self() ! cleanup_entered, _ = id(a) - 1, self() ! unreachable end end}, + {badarith, fun() -> try error(first) after + self() ! cleanup_entered, _ = id(a) * 2, self() ! unreachable end end}, + {badarith, fun() -> try error(first) after + self() ! cleanup_entered, _ = 1 / id(0), self() ! unreachable end end}, + {badarith, fun() -> try error(first) after + self() ! cleanup_entered, _ = -id(a), self() ! unreachable end end}, + {badarith, fun() -> try error(first) after + self() ! cleanup_entered, _ = bnot id(a), self() ! unreachable end end}, + {badarg, fun() -> try error(first) after + self() ! cleanup_entered, _ = not id(a), self() ! unreachable end end}]), + %% The stacktrace passes through unchanged. true = same_stacktrace(), [] = drain(), diff --git a/tests/try_operation_short_circuit.erl b/tests/try_operation_short_circuit.erl index 2a0450e..076b43c 100644 --- a/tests/try_operation_short_circuit.erl +++ b/tests/try_operation_short_circuit.erl @@ -2,8 +2,8 @@ %% Inside a try body, an operation or throw that raises goes to the handler %% at once, as on BEAM: no later send, put, receive or call runs, and a %% later operation cannot replace the reason. Guards inside the body still -%% just fail. Outside try, a failed send or spawn or a throw stops the -%% process at once, and a map update evaluates every value before it fails. +%% just fail. Outside try, migrated arithmetic, a failed send, spawn or throw +%% stops the process at once, and a map update evaluates every value before it fails. %% Run this same fixture on native Nera and BEAM. -module(try_operation_short_circuit). -export([test/0]). @@ -134,7 +134,15 @@ check({Name, Expected, ExpectedMessages, Body}) -> %% A guard that raises inside a try body fails the guard; the body goes on. guards() -> - [{guard_arith, fun() -> try case id(a) of X when X + 1 > 0 -> wrong; _ -> right end + [{guard_subtract, fun() -> try case id(a) of X when X - 1 > 0 -> wrong; _ -> right end + catch C:E -> {C, E} end end}, + {guard_divide, fun() -> try case id(0) of X when 1 / X > 0 -> wrong; _ -> right end + catch C:E -> {C, E} end end}, + {guard_negate, fun() -> try case id(a) of X when -X > 0 -> wrong; _ -> right end + catch C:E -> {C, E} end end}, + {guard_bnot, fun() -> try case id(a) of X when bnot X > 0 -> wrong; _ -> right end + catch C:E -> {C, E} end end}, + {guard_arith, fun() -> try case id(a) of X when X + 1 > 0 -> wrong; _ -> right end catch C:E -> {C, E} end end}, {guard_not, fun() -> try case id(a) of X when not X -> wrong; _ -> right end catch C:E -> {C, E} end end}, @@ -147,11 +155,18 @@ guards() -> guard({Name, Body}) -> {Name, right} = {Name, Body()}. -%% Outside try, the process stops at the failed operation. BEAM's exit -%% reason carries the stack, and an uncaught throw's is {nocatch, Reason}; -%% native Nera's are not yet. +%% Outside try, the process stops at the failed operation. This fixture +%% owns the reason and side effects, not the complete exit envelope; +%% uncaught_error_exit owns the stricter error-envelope assertions. unprotected() -> - [{send_unregistered, badarg, [], + [{add, badarith, [], fun(P) -> _ = 1 + id(a), P ! sent end}, + {subtract, badarith, [], fun(P) -> _ = id(a) - 1, P ! sent end}, + {multiply, badarith, [], fun(P) -> _ = id(a) * 2, P ! sent end}, + {divide, badarith, [], fun(P) -> _ = 1 / id(0), P ! sent end}, + {negate, badarith, [], fun(P) -> _ = -id(a), P ! sent end}, + {bit_not, badarith, [], fun(P) -> _ = bnot id(a), P ! sent end}, + {bool_not, badarg, [], fun(P) -> _ = not id(a), P ! sent end}, + {send_unregistered, badarg, [], fun(P) -> _ = id(no_such_registered_name) ! x, P ! sent end}, {send_tuple_unregistered, badarg, [], fun(P) -> _ = id(no_such_registered_name) ! {x, id(y)}, P ! sent end},