From 6b851b2e62b484192c35b6c0aa02f9e208d395b7 Mon Sep 17 00:00:00 2001 From: tomoaki Date: Tue, 30 Dec 2025 23:50:30 +0900 Subject: [PATCH] phase29ap(p2): route stdlib to_lower via plan char-map subset --- .../phase29ap_stringutils_tolower_min.hako | 3 + docs/development/current/main/10-Now.md | 2 +- docs/development/current/main/30-Backlog.md | 2 +- .../design/coreplan-migration-roadmap-ssot.md | 2 +- .../current/main/phases/phase-29ae/README.md | 1 + ...RN1-STDLIB-TO_LOWER-SUBSET-INSTRUCTIONS.md | 70 ++++ .../current/main/phases/phase-29ap/README.md | 14 +- .../control_flow/plan/composer/coreloop_v0.rs | 9 + .../control_flow/plan/composer/coreloop_v1.rs | 10 + .../builder/control_flow/plan/composer/mod.rs | 11 + .../control_flow/plan/extractors/pattern8.rs | 2 +- .../control_flow/plan/facts/loop_facts.rs | 23 +- .../builder/control_flow/plan/facts/mod.rs | 1 + .../plan/facts/pattern1_char_map_facts.rs | 351 ++++++++++++++++++ .../pattern8_bool_predicate_scan_facts.rs | 8 +- src/mir/builder/control_flow/plan/lowerer.rs | 4 + src/mir/builder/control_flow/plan/mod.rs | 29 ++ .../plan/normalize/canonicalize.rs | 3 + .../control_flow/plan/normalizer/mod.rs | 12 +- .../plan/normalizer/pattern1_char_map.rs | 244 ++++++++++++ .../pattern8_bool_predicate_scan.rs | 53 ++- .../control_flow/plan/planner/build.rs | 122 +++++- .../control_flow/plan/single_planner/rules.rs | 3 +- src/mir/builder/control_flow/plan/verifier.rs | 13 + .../joinir/phase29ae_regression_pack_vm.sh | 1 + .../phase29ap_stringutils_tolower_vm.sh | 17 + 26 files changed, 971 insertions(+), 39 deletions(-) create mode 100644 apps/tests/phase29ap_stringutils_tolower_min.hako create mode 100644 docs/development/current/main/phases/phase-29ap/P2-PATTERN1-STDLIB-TO_LOWER-SUBSET-INSTRUCTIONS.md create mode 100644 src/mir/builder/control_flow/plan/facts/pattern1_char_map_facts.rs create mode 100644 src/mir/builder/control_flow/plan/normalizer/pattern1_char_map.rs create mode 100644 tools/smokes/v2/profiles/integration/joinir/phase29ap_stringutils_tolower_vm.sh diff --git a/apps/tests/phase29ap_stringutils_tolower_min.hako b/apps/tests/phase29ap_stringutils_tolower_min.hako new file mode 100644 index 00000000..95a3c521 --- /dev/null +++ b/apps/tests/phase29ap_stringutils_tolower_min.hako @@ -0,0 +1,3 @@ +using "apps/lib/json_native/utils/string.hako" as StringUtils + +print(StringUtils.to_lower("WORLD")) diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index 32fc996a..0ac1efe5 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -3,7 +3,7 @@ ## Current Focus - Phase: `docs/development/current/main/phases/phase-29ap/README.md` -- Next: Phase 29ap P2 (planned; see `docs/development/current/main/phases/phase-29ap/README.md`) +- Next: Phase 29ap P3 (planned; see `docs/development/current/main/phases/phase-29ap/README.md`) ## Gate (SSOT) diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md index 9e11c2ea..b0015042 100644 --- a/docs/development/current/main/30-Backlog.md +++ b/docs/development/current/main/30-Backlog.md @@ -5,7 +5,7 @@ Scope: 「次にやる候補」を短く列挙するメモ。入口は `docs/dev ## Active -- Phase 29ap: `docs/development/current/main/phases/phase-29ap/README.md` (Next: P2 planned) +- Phase 29ap: `docs/development/current/main/phases/phase-29ap/README.md` (Next: P3 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` diff --git a/docs/development/current/main/design/coreplan-migration-roadmap-ssot.md b/docs/development/current/main/design/coreplan-migration-roadmap-ssot.md index 24462ad5..ca740251 100644 --- a/docs/development/current/main/design/coreplan-migration-roadmap-ssot.md +++ b/docs/development/current/main/design/coreplan-migration-roadmap-ssot.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 P1 (planned) +- Next step: Phase 29ap P3 (planned) ## 2. すでに固めた SSOT(再発防止の土台) diff --git a/docs/development/current/main/phases/phase-29ae/README.md b/docs/development/current/main/phases/phase-29ae/README.md index eab6e818..1acf2643 100644 --- a/docs/development/current/main/phases/phase-29ae/README.md +++ b/docs/development/current/main/phases/phase-29ae/README.md @@ -12,6 +12,7 @@ Goal: JoinIR の最小回帰セットを SSOT として固定する。 - Pattern3 (release adopt, VM): `phase29ao_pattern3_release_adopt_vm` - Pattern1 (strict shadow, VM): `phase29ao_pattern1_strict_shadow_vm` - Pattern1 (subset reject, VM): `phase29ao_pattern1_subset_reject_extra_stmt_vm` +- Pattern1 (stdlib to_lower, VM): `phase29ap_stringutils_tolower_vm` - Pattern5 (Break, VM): `phase286_pattern5_break_vm` - Pattern5 (strict shadow, VM): `phase29ao_pattern5_strict_shadow_vm` - Pattern5 (release adopt, VM): `phase29ao_pattern5_release_adopt_vm` diff --git a/docs/development/current/main/phases/phase-29ap/P2-PATTERN1-STDLIB-TO_LOWER-SUBSET-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-29ap/P2-PATTERN1-STDLIB-TO_LOWER-SUBSET-INSTRUCTIONS.md new file mode 100644 index 00000000..f0ae6504 --- /dev/null +++ b/docs/development/current/main/phases/phase-29ap/P2-PATTERN1-STDLIB-TO_LOWER-SUBSET-INSTRUCTIONS.md @@ -0,0 +1,70 @@ +--- +Status: Ready +Scope: code+tests+docs (Pattern1 stdlib to_lower subset via Plan SSOT) +Related: + - docs/development/current/main/phases/phase-29ap/README.md + - docs/development/current/main/design/coreplan-migration-roadmap-ssot.md + - docs/development/current/main/phases/phase-29ae/README.md + - tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh + - apps/lib/json_native/utils/string.hako + - src/mir/builder/control_flow/plan/facts/pattern1_char_map_facts.rs + - src/mir/builder/control_flow/plan/normalizer/pattern1_char_map.rs + - src/mir/builder/control_flow/joinir/patterns/router.rs +--- + +# Phase 29ap P2: Pattern1 stdlib to_lower subset via Plan SSOT + +Date: 2025-12-30 +Status: Ready for execution +Goal: StringUtils.to_lower の loop を Plan/Facts/Normalizer で受理し、JoinIR Pattern1 依存を撤去する(既定挙動は不変)。 + +## 非目的 + +- Pattern1 の一般拡張(任意の body を許す) +- 新しい env var / 恒常ログ追加 +- 既存の release/strict 挙動やエラー文字列の変更 + +## 実装方針(SSOT) + +### 1) Facts: stdlib to_lower 形状を SSOT 化 + +- 新規: `pattern1_char_map_facts.rs` + - 形状(超保守): + - condition: `i < s.length()` のみ + - body: `local ch = s.substring(i, i+1)` / `result = result + this.method(ch)` / `i = i + 1` + - break/continue/return/if は不可 + - 失敗は `Ok(None)`(fallback 維持) + +### 2) Planner: DomainPlan を追加 + +- 新規: `DomainPlan::Pattern1CharMap` +- Facts から 1 候補だけ生成(ambiguous は Freeze) + +### 3) Normalizer: CorePlan へ拡張 + +- 新規: `normalize_pattern1_char_map` + - 2 PHI(loop var + result) + - substring → transform method → result add を step block で構成 + - static box の `this.method()` は `current_static_box` から const 受け口で処理 + +### 4) Smoke: to_lower を gate に入れる + +- 新規 fixture: `apps/tests/phase29ap_stringutils_tolower_min.hako` +- 新規 smoke: `tools/smokes/v2/profiles/integration/joinir/phase29ap_stringutils_tolower_vm.sh` +- gate へ追加: `phase29ae_regression_pack_vm.sh` +- docs: `phase-29ae/README.md` に追記 + +### 5) JoinIR router の Pattern1 は保留(stdlib join が依存) + +- `LOOP_PATTERNS` の Pattern1 は残す(StringUtils.join が plan で未対応なため) +- to_lower は Plan 経路で通ることを smoke で確認 + +## 検証(必須) + +- `cargo build --release` +- `./tools/smokes/v2/run.sh --profile quick` +- `./tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh` + +## コミット + +- `git commit -m "phase29ap(p2): route stdlib to_lower via plan char-map subset"` diff --git a/docs/development/current/main/phases/phase-29ap/README.md b/docs/development/current/main/phases/phase-29ap/README.md index 60c422f8..327d3ecb 100644 --- a/docs/development/current/main/phases/phase-29ap/README.md +++ b/docs/development/current/main/phases/phase-29ap/README.md @@ -33,7 +33,17 @@ Gate (SSOT): - Guardrails: - No change to pattern selection or logs. +## P2: Pattern1 stdlib to_lower subset (Plan SSOT) ✅ + +- Scope: + - Add char-map Facts/Planner/Normalizer path for StringUtils.to_lower (subset). + - Add regression smoke for stdlib to_lower and wire it into JoinIR gate. + - Keep Pattern1 in JoinIR legacy router table for stdlib join until a plan-based subset exists. +- Guardrails: + - No new logs or error strings. + - Fallback remains `Ok(None)` when shape does not match. + ## Next (planned) -- P2: Router pattern-name branching reduction (planner outcome + composer SSOT) -- P3: Facts/Feature expansion if needed +- P3: Router pattern-name branching reduction (planner outcome + composer SSOT) +- P4: Facts/Feature expansion if needed diff --git a/src/mir/builder/control_flow/plan/composer/coreloop_v0.rs b/src/mir/builder/control_flow/plan/composer/coreloop_v0.rs index 8727839f..927459e0 100644 --- a/src/mir/builder/control_flow/plan/composer/coreloop_v0.rs +++ b/src/mir/builder/control_flow/plan/composer/coreloop_v0.rs @@ -204,6 +204,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -278,6 +279,7 @@ mod tests { condition: condition.clone(), loop_increment: loop_increment.clone(), }), + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -340,6 +342,7 @@ mod tests { condition: condition.clone(), loop_increment: loop_increment.clone(), }), + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -409,6 +412,7 @@ mod tests { }), split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -474,6 +478,7 @@ mod tests { }), split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -538,6 +543,7 @@ mod tests { }), split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -588,6 +594,7 @@ mod tests { shape: SplitScanShape::Minimal, }), pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -658,6 +665,7 @@ mod tests { shape: SplitScanShape::Minimal, }), pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -711,6 +719,7 @@ mod tests { shape: SplitScanShape::Minimal, }), pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, diff --git a/src/mir/builder/control_flow/plan/composer/coreloop_v1.rs b/src/mir/builder/control_flow/plan/composer/coreloop_v1.rs index 1b31eff2..d750c531 100644 --- a/src/mir/builder/control_flow/plan/composer/coreloop_v1.rs +++ b/src/mir/builder/control_flow/plan/composer/coreloop_v1.rs @@ -213,6 +213,7 @@ mod tests { shape: SplitScanShape::Minimal, }), pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -279,6 +280,7 @@ mod tests { shape: SplitScanShape::Minimal, }), pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -330,6 +332,7 @@ mod tests { shape: SplitScanShape::Minimal, }), pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -378,6 +381,7 @@ mod tests { }), split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -446,6 +450,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -506,6 +511,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -577,6 +583,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: Some(Pattern5InfiniteEarlyExitFacts { @@ -639,6 +646,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: Some(Pattern5InfiniteEarlyExitFacts { @@ -720,6 +728,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: Some(Pattern3IfPhiFacts { loop_var: "i".to_string(), carrier_var: "sum".to_string(), @@ -780,6 +789,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: Some(Pattern3IfPhiFacts { loop_var: "i".to_string(), carrier_var: "sum".to_string(), diff --git a/src/mir/builder/control_flow/plan/composer/mod.rs b/src/mir/builder/control_flow/plan/composer/mod.rs index e097ac32..22f27cfc 100644 --- a/src/mir/builder/control_flow/plan/composer/mod.rs +++ b/src/mir/builder/control_flow/plan/composer/mod.rs @@ -129,6 +129,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -170,6 +171,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: Some(pattern1), + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -199,6 +201,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -240,6 +243,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: Some(pattern1), + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -277,6 +281,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -327,6 +332,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: Some(pattern1), + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -375,6 +381,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -437,6 +444,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: Some(pattern1), + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -513,6 +521,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: Some(pattern1), + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -600,6 +609,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: Some(pattern1), + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -669,6 +679,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: Some(pattern1), + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, diff --git a/src/mir/builder/control_flow/plan/extractors/pattern8.rs b/src/mir/builder/control_flow/plan/extractors/pattern8.rs index 5238dadb..876218f8 100644 --- a/src/mir/builder/control_flow/plan/extractors/pattern8.rs +++ b/src/mir/builder/control_flow/plan/extractors/pattern8.rs @@ -149,7 +149,7 @@ fn extract_predicate_check( // Extract receiver (e.g., "me") let receiver = match object.as_ref() { ASTNode::Variable { name, .. } => name.clone(), - ASTNode::Me { .. } => "me".to_string(), + ASTNode::Me { .. } | ASTNode::This { .. } => "me".to_string(), _ => continue, }; diff --git a/src/mir/builder/control_flow/plan/facts/loop_facts.rs b/src/mir/builder/control_flow/plan/facts/loop_facts.rs index 046ab444..ba88e64b 100644 --- a/src/mir/builder/control_flow/plan/facts/loop_facts.rs +++ b/src/mir/builder/control_flow/plan/facts/loop_facts.rs @@ -17,6 +17,9 @@ use super::scan_shapes::LengthMethod; use super::pattern1_simplewhile_facts::{ Pattern1SimpleWhileFacts, try_extract_pattern1_simplewhile_facts, }; +use super::pattern1_char_map_facts::{ + Pattern1CharMapFacts, try_extract_pattern1_char_map_facts, +}; use super::pattern3_ifphi_facts::{ Pattern3IfPhiFacts, try_extract_pattern3_ifphi_facts, }; @@ -48,6 +51,7 @@ pub(in crate::mir::builder) struct LoopFacts { pub scan_with_init: Option, pub split_scan: Option, pub pattern1_simplewhile: Option, + pub pattern1_char_map: Option, pub pattern3_ifphi: Option, pub pattern4_continue: Option, pub pattern5_infinite_early_exit: Option, @@ -92,7 +96,7 @@ pub(in crate::mir::builder) fn try_build_loop_facts_with_ctx( Some(_) => false, None => true, }; - let allow_pattern8 = !ctx.in_static_box; + let allow_pattern8 = true; try_build_loop_facts_inner(condition, body, allow_pattern1, allow_pattern8) } @@ -115,6 +119,11 @@ fn try_build_loop_facts_inner( } else { None }; + let pattern1_char_map = if allow_pattern1 { + try_extract_pattern1_char_map_facts(condition, body, &condition_shape, &step_shape)? + } else { + None + }; let pattern3_ifphi = try_extract_pattern3_ifphi_facts(condition, body)?; let pattern4_continue = try_extract_pattern4_continue_facts(condition, body)?; let pattern5_infinite_early_exit = @@ -136,6 +145,7 @@ fn try_build_loop_facts_inner( let has_any = scan_with_init.is_some() || split_scan.is_some() || pattern1_simplewhile.is_some() + || pattern1_char_map.is_some() || pattern3_ifphi.is_some() || pattern4_continue.is_some() || pattern5_infinite_early_exit.is_some() @@ -165,6 +175,7 @@ fn try_build_loop_facts_inner( scan_with_init, split_scan, pattern1_simplewhile, + pattern1_char_map, pattern3_ifphi, pattern4_continue, pattern5_infinite_early_exit, @@ -883,7 +894,7 @@ mod tests { } #[test] - fn loopfacts_ctx_blocks_pattern8_in_static_box() { + fn loopfacts_ctx_allows_pattern8_in_static_box() { let condition = ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(v("i")), @@ -899,7 +910,7 @@ mod tests { condition: Box::new(ASTNode::UnaryOp { operator: crate::ast::UnaryOperator::Not, operand: Box::new(ASTNode::MethodCall { - object: Box::new(ASTNode::Me { + object: Box::new(ASTNode::This { span: Span::unknown(), }), method: "is_digit".to_string(), @@ -965,11 +976,11 @@ mod tests { .and_then(|facts| facts.pattern8_bool_predicate_scan.as_ref()) .is_some()); - let blocked = try_build_loop_facts_with_ctx(&block_ctx, &condition, &body).expect("Ok"); - assert!(blocked + let allow_static = try_build_loop_facts_with_ctx(&block_ctx, &condition, &body).expect("Ok"); + assert!(allow_static .as_ref() .and_then(|facts| facts.pattern8_bool_predicate_scan.as_ref()) - .is_none()); + .is_some()); } #[test] diff --git a/src/mir/builder/control_flow/plan/facts/mod.rs b/src/mir/builder/control_flow/plan/facts/mod.rs index 5da21c4d..f6c68f87 100644 --- a/src/mir/builder/control_flow/plan/facts/mod.rs +++ b/src/mir/builder/control_flow/plan/facts/mod.rs @@ -9,6 +9,7 @@ pub(in crate::mir::builder) mod loop_facts; pub(in crate::mir::builder) mod feature_facts; pub(in crate::mir::builder) mod pattern1_simplewhile_facts; +pub(in crate::mir::builder) mod pattern1_char_map_facts; pub(in crate::mir::builder) mod pattern3_ifphi_facts; pub(in crate::mir::builder) mod pattern4_continue_facts; pub(in crate::mir::builder) mod pattern5_infinite_early_exit_facts; diff --git a/src/mir/builder/control_flow/plan/facts/pattern1_char_map_facts.rs b/src/mir/builder/control_flow/plan/facts/pattern1_char_map_facts.rs new file mode 100644 index 00000000..6408b0e7 --- /dev/null +++ b/src/mir/builder/control_flow/plan/facts/pattern1_char_map_facts.rs @@ -0,0 +1,351 @@ +//! Phase 29ap P2: Pattern1CharMapFacts (stdlib to_lower subset) + +use crate::ast::{ASTNode, BinaryOperator, LiteralValue}; +use crate::mir::builder::control_flow::plan::extractors::common_helpers::{ + extract_loop_increment_plan, has_break_statement, has_continue_statement, has_if_else_statement, + has_return_statement, +}; +use crate::mir::builder::control_flow::plan::facts::scan_shapes::{ConditionShape, StepShape}; +use crate::mir::builder::control_flow::plan::planner::Freeze; + +#[derive(Debug, Clone)] +pub(in crate::mir::builder) struct Pattern1CharMapFacts { + pub loop_var: String, + pub condition: ASTNode, + pub loop_increment: ASTNode, + pub haystack_var: String, + pub result_var: String, + pub receiver_var: String, + pub transform_method: String, +} + +pub(in crate::mir::builder) fn try_extract_pattern1_char_map_facts( + condition: &ASTNode, + body: &[ASTNode], + condition_shape: &ConditionShape, + step_shape: &StepShape, +) -> Result, Freeze> { + let ConditionShape::VarLessLength { + idx_var, + haystack_var, + .. + } = condition_shape + else { + return Ok(None); + }; + + let StepShape::AssignAddConst { var: step_var, k: 1 } = step_shape else { + return Ok(None); + }; + + if step_var != idx_var { + return Ok(None); + } + + if has_break_statement(body) || has_continue_statement(body) || has_return_statement(body) { + return Ok(None); + } + + if has_if_else_statement(body) { + return Ok(None); + } + + if body.len() != 3 { + return Ok(None); + } + + let Some(ch_var) = extract_local_substring(&body[0], idx_var, haystack_var) else { + return Ok(None); + }; + + let Some((result_var, receiver_var, transform_method)) = + extract_result_update(&body[1], &ch_var) + else { + return Ok(None); + }; + + let loop_increment = match extract_loop_increment_plan(body, idx_var) { + Ok(Some(inc)) => inc, + _ => return Ok(None), + }; + + Ok(Some(Pattern1CharMapFacts { + loop_var: idx_var.clone(), + condition: condition.clone(), + loop_increment, + haystack_var: haystack_var.clone(), + result_var, + receiver_var, + transform_method, + })) +} + +fn extract_local_substring( + stmt: &ASTNode, + idx_var: &str, + haystack_var: &str, +) -> Option { + 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(init) = &initial_values[0] else { + return None; + }; + + let ASTNode::MethodCall { + object, + method, + arguments, + .. + } = init.as_ref() + else { + return None; + }; + + if method != "substring" || arguments.len() != 2 { + return None; + } + + match object.as_ref() { + ASTNode::Variable { name, .. } if name == haystack_var => {} + _ => return None, + } + + if !matches!(&arguments[0], ASTNode::Variable { name, .. } if name == idx_var) { + return None; + } + + let ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left, + right, + .. + } = &arguments[1] + else { + return None; + }; + + if !matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == idx_var) { + return None; + } + + if !matches!( + right.as_ref(), + ASTNode::Literal { + value: LiteralValue::Integer(1), + .. + } + ) { + return None; + } + + Some(ch_var) +} + +fn extract_result_update(stmt: &ASTNode, ch_var: &str) -> Option<(String, String, String)> { + let ASTNode::Assignment { target, value, .. } = stmt else { + return None; + }; + + let ASTNode::Variable { name: result_var, .. } = target.as_ref() else { + return None; + }; + + let ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left, + right, + .. + } = value.as_ref() + else { + return None; + }; + + if !matches!(left.as_ref(), ASTNode::Variable { name, .. } if name == result_var) { + return None; + } + + let ASTNode::MethodCall { + object, + method, + arguments, + .. + } = right.as_ref() + else { + return None; + }; + + if arguments.len() != 1 { + return None; + } + + if !matches!(&arguments[0], ASTNode::Variable { name, .. } if name == ch_var) { + return None; + } + + let receiver_var = match object.as_ref() { + ASTNode::This { .. } | ASTNode::Me { .. } => "me".to_string(), + ASTNode::Variable { name, .. } => name.clone(), + _ => return None, + }; + + Some((result_var.clone(), receiver_var, method.clone())) +} + +#[cfg(test)] +mod tests { + use super::{try_extract_pattern1_char_map_facts, Pattern1CharMapFacts}; + use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; + use crate::mir::builder::control_flow::plan::facts::scan_shapes::{ + ConditionShape, LengthMethod, StepShape, + }; + + fn v(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: Span::unknown(), + } + } + + fn lit_int(value: i64) -> ASTNode { + ASTNode::Literal { + value: LiteralValue::Integer(value), + span: Span::unknown(), + } + } + + fn method_call(object: ASTNode, method: &str, args: Vec) -> ASTNode { + ASTNode::MethodCall { + object: Box::new(object), + method: method.to_string(), + arguments: args, + span: Span::unknown(), + } + } + + #[test] + fn pattern1_char_map_facts_extracts_minimal_tolower_shape() { + let condition = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(v("i")), + right: Box::new(method_call(v("s"), "length", vec![])), + span: Span::unknown(), + }; + let condition_shape = ConditionShape::VarLessLength { + idx_var: "i".to_string(), + haystack_var: "s".to_string(), + method: LengthMethod::Length, + }; + let step_shape = StepShape::AssignAddConst { + var: "i".to_string(), + k: 1, + }; + + let local_ch = ASTNode::Local { + variables: vec!["ch".to_string()], + initial_values: vec![Some(Box::new(method_call( + v("s"), + "substring", + vec![ + v("i"), + ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(v("i")), + right: Box::new(lit_int(1)), + span: Span::unknown(), + }, + ], + )))], + span: Span::unknown(), + }; + + let result_update = ASTNode::Assignment { + target: Box::new(v("result")), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(v("result")), + right: Box::new(method_call( + ASTNode::This { span: Span::unknown() }, + "char_to_lower", + vec![v("ch")], + )), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + let step = ASTNode::Assignment { + target: Box::new(v("i")), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(v("i")), + right: Box::new(lit_int(1)), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + let body = vec![local_ch, result_update, step]; + let facts = try_extract_pattern1_char_map_facts( + &condition, + &body, + &condition_shape, + &step_shape, + ) + .expect("Ok") + .expect("Some"); + + assert_eq!(facts.loop_var, "i"); + assert_eq!(facts.haystack_var, "s"); + assert_eq!(facts.result_var, "result"); + assert_eq!(facts.receiver_var, "me"); + assert_eq!(facts.transform_method, "char_to_lower"); + let _: Pattern1CharMapFacts = facts; + } + + #[test] + fn pattern1_char_map_facts_rejects_nonmatching_body() { + let condition = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(v("i")), + right: Box::new(method_call(v("s"), "length", vec![])), + span: Span::unknown(), + }; + let condition_shape = ConditionShape::VarLessLength { + idx_var: "i".to_string(), + haystack_var: "s".to_string(), + method: LengthMethod::Length, + }; + let step_shape = StepShape::AssignAddConst { + var: "i".to_string(), + k: 1, + }; + + let body = vec![ASTNode::Assignment { + target: Box::new(v("i")), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(v("i")), + right: Box::new(lit_int(1)), + span: Span::unknown(), + }), + span: Span::unknown(), + }]; + + let facts = + try_extract_pattern1_char_map_facts(&condition, &body, &condition_shape, &step_shape) + .expect("Ok"); + assert!(facts.is_none()); + } +} diff --git a/src/mir/builder/control_flow/plan/facts/pattern8_bool_predicate_scan_facts.rs b/src/mir/builder/control_flow/plan/facts/pattern8_bool_predicate_scan_facts.rs index 63919737..fcd3ecba 100644 --- a/src/mir/builder/control_flow/plan/facts/pattern8_bool_predicate_scan_facts.rs +++ b/src/mir/builder/control_flow/plan/facts/pattern8_bool_predicate_scan_facts.rs @@ -121,7 +121,7 @@ fn extract_predicate_check( let receiver = match object.as_ref() { ASTNode::Variable { name, .. } => name.clone(), - ASTNode::Me { .. } => "me".to_string(), + ASTNode::Me { .. } | ASTNode::This { .. } => "me".to_string(), _ => continue, }; @@ -227,6 +227,10 @@ mod tests { ASTNode::Me { span: Span::unknown(), } + } else if receiver == "this" { + ASTNode::This { + span: Span::unknown(), + } } else { v(receiver) }), @@ -299,7 +303,7 @@ mod tests { fn facts_extracts_pattern8_success() { let condition = condition_length("i", "s"); let body = vec![ - predicate_if("me", "is_digit", "s", "i"), + predicate_if("this", "is_digit", "s", "i"), loop_increment("i", 1), ]; diff --git a/src/mir/builder/control_flow/plan/lowerer.rs b/src/mir/builder/control_flow/plan/lowerer.rs index 0124e4fa..59f36d55 100644 --- a/src/mir/builder/control_flow/plan/lowerer.rs +++ b/src/mir/builder/control_flow/plan/lowerer.rs @@ -18,6 +18,7 @@ //! - Pattern-specific emission functions (emit_scan_with_init_edgecfg) no longer used use super::{CoreEffectPlan, CoreExitPlan, CoreIfPlan, CoreLoopPlan, CorePlan}; +use crate::mir::builder::calls::CallTarget; use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext; use crate::mir::builder::MirBuilder; use crate::mir::{MirInstruction, ValueId}; @@ -300,6 +301,9 @@ impl PlanLowerer { effects: *effects, })?; } + CoreEffectPlan::GlobalCall { dst, func, args } => { + builder.emit_unified_call(*dst, CallTarget::Global(func.clone()), args.clone())?; + } CoreEffectPlan::BinOp { dst, lhs, op, rhs } => { builder.emit_instruction(MirInstruction::BinOp { dst: *dst, diff --git a/src/mir/builder/control_flow/plan/mod.rs b/src/mir/builder/control_flow/plan/mod.rs index dbd8a5e6..d0677684 100644 --- a/src/mir/builder/control_flow/plan/mod.rs +++ b/src/mir/builder/control_flow/plan/mod.rs @@ -63,6 +63,8 @@ pub(in crate::mir::builder) enum DomainPlan { Pattern4Continue(Pattern4ContinuePlan), /// Pattern1: Simple While Loop (Phase 286 P2.1) Pattern1SimpleWhile(Pattern1SimpleWhilePlan), + /// Pattern1: Char map loop (Phase 29ap P2) + Pattern1CharMap(Pattern1CharMapPlan), /// Pattern9: Accumulator Const Loop (Phase 286 P2.3) Pattern9AccumConstLoop(Pattern9AccumConstLoopPlan), /// Pattern8: Boolean Predicate Scan (Phase 286 P2.4) @@ -159,6 +161,27 @@ pub(in crate::mir::builder) struct Pattern1SimpleWhilePlan { pub loop_increment: ASTNode, } +/// Phase 29ap P2: Extracted structure for Pattern1 char-map loop +/// +/// This structure captures the stdlib-style `to_lower`/`to_upper` loop shape. +#[derive(Debug, Clone)] +pub(in crate::mir::builder) struct Pattern1CharMapPlan { + /// Loop variable name (e.g., "i") + pub loop_var: String, + /// Loop condition AST (e.g., `i < s.length()`) + pub condition: ASTNode, + /// Loop increment expression AST (e.g., `i + 1`) + pub loop_increment: ASTNode, + /// Haystack variable name (e.g., "s") + pub haystack_var: String, + /// Result accumulator variable name (e.g., "result") + pub result_var: String, + /// Receiver variable name for the transform method (e.g., "me") + pub receiver_var: String, + /// Transform method name (e.g., "char_to_lower") + pub transform_method: String, +} + /// Phase 286 P2.3: Extracted structure for Pattern9 (Accumulator Const Loop) /// /// This structure contains all the information needed to lower an accumulator loop. @@ -482,6 +505,12 @@ pub(in crate::mir::builder) enum CoreEffectPlan { args: Vec, effects: EffectMask, // P2: Side effect mask (PURE+Io or MUT) }, + /// Global/static call (box-level or free function) + GlobalCall { + dst: Option, + func: String, + args: Vec, + }, /// Binary operation BinOp { diff --git a/src/mir/builder/control_flow/plan/normalize/canonicalize.rs b/src/mir/builder/control_flow/plan/normalize/canonicalize.rs index 9d236125..09c14b3a 100644 --- a/src/mir/builder/control_flow/plan/normalize/canonicalize.rs +++ b/src/mir/builder/control_flow/plan/normalize/canonicalize.rs @@ -98,6 +98,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -134,6 +135,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -176,6 +178,7 @@ mod tests { }, }, ), + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, diff --git a/src/mir/builder/control_flow/plan/normalizer/mod.rs b/src/mir/builder/control_flow/plan/normalizer/mod.rs index 509a796d..aa9588de 100644 --- a/src/mir/builder/control_flow/plan/normalizer/mod.rs +++ b/src/mir/builder/control_flow/plan/normalizer/mod.rs @@ -13,6 +13,7 @@ mod helpers; mod pattern1_coreloop_builder; +mod pattern1_char_map; mod pattern1_simple_while; mod pattern2_break; mod pattern3_if_phi; @@ -29,10 +30,10 @@ mod common; mod value_join_demo_if2; use super::{ - CoreEffectPlan, CoreLoopPlan, CorePhiInfo, CorePlan, DomainPlan, Pattern1SimpleWhilePlan, - Pattern2BreakPlan, Pattern3IfPhiPlan, Pattern4ContinuePlan, Pattern5ExitKind, - Pattern5InfiniteEarlyExitPlan, Pattern8BoolPredicateScanPlan, Pattern9AccumConstLoopPlan, - ScanWithInitPlan, SplitScanPlan, + CoreEffectPlan, CoreLoopPlan, CorePhiInfo, CorePlan, DomainPlan, Pattern1CharMapPlan, + Pattern1SimpleWhilePlan, Pattern2BreakPlan, Pattern3IfPhiPlan, Pattern4ContinuePlan, + Pattern5ExitKind, Pattern5InfiniteEarlyExitPlan, Pattern8BoolPredicateScanPlan, + Pattern9AccumConstLoopPlan, ScanWithInitPlan, SplitScanPlan, }; use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext; use crate::mir::builder::MirBuilder; @@ -61,6 +62,9 @@ impl PlanNormalizer { DomainPlan::Pattern1SimpleWhile(parts) => { Self::normalize_pattern1_simple_while(builder, parts, ctx) } + DomainPlan::Pattern1CharMap(parts) => { + Self::normalize_pattern1_char_map(builder, parts, ctx) + } DomainPlan::Pattern9AccumConstLoop(parts) => { Self::normalize_pattern9_accum_const_loop(builder, parts, ctx) } diff --git a/src/mir/builder/control_flow/plan/normalizer/pattern1_char_map.rs b/src/mir/builder/control_flow/plan/normalizer/pattern1_char_map.rs new file mode 100644 index 00000000..3aa07b97 --- /dev/null +++ b/src/mir/builder/control_flow/plan/normalizer/pattern1_char_map.rs @@ -0,0 +1,244 @@ +use super::helpers::{create_phi_bindings, LoopBlocksStandard5}; +use super::{CoreEffectPlan, CoreLoopPlan, CorePhiInfo, CorePlan, Pattern1CharMapPlan}; +use crate::mir::basic_block::EdgeArgs; +use crate::mir::builder::control_flow::edgecfg::api::{BranchStub, EdgeStub, ExitKind, Frag}; +use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext; +use crate::mir::builder::MirBuilder; +use crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout; +use crate::mir::{BinaryOp, Effect, EffectMask, MirType}; +use std::collections::BTreeMap; + +impl super::PlanNormalizer { + /// Phase 29ap P2: Pattern1CharMap → CorePlan conversion + /// + /// Expands a stdlib-style char-map loop into a generic CorePlan: + /// - CFG structure: preheader → header → body → step → header (back-edge) + /// - 2 PHIs in header: loop index + result accumulator + /// - All body effects are emitted in the step block (body stays empty) + pub(super) fn normalize_pattern1_char_map( + builder: &mut MirBuilder, + parts: Pattern1CharMapPlan, + ctx: &LoopPatternContext, + ) -> Result { + use crate::mir::builder::control_flow::joinir::trace; + + let trace_logger = trace::trace(); + let debug = ctx.debug; + + if debug { + trace_logger.debug( + "normalizer/pattern1_char_map", + &format!( + "Phase 29ap P2: Normalizing Pattern1CharMap for {} (loop_var: {}, result: {})", + ctx.func_name, parts.loop_var, parts.result_var + ), + ); + } + + let loop_var_init = builder + .variable_ctx + .variable_map + .get(&parts.loop_var) + .copied() + .ok_or_else(|| format!("[normalizer] Loop variable {} not found", parts.loop_var))?; + + let result_init = builder + .variable_ctx + .variable_map + .get(&parts.result_var) + .copied() + .ok_or_else(|| format!("[normalizer] Result variable {} not found", parts.result_var))?; + + let haystack = builder + .variable_ctx + .variable_map + .get(&parts.haystack_var) + .copied() + .ok_or_else(|| format!("[normalizer] Haystack {} not found", parts.haystack_var))?; + + let mut static_box_name: Option = None; + let receiver = if let Some(&value) = builder + .variable_ctx + .variable_map + .get(&parts.receiver_var) + { + Some(value) + } else if parts.receiver_var == "me" && ctx.in_static_box { + let Some(box_name) = builder.comp_ctx.current_static_box.clone() else { + return Err("[normalizer] Static receiver missing current_static_box".to_string()); + }; + static_box_name = Some(box_name); + None + } else { + return Err(format!( + "[normalizer] Receiver {} not found", + parts.receiver_var + )); + }; + + let blocks = LoopBlocksStandard5::allocate(builder)?; + let LoopBlocksStandard5 { + preheader_bb, + header_bb, + body_bb, + step_bb, + after_bb, + } = blocks; + + let loop_var_current = builder.alloc_typed(MirType::Integer); + let result_current = builder.alloc_typed(MirType::String); + let cond_loop = builder.alloc_typed(MirType::Bool); + let loop_var_next = builder.alloc_typed(MirType::Integer); + let result_next = builder.alloc_typed(MirType::String); + let ch_value = builder.alloc_typed(MirType::String); + let mapped_value = builder.alloc_typed(MirType::String); + + let phi_bindings = create_phi_bindings(&[ + (&parts.loop_var, loop_var_current), + (&parts.result_var, result_current), + ]); + + let (loop_cond_lhs, loop_cond_op, loop_cond_rhs, loop_cond_consts) = + Self::lower_compare_ast(&parts.condition, builder, &phi_bindings)?; + + let (loop_inc_lhs, loop_inc_op, loop_inc_rhs, loop_inc_consts) = + Self::lower_binop_ast(&parts.loop_increment, builder, &phi_bindings)?; + + let preheader_effects = Vec::new(); + + let mut header_effects = loop_cond_consts; + header_effects.push(CoreEffectPlan::Compare { + dst: cond_loop, + lhs: loop_cond_lhs, + op: loop_cond_op, + rhs: loop_cond_rhs, + }); + + let mut step_effects = loop_inc_consts; + step_effects.push(CoreEffectPlan::BinOp { + dst: loop_var_next, + lhs: loop_inc_lhs, + op: loop_inc_op, + rhs: loop_inc_rhs, + }); + step_effects.push(CoreEffectPlan::MethodCall { + dst: Some(ch_value), + object: haystack, + method: "substring".to_string(), + args: vec![loop_var_current, loop_var_next], + effects: EffectMask::PURE.add(Effect::Io), + }); + if let Some(box_name) = static_box_name { + let func_name = format!("{}.{}/{}", box_name, parts.transform_method, 1); + step_effects.push(CoreEffectPlan::GlobalCall { + dst: Some(mapped_value), + func: func_name, + args: vec![ch_value], + }); + } else { + let receiver_id = receiver.ok_or_else(|| "[normalizer] Missing receiver".to_string())?; + step_effects.push(CoreEffectPlan::MethodCall { + dst: Some(mapped_value), + object: receiver_id, + method: parts.transform_method.clone(), + args: vec![ch_value], + effects: EffectMask::PURE.add(Effect::Io), + }); + } + step_effects.push(CoreEffectPlan::BinOp { + dst: result_next, + lhs: result_current, + op: BinaryOp::Add, + rhs: mapped_value, + }); + + let block_effects = vec![ + (preheader_bb, preheader_effects), + (header_bb, header_effects), + (body_bb, vec![]), + (step_bb, step_effects), + ]; + + let phis = vec![ + CorePhiInfo { + block: header_bb, + dst: loop_var_current, + inputs: vec![(preheader_bb, loop_var_init), (step_bb, loop_var_next)], + tag: format!("loop_var_{}", parts.loop_var), + }, + CorePhiInfo { + block: header_bb, + dst: result_current, + inputs: vec![(preheader_bb, result_init), (step_bb, result_next)], + tag: format!("result_var_{}", parts.result_var), + }, + ]; + + let empty_args = EdgeArgs { + layout: JumpArgsLayout::CarriersOnly, + values: vec![], + }; + + let branches = vec![BranchStub { + from: header_bb, + cond: cond_loop, + then_target: body_bb, + then_args: empty_args.clone(), + else_target: after_bb, + else_args: empty_args.clone(), + }]; + + let wires = vec![ + EdgeStub { + from: body_bb, + kind: ExitKind::Normal, + target: Some(step_bb), + args: empty_args.clone(), + }, + EdgeStub { + from: step_bb, + kind: ExitKind::Normal, + target: Some(header_bb), + args: empty_args.clone(), + }, + ]; + + let frag = Frag { + entry: header_bb, + block_params: BTreeMap::new(), + exits: BTreeMap::new(), + wires, + branches, + }; + + let final_values = vec![ + (parts.loop_var.clone(), loop_var_current), + (parts.result_var.clone(), result_current), + ]; + + let loop_plan = CoreLoopPlan { + preheader_bb, + header_bb, + body_bb, + step_bb, + after_bb, + found_bb: after_bb, + body: vec![], + cond_loop, + cond_match: cond_loop, + block_effects, + phis, + frag, + final_values, + }; + + if debug { + trace_logger.debug( + "normalizer/pattern1_char_map", + "CorePlan construction complete (5 blocks, 2 PHIs)", + ); + } + + Ok(CorePlan::Loop(loop_plan)) + } +} diff --git a/src/mir/builder/control_flow/plan/normalizer/pattern8_bool_predicate_scan.rs b/src/mir/builder/control_flow/plan/normalizer/pattern8_bool_predicate_scan.rs index f9b57aa8..583dd7a8 100644 --- a/src/mir/builder/control_flow/plan/normalizer/pattern8_bool_predicate_scan.rs +++ b/src/mir/builder/control_flow/plan/normalizer/pattern8_bool_predicate_scan.rs @@ -53,17 +53,27 @@ impl super::PlanNormalizer { .copied() .ok_or_else(|| format!("[normalizer/pattern8] Variable {} not found", parts.haystack))?; - let predicate_receiver_host = builder + let mut static_box_name: Option = None; + let predicate_receiver_host = if let Some(&value) = builder .variable_ctx .variable_map .get(&parts.predicate_receiver) - .copied() - .ok_or_else(|| { - format!( - "[normalizer/pattern8] Variable {} not found", - parts.predicate_receiver - ) - })?; + { + Some(value) + } else if parts.predicate_receiver == "me" && ctx.in_static_box { + let Some(box_name) = builder.comp_ctx.current_static_box.clone() else { + return Err( + "[normalizer/pattern8] Static receiver missing current_static_box".to_string(), + ); + }; + static_box_name = Some(box_name); + None + } else { + return Err(format!( + "[normalizer/pattern8] Variable {} not found", + parts.predicate_receiver + )); + }; let loop_var_init = builder .variable_ctx @@ -123,6 +133,25 @@ impl super::PlanNormalizer { }); // Step 8: Build body (predicate check) + let predicate_call = if let Some(box_name) = static_box_name { + let func_name = format!("{}.{}/{}", box_name, parts.predicate_method, 1); + CorePlan::Effect(CoreEffectPlan::GlobalCall { + dst: Some(cond_predicate), + func: func_name, + args: vec![ch], + }) + } else { + let receiver_id = predicate_receiver_host + .ok_or_else(|| "[normalizer/pattern8] Missing predicate receiver".to_string())?; + CorePlan::Effect(CoreEffectPlan::MethodCall { + dst: Some(cond_predicate), + object: receiver_id, + method: parts.predicate_method.clone(), + args: vec![ch], + effects: EffectMask::PURE.add(Effect::Io), + }) + }; + let body = vec![ CorePlan::Effect(CoreEffectPlan::Const { dst: one_val, @@ -145,13 +174,7 @@ impl super::PlanNormalizer { args: vec![loop_var_current, i_plus_one], effects: EffectMask::PURE.add(Effect::Io), }), - CorePlan::Effect(CoreEffectPlan::MethodCall { - dst: Some(cond_predicate), - object: predicate_receiver_host, - method: parts.predicate_method.clone(), - args: vec![ch], - effects: EffectMask::PURE.add(Effect::Io), - }), + predicate_call, CorePlan::Effect(CoreEffectPlan::Compare { dst: cond_not_predicate, lhs: cond_predicate, diff --git a/src/mir/builder/control_flow/plan/planner/build.rs b/src/mir/builder/control_flow/plan/planner/build.rs index 3e987ba0..905d956c 100644 --- a/src/mir/builder/control_flow/plan/planner/build.rs +++ b/src/mir/builder/control_flow/plan/planner/build.rs @@ -17,10 +17,10 @@ use super::context::PlannerContext; use super::outcome::build_plan_with_facts; use super::Freeze; use crate::mir::builder::control_flow::plan::{ - DomainPlan, Pattern1SimpleWhilePlan, Pattern2BreakPlan, Pattern2PromotionHint, Pattern3IfPhiPlan, - Pattern4ContinuePlan, Pattern5ExitKind, Pattern5InfiniteEarlyExitPlan, - Pattern8BoolPredicateScanPlan, Pattern9AccumConstLoopPlan, ScanDirection, ScanWithInitPlan, - SplitScanPlan, + DomainPlan, Pattern1CharMapPlan, Pattern1SimpleWhilePlan, Pattern2BreakPlan, + Pattern2PromotionHint, Pattern3IfPhiPlan, Pattern4ContinuePlan, Pattern5ExitKind, + Pattern5InfiniteEarlyExitPlan, Pattern8BoolPredicateScanPlan, Pattern9AccumConstLoopPlan, + ScanDirection, ScanWithInitPlan, SplitScanPlan, }; use crate::mir::loop_pattern_detection::LoopPatternKind; @@ -59,7 +59,7 @@ pub(in crate::mir::builder) fn build_plan_from_facts_ctx( Some(LoopPatternKind::Pattern1SimpleWhile) | None => true, Some(_) => false, }; - let allow_pattern8 = !ctx.in_static_box; + let allow_pattern8 = true; let _skeleton_kind = infer_skeleton_kind(&facts); let _exit_usage = infer_exit_usage(&facts); @@ -79,6 +79,7 @@ pub(in crate::mir::builder) fn build_plan_from_facts_ctx( push_pattern5_infinite_early_exit(&mut candidates, &facts); push_pattern8_bool_predicate_scan(&mut candidates, &facts, allow_pattern8); push_pattern9_accum_const_loop(&mut candidates, &facts); + push_pattern1_char_map(&mut candidates, &facts, allow_pattern1); push_pattern1_simplewhile(&mut candidates, &facts, allow_pattern1); candidates.finalize() @@ -120,6 +121,12 @@ fn debug_assert_exit_usage_matches_plan( "pattern1 requires no exit usage" ); } + DomainPlan::Pattern1CharMap(_) => { + debug_assert!( + !exit_usage.has_break && !exit_usage.has_continue && !exit_usage.has_return, + "pattern1 char map requires no exit usage" + ); + } DomainPlan::Pattern2Break(_) => { debug_assert!(exit_usage.has_break, "pattern2 requires break usage"); } @@ -332,6 +339,32 @@ fn push_pattern9_accum_const_loop(candidates: &mut CandidateSet, facts: &Canonic }); } +fn push_pattern1_char_map( + candidates: &mut CandidateSet, + facts: &CanonicalLoopFacts, + allow_pattern1: bool, +) { + if !allow_pattern1 { + return; + } + let Some(map) = &facts.facts.pattern1_char_map else { + return; + }; + + candidates.push(PlanCandidate { + plan: DomainPlan::Pattern1CharMap(Pattern1CharMapPlan { + loop_var: map.loop_var.clone(), + condition: map.condition.clone(), + loop_increment: map.loop_increment.clone(), + haystack_var: map.haystack_var.clone(), + result_var: map.result_var.clone(), + receiver_var: map.receiver_var.clone(), + transform_method: map.transform_method.clone(), + }), + rule: "loop/pattern1_char_map", + }); +} + fn push_pattern1_simplewhile( candidates: &mut CandidateSet, facts: &CanonicalLoopFacts, @@ -370,6 +403,9 @@ mod tests { use crate::mir::builder::control_flow::plan::facts::pattern1_simplewhile_facts::{ Pattern1SimpleWhileFacts, }; + use crate::mir::builder::control_flow::plan::facts::pattern1_char_map_facts::{ + Pattern1CharMapFacts, + }; use crate::mir::builder::control_flow::plan::facts::pattern3_ifphi_facts::{ Pattern3IfPhiFacts, }; @@ -454,6 +490,7 @@ mod tests { shape: crate::mir::builder::control_flow::plan::facts::scan_shapes::SplitScanShape::Minimal, }), pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -489,6 +526,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -519,6 +557,7 @@ mod tests { }), split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -558,6 +597,7 @@ mod tests { }), split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -591,6 +631,7 @@ mod tests { }), split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -634,6 +675,7 @@ mod tests { condition: loop_condition.clone(), loop_increment: loop_increment.clone(), }), + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -653,6 +695,67 @@ mod tests { } } + #[test] + fn planner_builds_pattern1_char_map_plan_from_facts() { + let loop_condition = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(v("i")), + right: Box::new(ASTNode::MethodCall { + object: Box::new(v("s")), + method: "length".to_string(), + arguments: vec![], + span: Span::unknown(), + }), + span: Span::unknown(), + }; + let loop_increment = ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(v("i")), + right: Box::new(lit_int(1)), + span: Span::unknown(), + }; + + let facts = LoopFacts { + condition_shape: ConditionShape::Unknown, + step_shape: StepShape::Unknown, + skeleton: SkeletonFacts { + kind: SkeletonKind::Loop, + }, + features: LoopFeatureFacts::default(), + scan_with_init: None, + split_scan: None, + pattern1_simplewhile: None, + pattern1_char_map: Some(Pattern1CharMapFacts { + loop_var: "i".to_string(), + condition: loop_condition.clone(), + loop_increment: loop_increment.clone(), + haystack_var: "s".to_string(), + result_var: "result".to_string(), + receiver_var: "me".to_string(), + transform_method: "char_to_lower".to_string(), + }), + pattern3_ifphi: None, + pattern4_continue: None, + pattern5_infinite_early_exit: None, + pattern8_bool_predicate_scan: None, + pattern9_accum_const_loop: None, + pattern2_break: None, + pattern2_loopbodylocal: None, + }; + let canonical = canonicalize_loop_facts(facts); + let plan = build_plan_from_facts(canonical).expect("Ok"); + + match plan { + Some(DomainPlan::Pattern1CharMap(plan)) => { + assert_eq!(plan.loop_var, "i"); + assert_eq!(plan.haystack_var, "s"); + assert_eq!(plan.result_var, "result"); + assert_eq!(plan.transform_method, "char_to_lower"); + } + other => panic!("expected pattern1 char map plan, got {:?}", other), + } + } + #[test] fn debug_exit_usage_invariant_pattern1_ok() { let loop_condition = ASTNode::BinaryOp { @@ -681,6 +784,7 @@ mod tests { condition: loop_condition.clone(), loop_increment: loop_increment.clone(), }), + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -734,6 +838,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -789,6 +894,7 @@ mod tests { condition: loop_condition.clone(), loop_increment: loop_increment.clone(), }), + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -844,6 +950,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: Some(Pattern3IfPhiFacts { loop_var: "i".to_string(), carrier_var: "sum".to_string(), @@ -913,6 +1020,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: Some(Pattern4ContinueFacts { loop_var: "i".to_string(), @@ -968,6 +1076,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: Some(Pattern5InfiniteEarlyExitFacts { @@ -1024,6 +1133,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -1084,6 +1194,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -1150,6 +1261,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, diff --git a/src/mir/builder/control_flow/plan/single_planner/rules.rs b/src/mir/builder/control_flow/plan/single_planner/rules.rs index c87f8947..27fc126e 100644 --- a/src/mir/builder/control_flow/plan/single_planner/rules.rs +++ b/src/mir/builder/control_flow/plan/single_planner/rules.rs @@ -34,7 +34,7 @@ pub(super) fn try_build_domain_plan_with_outcome( let rule_id = *rule_id; let name = rule_name(rule_id); let planner_hit = try_take_planner(&planner_opt, rule_id); - let allow_pattern8 = !ctx.in_static_box; + let allow_pattern8 = true; let (plan_opt, log_none) = if planner_hit.is_some() { (planner_hit, false) } else { @@ -72,6 +72,7 @@ pub(super) fn try_build_domain_plan_with_outcome( fn try_take_planner(planner_opt: &Option, kind: PlanRuleId) -> Option { match (kind, planner_opt.as_ref()) { (PlanRuleId::Pattern1, Some(DomainPlan::Pattern1SimpleWhile(_))) => planner_opt.clone(), + (PlanRuleId::Pattern1, Some(DomainPlan::Pattern1CharMap(_))) => planner_opt.clone(), (PlanRuleId::Pattern2, Some(DomainPlan::Pattern2Break(_))) => planner_opt.clone(), (PlanRuleId::Pattern3, Some(DomainPlan::Pattern3IfPhi(_))) => planner_opt.clone(), (PlanRuleId::Pattern4, Some(DomainPlan::Pattern4Continue(_))) => planner_opt.clone(), diff --git a/src/mir/builder/control_flow/plan/verifier.rs b/src/mir/builder/control_flow/plan/verifier.rs index ce3b2f0a..8c204e79 100644 --- a/src/mir/builder/control_flow/plan/verifier.rs +++ b/src/mir/builder/control_flow/plan/verifier.rs @@ -248,6 +248,17 @@ impl PlanVerifier { Self::verify_value_id_basic(*arg, depth, &format!("MethodCall.args[{}]", i))?; } } + CoreEffectPlan::GlobalCall { dst, func, args } => { + if let Some(dst_val) = dst { + Self::verify_value_id_basic(*dst_val, depth, "GlobalCall.dst")?; + } + if func.is_empty() { + return Err(format!("[V6] GlobalCall at depth {} has empty func", depth)); + } + for (i, arg) in args.iter().enumerate() { + Self::verify_value_id_basic(*arg, depth, &format!("GlobalCall.args[{}]", i))?; + } + } CoreEffectPlan::BinOp { dst, lhs, op: _, rhs } => { Self::verify_value_id_basic(*dst, depth, "BinOp.dst")?; Self::verify_value_id_basic(*lhs, depth, "BinOp.lhs")?; @@ -619,6 +630,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, @@ -650,6 +662,7 @@ mod tests { scan_with_init: None, split_scan: None, pattern1_simplewhile: None, + pattern1_char_map: None, pattern3_ifphi: None, pattern4_continue: None, pattern5_infinite_early_exit: None, diff --git a/tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh b/tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh index 66999381..b73ce991 100644 --- a/tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh +++ b/tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh @@ -25,6 +25,7 @@ run_filter "pattern3_ifphi_vm" "phase118_pattern3_if_sum_vm" || exit 1 run_filter "pattern3_release_adopt_vm" "phase29ao_pattern3_release_adopt_vm" || exit 1 run_filter "pattern1_strict_shadow_vm" "phase29ao_pattern1_strict_shadow_vm" || exit 1 run_filter "pattern1_subset_reject_extra_stmt_vm" "phase29ao_pattern1_subset_reject_extra_stmt_vm" || exit 1 +run_filter "pattern1_stringutils_tolower_vm" "phase29ap_stringutils_tolower_vm" || exit 1 run_filter "pattern5_break_vm" "phase286_pattern5_break_vm" || exit 1 run_filter "pattern5_strict_shadow_vm" "phase29ao_pattern5_strict_shadow_vm" || exit 1 run_filter "pattern5_release_adopt_vm" "phase29ao_pattern5_release_adopt_vm" || exit 1 diff --git a/tools/smokes/v2/profiles/integration/joinir/phase29ap_stringutils_tolower_vm.sh b/tools/smokes/v2/profiles/integration/joinir/phase29ap_stringutils_tolower_vm.sh new file mode 100644 index 00000000..9165d871 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/joinir/phase29ap_stringutils_tolower_vm.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# phase29ap_stringutils_tolower_vm.sh - StringUtils.to_lower via plan/composer (VM) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +require_env || exit 2 + +FIXTURE="$NYASH_ROOT/apps/tests/phase29ap_stringutils_tolower_min.hako" +export NYASH_ALLOW_USING_FILE=1 + +output=$(run_nyash_vm "$FIXTURE") + +expected=$(cat << 'TXT' +world +TXT +) + +compare_outputs "$expected" "$output" "phase29ap_stringutils_tolower_vm" || exit 1