feat(joinir): Phase 33-19 ContinueBranchNormalizer for unified continue handling
## Problem
Pattern 4 (loop with continue) needs to handle both:
- if (cond) { continue } (then-continue)
- if (cond) { body } else { continue } (else-continue)
Previously, else-continue patterns required separate handling, preventing unified processing.
## Solution
### 1. ContinueBranchNormalizer Implementation
New file: `src/mir/join_ir/lowering/continue_branch_normalizer.rs`
- Detects: `if (cond) { body } else { continue }`
- Transforms to: `if (!cond) { continue } else { body }`
- Enables uniform Pattern 4 handling of all continue patterns
- No-op for other if statements
### 2. Pattern 4 Integration
- Normalize loop body before lowering (line 140)
- Use normalized body for carrier analysis (line 169)
- Preserves existing then-continue patterns
### 3. Carrier Filtering Enhancement
Lines 171-178: Only treat updated variables as carriers
- Fixes: Constant variables (M, args) no longer misidentified as carriers
- Enables: Condition-only variables without carrier slot overhead
### 4. LoopUpdateAnalyzer Enhancement
- Recursively scan if-else branches for carrier updates
- Correctly detect updates in normalized code
## Test Results
✅ Pattern 3 (If PHI): sum=9
✅ Pattern 4 (Then-continue): 25 (1+3+5+7+9)
✅ Pattern 4 (Else-continue): New test cases added
✅ No SSA-undef errors
✅ Carrier filtering works correctly
## Files Changed
- New: continue_branch_normalizer.rs (comprehensive implementation + tests)
- Modified: pattern4_with_continue.rs (integrated normalizer)
- Modified: loop_update_analyzer.rs (recursive branch scanning)
- Modified: lowering/mod.rs (module export)
- Added: 3 test cases (then/else continue patterns)
## Impact
This enables JsonParserBox / trim and other continue-heavy loops to work with
JoinIR Phase 4 lowering, paving the way for Phase 166/170 integration.
🤖 Generated with Claude Code
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
26
apps/tests/loop_continue_else_pattern.hako
Normal file
26
apps/tests/loop_continue_else_pattern.hako
Normal file
@ -0,0 +1,26 @@
|
||||
// Phase 33-19: Pattern B test - else-side continue
|
||||
// Tests: if (cond) { body } else { continue }
|
||||
// This should be normalized to: if (!cond) { continue } else { body }
|
||||
|
||||
static box Main {
|
||||
main(args) {
|
||||
local i = 0
|
||||
local sum = 0
|
||||
local M = 5
|
||||
|
||||
loop(i < M) {
|
||||
i = i + 1
|
||||
// Pattern B: Process when i != M, continue when i == M
|
||||
if (i != M) {
|
||||
sum = sum + i
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Expected: sum = 1 + 2 + 3 + 4 = 10
|
||||
// (i=5 is skipped by continue)
|
||||
print(sum)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
25
apps/tests/loop_continue_else_simple.hako
Normal file
25
apps/tests/loop_continue_else_simple.hako
Normal file
@ -0,0 +1,25 @@
|
||||
// Phase 33-19: Pattern B test - else-side continue (simplified)
|
||||
// Tests: if (cond) { body } else { continue }
|
||||
// This version uses a hardcoded constant instead of variable M
|
||||
|
||||
static box Main {
|
||||
main(args) {
|
||||
local i = 0
|
||||
local sum = 0
|
||||
|
||||
loop(i < 5) {
|
||||
i = i + 1
|
||||
// Pattern B: Process when i != 5, continue when i == 5
|
||||
if (i != 5) {
|
||||
sum = sum + i
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Expected: sum = 1 + 2 + 3 + 4 = 10
|
||||
// (i=5 is skipped by continue)
|
||||
print(sum)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
23
apps/tests/loop_continue_then_simple.hako
Normal file
23
apps/tests/loop_continue_then_simple.hako
Normal file
@ -0,0 +1,23 @@
|
||||
// Pattern A test - then-side continue (for comparison)
|
||||
// Tests: if (cond) { continue } else { body }
|
||||
|
||||
static box Main {
|
||||
main(args) {
|
||||
local i = 0
|
||||
local sum = 0
|
||||
|
||||
loop(i < 5) {
|
||||
i = i + 1
|
||||
// Pattern A: Skip when i == 5, process otherwise
|
||||
if (i == 5) {
|
||||
continue
|
||||
} else {
|
||||
sum = sum + i
|
||||
}
|
||||
}
|
||||
|
||||
// Expected: sum = 1 + 2 + 3 + 4 = 10
|
||||
print(sum)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@ -124,6 +124,7 @@ impl MirBuilder {
|
||||
debug: bool,
|
||||
) -> Result<Option<ValueId>, String> {
|
||||
use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierVar};
|
||||
use crate::mir::join_ir::lowering::continue_branch_normalizer::ContinueBranchNormalizer;
|
||||
use crate::mir::join_ir::lowering::loop_update_analyzer::LoopUpdateAnalyzer;
|
||||
use crate::mir::join_ir::lowering::loop_with_continue_minimal::lower_loop_with_continue_minimal;
|
||||
use crate::mir::join_ir_vm_bridge::convert_join_module_to_mir_with_meta;
|
||||
@ -133,6 +134,12 @@ impl MirBuilder {
|
||||
// Phase 195: Use unified trace
|
||||
trace::trace().debug("pattern4", "Calling Pattern 4 minimal lowerer");
|
||||
|
||||
// Phase 33-19: Normalize else-continue patterns to then-continue
|
||||
// This transforms: if (cond) { body } else { continue }
|
||||
// into: if (!cond) { continue } else { body }
|
||||
let normalized_body = ContinueBranchNormalizer::normalize_loop_body(_body);
|
||||
let body_to_analyze = &normalized_body;
|
||||
|
||||
// Extract loop variables from condition (i and sum)
|
||||
let loop_var_name = self.extract_loop_variable_from_condition(condition)?;
|
||||
let loop_var_id = self
|
||||
@ -146,18 +153,30 @@ impl MirBuilder {
|
||||
)
|
||||
})?;
|
||||
|
||||
// Phase 196: Build CarrierInfo from variable_map
|
||||
// Collect all non-loop-var variables as potential carriers
|
||||
let mut carriers = Vec::new();
|
||||
// Phase 197: Analyze carrier update expressions FIRST to identify actual carriers
|
||||
// Phase 33-19: Use normalized_body for analysis (after else-continue transformation)
|
||||
// Build temporary carrier list for initial analysis
|
||||
let mut temp_carriers = Vec::new();
|
||||
for (var_name, &var_id) in &self.variable_map {
|
||||
if var_name != &loop_var_name {
|
||||
carriers.push(CarrierVar {
|
||||
temp_carriers.push(CarrierVar {
|
||||
name: var_name.clone(),
|
||||
host_id: var_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let carrier_updates = LoopUpdateAnalyzer::analyze_carrier_updates(body_to_analyze, &temp_carriers);
|
||||
|
||||
// Phase 33-19: Build CarrierInfo with ONLY updated variables as carriers
|
||||
// This prevents constant variables (like M, args) from being treated as carriers
|
||||
let mut carriers = Vec::new();
|
||||
for temp_carrier in &temp_carriers {
|
||||
if carrier_updates.contains_key(&temp_carrier.name) {
|
||||
carriers.push(temp_carrier.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let carrier_info = CarrierInfo {
|
||||
loop_var_name: loop_var_name.clone(),
|
||||
loop_var_id,
|
||||
@ -173,9 +192,6 @@ impl MirBuilder {
|
||||
)
|
||||
);
|
||||
|
||||
// Phase 197: Analyze carrier update expressions from loop body
|
||||
let carrier_updates = LoopUpdateAnalyzer::analyze_carrier_updates(_body, &carrier_info.carriers);
|
||||
|
||||
trace::trace().debug(
|
||||
"pattern4",
|
||||
&format!("Analyzed {} carrier update expressions", carrier_updates.len())
|
||||
|
||||
342
src/mir/join_ir/lowering/continue_branch_normalizer.rs
Normal file
342
src/mir/join_ir/lowering/continue_branch_normalizer.rs
Normal file
@ -0,0 +1,342 @@
|
||||
//! Phase 33-19: Continue Branch Normalizer
|
||||
//!
|
||||
//! Normalize if/else patterns with continue to simplify Pattern 4 lowering
|
||||
//!
|
||||
//! Pattern: if (cond) { body } else { continue }
|
||||
//! Transforms to: if (!cond) { continue } else { body }
|
||||
//!
|
||||
//! This allows Pattern 4 to handle all continue patterns uniformly by
|
||||
//! ensuring continue is always in the then branch.
|
||||
|
||||
use crate::ast::{ASTNode, UnaryOperator};
|
||||
|
||||
pub struct ContinueBranchNormalizer;
|
||||
|
||||
impl ContinueBranchNormalizer {
|
||||
/// Normalize if node for continue handling
|
||||
///
|
||||
/// Returns transformed AST node if pattern matches, otherwise returns clone of input.
|
||||
///
|
||||
/// # Pattern Detection
|
||||
///
|
||||
/// Matches: `if (cond) { body } else { continue }`
|
||||
/// Where else_body is a single Continue statement (or block containing only Continue).
|
||||
///
|
||||
/// # Transformation
|
||||
///
|
||||
/// - Creates negated condition: `!cond`
|
||||
/// - Swaps branches: then becomes else, continue becomes then
|
||||
/// - Result: `if (!cond) { continue } else { body }`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```text
|
||||
/// Input: if (i != M) { sum = sum + i } else { continue }
|
||||
/// Output: if (!(i != M)) { continue } else { sum = sum + i }
|
||||
/// ```
|
||||
pub fn normalize_if_for_continue(if_node: &ASTNode) -> ASTNode {
|
||||
// Check if this is an If node with else branch containing only continue
|
||||
match if_node {
|
||||
ASTNode::If {
|
||||
condition,
|
||||
then_body,
|
||||
else_body,
|
||||
span,
|
||||
} => {
|
||||
// Check if else_body is a single continue statement
|
||||
if let Some(else_stmts) = else_body {
|
||||
if Self::is_continue_only(else_stmts) {
|
||||
// Pattern matched: if (cond) { ... } else { continue }
|
||||
// Transform to: if (!cond) { continue } else { ... }
|
||||
|
||||
eprintln!("[continue_normalizer] Pattern matched: else-continue detected");
|
||||
eprintln!("[continue_normalizer] Original condition: {:?}", condition);
|
||||
|
||||
// Create negated condition: !cond
|
||||
let negated_cond = Box::new(ASTNode::UnaryOp {
|
||||
operator: UnaryOperator::Not,
|
||||
operand: condition.clone(),
|
||||
span: span.clone(),
|
||||
});
|
||||
|
||||
eprintln!("[continue_normalizer] Negated condition created");
|
||||
|
||||
// Swap branches: continue becomes then, body becomes else
|
||||
let result = ASTNode::If {
|
||||
condition: negated_cond,
|
||||
then_body: else_stmts.clone(), // Continue
|
||||
else_body: Some(then_body.clone()), // Original body
|
||||
span: span.clone(),
|
||||
};
|
||||
|
||||
eprintln!("[continue_normalizer] Transformation complete");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern not matched: return original if unchanged
|
||||
if_node.clone()
|
||||
}
|
||||
_ => {
|
||||
// Not an if node: return as-is
|
||||
if_node.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if statements contain only a continue statement
|
||||
///
|
||||
/// Returns true if:
|
||||
/// - Single statement: Continue
|
||||
/// - Or statements that effectively contain only continue
|
||||
fn is_continue_only(stmts: &[ASTNode]) -> bool {
|
||||
if stmts.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for single continue
|
||||
if stmts.len() == 1 {
|
||||
matches!(stmts[0], ASTNode::Continue { .. })
|
||||
} else {
|
||||
// For now, only handle single continue statement
|
||||
// Could be extended to handle blocks with continue as last statement
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a loop body contains an if-else pattern with else-continue
|
||||
///
|
||||
/// This is used by the pattern router to detect Pattern B cases.
|
||||
pub fn has_else_continue_pattern(body: &[ASTNode]) -> bool {
|
||||
for node in body {
|
||||
if let ASTNode::If { else_body, .. } = node {
|
||||
if let Some(else_stmts) = else_body {
|
||||
if Self::is_continue_only(else_stmts) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Normalize all if-statements in a loop body for continue handling
|
||||
///
|
||||
/// This is called by the loop lowerer before pattern matching.
|
||||
/// It transforms all else-continue patterns to then-continue patterns.
|
||||
pub fn normalize_loop_body(body: &[ASTNode]) -> Vec<ASTNode> {
|
||||
body.iter()
|
||||
.map(|node| Self::normalize_if_for_continue(node))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::{BinaryOperator, LiteralValue, Span};
|
||||
|
||||
#[test]
|
||||
fn test_normalize_else_continue() {
|
||||
// if (i != M) { sum = sum + i } else { continue }
|
||||
// → if (!(i != M)) { continue } else { sum = sum + i }
|
||||
|
||||
let span = Span::default();
|
||||
let condition = Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::NotEqual,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "i".to_string(),
|
||||
span: span.clone(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Variable {
|
||||
name: "M".to_string(),
|
||||
span: span.clone(),
|
||||
}),
|
||||
span: span.clone(),
|
||||
});
|
||||
|
||||
let then_body = vec![ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: "sum".to_string(),
|
||||
span: span.clone(),
|
||||
}),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "sum".to_string(),
|
||||
span: span.clone(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Variable {
|
||||
name: "i".to_string(),
|
||||
span: span.clone(),
|
||||
}),
|
||||
span: span.clone(),
|
||||
}),
|
||||
span: span.clone(),
|
||||
}];
|
||||
|
||||
let else_body = Some(vec![ASTNode::Continue { span: span.clone() }]);
|
||||
|
||||
let input = ASTNode::If {
|
||||
condition,
|
||||
then_body,
|
||||
else_body,
|
||||
span: span.clone(),
|
||||
};
|
||||
|
||||
let result = ContinueBranchNormalizer::normalize_if_for_continue(&input);
|
||||
|
||||
// Verify result is an If node
|
||||
if let ASTNode::If {
|
||||
condition: result_cond,
|
||||
then_body: result_then,
|
||||
else_body: result_else,
|
||||
..
|
||||
} = result
|
||||
{
|
||||
// Condition should be negated (UnaryOp Not)
|
||||
assert!(matches!(
|
||||
*result_cond,
|
||||
ASTNode::UnaryOp {
|
||||
operator: UnaryOperator::Not,
|
||||
..
|
||||
}
|
||||
));
|
||||
|
||||
// Then body should be continue
|
||||
assert_eq!(result_then.len(), 1);
|
||||
assert!(matches!(result_then[0], ASTNode::Continue { .. }));
|
||||
|
||||
// Else body should be original then body
|
||||
assert!(result_else.is_some());
|
||||
let else_stmts = result_else.unwrap();
|
||||
assert_eq!(else_stmts.len(), 1);
|
||||
assert!(matches!(else_stmts[0], ASTNode::Assignment { .. }));
|
||||
} else {
|
||||
panic!("Expected If node");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_op_then_continue() {
|
||||
// if (i != M) { continue } else { sum = sum + i }
|
||||
// Should NOT transform (continue is in then branch)
|
||||
|
||||
let span = Span::default();
|
||||
let condition = Box::new(ASTNode::Variable {
|
||||
name: "cond".to_string(),
|
||||
span: span.clone(),
|
||||
});
|
||||
|
||||
let then_body = vec![ASTNode::Continue { span: span.clone() }];
|
||||
let else_body = Some(vec![ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: span.clone(),
|
||||
}),
|
||||
value: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(1),
|
||||
span: span.clone(),
|
||||
}),
|
||||
span: span.clone(),
|
||||
}]);
|
||||
|
||||
let input = ASTNode::If {
|
||||
condition: condition.clone(),
|
||||
then_body: then_body.clone(),
|
||||
else_body: else_body.clone(),
|
||||
span: span.clone(),
|
||||
};
|
||||
|
||||
let result = ContinueBranchNormalizer::normalize_if_for_continue(&input);
|
||||
|
||||
// Should return unchanged (continue already in then)
|
||||
// We can't use PartialEq on ASTNode due to Span, so check structure
|
||||
if let ASTNode::If {
|
||||
condition: result_cond,
|
||||
then_body: result_then,
|
||||
..
|
||||
} = result
|
||||
{
|
||||
// Condition should NOT be negated
|
||||
assert!(matches!(*result_cond, ASTNode::Variable { .. }));
|
||||
|
||||
// Then should still be continue
|
||||
assert_eq!(result_then.len(), 1);
|
||||
assert!(matches!(result_then[0], ASTNode::Continue { .. }));
|
||||
} else {
|
||||
panic!("Expected If node");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_op_no_else() {
|
||||
// if (i != M) { sum = sum + i }
|
||||
// Should NOT transform (no else branch)
|
||||
|
||||
let span = Span::default();
|
||||
let input = ASTNode::If {
|
||||
condition: Box::new(ASTNode::Variable {
|
||||
name: "cond".to_string(),
|
||||
span: span.clone(),
|
||||
}),
|
||||
then_body: vec![ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: span.clone(),
|
||||
}),
|
||||
value: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(1),
|
||||
span: span.clone(),
|
||||
}),
|
||||
span: span.clone(),
|
||||
}],
|
||||
else_body: None,
|
||||
span: span.clone(),
|
||||
};
|
||||
|
||||
let result = ContinueBranchNormalizer::normalize_if_for_continue(&input);
|
||||
|
||||
// Should return unchanged
|
||||
if let ASTNode::If { else_body, .. } = result {
|
||||
assert!(else_body.is_none());
|
||||
} else {
|
||||
panic!("Expected If node");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_else_continue_pattern() {
|
||||
let span = Span::default();
|
||||
|
||||
// Body with else-continue pattern
|
||||
let body_with = vec![ASTNode::If {
|
||||
condition: Box::new(ASTNode::Variable {
|
||||
name: "cond".to_string(),
|
||||
span: span.clone(),
|
||||
}),
|
||||
then_body: vec![],
|
||||
else_body: Some(vec![ASTNode::Continue { span: span.clone() }]),
|
||||
span: span.clone(),
|
||||
}];
|
||||
|
||||
assert!(ContinueBranchNormalizer::has_else_continue_pattern(
|
||||
&body_with
|
||||
));
|
||||
|
||||
// Body without else-continue pattern
|
||||
let body_without = vec![ASTNode::If {
|
||||
condition: Box::new(ASTNode::Variable {
|
||||
name: "cond".to_string(),
|
||||
span: span.clone(),
|
||||
}),
|
||||
then_body: vec![ASTNode::Continue { span: span.clone() }],
|
||||
else_body: None,
|
||||
span: span.clone(),
|
||||
}];
|
||||
|
||||
assert!(!ContinueBranchNormalizer::has_else_continue_pattern(
|
||||
&body_without
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -68,9 +68,24 @@ impl LoopUpdateAnalyzer {
|
||||
// Extract carrier names for quick lookup
|
||||
let carrier_names: Vec<&str> = carriers.iter().map(|c| c.name.as_str()).collect();
|
||||
|
||||
// Scan all statements in the loop body
|
||||
for node in body {
|
||||
if let ASTNode::Assignment { target, value, .. } = node {
|
||||
// Recursively scan all statements in the loop body
|
||||
Self::scan_nodes(body, &carrier_names, &mut updates);
|
||||
|
||||
updates
|
||||
}
|
||||
|
||||
/// Recursively scan AST nodes for carrier updates
|
||||
///
|
||||
/// Phase 33-19: Extended to scan into if-else branches to handle
|
||||
/// Pattern B (else-continue) after normalization.
|
||||
fn scan_nodes(
|
||||
nodes: &[ASTNode],
|
||||
carrier_names: &[&str],
|
||||
updates: &mut HashMap<String, UpdateExpr>,
|
||||
) {
|
||||
for node in nodes {
|
||||
match node {
|
||||
ASTNode::Assignment { target, value, .. } => {
|
||||
// Check if this is a carrier update (e.g., sum = sum + i)
|
||||
if let Some(target_name) = Self::extract_variable_name(target) {
|
||||
if carrier_names.contains(&target_name.as_str()) {
|
||||
@ -81,9 +96,21 @@ impl LoopUpdateAnalyzer {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Phase 33-19: Recursively scan if-else branches
|
||||
ASTNode::If {
|
||||
then_body,
|
||||
else_body,
|
||||
..
|
||||
} => {
|
||||
Self::scan_nodes(then_body, carrier_names, updates);
|
||||
if let Some(else_stmts) = else_body {
|
||||
Self::scan_nodes(else_stmts, carrier_names, updates);
|
||||
}
|
||||
}
|
||||
// Add more recursive cases as needed (loops, etc.)
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
updates
|
||||
}
|
||||
|
||||
/// Extract variable name from AST node (for assignment target)
|
||||
|
||||
@ -23,6 +23,7 @@ pub mod bool_expr_lowerer; // Phase 168: Boolean expression lowering for complex
|
||||
pub mod carrier_info; // Phase 196: Carrier metadata for loop lowering
|
||||
pub mod common;
|
||||
pub mod condition_to_joinir; // Phase 169: JoinIR condition lowering helper
|
||||
pub mod continue_branch_normalizer; // Phase 33-19: Continue branch normalization for Pattern B
|
||||
pub mod loop_update_analyzer; // Phase 197: Update expression analyzer for carrier semantics
|
||||
pub mod loop_update_summary; // Phase 170-C-2: Update pattern summary for shape detection
|
||||
pub mod exit_args_resolver;
|
||||
|
||||
Reference in New Issue
Block a user