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:
455
src/mir/loop_pattern_detection/loop_body_cond_promoter.rs
Normal file
455
src/mir/loop_pattern_detection/loop_body_cond_promoter.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user