diff --git a/apps/tests/phase190_atoi_impl.hako b/apps/tests/phase190_atoi_impl.hako new file mode 100644 index 00000000..f80285be --- /dev/null +++ b/apps/tests/phase190_atoi_impl.hako @@ -0,0 +1,22 @@ +// Phase 190: Number accumulation test - atoi implementation +// Tests: result = result * 10 + digit pattern +// Uses Pattern 2 (break) to enable carrier support + +static box AtoiImpl { + method main() { + local result + result = 0 + local i + i = 0 + loop(i < 10) { + if i >= 3 { + break + } + local digit + digit = i + result = result * 10 + digit + i = i + 1 + } + return result + } +} diff --git a/apps/tests/phase190_parse_number_impl.hako b/apps/tests/phase190_parse_number_impl.hako new file mode 100644 index 00000000..e4b5f41f --- /dev/null +++ b/apps/tests/phase190_parse_number_impl.hako @@ -0,0 +1,20 @@ +// Phase 190: Number accumulation test - parse number implementation +// Tests: num = num * 10 + i pattern with different initial values +// Uses Pattern 2 (break) to enable carrier support + +static box ParseNumberImpl { + method main() { + local num + num = 0 + local i + i = 1 + loop(i < 10) { + if i > 3 { + break + } + num = num * 10 + i + i = i + 1 + } + print(num) + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/common_init.rs b/src/mir/builder/control_flow/joinir/patterns/common_init.rs index 11c385df..b0e9efb2 100644 --- a/src/mir/builder/control_flow/joinir/patterns/common_init.rs +++ b/src/mir/builder/control_flow/joinir/patterns/common_init.rs @@ -178,7 +178,8 @@ impl CommonPatternInitializer { let updates = LoopUpdateAnalyzer::analyze_carrier_updates(body, &dummy_carriers); // Phase 188: Check if any update is complex (reject only UpdateRhs::Other) - // Allow: Const (numeric), Variable (numeric/string), StringLiteral + // Phase 190: Allow NumberAccumulation pattern + // Allow: Const (numeric), Variable (numeric/string), StringLiteral, NumberAccumulation // Reject: Other (method calls, nested BinOp) for update in updates.values() { if let UpdateExpr::BinOp { rhs, .. } = update { @@ -193,6 +194,9 @@ impl CommonPatternInitializer { UpdateRhs::StringLiteral(_) => { // Phase 188: StringAppendLiteral: s = s + "x" (allowed) } + UpdateRhs::NumberAccumulation { .. } => { + // Phase 190: Number accumulation: result = result * 10 + digit (allowed) + } UpdateRhs::Other => { // Phase 188: Complex update (method call, nested BinOp) - reject eprintln!("[common_init/check_carriers] Phase 188: Complex update detected (UpdateRhs::Other), rejecting pattern"); diff --git a/src/mir/join_ir/lowering/carrier_update_emitter.rs b/src/mir/join_ir/lowering/carrier_update_emitter.rs index d7f5f47f..31e2ad96 100644 --- a/src/mir/join_ir/lowering/carrier_update_emitter.rs +++ b/src/mir/join_ir/lowering/carrier_update_emitter.rs @@ -134,6 +134,52 @@ pub fn emit_carrier_update_with_env( })); const_id } + // Phase 190: Number accumulation pattern: result = result * base + digit + // Emit as: tmp = carrier * base; result = tmp + digit + UpdateRhs::NumberAccumulation { base, digit_var } => { + // Step 1: Emit const for base + let base_id = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::Const { + dst: base_id, + value: ConstValue::Integer(*base), + })); + + // Step 2: Emit multiplication: tmp = carrier * base + let tmp_id = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: tmp_id, + op: BinOpKind::Mul, + lhs: carrier_param, + rhs: base_id, + })); + + // Step 3: Resolve digit variable + let digit_id = env.resolve(digit_var).ok_or_else(|| { + format!( + "Number accumulation digit variable '{}' not found in UpdateEnv", + digit_var + ) + })?; + + // Step 4: Emit addition: result = tmp + digit + // This will be handled by the outer BinOp emission + // For now, return digit_id to be used as RHS + // We need to handle this specially - return tmp_id instead + // and adjust the outer BinOp to use correct values + + // Actually, we need to emit both operations here + // Final result = tmp + digit + let result = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: result, + op: *op, // Use the operation from outer UpdateExpr + lhs: tmp_id, + rhs: digit_id, + })); + + // Return result directly - we've already emitted everything + return Ok(result); + } // Phase 178/188: Complex updates (method calls) still rejected UpdateRhs::Other => { return Err(format!( @@ -276,6 +322,45 @@ pub fn emit_carrier_update( })); const_id } + // Phase 190: Number accumulation pattern: result = result * base + digit + // Emit as: tmp = carrier * base; result = tmp + digit + UpdateRhs::NumberAccumulation { base, digit_var } => { + // Step 1: Emit const for base + let base_id = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::Const { + dst: base_id, + value: ConstValue::Integer(*base), + })); + + // Step 2: Emit multiplication: tmp = carrier * base + let tmp_id = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: tmp_id, + op: BinOpKind::Mul, + lhs: carrier_param, + rhs: base_id, + })); + + // Step 3: Resolve digit variable + let digit_id = env.get(digit_var).ok_or_else(|| { + format!( + "Number accumulation digit variable '{}' not found in ConditionEnv", + digit_var + ) + })?; + + // Step 4: Emit addition: result = tmp + digit + let result = alloc_value(); + instructions.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: result, + op: *op, // Use the operation from outer UpdateExpr + lhs: tmp_id, + rhs: digit_id, + })); + + // Return result directly - we've already emitted everything + return Ok(result); + } // Phase 178/188: Complex updates (method calls) still rejected UpdateRhs::Other => { return Err(format!( @@ -743,4 +828,124 @@ mod tests { assert!(result.is_ok()); assert_eq!(instructions.len(), 2); // Const + BinOp } + + #[test] + fn test_emit_number_accumulation_base10() { + // Phase 190: Test number accumulation pattern: result = result * 10 + digit + let mut cond_env = ConditionEnv::new(); + cond_env.insert("result".to_string(), ValueId(20)); // Carrier parameter + cond_env.insert("digit".to_string(), ValueId(30)); // Digit variable + + let body_env = LoopBodyLocalEnv::new(); + let update_env = UpdateEnv::new(&cond_env, &body_env); + + let carrier = test_carrier("result", 200); + let update = UpdateExpr::BinOp { + lhs: "result".to_string(), + op: BinOpKind::Add, + rhs: UpdateRhs::NumberAccumulation { + base: 10, + digit_var: "digit".to_string(), + }, + }; + + let mut value_counter = 150u32; + let mut alloc_value = || { + let id = ValueId(value_counter); + value_counter += 1; + id + }; + + let mut instructions = Vec::new(); + let result = emit_carrier_update_with_env( + &carrier, + &update, + &mut alloc_value, + &update_env, + &mut instructions, + ); + + assert!(result.is_ok()); + let result_id = result.unwrap(); + + // Should generate 3 instructions: + // 1. Const(10) for base + // 2. BinOp(Mul, result, base) for tmp + // 3. BinOp(Add, tmp, digit) for final result + assert_eq!(instructions.len(), 3); + + // Instruction 1: Const(10) + match &instructions[0] { + JoinInst::Compute(MirLikeInst::Const { dst, value }) => { + assert_eq!(*dst, ValueId(150)); // First allocated + assert!(matches!(value, ConstValue::Integer(10))); + } + _ => panic!("Expected Const instruction"), + } + + // Instruction 2: BinOp(Mul, result, base) + match &instructions[1] { + JoinInst::Compute(MirLikeInst::BinOp { dst, op, lhs, rhs }) => { + assert_eq!(*dst, ValueId(151)); // Second allocated (tmp) + assert_eq!(*op, BinOpKind::Mul); + assert_eq!(*lhs, ValueId(20)); // result from env + assert_eq!(*rhs, ValueId(150)); // base const + } + _ => panic!("Expected BinOp(Mul) instruction"), + } + + // Instruction 3: BinOp(Add, tmp, digit) + match &instructions[2] { + JoinInst::Compute(MirLikeInst::BinOp { dst, op, lhs, rhs }) => { + assert_eq!(*dst, ValueId(152)); // Third allocated (final result) + assert_eq!(*op, BinOpKind::Add); + assert_eq!(*lhs, ValueId(151)); // tmp from previous mul + assert_eq!(*rhs, ValueId(30)); // digit from env + } + _ => panic!("Expected BinOp(Add) instruction"), + } + + assert_eq!(result_id, ValueId(152)); + } + + #[test] + fn test_emit_number_accumulation_digit_not_found() { + // Phase 190: Test error when digit variable not in env + let mut cond_env = ConditionEnv::new(); + cond_env.insert("result".to_string(), ValueId(20)); + // Note: digit NOT in env + + let body_env = LoopBodyLocalEnv::new(); + let update_env = UpdateEnv::new(&cond_env, &body_env); + + let carrier = test_carrier("result", 200); + let update = UpdateExpr::BinOp { + lhs: "result".to_string(), + op: BinOpKind::Add, + rhs: UpdateRhs::NumberAccumulation { + base: 10, + digit_var: "digit".to_string(), + }, + }; + + let mut value_counter = 160u32; + let mut alloc_value = || { + let id = ValueId(value_counter); + value_counter += 1; + id + }; + + let mut instructions = Vec::new(); + let result = emit_carrier_update_with_env( + &carrier, + &update, + &mut alloc_value, + &update_env, + &mut instructions, + ); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.contains("Number accumulation digit variable 'digit' not found")); + } } diff --git a/src/mir/join_ir/lowering/loop_update_analyzer.rs b/src/mir/join_ir/lowering/loop_update_analyzer.rs index ab25c755..091c43ce 100644 --- a/src/mir/join_ir/lowering/loop_update_analyzer.rs +++ b/src/mir/join_ir/lowering/loop_update_analyzer.rs @@ -40,6 +40,7 @@ pub enum UpdateExpr { /// Right-hand side of update expression /// /// Phase 178: Extended to detect string updates for multi-carrier loops. +/// Phase 190: Extended to detect number accumulation pattern (result = result * base + digit). /// The goal is "carrier detection", not full semantic understanding. #[derive(Debug, Clone)] pub enum UpdateRhs { @@ -49,6 +50,14 @@ pub enum UpdateRhs { Variable(String), /// Phase 178: String literal: result + "x" StringLiteral(String), + /// Phase 190: Number accumulation pattern: result = result * base + digit + /// Represents expressions like: result * 10 + digit + NumberAccumulation { + /// Multiplication base (e.g., 10 for decimal, 2 for binary) + base: i64, + /// Variable name containing the digit to add + digit_var: String, + }, /// Phase 178: Other expression (method call, complex expr) /// Used to detect "carrier has an update" without understanding semantics Other, @@ -136,6 +145,7 @@ impl LoopUpdateAnalyzer { /// Recognizes patterns like: /// - `sum + i` → BinOp { lhs: "sum", op: Add, rhs: Variable("i") } /// - `count + 1` → BinOp { lhs: "count", op: Add, rhs: Const(1) } + /// - Phase 190: `result * 10 + digit` → BinOp { lhs: "result", op: Add, rhs: NumberAccumulation {...} } fn analyze_update_value(carrier_name: &str, value: &ASTNode) -> Option { match value { ASTNode::BinaryOp { @@ -144,6 +154,43 @@ impl LoopUpdateAnalyzer { right, .. } => { + // Phase 190: Check for number accumulation pattern first + // Pattern: (carrier * base) + digit + if matches!(operator, BinaryOperator::Add | BinaryOperator::Subtract) { + if let ASTNode::BinaryOp { + operator: BinaryOperator::Multiply, + left: mul_left, + right: mul_right, + .. + } = left.as_ref() + { + // Check if multiplication is: carrier * base + if let Some(mul_lhs_name) = Self::extract_variable_name(mul_left) { + if mul_lhs_name == carrier_name { + if let ASTNode::Literal { + value: LiteralValue::Integer(base), + .. + } = mul_right.as_ref() + { + // Check if RHS is a variable (digit) + if let Some(digit_var) = Self::extract_variable_name(right) { + // This is number accumulation pattern! + let op = Self::convert_operator(operator)?; + return Some(UpdateExpr::BinOp { + lhs: carrier_name.to_string(), + op, + rhs: UpdateRhs::NumberAccumulation { + base: *base, + digit_var, + }, + }); + } + } + } + } + } + } + // Check if LHS is the carrier itself (e.g., sum in "sum + i") if let Some(lhs_name) = Self::extract_variable_name(left) { if lhs_name == carrier_name { @@ -169,6 +216,7 @@ impl LoopUpdateAnalyzer { /// Analyze right-hand side of update expression /// /// Phase 178: Extended to detect string updates. + /// Phase 190: Extended to detect number accumulation pattern. /// Goal: "carrier has update" detection, not semantic understanding. fn analyze_rhs(node: &ASTNode) -> Option { match node { @@ -189,11 +237,16 @@ impl LoopUpdateAnalyzer { Some(UpdateRhs::Variable(name.clone())) } + // Phase 190: Number accumulation pattern detection + // This is called from analyze_update_value, so we're analyzing the full RHS of an assignment + // Pattern not detected at this level - handled in analyze_update_value + // BinaryOp nodes that aren't simple Add/Sub are treated as complex + ASTNode::BinaryOp { .. } => Some(UpdateRhs::Other), + // Phase 178: Method call or other complex expression // e.g., result + s.substring(pos, end) ASTNode::Call { .. } | ASTNode::MethodCall { .. } - | ASTNode::BinaryOp { .. } | ASTNode::UnaryOp { .. } => Some(UpdateRhs::Other), _ => None, @@ -264,4 +317,165 @@ mod tests { panic!("Expected BinOp, got {:?}", updates.get("count")); } } + + #[test] + fn test_analyze_number_accumulation_base10() { + // Test case: result = result * 10 + digit + use crate::ast::Span; + + let body = vec![ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "result".to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Multiply, + left: Box::new(ASTNode::Variable { + name: "result".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(10), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Variable { + name: "digit".to_string(), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + }]; + + let carriers = vec![CarrierVar { + name: "result".to_string(), + host_id: crate::mir::ValueId(0), + join_id: None, + }]; + + let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers); + + assert_eq!(updates.len(), 1); + assert!(updates.contains_key("result")); + + if let Some(UpdateExpr::BinOp { lhs, op, rhs }) = updates.get("result") { + assert_eq!(lhs, "result"); + assert_eq!(*op, BinOpKind::Add); + if let UpdateRhs::NumberAccumulation { base, digit_var } = rhs { + assert_eq!(*base, 10); + assert_eq!(digit_var, "digit"); + } else { + panic!("Expected NumberAccumulation, got {:?}", rhs); + } + } else { + panic!("Expected BinOp, got {:?}", updates.get("result")); + } + } + + #[test] + fn test_analyze_number_accumulation_base2() { + // Test case: result = result * 2 + bit + use crate::ast::Span; + + let body = vec![ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "result".to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Multiply, + left: Box::new(ASTNode::Variable { + name: "result".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(2), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Variable { + name: "bit".to_string(), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + }]; + + let carriers = vec![CarrierVar { + name: "result".to_string(), + host_id: crate::mir::ValueId(0), + join_id: None, + }]; + + let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers); + + assert_eq!(updates.len(), 1); + assert!(updates.contains_key("result")); + + if let Some(UpdateExpr::BinOp { lhs, op, rhs }) = updates.get("result") { + assert_eq!(lhs, "result"); + assert_eq!(*op, BinOpKind::Add); + if let UpdateRhs::NumberAccumulation { base, digit_var } = rhs { + assert_eq!(*base, 2); + assert_eq!(digit_var, "bit"); + } else { + panic!("Expected NumberAccumulation, got {:?}", rhs); + } + } else { + panic!("Expected BinOp, got {:?}", updates.get("result")); + } + } + + #[test] + fn test_analyze_number_accumulation_wrong_lhs() { + // Test case: result = other * 10 + digit (should be Complex/Other) + use crate::ast::Span; + + let body = vec![ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "result".to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Multiply, + left: Box::new(ASTNode::Variable { + name: "other".to_string(), // Wrong variable! + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(10), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Variable { + name: "digit".to_string(), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + }]; + + let carriers = vec![CarrierVar { + name: "result".to_string(), + host_id: crate::mir::ValueId(0), + join_id: None, + }]; + + let updates = LoopUpdateAnalyzer::analyze_carrier_updates(&body, &carriers); + + // Should detect assignment but with Other (complex) RHS + assert_eq!(updates.len(), 0); // Won't match because lhs != carrier + } } diff --git a/src/mir/join_ir/lowering/loop_with_continue_minimal.rs b/src/mir/join_ir/lowering/loop_with_continue_minimal.rs index 52d5191a..c3ffc89b 100644 --- a/src/mir/join_ir/lowering/loop_with_continue_minimal.rs +++ b/src/mir/join_ir/lowering/loop_with_continue_minimal.rs @@ -387,6 +387,24 @@ pub(crate) fn lower_loop_with_continue_minimal( const_1 } } + // Phase 190: Number accumulation not supported in Pattern 4 yet + // Skip JoinIR update - use Select passthrough to keep carrier_merged defined + UpdateRhs::NumberAccumulation { .. } => { + eprintln!( + "[loop_with_continue_minimal] Phase 190: Carrier '{}' has number accumulation - not supported in Pattern 4, using Select passthrough", + carrier_name + ); + // Emit Select with same values: carrier_merged = Select(_, carrier_param, carrier_param) + // This is effectively a passthrough (no JoinIR update) + loop_step_func.body.push(JoinInst::Select { + dst: carrier_merged, + cond: continue_cond, // Condition doesn't matter when both values are same + then_val: carrier_param, + else_val: carrier_param, + type_hint: None, + }); + continue; // Skip the BinOp and normal Select below + } // Phase 178: String updates detected but not lowered to JoinIR yet // Skip JoinIR update - use Select passthrough to keep carrier_merged defined UpdateRhs::StringLiteral(_) | UpdateRhs::Other => {