phase29ap(p6): extend pattern2 plan subsets for stdlib

This commit is contained in:
2025-12-31 06:06:07 +09:00
parent 6910949583
commit 37172791ea
14 changed files with 838 additions and 552 deletions

View File

@ -3,7 +3,7 @@
## Current Focus
- Phase: `docs/development/current/main/phases/phase-29ap/README.md`
- Next: Phase 29ap P5 (planned; see `docs/development/current/main/phases/phase-29ap/README.md`)
- Next: Phase 29ap P7 (planned; see `docs/development/current/main/phases/phase-29ap/README.md`)
## Gate (SSOT)

View File

@ -5,7 +5,7 @@ Scope: 「次にやる候補」を短く列挙するメモ。入口は `docs/dev
## Active
- Phase 29ap: `docs/development/current/main/phases/phase-29ap/README.md` (Next: P5 planned)
- Phase 29ap: `docs/development/current/main/phases/phase-29ap/README.md` (Next: P7 planned)
- JoinIR regression gate SSOT: `docs/development/current/main/phases/phase-29ae/README.md`
- CorePlan hardening (docs-first): `docs/development/current/main/phases/phase-29al/README.md`

View File

@ -34,7 +34,7 @@ Related:
## 1.1 Current (active)
- Active phase: `docs/development/current/main/phases/phase-29ap/README.md`
- Next step: Phase 29ap P5 (planned)
- Next step: Phase 29ap P7 (planned)
## 2. すでに固めた SSOT再発防止の土台

View File

@ -57,12 +57,30 @@ Gate (SSOT):
- Scope:
- Remove Pattern8 from `LOOP_PATTERNS` so plan/composer stays SSOT for normal loops.
- Keep legacy table as last resort for Pattern2/4/9 only.
- Keep legacy table as last resort for Pattern6_NestedLoopMinimal / Pattern4 / Pattern9 only.
- Guardrails:
- No change to logs or error strings.
- Legacy routing remains a last-resort fallback.
## P5: Remove Pattern2 from JoinIR legacy table ✅
- Scope:
- Remove Pattern2 (with break) from `LOOP_PATTERNS`.
- Keep plan/composer as the only routing path for Pattern2.
- Guardrails:
- No change to logs or error strings.
- Fallback remains `Ok(None)` for non-matching plan cases.
## P6: stdlib trim_start/trim_end subset (Pattern2BreakFacts) ✅
- Scope:
- Add a conservative Pattern2BreakFacts subset for `trim_start`/`trim_end`.
- Normalize `not is_whitespace(...)` into `is_whitespace(...) == false` in facts.
- Restore quick smoke by ensuring stdlib loops are handled by plan/composer.
- Guardrails:
- No new logs or error strings.
- Subset only: if shape deviates, return `Ok(None)`.
## Next (planned)
- P5: Router pattern-name branching reduction (planner outcome + composer SSOT)
- P6: Facts/Feature expansion if needed
- P7: Legacy table shrink (Pattern9 removal) or leave as-is with justification

View File

@ -77,7 +77,7 @@ impl ConditionEnvBuilder {
// Phase 79-2: Register loop variable BindingId (dev-only)
// NOTE: We don't have access to builder.binding_map here, so this registration
// needs to happen at the call site (in pattern2_with_break.rs, pattern3_with_if_phi.rs, etc.)
// needs to happen at the call site (pattern2 lowerer/orchestrator, pattern3_with_if_phi.rs, etc.)
// This comment serves as a reminder for future developers.
// For each condition variable, allocate JoinIR-local ValueId and build binding

View File

@ -2,7 +2,6 @@
//!
//! Phase 2: Extracted from control_flow.rs
//! - Pattern 1: Simple While Loop (pattern1_minimal.rs)
//! - Pattern 2: Loop with Conditional Break (pattern2_with_break.rs)
//! - Pattern 3: Loop with If-Else PHI (pattern3_with_if_phi.rs)
//! - Pattern 4: Loop with Continue (pattern4_with_continue.rs) [Phase 194+]
//!
@ -78,7 +77,6 @@ pub(in crate::mir::builder) mod exit_binding_constructor; // Phase 222.5-C
pub(in crate::mir::builder) mod exit_binding_validator; // Phase 222.5-C
pub(in crate::mir::builder) mod loop_scope_shape_builder;
pub(in crate::mir::builder) mod pattern1_minimal;
pub(in crate::mir::builder) mod pattern2_with_break;
pub(in crate::mir::builder) mod pattern3_with_if_phi;
pub(in crate::mir::builder) mod pattern4_carrier_analyzer;
pub(in crate::mir::builder) mod pattern4_with_continue;

View File

@ -1,7 +1,7 @@
//! Pattern2 break-condition routing (policy → break_cond + allow-list + flags)
//!
//! This box exists to keep `pattern2_with_break.rs` focused on orchestration and
//! JoinIR wiring. It applies Pattern2-specific policies to derive:
//! This box exists to keep the Pattern2 lowering orchestrator focused on wiring.
//! It applies Pattern2-specific policies to derive:
//! - a normalized `break when true` condition node
//! - an allow-list of body-local variables permitted in conditions (Phase 92 P3)
//! - flags that affect later lowering (e.g. schedule and promotion heuristics)

View File

@ -1,7 +1,7 @@
//! Phase 106: Pattern2 Step Boxes (SSOT)
//!
//! Goal: keep `pattern2_with_break.rs` and the orchestrator thin by splitting
//! the Pattern2 pipeline into explicit steps with clear boundaries.
//! Goal: keep the Pattern2 lowering orchestrator thin by splitting the pipeline
//! into explicit steps with clear boundaries.
//!
//! Each step should have a single responsibility and fail-fast with a clear tag
//! at the step boundary.

View File

@ -1,471 +0,0 @@
//! Pattern 2: Loop with Conditional Break minimal lowerer
use super::super::trace;
use crate::ast::ASTNode;
use crate::mir::builder::MirBuilder;
use crate::mir::ValueId;
/// Phase 194: Detection function for Pattern 2
///
/// Phase 282 P4: Updated to ExtractionBased detection with safety valve
///
/// Pattern 2 matches:
/// - Pattern kind is Pattern2Break (safety valve, O(1) early rejection)
/// - Extraction validates: 比較条件/loop(true) + HAS break + NO continue/return
pub(crate) fn can_lower(builder: &MirBuilder, ctx: &super::router::LoopPatternContext) -> bool {
use crate::mir::loop_pattern_detection::LoopPatternKind;
// Step 1: Early rejection guard (safety valve, O(1))
if ctx.pattern_kind != LoopPatternKind::Pattern2Break {
if ctx.debug {
trace::trace().debug(
"pattern2/can_lower",
&format!("reject: pattern_kind={:?}", ctx.pattern_kind),
);
}
return false;
}
// Step 2: ExtractionBased validation (SSOT, deep check)
use super::extractors::pattern2::extract_loop_with_break_parts;
let parts = match extract_loop_with_break_parts(ctx.condition, ctx.body) {
Ok(Some(p)) => p,
Ok(None) => {
if ctx.debug {
trace::trace().debug(
"pattern2/can_lower",
"reject: not Pattern2 (no break or has continue/return)",
);
}
return false;
}
Err(e) => {
if ctx.debug {
trace::trace().debug("pattern2/can_lower", &format!("error: {}", e));
}
return false;
}
};
if ctx.debug {
trace::trace().debug(
"pattern2/can_lower",
&format!(
"accept: extractable (break_count={}, is_loop_true={}) (Phase 282 P4)",
parts.break_count, parts.is_loop_true
),
);
}
// Step 3: Extract loop variable (hybrid for loop(true))
use super::loop_true_counter_extractor::LoopTrueCounterExtractorBox;
let loop_var_name = if parts.is_loop_true {
// loop(true): Use LoopTrueCounterExtractorBox
match LoopTrueCounterExtractorBox::extract_loop_counter_from_body(
ctx.body,
&builder.variable_ctx.variable_map,
) {
Ok((name, _)) => name,
Err(_) => {
if ctx.debug {
trace::trace().debug(
"pattern2/can_lower",
"reject: loop(true) but no counter found",
);
}
return false;
}
}
} else {
// Normal loop: Use condition-based extraction
match builder.extract_loop_variable_from_condition(ctx.condition) {
Ok(name) => name,
Err(_) => {
// Try parts.loop_var as fallback
if parts.loop_var.is_empty() {
if ctx.debug {
trace::trace().debug(
"pattern2/can_lower",
"reject: cannot extract loop variable",
);
}
return false;
}
parts.loop_var.clone()
}
}
};
// Step 4: Carrier update validation (SSOT)
use super::common_init::CommonPatternInitializer;
match CommonPatternInitializer::check_carrier_updates_allowed(
ctx.body,
&loop_var_name,
&builder.variable_ctx.variable_map,
) {
true => true,
false => {
if ctx.debug {
trace::trace().debug(
"pattern2/can_lower",
"reject: carrier updates not allowed (method calls)",
);
}
false
}
}
}
/// Phase 194: Lowering function for Pattern 2
///
/// Phase 282 P4: Re-extracts for SSOT (no caching from can_lower)
///
/// Wrapper around cf_loop_pattern2_with_break_impl to match router signature
pub(crate) fn lower(
builder: &mut MirBuilder,
ctx: &super::router::LoopPatternContext,
) -> Result<Option<ValueId>, String> {
use super::extractors::pattern2::extract_loop_with_break_parts;
// Re-extract (SSOT principle - no caching from can_lower)
let parts = extract_loop_with_break_parts(ctx.condition, ctx.body)?
.ok_or_else(|| "[pattern2] Not a loop with break pattern in lower()".to_string())?;
if ctx.debug {
trace::trace().debug(
"pattern2/lower",
&format!(
"loop_var={}, break_count={}, is_loop_true={} (Phase 282 P4)",
parts.loop_var, parts.break_count, parts.is_loop_true
),
);
}
// Call existing orchestrator implementation (completely unchanged)
// Note: ctx.skeleton already provided by router if needed
builder.cf_loop_pattern2_with_break_impl(
ctx.condition,
ctx.body,
ctx.func_name,
ctx.debug,
ctx.fn_body,
ctx.skeleton, // Use existing ctx.skeleton (no new env var)
)
}
impl MirBuilder {
/// Phase 179-B: Pattern 2 (Loop with Conditional Break) minimal lowerer
///
/// **Refactored**: Now uses PatternPipelineContext for unified preprocessing
/// **Phase 200-C**: Added fn_body parameter for capture analysis
/// **Phase 92 P0-3**: Added skeleton parameter for ConditionalStep support
///
/// # Pipeline (Phase 179-B)
/// 1. Build preprocessing context → PatternPipelineContext
/// 2. Pattern 2-specific processing (Trim, carrier updates, break condition)
/// 3. Call JoinIR lowerer → JoinModule
/// 4. Create boundary from context → JoinInlineBoundary
/// 5. Merge MIR blocks → JoinIRConversionPipeline
///
/// Note: Pattern 2 has complex Trim pattern logic that remains inline
/// for now. Future Phase 180+ will move Trim logic to dedicated module.
#[allow(dead_code)]
pub(in crate::mir::builder) fn cf_loop_pattern2_with_break(
&mut self,
condition: &ASTNode,
_body: &[ASTNode],
_func_name: &str,
debug: bool,
) -> Result<Option<ValueId>, String> {
// Phase 200-C: Delegate to impl function with fn_body=None for backward compatibility
// Phase 92 P0-3: skeleton=None for backward compatibility
self.cf_loop_pattern2_with_break_impl(condition, _body, _func_name, debug, None, None)
}
/// Phase 200-C: Pattern 2 implementation with optional fn_body for capture analysis
/// Phase 92 P0-3: Added skeleton parameter for ConditionalStep support
fn cf_loop_pattern2_with_break_impl(
&mut self,
condition: &ASTNode,
_body: &[ASTNode],
_func_name: &str,
debug: bool,
fn_body: Option<&[ASTNode]>,
skeleton: Option<&crate::mir::loop_canonicalizer::LoopSkeleton>,
) -> Result<Option<ValueId>, String> {
super::pattern2_lowering_orchestrator::Pattern2LoweringOrchestrator::run(
self,
condition,
_body,
_func_name,
debug,
fn_body,
skeleton,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{BinaryOperator, LiteralValue, Span};
use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext;
use crate::mir::loop_pattern_detection::LoopPatternKind;
fn span() -> Span {
Span::unknown()
}
fn var(name: &str) -> ASTNode {
ASTNode::Variable {
name: name.to_string(),
span: span(),
}
}
fn lit_i(value: i64) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::Integer(value),
span: span(),
}
}
fn bin(op: BinaryOperator, left: ASTNode, right: ASTNode) -> ASTNode {
ASTNode::BinaryOp {
operator: op,
left: Box::new(left),
right: Box::new(right),
span: span(),
}
}
#[test]
#[cfg(feature = "normalized_dev")]
fn phase78_promoted_binding_is_recorded_for_digitpos() {
use super::super::pattern_pipeline::{build_pattern_context, PatternVariant};
use crate::ast::Span;
use crate::mir::ValueId;
use super::super::pattern2_inputs_facts_box::Pattern2InputsFactsBox;
use super::super::pattern2_steps::apply_policy_step_box::ApplyPolicyStepBox;
let mut builder = MirBuilder::new();
builder
.variable_ctx
.variable_map
.insert("i".to_string(), ValueId(1));
builder
.variable_ctx
.variable_map
.insert("len".to_string(), ValueId(2));
builder
.variable_ctx
.variable_map
.insert("s".to_string(), ValueId(3));
builder
.variable_ctx
.variable_map
.insert("digits".to_string(), ValueId(4));
builder
.variable_ctx
.variable_map
.insert("result".to_string(), ValueId(5));
let condition = bin(BinaryOperator::Less, var("i"), var("len"));
let local_ch = ASTNode::Local {
variables: vec!["ch".to_string()],
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
object: Box::new(var("s")),
method: "substring".to_string(),
arguments: vec![],
span: Span::unknown(),
}))],
span: Span::unknown(),
};
let local_digit_pos = ASTNode::Local {
variables: vec!["digit_pos".to_string()],
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
object: Box::new(var("digits")),
method: "indexOf".to_string(),
arguments: vec![var("ch")],
span: Span::unknown(),
}))],
span: Span::unknown(),
};
let break_if = ASTNode::If {
condition: Box::new(bin(BinaryOperator::Less, var("digit_pos"), lit_i(0))),
then_body: vec![ASTNode::Break {
span: Span::unknown(),
}],
else_body: None,
span: Span::unknown(),
};
let body = vec![
local_ch,
local_digit_pos,
break_if,
ASTNode::Assignment {
target: Box::new(var("result")),
value: Box::new(bin(
BinaryOperator::Add,
bin(BinaryOperator::Multiply, var("result"), lit_i(10)),
var("digit_pos"),
)),
span: Span::unknown(),
},
ASTNode::Assignment {
target: Box::new(var("i")),
value: Box::new(bin(BinaryOperator::Add, var("i"), lit_i(1))),
span: Span::unknown(),
},
];
let ctx = build_pattern_context(&mut builder, &condition, &body, PatternVariant::Pattern2)
.expect("build_pattern_context");
let facts =
Pattern2InputsFactsBox::analyze(&builder, &condition, &body, None, &ctx, false)
.expect("Pattern2InputsFactsBox::analyze");
let mut inputs = ApplyPolicyStepBox::apply(&condition, &body, facts)
.expect("ApplyPolicyStepBox::apply");
PromoteStepBox::promote_and_prepare_carriers(&mut builder, &condition, &body, &mut inputs, false, false)
.expect("promote_and_prepare_carriers");
assert!(
inputs
.carrier_info
.promoted_loopbodylocals
.contains(&"digit_pos".to_string()),
"digit_pos should be recorded as promoted"
);
assert_eq!(
inputs.carrier_info.promoted_bindings.len(),
1,
"promoted_bindings should contain exactly one mapping"
);
let (original_bid, promoted_bid) = inputs
.carrier_info
.promoted_bindings
.iter()
.next()
.map(|(k, v)| (*k, *v))
.unwrap();
let promoted_carrier = inputs
.carrier_info
.find_carrier("is_digit_pos")
.expect("promoted carrier exists");
assert_eq!(
promoted_carrier.binding_id,
Some(promoted_bid),
"CarrierVar.binding_id should be set for promoted carrier"
);
let promoted_join_id = promoted_carrier.join_id.expect("join_id allocated");
assert_eq!(
inputs.env.binding_id_map.get(&promoted_bid).copied(),
Some(promoted_join_id),
"ConditionEnv should register promoted binding_id -> join_id"
);
assert_eq!(
inputs.env.get("digit_pos"),
Some(promoted_join_id),
"Name-based alias (digit_pos -> is_digit_pos) should be installed for legacy paths"
);
// Ensure the mapping is not degenerate.
assert_ne!(original_bid, promoted_bid);
}
#[test]
fn parse_number_like_loop_is_routed_to_pattern2() {
let condition = bin(BinaryOperator::Less, var("p"), var("len"));
let break_cond = bin(BinaryOperator::Less, var("digit_pos"), lit_i(0));
let body = vec![
ASTNode::If {
condition: Box::new(break_cond),
then_body: vec![ASTNode::Break { span: span() }],
else_body: None,
span: span(),
},
ASTNode::Assignment {
target: Box::new(var("p")),
value: Box::new(bin(BinaryOperator::Add, var("p"), lit_i(1))),
span: span(),
},
// num_str = num_str + ch (string append is allowed by CommonPatternInitializer)
ASTNode::Assignment {
target: Box::new(var("num_str")),
value: Box::new(bin(BinaryOperator::Add, var("num_str"), var("ch"))),
span: span(),
},
];
let ctx = LoopPatternContext::new(&condition, &body, "parse_number_like", true, false);
let builder = MirBuilder::new();
assert_eq!(ctx.pattern_kind, LoopPatternKind::Pattern2Break);
assert!(
can_lower(&builder, &ctx),
"Pattern2 lowerer should accept JsonParser-like break loop"
);
}
#[test]
fn test_atoi_loop_routed_to_pattern2() {
// Phase 246-EX Step 2: _atoi loop router integration test
// loop(i < len) {
// if digit_pos < 0 { break }
// result = result * 10 + digit_pos // NumberAccumulation
// i = i + 1
// }
let condition = bin(BinaryOperator::Less, var("i"), var("len"));
let break_cond = bin(BinaryOperator::Less, var("digit_pos"), lit_i(0));
// result = result * 10 + digit_pos (NumberAccumulation pattern)
let mul_expr = bin(BinaryOperator::Multiply, var("result"), lit_i(10));
let result_update_value = bin(BinaryOperator::Add, mul_expr, var("digit_pos"));
let body = vec![
ASTNode::If {
condition: Box::new(break_cond),
then_body: vec![ASTNode::Break { span: span() }],
else_body: None,
span: span(),
},
ASTNode::Assignment {
target: Box::new(var("result")),
value: Box::new(result_update_value),
span: span(),
},
ASTNode::Assignment {
target: Box::new(var("i")),
value: Box::new(bin(BinaryOperator::Add, var("i"), lit_i(1))),
span: span(),
},
];
let ctx = LoopPatternContext::new(&condition, &body, "_atoi", true, false);
let builder = MirBuilder::new();
// Verify pattern classification
assert_eq!(
ctx.pattern_kind,
LoopPatternKind::Pattern2Break,
"_atoi loop should be classified as Pattern2Break"
);
// Verify Pattern2 lowerer accepts it
assert!(
can_lower(&builder, &ctx),
"Pattern2 lowerer should accept _atoi loop with NumberAccumulation"
);
}
}

View File

@ -180,7 +180,7 @@ fn lower_via_plan(
/// - Cons: Extraction can be expensive (but amortized over lowering)
///
/// ## StructureBased (Feature Classification)
/// - Used by: Pattern2, Pattern4 (legacy)
/// - Used by: Pattern4 (legacy)
/// - Strategy: Check pattern_kind (from LoopPatternContext), plus optional structural checks
/// - Pros: Fast classification, reuses centralized feature detection
/// - Cons: Two sources of truth (classify + structural checks)
@ -188,7 +188,7 @@ fn lower_via_plan(
/// ## Rationale for Dual Strategy:
/// - Pattern6/7: Complex extraction logic (variable step, carrier tracking)
/// → ExtractionBased avoids duplication between detect and extract
/// - Other patterns: Simple structural features (break/continue/if-phi)
/// - Other patterns: Simple structural features (continue)
/// → StructureBased leverages centralized LoopFeatures classification
///
/// This documentation prevents bugs like Phase 272 P0.2's Pattern7 issue
@ -201,7 +201,7 @@ pub(crate) enum CanLowerStrategy {
ExtractionBased,
/// Structure-based detection: Check pattern_kind from LoopPatternContext
/// Used by Pattern2, Pattern4
/// Used by Pattern4
StructureBased,
}
@ -222,14 +222,14 @@ pub(crate) struct LoopPatternEntry {
///
/// **IMPORTANT**: Patterns are tried in array order (SSOT).
/// Array order defines priority - earlier entries are tried first.
/// Pattern4 → Pattern9 → Pattern2
/// Pattern6_NestedLoopMinimal → Pattern4 → Pattern9
///
/// # Current Patterns (Structure-based detection, established Phase 131-11+)
///
/// Pattern detection strategies (updated Phase 286):
/// - Pattern6/7: ExtractionBased (Plan line, Phase 273+)
/// - Pattern8/9: ExtractionBased (extraction functions, Plan line Phase 286+)
/// - Pattern2/4: StructureBased (LoopFeatures classification, legacy)
/// - Pattern4: StructureBased (LoopFeatures classification, legacy)
///
/// - Pattern 4: Loop with Continue (loop_continue_pattern4.hako)
/// - Detection: pattern_kind == Pattern4Continue
@ -237,13 +237,10 @@ pub(crate) struct LoopPatternEntry {
///
/// - Pattern3: moved to plan-based routing (planner+composer SSOT)
///
/// - Pattern 2: Loop with Conditional Break (joinir_min_loop.hako)
/// - Detection: pattern_kind == Pattern2Break
/// - Structure: has_break && !has_continue
///
/// Note: func_name is now only used for debug logging, not pattern detection
/// Phase 286: Pattern5 removed (migrated to Plan-based routing)
/// Phase 29ap P4: Pattern8 removed (migrated to Plan-based routing)
/// Phase 29ap P5: Pattern2 removed (migrated to Plan-based routing)
pub(crate) static LOOP_PATTERNS: &[LoopPatternEntry] = &[
LoopPatternEntry {
name: "Pattern6_NestedLoopMinimal", // Phase 188.3: 1-level nested loop
@ -264,11 +261,6 @@ pub(crate) static LOOP_PATTERNS: &[LoopPatternEntry] = &[
detect: super::pattern9_accum_const_loop::can_lower,
lower: super::pattern9_accum_const_loop::lower,
},
LoopPatternEntry {
name: "Pattern2_WithBreak",
detect: super::pattern2_with_break::can_lower,
lower: super::pattern2_with_break::lower,
},
];
/// Try all registered patterns in priority order.
@ -282,14 +274,14 @@ pub(crate) static LOOP_PATTERNS: &[LoopPatternEntry] = &[
/// This router uses multiple detection strategies:
/// - Plan-based (Pattern6/7): extract_*_plan() → DomainPlan (Phase 273+ SSOT)
/// - Extraction-based (Pattern8/9): extract_*() functions (already implemented)
/// - Structure-based (Pattern2/4): ctx.pattern_kind classification (legacy)
/// - Structure-based (Pattern4): ctx.pattern_kind classification (legacy)
///
/// # Plan Line SSOT for Pattern6/7 (Phase 273+)
///
/// This function implements the following routing strategy:
/// 1. Try Plan-based Pattern6 (extract_scan_with_init_plan) → DomainPlan
/// 2. Try Plan-based Pattern7 (extract_split_scan_plan) → DomainPlan
/// 3. Fall through to legacy Pattern2/4/9 table for other patterns
/// 3. Fall through to legacy Pattern4/9 table for other patterns
///
/// The Plan line (Extractor → Normalizer → Verifier → Lowerer) is the
/// current operational SSOT for Pattern6/7. Legacy patterns (1/2/4/8/9) use
@ -304,7 +296,8 @@ pub(crate) static LOOP_PATTERNS: &[LoopPatternEntry] = &[
/// SSOT Entry Points:
/// - Pattern6: src/mir/builder/control_flow/plan/normalizer.rs (ScanWithInit normalization)
/// - Pattern7: src/mir/builder/control_flow/plan/normalizer.rs (SplitScan normalization)
/// - Pattern2/4/9: src/mir/builder/control_flow/joinir/patterns/pattern*.rs (direct lowering)
/// - Pattern4/9: src/mir/builder/control_flow/joinir/patterns/pattern*.rs (direct lowering)
/// - Pattern6_NestedLoopMinimal: src/mir/builder/control_flow/joinir/patterns/pattern6_nested_minimal.rs
pub(crate) fn route_loop_pattern(
builder: &mut MirBuilder,
ctx: &LoopPatternContext,

View File

@ -179,17 +179,18 @@ pub(crate) fn extract_pattern2_plan(
return Ok(None); // Multiple breaks → legacy fallback
}
// Step 4: Body structure must be: if { ... break } + carrier_update + loop_increment
// Expected structure:
// body[0] = If { then_body contains break }
// body[1] = carrier update assignment
// body[2] = loop_var update assignment
// Step 4: Body structure must be one of:
// A) if { ... break } + carrier_update + loop_increment
// B) local seg = s.substring(i, i + 1) + if { ... break } + loop_increment
if body.len() != 3 {
return Ok(None); // Unexpected body length → legacy fallback
}
// Step 4.1: First element must be an If with break in then_body
let (break_condition, carrier_update_in_break) = match &body[0] {
let has_leading_local = match_local_substring_char(&body[0], &parts.loop_var);
let break_stmt_idx = if has_leading_local { 1 } else { 0 };
// Step 4.1: Break guard (If with break in then_body)
let (break_condition, carrier_update_in_break) = match &body[break_stmt_idx] {
ASTNode::If {
condition: if_cond,
then_body,
@ -230,19 +231,26 @@ pub(crate) fn extract_pattern2_plan(
(break_cond, carrier_update)
}
_ => return Ok(None), // First element not If → legacy
_ => return Ok(None), // Break guard missing → legacy
};
// Step 4.2: Extract carrier update in body (second element)
let (carrier_var, carrier_update_in_body) = match &body[1] {
ASTNode::Assignment { target, value, .. } => {
let carrier_name = match target.as_ref() {
ASTNode::Variable { name, .. } => name.clone(),
_ => return Ok(None),
};
(carrier_name, value.as_ref().clone())
// Step 4.2: Extract carrier update in body (optional)
let (carrier_var, carrier_update_in_body) = if has_leading_local {
let loop_increment =
super::common_helpers::extract_loop_increment_plan(body, &parts.loop_var)?
.ok_or_else(|| "Pattern2: missing loop increment".to_string())?;
(parts.loop_var.clone(), loop_increment.clone())
} else {
match &body[1] {
ASTNode::Assignment { target, value, .. } => {
let carrier_name = match target.as_ref() {
ASTNode::Variable { name, .. } => name.clone(),
_ => return Ok(None),
};
(carrier_name, value.as_ref().clone())
}
_ => return Ok(None),
}
_ => return Ok(None),
};
// Step 4.3: Extract loop increment (third element)
@ -251,7 +259,7 @@ pub(crate) fn extract_pattern2_plan(
None => return Ok(None), // No loop increment found → legacy
};
// Step 5: Validate loop condition is `<var> < <int_lit>` (same as Pattern1)
// Step 5: Validate loop condition is `<var> < <int_lit>` or `<var> < <len()>`
if !validate_loop_condition_for_plan(condition, &parts.loop_var) {
return Ok(None);
}
@ -290,23 +298,82 @@ fn validate_loop_condition_for_plan(cond: &ASTNode, loop_var: &str) -> bool {
return false;
}
// Right must be integer literal
if !matches!(
// Right must be integer literal or `<var>.length()`
if matches!(
right.as_ref(),
ASTNode::Literal {
value: LiteralValue::Integer(_),
..
}
) {
return false;
return true;
}
true
matches!(
right.as_ref(),
ASTNode::MethodCall { object, method, arguments, .. }
if method == "length"
&& arguments.is_empty()
&& matches!(object.as_ref(), ASTNode::Variable { .. })
)
} else {
false
}
}
fn match_local_substring_char(stmt: &ASTNode, loop_var: &str) -> bool {
let ASTNode::Local {
variables,
initial_values,
..
} = stmt
else {
return false;
};
if variables.len() != 1 || initial_values.len() != 1 {
return false;
}
let Some(expr) = initial_values[0].as_ref() else {
return false;
};
let ASTNode::MethodCall {
object,
method,
arguments,
..
} = expr.as_ref()
else {
return false;
};
if method != "substring" || arguments.len() != 2 {
return false;
}
if !matches!(object.as_ref(), ASTNode::Variable { .. }) {
return false;
}
if !matches!(&arguments[0], ASTNode::Variable { name, .. } if name == loop_var) {
return false;
}
match &arguments[1] {
ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left,
right,
..
} => {
matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == loop_var)
&& matches!(
right.as_ref(),
ASTNode::Literal {
value: LiteralValue::Integer(1),
..
}
)
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -26,6 +26,16 @@ pub(in crate::mir::builder) fn try_extract_pattern2_break_facts(
condition: &ASTNode,
body: &[ASTNode],
) -> Result<Option<Pattern2BreakFacts>, Freeze> {
if let Some(parse_int) = try_extract_pattern2_break_parse_integer_subset(condition, body) {
return Ok(Some(parse_int));
}
if let Some(trim_whitespace) =
try_extract_pattern2_break_trim_whitespace_subset(condition, body)
{
return Ok(Some(trim_whitespace));
}
if let Some(realworld) = try_extract_pattern2_break_realworld_subset(condition, body) {
return Ok(Some(realworld));
}
@ -86,6 +96,490 @@ pub(in crate::mir::builder) fn try_extract_pattern2_break_facts(
}))
}
fn try_extract_pattern2_break_parse_integer_subset(
condition: &ASTNode,
body: &[ASTNode],
) -> Option<Pattern2BreakFacts> {
let loop_var = extract_loop_var_for_len_condition(condition)?;
let counts = count_control_flow(body, ControlFlowDetector::default());
if counts.break_count != 1 || counts.continue_count > 0 || counts.return_count > 0 {
return None;
}
if body.len() != 5 {
return None;
}
// local ch = s.substring(i, i + 1)
let (ch_var, haystack_var, ch_expr) = match_local_substring_char(&body[0], &loop_var)?;
// local d = this.index_of(digits, ch)
let (digits_var, _d_var) = match_local_this_index_of(&body[1], &ch_var)?;
// if d < 0 { break }
let break_var = match_break_if_less_than_zero(&body[2])?;
// acc = acc * 10 + d
let carrier_var = match_acc_update_mul10_plus_d(&body[3], &break_var)?;
// i = i + 1
let loop_increment = extract_loop_increment_at_end(body, &loop_var)?;
// Rebuild break_condition and carrier_update_in_body without relying on the local `d`.
// This avoids requiring a local binding in PlanNormalizer while keeping semantics.
let index_expr = ASTNode::MethodCall {
object: Box::new(ASTNode::This { span: Span::unknown() }),
method: "index_of".to_string(),
arguments: vec![var(&digits_var), ch_expr],
span: Span::unknown(),
};
let break_condition = ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(index_expr.clone()),
right: Box::new(lit_int(0)),
span: Span::unknown(),
};
let carrier_update_in_body = ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Multiply,
left: Box::new(var(&carrier_var)),
right: Box::new(lit_int(10)),
span: Span::unknown(),
}),
right: Box::new(index_expr),
span: Span::unknown(),
};
// Also confirm the substring source matches the haystack var and loop var.
// (We don't need to store them, but the match helps prevent accidental overlap.)
let _ = &haystack_var;
Some(Pattern2BreakFacts {
loop_var,
carrier_var,
loop_condition: condition.clone(),
break_condition,
carrier_update_in_break: None,
carrier_update_in_body,
loop_increment,
})
}
fn match_local_substring_char(
stmt: &ASTNode,
loop_var: &str,
) -> Option<(String, String, ASTNode)> {
let ASTNode::Local {
variables,
initial_values,
..
} = stmt else {
return None;
};
if variables.len() != 1 || initial_values.len() != 1 {
return None;
}
let ch_var = variables[0].clone();
let Some(expr) = initial_values[0].as_ref() else {
return None;
};
let ASTNode::MethodCall {
object,
method,
arguments,
..
} = expr.as_ref() else {
return None;
};
if method != "substring" || arguments.len() != 2 {
return None;
}
let ASTNode::Variable { name: haystack_var, .. } = object.as_ref() else {
return None;
};
if !matches!(&arguments[0], ASTNode::Variable { name, .. } if name == loop_var) {
return None;
}
// end = i + 1
match &arguments[1] {
ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left,
right,
..
} => {
if !matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == loop_var) {
return None;
}
if !matches!(
right.as_ref(),
ASTNode::Literal {
value: LiteralValue::Integer(1),
..
}
) {
return None;
}
}
_ => return None,
}
Some((ch_var, haystack_var.clone(), expr.as_ref().clone()))
}
fn match_local_this_index_of(stmt: &ASTNode, ch_var: &str) -> Option<(String, String)> {
let ASTNode::Local {
variables,
initial_values,
..
} = stmt else {
return None;
};
if variables.len() != 1 || initial_values.len() != 1 {
return None;
}
let d_var = variables[0].clone();
let Some(expr) = initial_values[0].as_ref() else {
return None;
};
let ASTNode::MethodCall {
object,
method,
arguments,
..
} = expr.as_ref() else {
return None;
};
if method != "index_of" || arguments.len() != 2 {
return None;
}
if !matches!(object.as_ref(), ASTNode::This { .. } | ASTNode::Me { .. }) {
return None;
}
let ASTNode::Variable { name: digits_var, .. } = &arguments[0] else {
return None;
};
if !matches!(&arguments[1], ASTNode::Variable { name, .. } if name == ch_var) {
return None;
}
Some((digits_var.clone(), d_var))
}
fn match_break_if_less_than_zero(stmt: &ASTNode) -> Option<String> {
let (cond, update_opt) = extract_break_if_parts(stmt)?;
if update_opt.is_some() {
return None;
}
let ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left,
right,
..
} = cond else {
return None;
};
let ASTNode::Variable { name, .. } = left.as_ref() else {
return None;
};
if !matches!(
right.as_ref(),
ASTNode::Literal {
value: LiteralValue::Integer(0),
..
}
) {
return None;
}
Some(name.clone())
}
fn match_acc_update_mul10_plus_d(stmt: &ASTNode, d_var: &str) -> Option<String> {
let ASTNode::Assignment { target, value, .. } = stmt else {
return None;
};
let ASTNode::Variable { name: carrier_var, .. } = target.as_ref() else {
return None;
};
let ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left,
right,
..
} = value.as_ref() else {
return None;
};
// left: acc * 10
match left.as_ref() {
ASTNode::BinaryOp {
operator: BinaryOperator::Multiply,
left: mul_lhs,
right: mul_rhs,
..
} => {
if !matches!(mul_lhs.as_ref(), ASTNode::Variable { name, .. } if name == carrier_var) {
return None;
}
if !matches!(
mul_rhs.as_ref(),
ASTNode::Literal {
value: LiteralValue::Integer(10),
..
}
) {
return None;
}
}
_ => return None,
}
// right: d
if !matches!(right.as_ref(), ASTNode::Variable { name, .. } if name == d_var) {
return None;
}
Some(carrier_var.clone())
}
fn try_extract_pattern2_break_trim_whitespace_subset(
condition: &ASTNode,
body: &[ASTNode],
) -> Option<Pattern2BreakFacts> {
let loop_var = extract_trim_loop_var(condition)?;
let counts = count_control_flow(body, ControlFlowDetector::default());
if counts.break_count != 1 || counts.continue_count > 0 || counts.return_count > 0 {
return None;
}
if body.len() != 2 {
return None;
}
let break_condition = extract_trim_break_condition(&body[0], &loop_var)?;
let loop_increment = extract_trim_loop_increment(&body[1], &loop_var)?;
Some(Pattern2BreakFacts {
loop_var: loop_var.clone(),
carrier_var: loop_var,
loop_condition: condition.clone(),
break_condition,
carrier_update_in_break: None,
carrier_update_in_body: loop_increment.clone(),
loop_increment,
})
}
fn extract_trim_loop_var(condition: &ASTNode) -> Option<String> {
let ASTNode::BinaryOp {
operator,
left,
right,
..
} = condition
else {
return None;
};
let ASTNode::Variable { name: loop_var, .. } = left.as_ref() else {
return None;
};
match operator {
BinaryOperator::Less | BinaryOperator::LessEqual => {
if matches!(
right.as_ref(),
ASTNode::MethodCall { object, method, arguments, .. }
if method == "length"
&& arguments.is_empty()
&& matches!(object.as_ref(), ASTNode::Variable { .. })
) {
return Some(loop_var.clone());
}
}
BinaryOperator::GreaterEqual => {
if matches!(
right.as_ref(),
ASTNode::Literal {
value: LiteralValue::Integer(0),
..
}
) {
return Some(loop_var.clone());
}
}
_ => {}
}
None
}
fn extract_trim_break_condition(stmt: &ASTNode, loop_var: &str) -> Option<ASTNode> {
let ASTNode::If {
condition,
then_body,
else_body,
..
} = stmt
else {
return None;
};
if else_body.is_some() {
return None;
}
if then_body.len() != 1 || !matches!(then_body[0], ASTNode::Break { .. }) {
return None;
}
let whitespace_call = match condition.as_ref() {
ASTNode::UnaryOp { operator, operand, .. } => {
use crate::ast::UnaryOperator;
if !matches!(operator, UnaryOperator::Not) {
return None;
}
match_is_whitespace_call(operand.as_ref(), loop_var)?
}
ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left,
right,
..
} => {
if matches!(
right.as_ref(),
ASTNode::Literal {
value: LiteralValue::Bool(false),
..
}
) {
match_is_whitespace_call(left.as_ref(), loop_var)?
} else if matches!(
left.as_ref(),
ASTNode::Literal {
value: LiteralValue::Bool(false),
..
}
) {
match_is_whitespace_call(right.as_ref(), loop_var)?
} else {
return None;
}
}
_ => return None,
};
Some(ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(whitespace_call),
right: Box::new(lit_bool(false)),
span: Span::unknown(),
})
}
fn match_is_whitespace_call(expr: &ASTNode, loop_var: &str) -> Option<ASTNode> {
let ASTNode::MethodCall {
object,
method,
arguments,
..
} = expr
else {
return None;
};
if method != "is_whitespace" || arguments.len() != 1 {
return None;
}
let normalized_object = match object.as_ref() {
ASTNode::This { .. } => ASTNode::This { span: Span::unknown() },
ASTNode::Me { .. } => ASTNode::This { span: Span::unknown() },
_ => return None,
};
if !matches_substring_at_loop_var(&arguments[0], loop_var) {
return None;
}
Some(ASTNode::MethodCall {
object: Box::new(normalized_object),
method: method.clone(),
arguments: arguments.clone(),
span: Span::unknown(),
})
}
fn matches_substring_at_loop_var(expr: &ASTNode, loop_var: &str) -> bool {
let ASTNode::MethodCall {
object,
method,
arguments,
..
} = expr
else {
return false;
};
if method != "substring" || arguments.len() != 2 {
return false;
}
if !matches!(object.as_ref(), ASTNode::Variable { .. }) {
return false;
}
if !matches!(&arguments[0], ASTNode::Variable { name, .. } if name == loop_var) {
return false;
}
match &arguments[1] {
ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left,
right,
..
} => {
matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == loop_var)
&& matches!(
right.as_ref(),
ASTNode::Literal {
value: LiteralValue::Integer(1),
..
}
)
}
_ => false,
}
}
fn extract_trim_loop_increment(stmt: &ASTNode, loop_var: &str) -> Option<ASTNode> {
let ASTNode::Assignment { target, value, .. } = stmt else {
return None;
};
let ASTNode::Variable { name, .. } = target.as_ref() else {
return None;
};
if name != loop_var {
return None;
}
let ASTNode::BinaryOp {
operator: BinaryOperator::Add | BinaryOperator::Subtract,
left,
right,
..
} = value.as_ref()
else {
return None;
};
if !matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == loop_var) {
return None;
}
if !matches!(
right.as_ref(),
ASTNode::Literal {
value: LiteralValue::Integer(1),
..
}
) {
return None;
}
Some(value.as_ref().clone())
}
fn try_extract_pattern2_break_realworld_subset(
condition: &ASTNode,
body: &[ASTNode],
@ -729,6 +1223,13 @@ fn lit_int(value: i64) -> ASTNode {
}
}
fn lit_bool(value: bool) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::Bool(value),
span: Span::unknown(),
}
}
fn lit_str(value: &str) -> ASTNode {
ASTNode::Literal {
value: LiteralValue::String(value.to_string()),
@ -838,6 +1339,15 @@ mod tests {
}
}
fn this_method_call(method: &str, args: Vec<ASTNode>) -> ASTNode {
ASTNode::MethodCall {
object: Box::new(ASTNode::This { span: Span::unknown() }),
method: method.to_string(),
arguments: args,
span: Span::unknown(),
}
}
fn binop(operator: BinaryOperator, left: ASTNode, right: ASTNode) -> ASTNode {
ASTNode::BinaryOp {
operator,
@ -847,6 +1357,60 @@ mod tests {
}
}
#[test]
fn extract_pattern2_break_parse_integer_subset() {
let condition = binop(
BinaryOperator::Less,
v("i"),
method_call("s", "length", vec![]),
);
let body = vec![
local(
"ch",
method_call(
"s",
"substring",
vec![v("i"), binop(BinaryOperator::Add, v("i"), lit_int(1))],
),
),
ASTNode::Local {
variables: vec!["d".to_string()],
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
object: Box::new(ASTNode::This { span: Span::unknown() }),
method: "index_of".to_string(),
arguments: vec![v("digits"), v("ch")],
span: Span::unknown(),
}))],
span: Span::unknown(),
},
ASTNode::If {
condition: Box::new(binop(BinaryOperator::Less, v("d"), lit_int(0))),
then_body: vec![ASTNode::Break { span: Span::unknown() }],
else_body: None,
span: Span::unknown(),
},
assign(
"acc",
binop(
BinaryOperator::Add,
binop(BinaryOperator::Multiply, v("acc"), lit_int(10)),
v("d"),
),
),
assign("i", binop(BinaryOperator::Add, v("i"), lit_int(1))),
];
let facts = try_extract_pattern2_break_facts(&condition, &body)
.expect("Ok")
.expect("Some facts");
assert_eq!(facts.loop_var, "i");
assert_eq!(facts.carrier_var, "acc");
assert!(matches!(
facts.break_condition,
ASTNode::BinaryOp { operator: BinaryOperator::Less, .. }
));
}
#[test]
fn extract_pattern2_break_realworld_subset() {
let condition = lit_bool(true);
@ -927,6 +1491,113 @@ mod tests {
}
}
#[test]
fn extract_pattern2_break_trim_whitespace_subset_start() {
use crate::ast::UnaryOperator;
let condition = binop(
BinaryOperator::Less,
v("i"),
method_call("s", "length", vec![]),
);
let body = vec![
ASTNode::If {
condition: Box::new(ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand: Box::new(this_method_call(
"is_whitespace",
vec![method_call(
"s",
"substring",
vec![v("i"), binop(BinaryOperator::Add, v("i"), lit_int(1))],
)],
)),
span: Span::unknown(),
}),
then_body: vec![ASTNode::Break { span: Span::unknown() }],
else_body: None,
span: Span::unknown(),
},
assign("i", binop(BinaryOperator::Add, v("i"), lit_int(1))),
];
let facts = try_extract_pattern2_break_facts(&condition, &body)
.expect("Ok")
.expect("Some facts");
assert_eq!(facts.loop_var, "i");
assert_eq!(facts.carrier_var, "i");
assert!(matches!(
facts.break_condition,
ASTNode::BinaryOp { operator: BinaryOperator::Equal, .. }
));
}
#[test]
fn extract_pattern2_break_trim_whitespace_subset_end() {
use crate::ast::UnaryOperator;
let condition = binop(BinaryOperator::GreaterEqual, v("i"), lit_int(0));
let body = vec![
ASTNode::If {
condition: Box::new(ASTNode::UnaryOp {
operator: UnaryOperator::Not,
operand: Box::new(this_method_call(
"is_whitespace",
vec![method_call(
"s",
"substring",
vec![v("i"), binop(BinaryOperator::Add, v("i"), lit_int(1))],
)],
)),
span: Span::unknown(),
}),
then_body: vec![ASTNode::Break { span: Span::unknown() }],
else_body: None,
span: Span::unknown(),
},
assign("i", binop(BinaryOperator::Subtract, v("i"), lit_int(1))),
];
let facts = try_extract_pattern2_break_facts(&condition, &body)
.expect("Ok")
.expect("Some facts");
assert_eq!(facts.loop_var, "i");
assert_eq!(facts.carrier_var, "i");
assert!(matches!(
facts.loop_increment,
ASTNode::BinaryOp { operator: BinaryOperator::Subtract, .. }
));
}
#[test]
fn extract_pattern2_break_trim_whitespace_subset_rejects_missing_not() {
let condition = binop(
BinaryOperator::Less,
v("i"),
method_call("s", "length", vec![]),
);
let body = vec![
ASTNode::If {
condition: Box::new(this_method_call(
"is_whitespace",
vec![method_call(
"s",
"substring",
vec![v("i"), binop(BinaryOperator::Add, v("i"), lit_int(1))],
)],
)),
then_body: vec![ASTNode::Break { span: Span::unknown() }],
else_body: None,
span: Span::unknown(),
},
assign("i", binop(BinaryOperator::Add, v("i"), lit_int(1))),
];
let facts = try_extract_pattern2_break_facts(&condition, &body)
.expect("Ok");
assert!(facts.is_none());
}
#[test]
fn extract_pattern2_break_loopbodylocal_trim_seg_subset() {
let condition = binop(

View File

@ -215,27 +215,6 @@ impl super::PlanNormalizer {
Ok((value_id, vec![const_effect]))
}
ASTNode::MethodCall { object, method, arguments, .. } => {
let object_name = match object.as_ref() {
ASTNode::Variable { name, .. } => name.clone(),
_ => {
return Err(format!(
"[normalizer] Method call on non-variable object: {:?}",
object
))
}
};
let object_id = if let Some(&phi_dst) = phi_bindings.get(&object_name) {
phi_dst
} else if let Some(&value_id) = builder.variable_ctx.variable_map.get(&object_name) {
value_id
} else {
return Err(format!(
"[normalizer] Method call object {} not found",
object_name
));
};
let mut arg_ids = Vec::new();
let mut arg_effects = Vec::new();
for arg in arguments {
@ -247,13 +226,44 @@ impl super::PlanNormalizer {
let result_id = builder.next_value_id();
builder.type_ctx.value_types.insert(result_id, MirType::Integer);
arg_effects.push(CoreEffectPlan::MethodCall {
dst: Some(result_id),
object: object_id,
method: method.clone(),
args: arg_ids,
effects: EffectMask::PURE.add(Effect::Io),
});
match object.as_ref() {
ASTNode::Variable { name, .. } => {
let object_id = if let Some(&phi_dst) = phi_bindings.get(name) {
phi_dst
} else if let Some(&value_id) = builder.variable_ctx.variable_map.get(name) {
value_id
} else {
return Err(format!(
"[normalizer] Method call object {} not found",
name
));
};
arg_effects.push(CoreEffectPlan::MethodCall {
dst: Some(result_id),
object: object_id,
method: method.clone(),
args: arg_ids,
effects: EffectMask::PURE.add(Effect::Io),
});
}
ASTNode::This { .. } | ASTNode::Me { .. } => {
let Some(box_name) = builder.comp_ctx.current_static_box.clone() else {
return Err("[normalizer] this.method() without current_static_box".to_string());
};
let func = format!("{}.{}/{}", box_name, method, arguments.len());
arg_effects.push(CoreEffectPlan::GlobalCall {
dst: Some(result_id),
func,
args: arg_ids,
});
}
_ => {
return Err(format!(
"[normalizer] Method call on unsupported object: {:?}",
object
));
}
}
Ok((result_id, arg_effects))
}

View File

@ -107,7 +107,7 @@ pub fn lower_loop_with_break_to_joinir(
//
// Related code:
// - Router callsite: loop_pattern_router.rs:148
// - Actual implementation: control_flow::cf_loop_pattern2_with_break()
// - Actual implementation: Plan/Composer route (PlanLowerer)
// - Minimal lowerer: loop_with_break_minimal::lower_loop_with_break_minimal()
eprintln!("[loop_patterns] Pattern 2: Lowering delegated to control_flow.rs (stub)");