feat(joinir): Phase 223-3 - LoopBodyCondPromoter implementation

Implements LoopBodyLocal condition promotion for Pattern4/continue patterns.
Previously, loops with LoopBodyLocal in conditions would Fail-Fast.
Now, safe Trim/skip_whitespace patterns (Category A-3) are promoted and lowering continues.

## Implementation

- **LoopBodyCondPromoter Box** (loop_body_cond_promoter.rs)
  - extract_continue_condition(): Extract if-condition from continue branches
  - try_promote_for_condition(): Thin wrapper delegating to LoopBodyCarrierPromoter
  - ConditionPromotionRequest/Result API for Pattern2/4 integration

- **Pattern4 Integration** (pattern4_with_continue.rs)
  - Analyze both header condition + continue condition
  - On promotion success: merge carrier_info → continue lowering
  - On promotion failure: Fail-Fast with clear error message

- **Unit Tests** (5 tests, all PASS)
  - test_cond_promoter_skip_whitespace_pattern
  - test_cond_promoter_break_condition
  - test_cond_promoter_non_substring_pattern
  - test_cond_promoter_no_scope_shape
  - test_cond_promoter_no_body_locals

- **E2E Test** (phase223_p4_skip_whitespace_min.hako)
  - Category A-3 pattern: local ch = s.substring(...); if ch == " " { continue }
  - Verifies promotion succeeds and Pattern4 lowering proceeds

## Documentation

- joinir-architecture-overview.md: Updated LoopBodyCondPromoter section (Phase 223-3 完了)
- CURRENT_TASK.md: Added Phase 223-3 completion summary
- PHASE_223_SUMMARY.md: Phase overview and status tracking
- phase223-loopbodylocal-condition-*.md: Design docs and inventory

## Achievement

- **Before**: LoopBodyLocal in condition → Fail-Fast
- **Now**: Trim/skip_whitespace patterns promoted → Pattern4 lowering continues
- **Remaining**: Phase 172+ JoinIR Trim lowering for complete RC correctness

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-10 15:00:20 +09:00
parent 9dbf053781
commit 89a769198a
9 changed files with 1962 additions and 40 deletions

View File

@ -0,0 +1,455 @@
//! Phase 223-3: LoopBodyCondPromoter Box
//!
//! Handles promotion of LoopBodyLocal variables used in loop conditions
//! to bool carriers. Supports Pattern2 (break), Pattern4 (continue),
//! and future patterns.
//!
//! ## Responsibilities
//!
//! - Detect safe promotion patterns (Category A-3 from phase223-loopbodylocal-condition-inventory.md)
//! - Coordinate with LoopBodyCarrierPromoter for actual promotion logic
//! - Provide uniform API for Pattern2/Pattern4 integration
//!
//! ## Design Principle
//!
//! This is a **thin coordinator** that reuses existing boxes:
//! - LoopBodyCarrierPromoter: Promotion logic (Trim pattern detection)
//! - TrimLoopHelper: Pattern-specific metadata
//! - ConditionEnvBuilder: Binding generation
//!
//! ## P0 Requirements (Category A-3)
//!
//! - Single LoopBodyLocal variable (e.g., `ch`)
//! - Definition: `local ch = s.substring(...)` or similar
//! - Condition: Simple equality chain (e.g., `ch == " " || ch == "\t"`)
//! - Pattern: Identical to existing Trim pattern
use crate::ast::ASTNode;
use crate::mir::join_ir::lowering::carrier_info::CarrierInfo;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use crate::mir::loop_pattern_detection::loop_body_carrier_promoter::{
LoopBodyCarrierPromoter, PromotionRequest, PromotionResult,
};
use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScope;
/// Promotion request for condition variables
///
/// Unified API for Pattern2 (break) and Pattern4 (continue)
pub struct ConditionPromotionRequest<'a> {
/// Loop parameter name (e.g., "i")
pub loop_param_name: &'a str,
/// Condition scope analysis result
pub cond_scope: &'a LoopConditionScope,
/// Loop structure metadata (crate-internal)
pub(crate) scope_shape: Option<&'a LoopScopeShape>,
/// Break condition AST (Pattern2: Some, Pattern4: None)
pub break_cond: Option<&'a ASTNode>,
/// Continue condition AST (Pattern4: Some, Pattern2: None)
pub continue_cond: Option<&'a ASTNode>,
/// Loop body statements
pub loop_body: &'a [ASTNode],
}
/// Promotion result
pub enum ConditionPromotionResult {
/// Promotion successful
Promoted {
/// Carrier metadata (from TrimLoopHelper)
carrier_info: CarrierInfo,
/// Variable name that was promoted (e.g., "ch")
promoted_var: String,
/// Promoted carrier name (e.g., "is_whitespace")
carrier_name: String,
},
/// Cannot promote (Fail-Fast)
CannotPromote {
/// Human-readable reason
reason: String,
/// List of problematic LoopBodyLocal variables
vars: Vec<String>,
},
}
/// Phase 223-3: LoopBodyCondPromoter Box
///
/// Coordinates LoopBodyLocal condition promotion for Pattern2/Pattern4.
pub struct LoopBodyCondPromoter;
impl LoopBodyCondPromoter {
/// Extract continue condition from loop body
///
/// Finds the first if statement with continue in then-branch and returns its condition.
/// This is used for Pattern4 skip_whitespace pattern detection.
///
/// # Pattern
///
/// ```nyash
/// loop(i < n) {
/// local ch = s.substring(...)
/// if ch == " " || ch == "\t" { // ← This condition is returned
/// i = i + 1
/// continue
/// }
/// break
/// }
/// ```
///
/// # Returns
///
/// The condition AST if found, None otherwise
pub fn extract_continue_condition(body: &[ASTNode]) -> Option<&ASTNode> {
for stmt in body {
if let ASTNode::If {
condition,
then_body,
..
} = stmt
{
// Check if then_body contains continue
if Self::contains_continue(then_body) {
return Some(condition.as_ref());
}
}
}
None
}
/// Check if statements contain a continue statement
fn contains_continue(stmts: &[ASTNode]) -> bool {
for stmt in stmts {
match stmt {
ASTNode::Continue { .. } => return true,
ASTNode::If {
then_body,
else_body,
..
} => {
if Self::contains_continue(then_body) {
return true;
}
if let Some(else_stmts) = else_body {
if Self::contains_continue(else_stmts) {
return true;
}
}
}
_ => {}
}
}
false
}
}
impl LoopBodyCondPromoter {
/// Try to promote LoopBodyLocal variables in conditions
///
/// ## P0 Requirements (Category A-3)
///
/// - Single LoopBodyLocal variable (e.g., `ch`)
/// - Definition: `local ch = s.substring(...)` or similar
/// - Condition: Simple equality chain (e.g., `ch == " " || ch == "\t"`)
/// - Pattern: Identical to existing Trim pattern
///
/// ## Algorithm (Delegated to LoopBodyCarrierPromoter)
///
/// 1. Extract LoopBodyLocal names from cond_scope
/// 2. Build PromotionRequest for LoopBodyCarrierPromoter
/// 3. Call LoopBodyCarrierPromoter::try_promote()
/// 4. Convert PromotionResult to ConditionPromotionResult
/// 5. Return result (Promoted or CannotPromote)
///
/// ## Differences from TrimLoopLowerer
///
/// - TrimLoopLowerer: Full lowering pipeline (detection + code generation)
/// - LoopBodyCondPromoter: Detection + metadata only (no code generation)
pub fn try_promote_for_condition(req: ConditionPromotionRequest) -> ConditionPromotionResult {
// P0 constraint: Need LoopScopeShape for LoopBodyCarrierPromoter
let scope_shape = match req.scope_shape {
Some(s) => s,
None => {
return ConditionPromotionResult::CannotPromote {
reason: "No LoopScopeShape provided".to_string(),
vars: vec![],
};
}
};
// Determine which condition to use for break_cond in LoopBodyCarrierPromoter
// Pattern2: break_cond
// Pattern4: continue_cond (use as break_cond for Trim pattern detection)
let condition_for_promotion = req.break_cond.or(req.continue_cond);
// Build request for LoopBodyCarrierPromoter
let promotion_request = PromotionRequest {
scope: scope_shape,
cond_scope: req.cond_scope,
break_cond: condition_for_promotion,
loop_body: req.loop_body,
};
// Delegate to existing LoopBodyCarrierPromoter
match LoopBodyCarrierPromoter::try_promote(&promotion_request) {
PromotionResult::Promoted { trim_info } => {
eprintln!(
"[cond_promoter] LoopBodyLocal '{}' promoted to carrier '{}'",
trim_info.var_name, trim_info.carrier_name
);
// Convert TrimPatternInfo to CarrierInfo
let carrier_info = trim_info.to_carrier_info();
ConditionPromotionResult::Promoted {
carrier_info,
promoted_var: trim_info.var_name,
carrier_name: trim_info.carrier_name,
}
}
PromotionResult::CannotPromote { reason, vars } => {
eprintln!(
"[cond_promoter] Cannot promote LoopBodyLocal variables {:?}: {}",
vars, reason
);
ConditionPromotionResult::CannotPromote { reason, vars }
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{BinaryOperator, LiteralValue, Span};
use crate::mir::loop_pattern_detection::loop_condition_scope::{
CondVarScope, LoopConditionScope,
};
use crate::mir::BasicBlockId;
use std::collections::{BTreeMap, BTreeSet};
fn minimal_scope() -> LoopScopeShape {
LoopScopeShape {
header: BasicBlockId(0),
body: BasicBlockId(1),
latch: BasicBlockId(2),
exit: BasicBlockId(3),
pinned: BTreeSet::new(),
carriers: BTreeSet::new(),
body_locals: BTreeSet::new(),
exit_live: BTreeSet::new(),
progress_carrier: None,
variable_definitions: BTreeMap::new(),
}
}
fn cond_scope_with_body_local(var_name: &str) -> LoopConditionScope {
let mut scope = LoopConditionScope::new();
scope.add_var(var_name.to_string(), CondVarScope::LoopBodyLocal);
scope
}
// Helper: Create a Variable node
fn var_node(name: &str) -> ASTNode {
ASTNode::Variable {
name: name.to_string(),
span: Span::unknown(),
}
}
// Helper: Create a String literal node
fn str_literal(s: &str) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::String(s.to_string()),
span: Span::unknown(),
}
}
// Helper: Create an equality comparison (var == literal)
fn eq_cmp(var_name: &str, literal: &str) -> ASTNode {
ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(var_node(var_name)),
right: Box::new(str_literal(literal)),
span: Span::unknown(),
}
}
// Helper: Create an Or expression
fn or_expr(left: ASTNode, right: ASTNode) -> ASTNode {
ASTNode::BinaryOp {
operator: BinaryOperator::Or,
left: Box::new(left),
right: Box::new(right),
span: Span::unknown(),
}
}
// Helper: Create a MethodCall node
fn method_call(object: &str, method: &str) -> ASTNode {
ASTNode::MethodCall {
object: Box::new(var_node(object)),
method: method.to_string(),
arguments: vec![],
span: Span::unknown(),
}
}
// Helper: Create an Assignment node
fn assignment(target: &str, value: ASTNode) -> ASTNode {
ASTNode::Assignment {
target: Box::new(var_node(target)),
value: Box::new(value),
span: Span::unknown(),
}
}
#[test]
fn test_cond_promoter_no_scope_shape() {
let cond_scope = LoopConditionScope::new();
let req = ConditionPromotionRequest {
loop_param_name: "i",
cond_scope: &cond_scope,
scope_shape: None, // No scope shape
break_cond: None,
continue_cond: None,
loop_body: &[],
};
match LoopBodyCondPromoter::try_promote_for_condition(req) {
ConditionPromotionResult::CannotPromote { reason, .. } => {
assert!(reason.contains("No LoopScopeShape"));
}
_ => panic!("Expected CannotPromote when no scope shape provided"),
}
}
#[test]
fn test_cond_promoter_no_body_locals() {
let scope_shape = minimal_scope();
let cond_scope = LoopConditionScope::new(); // Empty, no LoopBodyLocal
let req = ConditionPromotionRequest {
loop_param_name: "i",
cond_scope: &cond_scope,
scope_shape: Some(&scope_shape),
break_cond: None,
continue_cond: None,
loop_body: &[],
};
match LoopBodyCondPromoter::try_promote_for_condition(req) {
ConditionPromotionResult::CannotPromote { vars, .. } => {
assert!(vars.is_empty());
}
_ => panic!("Expected CannotPromote when no LoopBodyLocal variables"),
}
}
#[test]
fn test_cond_promoter_skip_whitespace_pattern() {
// Full Trim/skip_whitespace pattern test (Category A-3):
// - LoopBodyLocal: ch
// - Definition: ch = s.substring(...)
// - Continue condition: ch == " " || ch == "\t"
let scope_shape = minimal_scope();
let cond_scope = cond_scope_with_body_local("ch");
let loop_body = vec![assignment("ch", method_call("s", "substring"))];
let continue_cond = or_expr(eq_cmp("ch", " "), eq_cmp("ch", "\t"));
let req = ConditionPromotionRequest {
loop_param_name: "i",
cond_scope: &cond_scope,
scope_shape: Some(&scope_shape),
break_cond: None,
continue_cond: Some(&continue_cond),
loop_body: &loop_body,
};
match LoopBodyCondPromoter::try_promote_for_condition(req) {
ConditionPromotionResult::Promoted {
promoted_var,
carrier_name,
carrier_info,
} => {
assert_eq!(promoted_var, "ch");
assert_eq!(carrier_name, "is_ch_match");
// CarrierInfo should have trim_helper attached
assert!(carrier_info.trim_helper.is_some());
}
ConditionPromotionResult::CannotPromote { reason, .. } => {
panic!("Expected Promoted, got CannotPromote: {}", reason);
}
}
}
#[test]
fn test_cond_promoter_break_condition() {
// Pattern2 style: break_cond instead of continue_cond
let scope_shape = minimal_scope();
let cond_scope = cond_scope_with_body_local("ch");
let loop_body = vec![assignment("ch", method_call("s", "substring"))];
let break_cond = or_expr(eq_cmp("ch", " "), eq_cmp("ch", "\t"));
let req = ConditionPromotionRequest {
loop_param_name: "i",
cond_scope: &cond_scope,
scope_shape: Some(&scope_shape),
break_cond: Some(&break_cond),
continue_cond: None,
loop_body: &loop_body,
};
match LoopBodyCondPromoter::try_promote_for_condition(req) {
ConditionPromotionResult::Promoted { promoted_var, .. } => {
assert_eq!(promoted_var, "ch");
}
ConditionPromotionResult::CannotPromote { reason, .. } => {
panic!("Expected Promoted, got CannotPromote: {}", reason);
}
}
}
#[test]
fn test_cond_promoter_non_substring_pattern() {
// Non-substring method call should NOT be promoted
let scope_shape = minimal_scope();
let cond_scope = cond_scope_with_body_local("ch");
// ch = s.length() (not substring)
let loop_body = vec![assignment("ch", method_call("s", "length"))];
let continue_cond = eq_cmp("ch", "5"); // Some comparison
let req = ConditionPromotionRequest {
loop_param_name: "i",
cond_scope: &cond_scope,
scope_shape: Some(&scope_shape),
break_cond: None,
continue_cond: Some(&continue_cond),
loop_body: &loop_body,
};
match LoopBodyCondPromoter::try_promote_for_condition(req) {
ConditionPromotionResult::CannotPromote { vars, reason } => {
// Should fail because it's not a substring pattern
assert!(vars.contains(&"ch".to_string()) || reason.contains("Trim pattern"));
}
_ => panic!("Expected CannotPromote for non-substring pattern"),
}
}
}

View File

@ -778,6 +778,9 @@ pub mod error_messages;
// Phase 171-C: LoopBodyLocal Carrier Promotion
pub mod loop_body_carrier_promoter;
// Phase 223-3: LoopBodyLocal Condition Promotion (for Pattern4)
pub mod loop_body_cond_promoter;
// Phase 171-C-5: Trim Pattern Helper
pub mod trim_loop_helper;
pub use trim_loop_helper::TrimLoopHelper;