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:
nyash-codex
2025-12-07 19:00:12 +09:00
parent 53af63c96c
commit 45aeb11cab
7 changed files with 478 additions and 18 deletions

View 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
}
}

View 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
}
}

View 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
}
}

View File

@ -124,6 +124,7 @@ impl MirBuilder {
debug: bool, debug: bool,
) -> Result<Option<ValueId>, String> { ) -> Result<Option<ValueId>, String> {
use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierVar}; 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_update_analyzer::LoopUpdateAnalyzer;
use crate::mir::join_ir::lowering::loop_with_continue_minimal::lower_loop_with_continue_minimal; 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; 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 // Phase 195: Use unified trace
trace::trace().debug("pattern4", "Calling Pattern 4 minimal lowerer"); 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) // Extract loop variables from condition (i and sum)
let loop_var_name = self.extract_loop_variable_from_condition(condition)?; let loop_var_name = self.extract_loop_variable_from_condition(condition)?;
let loop_var_id = self let loop_var_id = self
@ -146,18 +153,30 @@ impl MirBuilder {
) )
})?; })?;
// Phase 196: Build CarrierInfo from variable_map // Phase 197: Analyze carrier update expressions FIRST to identify actual carriers
// Collect all non-loop-var variables as potential carriers // Phase 33-19: Use normalized_body for analysis (after else-continue transformation)
let mut carriers = Vec::new(); // Build temporary carrier list for initial analysis
let mut temp_carriers = Vec::new();
for (var_name, &var_id) in &self.variable_map { for (var_name, &var_id) in &self.variable_map {
if var_name != &loop_var_name { if var_name != &loop_var_name {
carriers.push(CarrierVar { temp_carriers.push(CarrierVar {
name: var_name.clone(), name: var_name.clone(),
host_id: var_id, 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 { let carrier_info = CarrierInfo {
loop_var_name: loop_var_name.clone(), loop_var_name: loop_var_name.clone(),
loop_var_id, 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( trace::trace().debug(
"pattern4", "pattern4",
&format!("Analyzed {} carrier update expressions", carrier_updates.len()) &format!("Analyzed {} carrier update expressions", carrier_updates.len())

View 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
));
}
}

View File

@ -68,22 +68,49 @@ impl LoopUpdateAnalyzer {
// Extract carrier names for quick lookup // Extract carrier names for quick lookup
let carrier_names: Vec<&str> = carriers.iter().map(|c| c.name.as_str()).collect(); let carrier_names: Vec<&str> = carriers.iter().map(|c| c.name.as_str()).collect();
// Scan all statements in the loop body // Recursively scan all statements in the loop body
for node in body { Self::scan_nodes(body, &carrier_names, &mut updates);
if let ASTNode::Assignment { target, value, .. } = node {
// Check if this is a carrier update (e.g., sum = sum + i) updates
if let Some(target_name) = Self::extract_variable_name(target) { }
if carrier_names.contains(&target_name.as_str()) {
// This is a carrier update, analyze the RHS /// Recursively scan AST nodes for carrier updates
if let Some(update_expr) = Self::analyze_update_value(&target_name, value) { ///
updates.insert(target_name, update_expr); /// 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()) {
// This is a carrier update, analyze the RHS
if let Some(update_expr) = Self::analyze_update_value(&target_name, value) {
updates.insert(target_name, update_expr);
}
} }
} }
} }
// 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) /// Extract variable name from AST node (for assignment target)

View File

@ -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 carrier_info; // Phase 196: Carrier metadata for loop lowering
pub mod common; pub mod common;
pub mod condition_to_joinir; // Phase 169: JoinIR condition lowering helper 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_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 loop_update_summary; // Phase 170-C-2: Update pattern summary for shape detection
pub mod exit_args_resolver; pub mod exit_args_resolver;