feat(joinir): Phase 170-B else-break pattern detection and negation

Expand Pattern 2 (loop with break) to handle else-break patterns:
- If condition is in else clause: `if (cond) { ... } else { break }`
- Extract and negate condition for proper break detection
- Added has_break_in_else_clause() helper in ast_feature_extractor
- Pattern2 now handles both then-break and else-break structures

Implementation:
- ast_feature_extractor: Added else-break pattern detection
- pattern2_with_break: Detect else-break case and wrap condition in UnaryOp Not
- Enables support for patterns like trim() with inverted break logic

Known limitation:
- Pattern 2 requires break conditions to only depend on:
  * Loop parameter (e.g., 'start' in loop(start < end))
  * Condition-only variables from outer scope (e.g., 'end')
- Does NOT support break conditions using loop-body variables (e.g., 'ch')
- Future Pattern 5+ will handle more complex break conditions

🤖 Generated with Claude Code
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-07 21:09:01 +09:00
parent 4e32a803a7
commit 7b83d214ae
2 changed files with 75 additions and 6 deletions

View File

@ -203,9 +203,39 @@ fn has_break_node(node: &ASTNode) -> bool {
}
}
/// Phase 170-B: Check if break is in else clause
///
/// Helper function to determine if a break statement is in the else clause
/// of an if-else statement, as opposed to the then clause.
///
/// # Arguments
///
/// * `body` - Loop body statements to search
///
/// # Returns
///
/// `true` if an `if ... else { break }` pattern is found
pub fn has_break_in_else_clause(body: &[ASTNode]) -> bool {
for stmt in body {
if let ASTNode::If {
else_body: Some(else_body),
..
} = stmt
{
if else_body.iter().any(|node| matches!(node, ASTNode::Break { .. })) {
return true;
}
}
}
false
}
/// Phase 170-B: Extract break condition from loop body
///
/// Searches for the first `if <condition> { break }` pattern in the loop body.
/// Searches for the first break pattern in an if statement:
/// - `if <condition> { break }` - returns <condition>
/// - `if <condition> { ... } else { break }` - returns `!<condition>` (negated)
///
/// This is used to delegate break condition lowering to `condition_to_joinir`.
///
/// # Arguments
@ -214,29 +244,50 @@ fn has_break_node(node: &ASTNode) -> bool {
///
/// # Returns
///
/// `Some(&ASTNode)` - The condition AST node from `if <condition> { break }`
/// `Some(&ASTNode)` - The condition AST node (negated for else-break pattern)
/// `None` - No break statement found or break is not in a simple if statement
///
/// # Example
/// # Examples
///
/// ```nyash
/// // Pattern 1: if condition { break }
/// loop(i < 3) {
/// if i >= 2 { break } // <- Returns the "i >= 2" condition
/// i = i + 1
/// }
///
/// // Pattern 2: if condition { ... } else { break }
/// loop(start < end) {
/// if ch == " " { start = start + 1 } else { break }
/// // <- Returns the "!(ch == " ")" condition (negated)
/// }
/// ```
pub fn extract_break_condition(body: &[ASTNode]) -> Option<&ASTNode> {
for stmt in body {
if let ASTNode::If {
condition,
then_body,
else_body,
..
} = stmt
{
// Check if the then_body contains a break statement
// Pattern 1: Check if the then_body contains a break statement
if then_body.iter().any(|node| matches!(node, ASTNode::Break { .. })) {
return Some(condition.as_ref());
}
// Pattern 2: Check if the else_body contains a break statement
// In this case, we need to negate the condition
// However, we can't easily negate here without modifying the AST
// Instead, we'll return the condition and document that the caller
// must handle the negation (or we could return a wrapped node)
if let Some(else_body) = else_body {
if else_body.iter().any(|node| matches!(node, ASTNode::Break { .. })) {
// For else-break pattern, return the condition
// The caller (pattern2_with_break) must negate it
return Some(condition.as_ref());
}
}
}
}
None

View File

@ -138,12 +138,30 @@ impl MirBuilder {
// Phase 170-B: Extract break condition from loop body
use super::ast_feature_extractor;
let break_condition = ast_feature_extractor::extract_break_condition(_body)
let break_condition_raw = ast_feature_extractor::extract_break_condition(_body)
.ok_or_else(|| "[cf_loop/pattern2] Failed to extract break condition from loop body".to_string())?;
// Phase 170-B: Check if break is in else clause (requires negation)
let break_in_else = ast_feature_extractor::has_break_in_else_clause(_body);
// Wrap condition in UnaryOp Not if break is in else clause
use crate::ast::UnaryOperator;
let break_condition_node = if break_in_else {
// Extract span from the raw condition node (use unknown as default)
let span = crate::ast::Span::unknown();
ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand: Box::new(break_condition_raw.clone()),
span,
}
} else {
break_condition_raw.clone()
};
// Phase 169 / Phase 171-fix / Phase 172-3 / Phase 170-B: Call Pattern 2 lowerer with break_condition
// Phase 33-14: Now returns (JoinModule, JoinFragmentMeta) for expr_result + carrier separation
let (join_module, fragment_meta) = match lower_loop_with_break_minimal(scope, condition, break_condition, &env, &loop_var_name) {
let (join_module, fragment_meta) = match lower_loop_with_break_minimal(scope, condition, &break_condition_node, &env, &loop_var_name) {
Ok((module, meta)) => (module, meta),
Err(e) => {
// Phase 195: Use unified trace