//! Condition Expression Lowerer //! //! This module provides the core logic for lowering AST condition expressions //! to JoinIR instructions. It handles comparisons, logical operators, and //! arithmetic expressions. //! //! ## Design Philosophy //! //! **Single Responsibility**: This module ONLY performs AST → JoinIR lowering. //! It does NOT: //! - Manage variable environments (that's condition_env.rs) //! - Extract variables from AST (that's condition_var_extractor.rs) //! - Manage HOST ↔ JoinIR bindings (that's inline_boundary.rs) use crate::ast::{ASTNode, BinaryOperator, LiteralValue, UnaryOperator}; use crate::mir::join_ir::{BinOpKind, CompareOp, ConstValue, JoinInst, MirLikeInst, UnaryOp}; use crate::mir::ValueId; use super::condition_env::ConditionEnv; use super::loop_body_local_env::LoopBodyLocalEnv; // Phase 92 P2-2: Body-local support use super::method_call_lowerer::MethodCallLowerer; /// Lower an AST condition to JoinIR instructions /// /// # Arguments /// /// * `cond_ast` - AST node representing the boolean condition /// * `alloc_value` - ValueId allocator function /// * `env` - ConditionEnv for variable resolution (JoinIR-local ValueIds) /// * `body_local_env` - Phase 92 P2-2: Optional body-local variable environment /// /// # Returns /// /// * `Ok((ValueId, Vec))` - Condition result ValueId and evaluation instructions /// * `Err(String)` - Lowering error message /// /// # Supported Patterns /// /// - Comparisons: `i < n`, `x == y`, `a != b`, `x <= y`, `x >= y`, `x > y` /// - Logical: `a && b`, `a || b`, `!cond` /// - Variables and literals /// /// # Phase 92 P2-2: Body-Local Variable Support /// /// When lowering conditions that reference body-local variables (e.g., `ch == '\\'` /// in escape patterns), the `body_local_env` parameter provides name → ValueId /// mappings for variables defined in the loop body. /// /// Variable resolution priority: /// 1. ConditionEnv (loop parameters, captured variables) /// 2. LoopBodyLocalEnv (body-local variables like `ch`) /// /// # Example /// /// ```ignore /// let mut env = ConditionEnv::new(); /// env.insert("i".to_string(), ValueId(0)); /// env.insert("end".to_string(), ValueId(1)); /// /// let mut body_env = LoopBodyLocalEnv::new(); /// body_env.insert("ch".to_string(), ValueId(5)); // Phase 92 P2-2 /// /// let mut value_counter = 2u32; /// let mut alloc_value = || { /// let id = ValueId(value_counter); /// value_counter += 1; /// id /// }; /// /// // Lower condition: ch == '\\' /// let (cond_value, cond_insts) = lower_condition_to_joinir( /// condition_ast, /// &mut alloc_value, /// &env, /// Some(&body_env), // Phase 92 P2-2: Body-local support /// )?; /// ``` pub fn lower_condition_to_joinir( cond_ast: &ASTNode, alloc_value: &mut dyn FnMut() -> ValueId, env: &ConditionEnv, body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2 ) -> Result<(ValueId, Vec), String> { let mut instructions = Vec::new(); let result_value = lower_condition_recursive(cond_ast, alloc_value, env, body_local_env, &mut instructions)?; Ok((result_value, instructions)) } /// Convenience wrapper: lower a condition without body-local support. pub fn lower_condition_to_joinir_no_body_locals( cond_ast: &ASTNode, alloc_value: &mut dyn FnMut() -> ValueId, env: &ConditionEnv, ) -> Result<(ValueId, Vec), String> { lower_condition_to_joinir(cond_ast, alloc_value, env, None) } /// Recursive helper for condition lowering /// /// Handles all supported AST node types and emits appropriate JoinIR instructions. /// /// # Phase 92 P2-2 /// /// Added `body_local_env` parameter to support body-local variable resolution. fn lower_condition_recursive( cond_ast: &ASTNode, alloc_value: &mut dyn FnMut() -> ValueId, env: &ConditionEnv, body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2 instructions: &mut Vec, ) -> Result { match cond_ast { // Comparison operations: <, ==, !=, <=, >=, > ASTNode::BinaryOp { operator, left, right, .. } => match operator { BinaryOperator::Less | BinaryOperator::Equal | BinaryOperator::NotEqual | BinaryOperator::LessEqual | BinaryOperator::GreaterEqual | BinaryOperator::Greater => { lower_comparison(operator, left, right, alloc_value, env, body_local_env, instructions) } BinaryOperator::And => lower_logical_and(left, right, alloc_value, env, body_local_env, instructions), BinaryOperator::Or => lower_logical_or(left, right, alloc_value, env, body_local_env, instructions), _ => Err(format!( "Unsupported binary operator in condition: {:?}", operator )), }, // Unary NOT operator ASTNode::UnaryOp { operator: UnaryOperator::Not, operand, .. } => lower_not_operator(operand, alloc_value, env, body_local_env, instructions), // Phase 92 P2-2: Variables - resolve from ConditionEnv or LoopBodyLocalEnv ASTNode::Variable { name, .. } => { // Priority 1: ConditionEnv (loop parameters, captured variables) if let Some(value_id) = env.get(name) { return Ok(value_id); } // Priority 2: LoopBodyLocalEnv (body-local variables like `ch`) if let Some(body_env) = body_local_env { if let Some(value_id) = body_env.get(name) { return Ok(value_id); } } Err(format!("Variable '{}' not found in ConditionEnv or LoopBodyLocalEnv", name)) } // Literals - emit as constants ASTNode::Literal { value, .. } => lower_literal(value, alloc_value, instructions), _ => Err(format!("Unsupported AST node in condition: {:?}", cond_ast)), } } /// Lower a comparison operation (e.g., `i < end`) fn lower_comparison( operator: &BinaryOperator, left: &ASTNode, right: &ASTNode, alloc_value: &mut dyn FnMut() -> ValueId, env: &ConditionEnv, body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2 instructions: &mut Vec, ) -> Result { // Lower left and right sides let lhs = lower_value_expression(left, alloc_value, env, body_local_env, instructions)?; let rhs = lower_value_expression(right, alloc_value, env, body_local_env, instructions)?; let dst = alloc_value(); let cmp_op = match operator { BinaryOperator::Less => CompareOp::Lt, BinaryOperator::Equal => CompareOp::Eq, BinaryOperator::NotEqual => CompareOp::Ne, BinaryOperator::LessEqual => CompareOp::Le, BinaryOperator::GreaterEqual => CompareOp::Ge, BinaryOperator::Greater => CompareOp::Gt, _ => unreachable!(), }; // Emit Compare instruction instructions.push(JoinInst::Compute(MirLikeInst::Compare { dst, op: cmp_op, lhs, rhs, })); Ok(dst) } /// Lower logical AND operation (e.g., `a && b`) fn lower_logical_and( left: &ASTNode, right: &ASTNode, alloc_value: &mut dyn FnMut() -> ValueId, env: &ConditionEnv, body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2 instructions: &mut Vec, ) -> Result { // Logical AND: evaluate both sides and combine let lhs = lower_condition_recursive(left, alloc_value, env, body_local_env, instructions)?; let rhs = lower_condition_recursive(right, alloc_value, env, body_local_env, instructions)?; let dst = alloc_value(); // Emit BinOp And instruction instructions.push(JoinInst::Compute(MirLikeInst::BinOp { dst, op: BinOpKind::And, lhs, rhs, })); Ok(dst) } /// Lower logical OR operation (e.g., `a || b`) fn lower_logical_or( left: &ASTNode, right: &ASTNode, alloc_value: &mut dyn FnMut() -> ValueId, env: &ConditionEnv, body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2 instructions: &mut Vec, ) -> Result { // Logical OR: evaluate both sides and combine let lhs = lower_condition_recursive(left, alloc_value, env, body_local_env, instructions)?; let rhs = lower_condition_recursive(right, alloc_value, env, body_local_env, instructions)?; let dst = alloc_value(); // Emit BinOp Or instruction instructions.push(JoinInst::Compute(MirLikeInst::BinOp { dst, op: BinOpKind::Or, lhs, rhs, })); Ok(dst) } /// Lower NOT operator (e.g., `!cond`) fn lower_not_operator( operand: &ASTNode, alloc_value: &mut dyn FnMut() -> ValueId, env: &ConditionEnv, body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2 instructions: &mut Vec, ) -> Result { let operand_val = lower_condition_recursive(operand, alloc_value, env, body_local_env, instructions)?; let dst = alloc_value(); // Emit UnaryOp Not instruction instructions.push(JoinInst::Compute(MirLikeInst::UnaryOp { dst, op: UnaryOp::Not, operand: operand_val, })); Ok(dst) } /// Lower a literal value (e.g., `10`, `true`, `"text"`) fn lower_literal( value: &LiteralValue, alloc_value: &mut dyn FnMut() -> ValueId, instructions: &mut Vec, ) -> Result { let dst = alloc_value(); let const_value = match value { LiteralValue::Integer(n) => ConstValue::Integer(*n), LiteralValue::String(s) => ConstValue::String(s.clone()), LiteralValue::Bool(b) => ConstValue::Bool(*b), LiteralValue::Float(_) => { return Err("Float literals not supported in JoinIR conditions yet".to_string()); } _ => { return Err(format!( "Unsupported literal type in condition: {:?}", value )); } }; instructions.push(JoinInst::Compute(MirLikeInst::Const { dst, value: const_value, })); Ok(dst) } /// Lower a value expression (for comparison operands, etc.) /// /// This handles the common case where we need to evaluate a simple value /// (variable or literal) as part of a comparison. /// /// # Phase 92 P2-2 /// /// Added `body_local_env` parameter to support body-local variable resolution /// (e.g., `ch` in `ch == '\\'`). pub fn lower_value_expression( expr: &ASTNode, alloc_value: &mut dyn FnMut() -> ValueId, env: &ConditionEnv, body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2 instructions: &mut Vec, ) -> Result { match expr { // Phase 92 P2-2: Variables - resolve from ConditionEnv or LoopBodyLocalEnv ASTNode::Variable { name, .. } => { // Priority 1: ConditionEnv (loop parameters, captured variables) if let Some(value_id) = env.get(name) { return Ok(value_id); } // Priority 2: LoopBodyLocalEnv (body-local variables like `ch`) if let Some(body_env) = body_local_env { if let Some(value_id) = body_env.get(name) { return Ok(value_id); } } Err(format!("Variable '{}' not found in ConditionEnv or LoopBodyLocalEnv", name)) } // Literals - emit as constants ASTNode::Literal { value, .. } => lower_literal(value, alloc_value, instructions), // Binary operations (for arithmetic in conditions like i + 1 < n) ASTNode::BinaryOp { operator, left, right, .. } => lower_arithmetic_binop(operator, left, right, alloc_value, env, body_local_env, instructions), // Phase 224-C: MethodCall support with arguments (e.g., s.length(), s.indexOf(ch)) ASTNode::MethodCall { object, method, arguments, .. } => { // 1. Lower receiver (object) to ValueId let recv_val = lower_value_expression(object, alloc_value, env, body_local_env, instructions)?; // 2. Lower method call using MethodCallLowerer (will lower arguments internally) MethodCallLowerer::lower_for_condition( recv_val, method, arguments, alloc_value, env, instructions, ) } _ => Err(format!( "Unsupported expression in value context: {:?}", expr )), } } /// Lower an arithmetic binary operation (e.g., `i + 1`) fn lower_arithmetic_binop( operator: &BinaryOperator, left: &ASTNode, right: &ASTNode, alloc_value: &mut dyn FnMut() -> ValueId, env: &ConditionEnv, body_local_env: Option<&LoopBodyLocalEnv>, // Phase 92 P2-2 instructions: &mut Vec, ) -> Result { let lhs = lower_value_expression(left, alloc_value, env, body_local_env, instructions)?; let rhs = lower_value_expression(right, alloc_value, env, body_local_env, instructions)?; let dst = alloc_value(); let bin_op = match operator { BinaryOperator::Add => BinOpKind::Add, BinaryOperator::Subtract => BinOpKind::Sub, BinaryOperator::Multiply => BinOpKind::Mul, BinaryOperator::Divide => BinOpKind::Div, BinaryOperator::Modulo => BinOpKind::Mod, _ => { return Err(format!( "Unsupported binary operator in expression: {:?}", operator )); } }; instructions.push(JoinInst::Compute(MirLikeInst::BinOp { dst, op: bin_op, lhs, rhs, })); Ok(dst) } #[cfg(test)] mod tests { use super::*; use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; /// Helper to create a test ConditionEnv with variables fn create_test_env() -> ConditionEnv { let mut env = ConditionEnv::new(); // Register test variables (using JoinIR-local ValueIds) env.insert("i".to_string(), ValueId(0)); env.insert("end".to_string(), ValueId(1)); env } #[test] fn test_simple_comparison() { let env = create_test_env(); let mut value_counter = 2u32; // Start after i=0, end=1 let mut alloc_value = || { let id = ValueId(value_counter); value_counter += 1; id }; // AST: i < end let ast = ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "i".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "end".to_string(), span: Span::unknown(), }), span: Span::unknown(), }; let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env); assert!(result.is_ok(), "Simple comparison should succeed"); let (_cond_value, instructions) = result.unwrap(); assert_eq!( instructions.len(), 1, "Should generate 1 Compare instruction" ); } #[test] fn test_comparison_with_literal() { let env = create_test_env(); let mut value_counter = 2u32; let mut alloc_value = || { let id = ValueId(value_counter); value_counter += 1; id }; // AST: i < 10 let ast = ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "i".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(10), span: Span::unknown(), }), span: Span::unknown(), }; let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env); assert!(result.is_ok(), "Comparison with literal should succeed"); let (_cond_value, instructions) = result.unwrap(); // Should have: Const(10), Compare assert_eq!(instructions.len(), 2, "Should generate Const + Compare"); } #[test] fn test_logical_or() { let mut env = ConditionEnv::new(); env.insert("a".to_string(), ValueId(2)); env.insert("b".to_string(), ValueId(3)); let mut value_counter = 4u32; let mut alloc_value = || { let id = ValueId(value_counter); value_counter += 1; id }; // AST: a < 5 || b < 5 let ast = ASTNode::BinaryOp { operator: BinaryOperator::Or, left: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "a".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(5), span: Span::unknown(), }), span: Span::unknown(), }), right: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "b".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(5), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }; let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env); assert!(result.is_ok(), "OR expression should succeed"); let (_cond_value, instructions) = result.unwrap(); // Should have: Const(5), Compare(a<5), Const(5), Compare(b<5), BinOp(Or) assert_eq!(instructions.len(), 5, "Should generate proper OR chain"); } #[test] fn test_not_operator() { let env = create_test_env(); let mut value_counter = 2u32; let mut alloc_value = || { let id = ValueId(value_counter); value_counter += 1; id }; // AST: !(i < end) let ast = ASTNode::UnaryOp { operator: UnaryOperator::Not, operand: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "i".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "end".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }; let result = lower_condition_to_joinir_no_body_locals(&ast, &mut alloc_value, &env); assert!(result.is_ok(), "NOT operator should succeed"); let (_cond_value, instructions) = result.unwrap(); // Should have: Compare, UnaryOp(Not) assert_eq!(instructions.len(), 2, "Should generate Compare + Not"); } /// Phase 92 P4 Level 2: Test body-local variable resolution /// /// This test verifies that conditions can reference body-local variables /// (e.g., `ch == '\\'` in escape sequence patterns). /// /// Variable resolution priority: /// 1. ConditionEnv (loop parameters, captured variables) /// 2. LoopBodyLocalEnv (body-local variables like `ch`) #[test] fn test_body_local_variable_resolution() { // Setup ConditionEnv with loop variable let mut env = ConditionEnv::new(); env.insert("i".to_string(), ValueId(100)); // Setup LoopBodyLocalEnv with body-local variable let mut body_local_env = LoopBodyLocalEnv::new(); body_local_env.insert("ch".to_string(), ValueId(200)); let mut value_counter = 300u32; let mut alloc_value = || { let id = ValueId(value_counter); value_counter += 1; id }; // AST: ch == "\\" let ast = ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::String("\\".to_string()), span: Span::unknown(), }), span: Span::unknown(), }; // Phase 92 P2-2: Use lower_condition_to_joinir with body_local_env let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, Some(&body_local_env)); assert!( result.is_ok(), "Body-local variable resolution should succeed" ); let (cond_value, instructions) = result.unwrap(); // Should have: Const("\\"), Compare(ch == "\\") assert_eq!( instructions.len(), 2, "Should generate Const + Compare for body-local variable" ); // Verify the comparison uses the body-local variable's ValueId(200) if let Some(JoinInst::Compute(MirLikeInst::Compare { lhs, .. })) = instructions.get(1) { assert_eq!( *lhs, ValueId(200), "Compare should use body-local variable ValueId(200)" ); } else { panic!("Expected Compare instruction at position 1"); } assert!(cond_value.0 >= 300, "Result should use newly allocated ValueId"); } /// Phase 92 P4 Level 2: Test variable resolution priority (ConditionEnv takes precedence) /// /// When a variable exists in both ConditionEnv and LoopBodyLocalEnv, /// ConditionEnv should take priority. #[test] fn test_variable_resolution_priority() { // Setup both environments with overlapping variable "x" let mut env = ConditionEnv::new(); env.insert("x".to_string(), ValueId(100)); // ConditionEnv priority let mut body_local_env = LoopBodyLocalEnv::new(); body_local_env.insert("x".to_string(), ValueId(200)); // Should be shadowed let mut value_counter = 300u32; let mut alloc_value = || { let id = ValueId(value_counter); value_counter += 1; id }; // AST: x == 42 let ast = ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(ASTNode::Variable { name: "x".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(42), span: Span::unknown(), }), span: Span::unknown(), }; let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, Some(&body_local_env)); assert!(result.is_ok(), "Variable resolution should succeed"); let (_cond_value, instructions) = result.unwrap(); // Verify the comparison uses ConditionEnv's ValueId(100), not LoopBodyLocalEnv's ValueId(200) if let Some(JoinInst::Compute(MirLikeInst::Compare { lhs, .. })) = instructions.get(1) { assert_eq!( *lhs, ValueId(100), "ConditionEnv should take priority over LoopBodyLocalEnv" ); } else { panic!("Expected Compare instruction at position 1"); } } /// Phase 92 P4 Level 2: Test error handling for undefined variables /// /// Variables not found in either environment should produce clear error messages. #[test] fn test_undefined_variable_error() { let env = ConditionEnv::new(); let body_local_env = LoopBodyLocalEnv::new(); let mut value_counter = 300u32; let mut alloc_value = || { let id = ValueId(value_counter); value_counter += 1; id }; // AST: undefined_var == 42 let ast = ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(ASTNode::Variable { name: "undefined_var".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(42), span: Span::unknown(), }), span: Span::unknown(), }; let result = lower_condition_to_joinir(&ast, &mut alloc_value, &env, Some(&body_local_env)); assert!(result.is_err(), "Undefined variable should fail"); let err = result.unwrap_err(); assert!( err.contains("undefined_var"), "Error message should mention the undefined variable name" ); assert!( err.contains("not found"), "Error message should indicate variable was not found" ); } }