phase29ap(p6): extend pattern2 plan subsets for stdlib
This commit is contained in:
@ -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)
|
||||
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
@ -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(再発防止の土台)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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::*;
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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)");
|
||||
|
||||
Reference in New Issue
Block a user