//! Phase 169: JoinIR Condition Lowering Helper //! //! This module provides AST → JoinIR condition lowering for loop patterns. //! Unlike BoolExprLowerer (which generates MIR), this generates JoinIR instructions. //! //! ## Design Philosophy //! //! **Separation of Concerns**: //! - BoolExprLowerer: AST → MIR (for regular control flow) //! - condition_to_joinir: AST → JoinIR (for loop lowerers) //! //! This dual approach maintains clean boundaries: //! - Loop lowerers work in JoinIR space (pure functional transformation) //! - Regular control flow uses MIR space (stateful builder) //! //! ## Usage Example //! //! ```rust //! let mut value_counter = 0u32; //! let mut alloc_value = || { //! let id = ValueId(value_counter); //! value_counter += 1; //! id //! }; //! //! // Lower condition: i < end //! let (cond_value, cond_insts) = lower_condition_to_joinir( //! condition_ast, //! &mut alloc_value, //! builder, //! )?; //! //! // cond_value: ValueId holding boolean result //! // cond_insts: Vec to evaluate condition //! ``` use crate::ast::{ASTNode, BinaryOperator, LiteralValue, UnaryOperator}; use crate::mir::join_ir::{BinOpKind, CompareOp, ConstValue, JoinInst, MirLikeInst}; use crate::mir::ValueId; use std::collections::HashMap; /// Phase 171-fix: Environment for condition expression lowering /// Maps variable names to JoinIR-local ValueIds #[derive(Debug, Clone, Default)] pub struct ConditionEnv { pub name_to_join: HashMap, } impl ConditionEnv { pub fn new() -> Self { Self { name_to_join: HashMap::new() } } pub fn insert(&mut self, name: String, join_id: ValueId) { self.name_to_join.insert(name, join_id); } pub fn get(&self, name: &str) -> Option { self.name_to_join.get(name).copied() } } /// Phase 171-fix: Binding between HOST and JoinIR ValueIds for condition variables #[derive(Debug, Clone)] pub struct ConditionBinding { pub name: String, pub host_value: ValueId, pub join_value: ValueId, } /// Lower an AST condition to JoinIR instructions /// /// Phase 171-fix: Changed to use ConditionEnv instead of builder /// /// # Arguments /// /// * `cond_ast` - AST node representing the boolean condition /// * `alloc_value` - ValueId allocator function /// * `env` - ConditionEnv for variable resolution (JoinIR-local ValueIds) /// /// # 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 pub fn lower_condition_to_joinir( cond_ast: &ASTNode, alloc_value: &mut F, env: &ConditionEnv, ) -> Result<(ValueId, Vec), String> where F: FnMut() -> ValueId, { let mut instructions = Vec::new(); let result_value = lower_condition_recursive(cond_ast, alloc_value, env, &mut instructions)?; Ok((result_value, instructions)) } /// Recursive helper for condition lowering /// /// Phase 171-fix: Changed to use ConditionEnv instead of builder fn lower_condition_recursive( cond_ast: &ASTNode, alloc_value: &mut F, env: &ConditionEnv, instructions: &mut Vec, ) -> Result where F: FnMut() -> ValueId, { 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 left and right sides let lhs = lower_value_expression(left, alloc_value, env, instructions)?; let rhs = lower_value_expression(right, alloc_value, 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) } BinaryOperator::And => { // Logical AND: evaluate both sides and combine let lhs = lower_condition_recursive(left, alloc_value, env, instructions)?; let rhs = lower_condition_recursive(right, alloc_value, env, instructions)?; let dst = alloc_value(); // Emit BinOp And instruction instructions.push(JoinInst::Compute(MirLikeInst::BinOp { dst, op: BinOpKind::And, lhs, rhs, })); Ok(dst) } BinaryOperator::Or => { // Logical OR: evaluate both sides and combine let lhs = lower_condition_recursive(left, alloc_value, env, instructions)?; let rhs = lower_condition_recursive(right, alloc_value, env, instructions)?; let dst = alloc_value(); // Emit BinOp Or instruction instructions.push(JoinInst::Compute(MirLikeInst::BinOp { dst, op: BinOpKind::Or, lhs, rhs, })); Ok(dst) } _ => { // Other operators (arithmetic, etc.) Err(format!( "Unsupported binary operator in condition: {:?}", operator )) } }, // Unary NOT operator ASTNode::UnaryOp { operator: UnaryOperator::Not, operand, .. } => { let operand_val = lower_condition_recursive(operand, alloc_value, env, instructions)?; let dst = alloc_value(); // Emit UnaryOp Not instruction instructions.push(JoinInst::Compute(MirLikeInst::UnaryOp { dst, op: crate::mir::join_ir::UnaryOp::Not, operand: operand_val, })); Ok(dst) } // Variables - resolve from ConditionEnv (Phase 171-fix) ASTNode::Variable { name, .. } => { // Look up variable in ConditionEnv (JoinIR-local ValueIds) env.get(name) .ok_or_else(|| format!("Variable '{}' not bound in ConditionEnv", name)) } // Literals - emit as constants ASTNode::Literal { value, .. } => { 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(_) => { // Float literals not supported in JoinIR ConstValue yet 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) } _ => Err(format!("Unsupported AST node in condition: {:?}", cond_ast)), } } /// Lower a value expression (for comparison operands, etc.) /// /// Phase 171-fix: Changed to use ConditionEnv instead of builder /// /// This handles the common case where we need to evaluate a simple value /// (variable or literal) as part of a comparison. fn lower_value_expression( expr: &ASTNode, alloc_value: &mut F, env: &ConditionEnv, instructions: &mut Vec, ) -> Result where F: FnMut() -> ValueId, { match expr { // Variables - look up in ConditionEnv (Phase 171-fix) ASTNode::Variable { name, .. } => env .get(name) .ok_or_else(|| format!("Variable '{}' not bound in ConditionEnv", name)), // Literals - emit as constants ASTNode::Literal { value, .. } => { 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(_) => { // Float literals not supported in JoinIR ConstValue yet return Err("Float literals not supported in JoinIR value expressions yet".to_string()); } _ => { return Err(format!("Unsupported literal type: {:?}", value)); } }; instructions.push(JoinInst::Compute(MirLikeInst::Const { dst, value: const_value, })); Ok(dst) } // Binary operations (for arithmetic in conditions like i + 1 < n) ASTNode::BinaryOp { operator, left, right, .. } => { let lhs = lower_value_expression(left, alloc_value, env, instructions)?; let rhs = lower_value_expression(right, alloc_value, 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) } _ => Err(format!("Unsupported expression in value context: {:?}", expr)), } } /// Extract all variable names used in a condition AST (Phase 171) /// /// This helper recursively traverses the condition AST and collects all /// unique variable names. Used to determine which variables need to be /// available in JoinIR scope. /// /// # Arguments /// /// * `cond_ast` - AST node representing the condition /// * `exclude_vars` - Variable names to exclude (e.g., loop parameters already registered) /// /// # Returns /// /// Sorted vector of unique variable names found in the condition /// /// # Example /// /// ```ignore /// // For condition: start < end && i < len /// let vars = extract_condition_variables( /// condition_ast, /// &["i".to_string()], // Exclude loop variable 'i' /// ); /// // Result: ["end", "len", "start"] (sorted, 'i' excluded) /// ``` pub fn extract_condition_variables( cond_ast: &ASTNode, exclude_vars: &[String], ) -> Vec { use std::collections::BTreeSet; let mut all_vars = BTreeSet::new(); collect_variables_recursive(cond_ast, &mut all_vars); // Filter out excluded variables and return sorted list all_vars.into_iter() .filter(|name| !exclude_vars.contains(name)) .collect() } /// Recursive helper to collect variable names fn collect_variables_recursive(ast: &ASTNode, vars: &mut std::collections::BTreeSet) { match ast { ASTNode::Variable { name, .. } => { vars.insert(name.clone()); } ASTNode::BinaryOp { left, right, .. } => { collect_variables_recursive(left, vars); collect_variables_recursive(right, vars); } ASTNode::UnaryOp { operand, .. } => { collect_variables_recursive(operand, vars); } ASTNode::Literal { .. } => { // Literals have no variables } _ => { // Other AST nodes not expected in conditions } } } #[cfg(test)] mod tests { use super::*; use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; /// Phase 171-fix: 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(&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(&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(&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_extract_condition_variables_simple() { // AST: start < end let ast = ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "start".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "end".to_string(), span: Span::unknown(), }), span: Span::unknown(), }; let vars = extract_condition_variables(&ast, &[]); assert_eq!(vars, vec!["end", "start"]); // Sorted order } #[test] fn test_extract_condition_variables_with_exclude() { // 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 vars = extract_condition_variables(&ast, &["i".to_string()]); assert_eq!(vars, vec!["end"]); // 'i' excluded } #[test] fn test_extract_condition_variables_complex() { // AST: start < end && i < len let ast = ASTNode::BinaryOp { operator: BinaryOperator::And, left: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "start".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "end".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), right: 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: "len".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }; let vars = extract_condition_variables(&ast, &["i".to_string()]); assert_eq!(vars, vec!["end", "len", "start"]); // Sorted, 'i' excluded } }