From 1d24e9a106cc2c259a998084c58ca549c702b247 Mon Sep 17 00:00:00 2001 From: tomoaki Date: Fri, 26 Dec 2025 02:38:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(joinir):=20Phase=20286=20P2.3=20+=20Phase?= =?UTF-8?q?=20287=20-=20Pattern9=20Plan=E5=8C=96=20+=20Router=20table-driv?= =?UTF-8?q?en?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Phase 286 P2.3: Pattern9 AccumConstLoop Plan化 PoC - DomainPlan::Pattern9AccumConstLoop 追加 - PlanNormalizer::normalize_pattern9_accum_const_loop() 実装 - PHI 2本(loop_var, acc_var) - const/var 両方 OK(sum = sum + 1 または sum = sum + i) - Pattern9 は Pattern1 より優先(より具体的なパターン) - Integration test: phase286_pattern9_frag_poc PASS (return: 3) - Regression: quick 154 PASS ## Phase 287: Router table-driven Plan extraction - PLAN_EXTRACTORS static table で Pattern6/7/4/9/1 を統一管理 - PlanExtractorEntry/PlanExtractorVariant 構造体追加 - try_plan_extractors() で ~100行 → 3行に集約 - メンテナンス性向上(新 Pattern 追加はテーブル1行追加のみ) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CURRENT_TASK.md | 4 +- apps/tests/phase286_pattern9_frag_poc.hako | 17 + docs/development/current/main/30-Backlog.md | 7 +- .../main/joinir-architecture-overview.md | 5 + .../current/main/phases/phase-286/README.md | 29 +- .../joinir/patterns/extractors/mod.rs | 1 + .../joinir/patterns/extractors/pattern9.rs | 318 ++++++++++++++++++ .../control_flow/joinir/patterns/router.rs | 199 ++++++----- src/mir/builder/control_flow/plan/mod.rs | 20 ++ .../builder/control_flow/plan/normalizer.rs | 223 +++++++++++- .../apps/phase286_pattern9_frag_poc.sh | 36 ++ 11 files changed, 760 insertions(+), 99 deletions(-) create mode 100644 apps/tests/phase286_pattern9_frag_poc.hako create mode 100644 src/mir/builder/control_flow/joinir/patterns/extractors/pattern9.rs create mode 100644 tools/smokes/v2/profiles/integration/apps/phase286_pattern9_frag_poc.sh diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index f26260d1..b80b9c25 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -15,8 +15,8 @@ Scope: Repo root の旧リンク互換。現行の入口は `docs/development/cu **2025-12-26: Phase 286 P2.2 COMPLETE (Hygiene: extractor重複排除 + router小整理)** Pattern1/Pattern4 の Plan/Frag PoC 完了後、extractor の `extract_loop_increment_plan` を `common_helpers.rs` に統一、router の 3行パターン(normalize→verify→lower)を `lower_via_plan()` ヘルパーで共通化。~65行削減、quick 154 PASS 維持。 -**2025-12-26: Phase 286 P2.3 準備中 (Pattern9 Plan化 PoC 準備)** -次のPoC対象は Pattern9 (AccumConstLoop) に決定。橋渡しパターンで粒度が小さく、Plan化の流れが確立しているため早期完了が期待できる。P2/P2.1 と同じ戦略:extract PoC サブセット → normalize CorePlan → router 統合。 +**2025-12-26: Phase 286 P2.3 COMPLETE (Pattern9 AccumConstLoop Plan化 PoC)** +Pattern9 (AccumConstLoop) を Plan/Frag SSOT に移行完了。PHI 2本(loop_var, acc_var)、const/var 両方 OK。Pattern9 は Pattern1 より先にチェック(より具体的なパターン)。quick 154 PASS 維持。 - 現行の入口: `docs/development/current/main/10-Now.md` - Phase 286 詳細: `docs/development/current/main/phases/phase-286/README.md` diff --git a/apps/tests/phase286_pattern9_frag_poc.hako b/apps/tests/phase286_pattern9_frag_poc.hako new file mode 100644 index 00000000..fbe67a6d --- /dev/null +++ b/apps/tests/phase286_pattern9_frag_poc.hako @@ -0,0 +1,17 @@ +// Phase 286 P2.3: Pattern9 → Frag PoC minimal fixture +// Expected: return 3 (sum = 0 + 1 + 1 + 1 = 3) +// Design: Pattern1 + carrier1本 + const加算の最小固定 + +static box Main { + main() { + local i + local sum + i = 0 + sum = 0 + loop(i < 3) { + sum = sum + 1 + i = i + 1 + } + return sum + } +} diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md index 171e04fa..2c25dd3e 100644 --- a/docs/development/current/main/30-Backlog.md +++ b/docs/development/current/main/30-Backlog.md @@ -8,12 +8,12 @@ Related: ## 直近(JoinIR/selfhost) -- **Phase 288(✅ P0–P3 complete): REPL mode** +- **Phase 288(✅ P0–P3 + 288.1 complete): REPL mode** - 入口: `docs/development/current/main/phases/phase-288/README.md` - SSOT: `docs/reference/language/repl.md` - - 次: Phase 288.1(session persistence / auto-display) `docs/development/current/main/phases/phase-288/P1-INSTRUCTIONS.md` + - 次: Phase 288.2+(任意): REPL UX improvements(history / multiline / load 等) -- **Phase 287(active, stabilization): ビルド/テスト軽量化(quick が重すぎる)** +- **Phase 287(✅ complete): ビルド/テスト軽量化(quick gate の責務分離)** - 目的: `tools/smokes/v2/run.sh --profile quick` を 45秒以内(目安)へ戻し、開発サイクルを軽くする - 入口: `docs/development/current/main/phases/phase-287/README.md` - 手順: `docs/development/current/main/phases/phase-287/P1-INSTRUCTIONS.md` @@ -64,6 +64,7 @@ Related: - ねらい: `return/break/continue` のような “大きな出口語彙” の実装場所が揺れない状態にする - 入口: `docs/development/current/main/phases/phase-286/README.md` - P0(docs-only): `docs/development/current/main/phases/phase-286/P0-INSTRUCTIONS.md` + - 状況: P2(Pattern4 PoC)/P2.1(Pattern1 PoC)完了。次は P2.2(hygiene)→ P2.3(Pattern2 Plan化の再開条件が揃えば) - SSOT: - Plan/Frag: `compose::*` + `emit_frag()`(Phase 280/281) - JoinIR line 共通入口: `src/mir/builder/control_flow/joinir/patterns/conversion_pipeline.rs` diff --git a/docs/development/current/main/joinir-architecture-overview.md b/docs/development/current/main/joinir-architecture-overview.md index 4921b544..a224fa32 100644 --- a/docs/development/current/main/joinir-architecture-overview.md +++ b/docs/development/current/main/joinir-architecture-overview.md @@ -900,6 +900,11 @@ Pattern6/7 は **Plan line(Extractor → Normalizer → Verifier → Lowerer - 許可: `UpdateRhs::Const`, `UpdateRhs::Variable`, `UpdateRhs::StringLiteral` - 拒否: `UpdateRhs::Other`(method call / nested BinOp 等の複雑パターンのみ) - Pattern2/4 の can_lower() で選別、carrier_update_emitter.rs で JoinIR 生成 + - 現状制約(重要 / PoC の前提): + - Pattern2 の「出口で値が持ち帰られる変数」は、基本的に **carrier として認識されたもの**のみが `exit_bindings` 経由で再接続される。 + - 現状の carrier 認識は “mutable-acc” 形(例: `result = result + rhs`)を中心に成立しており、`result = 42` のような **direct assignment** は carrier として扱われないケースがある。 + - その場合、break 後に `return result` しても「ループ前の値」が返りうる(再接続されないため)。 + - Phase 286 P2 の PoC fixture は、この制約を踏んで **mutable-acc 形で固定**する(direct assignment 対応は別フェーズで箱化して拡張する)。 - **ExitMeta / JoinFragmentMeta** - ファイル: `carrier_info.rs` diff --git a/docs/development/current/main/phases/phase-286/README.md b/docs/development/current/main/phases/phase-286/README.md index 80e05d99..5290b600 100644 --- a/docs/development/current/main/phases/phase-286/README.md +++ b/docs/development/current/main/phases/phase-286/README.md @@ -1,6 +1,6 @@ # Phase 286: JoinIR Line Absorption(JoinIR→CorePlan/Frag 収束) -Status: In Progress (P0, P1, P2, P3, 286C-2 COMPLETE) +Status: In Progress (P0, P1, P2, P2.1, P2.2, P2.3, P3, 286C-2 COMPLETE) ## Goal @@ -147,6 +147,33 @@ Phase 286 では JoinIR line を “第2の lowerer” として放置せず、* - Regression test: quick smoke 154 PASS - Pattern1 PoC: PASS, Pattern4 PoC: PASS +### P2.3 (Pattern9 AccumConstLoop Plan化 PoC) ✅ COMPLETE (2025-12-26) + +**完了内容**: +- **Pattern9 (AccumConstLoop) を Plan/Frag SSOT に移行** + - DomainPlan::Pattern9AccumConstLoop 追加 + - PlanNormalizer::normalize_pattern9_accum_const_loop() 実装(PHI 2本: loop_var, acc_var) + - Router integration(Plan line routing → legacy fallback) + - Pattern9 は Pattern1 より優先(より具体的なパターン) + +**設計決定**: +- **PoC は const/var 両方 OK**: `sum = sum + 1`(定数)または `sum = sum + i`(変数) +- **本体の順序固定**: 1行目=累積更新, 2行目=ループ変数更新 +- **CFG 構造**: Pattern1 と同じ骨格、PHI 2本(i_current, sum_current) + +**成果物**: +- `apps/tests/phase286_pattern9_frag_poc.hako` (最小fixture: const accumulation) +- `tools/smokes/v2/profiles/integration/apps/phase286_pattern9_frag_poc.sh` (integration smoke) +- `src/mir/builder/control_flow/plan/mod.rs` (Pattern9AccumConstLoopPlan + DomainPlan variant) +- `src/mir/builder/control_flow/joinir/patterns/extractors/pattern9.rs` (extract_pattern9_plan() 新規) +- `src/mir/builder/control_flow/joinir/patterns/extractors/mod.rs` (pattern9 モジュール追加) +- `src/mir/builder/control_flow/plan/normalizer.rs` (normalize_pattern9_accum_const_loop()) +- `src/mir/builder/control_flow/joinir/patterns/router.rs` (Pattern9 Plan line routing) + +**検証結果**: +- Integration test: `phase286_pattern9_frag_poc` PASS (return: 3) +- Regression test: quick smoke 154 PASS, 0 FAILED + ### P3 (error context enrichment) ✅ COMPLETE (2025-12-25) **完了内容**: diff --git a/src/mir/builder/control_flow/joinir/patterns/extractors/mod.rs b/src/mir/builder/control_flow/joinir/patterns/extractors/mod.rs index e0b6e799..0bfd1721 100644 --- a/src/mir/builder/control_flow/joinir/patterns/extractors/mod.rs +++ b/src/mir/builder/control_flow/joinir/patterns/extractors/mod.rs @@ -25,6 +25,7 @@ pub(crate) mod pattern2; // Phase 282 P4: Pattern2 extraction pub(crate) mod pattern3; // Phase 282 P5: Pattern3 extraction pub(crate) mod pattern4; // Phase 282 P6: Pattern4 extraction pub(crate) mod pattern5; // Phase 282 P7: Pattern5 extraction +pub(crate) mod pattern9; // Phase 286 P2.3: Pattern9 Plan extraction // Phase 282 P9a: Common extraction helpers // Provides shared utilities for Pattern1, 2, 4, 5 (~400 lines reduction): diff --git a/src/mir/builder/control_flow/joinir/patterns/extractors/pattern9.rs b/src/mir/builder/control_flow/joinir/patterns/extractors/pattern9.rs new file mode 100644 index 00000000..11fa00c3 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/extractors/pattern9.rs @@ -0,0 +1,318 @@ +//! Phase 286 P2.3: Pattern9 (AccumConstLoop) Extraction +//! +//! Minimal subset extractor for Pattern9 Plan line. +//! +//! # Supported subset (PoC safety) +//! +//! - Loop condition: ` < ` +//! - Body: 2 assignments only (順序固定) +//! 1. ` = + ` (const OR var accumulation) +//! 2. ` = + ` (increment) +//! - No break/continue/if/return +//! +//! Returns Ok(None) for unsupported patterns → legacy fallback + +use crate::ast::{ASTNode, BinaryOperator, LiteralValue}; +use crate::mir::builder::control_flow::plan::{DomainPlan, Pattern9AccumConstLoopPlan}; + +// Phase 286 P2.3: Use common_helpers +use super::common_helpers::{extract_loop_increment_plan, has_control_flow_statement}; + +/// Phase 286 P2.3: Minimal subset extractor for Pattern9 Plan line +/// +/// # Detection Criteria +/// +/// 1. **Condition**: ` < ` +/// 2. **Body**: Exactly 2 assignments in fixed order +/// - 1st: ` = + ` (accumulation) +/// - 2nd: ` = + ` (increment) +/// 3. **No control flow**: No break/continue/if-else +/// +/// # Returns +/// +/// - `Ok(Some(plan))`: Pattern9 match confirmed +/// - `Ok(None)`: Not Pattern9 (構造不一致 or unsupported) +/// - `Err(msg)`: Logic bug (malformed AST) +pub(crate) fn extract_pattern9_plan( + condition: &ASTNode, + body: &[ASTNode], +) -> Result, String> { + // Step 1: Validate loop condition is ` < ` + let loop_var = match validate_loop_condition_plan(condition) { + Some(var) => var, + None => return Ok(None), // Unsupported condition format + }; + + // Step 2: Reject control flow (break/continue/if-else) + if has_control_flow_statement(body) { + return Ok(None); // Has break/continue → Not Pattern9 for Plan line + } + + // Step 3: Validate body has exactly 2 assignments + if body.len() != 2 { + return Ok(None); // Must be exactly 2 statements + } + + // Step 4: Extract accumulator update (1st statement) + // Supports: ` = + ` (const OR var) + let (acc_var, acc_update) = match extract_accum_update(&body[0], &loop_var) { + Some((var, update)) => (var, update), + None => return Ok(None), // Not a valid accumulator update + }; + + // Step 5: Extract loop increment (2nd statement) + // Uses common_helpers::extract_loop_increment_plan + let loop_increment = match extract_loop_increment_plan(&body[1..], &loop_var)? { + Some(inc) => inc, + None => return Ok(None), // No loop increment found + }; + + Ok(Some(DomainPlan::Pattern9AccumConstLoop(Pattern9AccumConstLoopPlan { + loop_var, + acc_var, + condition: condition.clone(), + acc_update, + loop_increment, + }))) +} + +/// Validate loop condition: supports ` < ` only +fn validate_loop_condition_plan(cond: &ASTNode) -> Option { + if let ASTNode::BinaryOp { operator, left, right, .. } = cond { + if !matches!(operator, BinaryOperator::Less) { + return None; // Only < supported for PoC + } + + // Left must be a variable + let var_name = if let ASTNode::Variable { name, .. } = left.as_ref() { + name.clone() + } else { + return None; + }; + + // Right must be integer literal + if !matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(_), .. }) { + return None; + } + + Some(var_name) + } else { + None + } +} + +/// Extract accumulator update from assignment +/// +/// Supports: ` = + ` where is const OR var +/// +/// Returns: Some((acc_var, update_expr)) or None +fn extract_accum_update(stmt: &ASTNode, loop_var: &str) -> Option<(String, ASTNode)> { + if let ASTNode::Assignment { target, value, .. } = stmt { + // target must be a variable (different from loop_var) + if let ASTNode::Variable { name: target_var, .. } = target.as_ref() { + if target_var == loop_var { + return None; // This is the loop increment, not accumulator + } + + // value must be ` + ` + if let ASTNode::BinaryOp { operator, left, right, .. } = value.as_ref() { + if !matches!(operator, BinaryOperator::Add) { + return None; // Only + supported + } + + // Left must be same as target (acc_var = acc_var + ...) + if let ASTNode::Variable { name: left_var, .. } = left.as_ref() { + if left_var != target_var { + return None; // Not a self-update + } + } else { + return None; + } + + // Right can be const OR var (ChatGPT feedback: both OK) + // Accept: Literal (const) or Variable (var) + match right.as_ref() { + ASTNode::Literal { value: LiteralValue::Integer(_), .. } => { + // const accumulation: sum = sum + 1 + return Some((target_var.clone(), value.as_ref().clone())); + } + ASTNode::Variable { .. } => { + // var accumulation: sum = sum + i + return Some((target_var.clone(), value.as_ref().clone())); + } + _ => return None, // Complex expression not supported + } + } + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::Span; + + fn make_condition(var: &str, bound: i64) -> ASTNode { + ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(ASTNode::Variable { + name: var.to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(bound), + span: Span::unknown(), + }), + span: Span::unknown(), + } + } + + fn make_accum_const_update(acc_var: &str, val: i64) -> ASTNode { + ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: acc_var.to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: acc_var.to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(val), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + } + } + + fn make_accum_var_update(acc_var: &str, rhs_var: &str) -> ASTNode { + ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: acc_var.to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: acc_var.to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Variable { + name: rhs_var.to_string(), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + } + } + + fn make_loop_increment(var: &str, step: i64) -> ASTNode { + ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: var.to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: var.to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(step), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + } + } + + #[test] + fn test_extract_pattern9_const_accum_success() { + // loop(i < 3) { sum = sum + 1; i = i + 1 } + let condition = make_condition("i", 3); + let body = vec![ + make_accum_const_update("sum", 1), + make_loop_increment("i", 1), + ]; + + let result = extract_pattern9_plan(&condition, &body); + assert!(result.is_ok()); + let plan = result.unwrap(); + assert!(plan.is_some()); + + if let Some(DomainPlan::Pattern9AccumConstLoop(p)) = plan { + assert_eq!(p.loop_var, "i"); + assert_eq!(p.acc_var, "sum"); + } else { + panic!("Expected Pattern9AccumConstLoop"); + } + } + + #[test] + fn test_extract_pattern9_var_accum_success() { + // loop(i < 3) { sum = sum + i; i = i + 1 } + let condition = make_condition("i", 3); + let body = vec![ + make_accum_var_update("sum", "i"), + make_loop_increment("i", 1), + ]; + + let result = extract_pattern9_plan(&condition, &body); + assert!(result.is_ok()); + let plan = result.unwrap(); + assert!(plan.is_some()); + + if let Some(DomainPlan::Pattern9AccumConstLoop(p)) = plan { + assert_eq!(p.loop_var, "i"); + assert_eq!(p.acc_var, "sum"); + } else { + panic!("Expected Pattern9AccumConstLoop"); + } + } + + #[test] + fn test_extract_pattern9_wrong_order_returns_none() { + // loop(i < 3) { i = i + 1; sum = sum + 1 } <- wrong order + let condition = make_condition("i", 3); + let body = vec![ + make_loop_increment("i", 1), + make_accum_const_update("sum", 1), + ]; + + let result = extract_pattern9_plan(&condition, &body); + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); // Wrong order → None + } + + #[test] + fn test_extract_pattern9_with_break_returns_none() { + let condition = make_condition("i", 3); + let body = vec![ + ASTNode::Break { span: Span::unknown() }, + make_accum_const_update("sum", 1), + make_loop_increment("i", 1), + ]; + + let result = extract_pattern9_plan(&condition, &body); + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); // Has break → None + } + + #[test] + fn test_extract_pattern9_single_stmt_returns_none() { + let condition = make_condition("i", 3); + let body = vec![make_loop_increment("i", 1)]; + + let result = extract_pattern9_plan(&condition, &body); + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); // Only 1 statement → None + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/router.rs b/src/mir/builder/control_flow/joinir/patterns/router.rs index 2e837e98..2bb0ac9f 100644 --- a/src/mir/builder/control_flow/joinir/patterns/router.rs +++ b/src/mir/builder/control_flow/joinir/patterns/router.rs @@ -153,6 +153,109 @@ fn lower_via_plan( PlanLowerer::lower(builder, core_plan, ctx) } +/// Phase 287: Plan extractor function type for routing table +/// +/// Extractors have two signatures: +/// - Simple: (condition, body) - Pattern1/4/9 +/// - WithFnBody: (condition, body, fn_body) - Pattern6/7 +type SimplePlanExtractor = fn(&ASTNode, &[ASTNode]) -> Result, String>; + +/// Phase 287: Plan extractor table entry +struct PlanExtractorEntry { + /// Pattern name for debug logging + name: &'static str, + + /// Extractor function + extractor: PlanExtractorVariant, +} + +/// Phase 287: Extractor function variant (handles different signatures) +enum PlanExtractorVariant { + /// Simple extractor: (condition, body) + Simple(SimplePlanExtractor), + + /// Extractor with fn_body: (condition, body, fn_body) - Pattern6 + WithFnBody(fn(&ASTNode, &[ASTNode], Option<&[ASTNode]>) -> Result, String>), + + /// Extractor with post_loop_code: (condition, body, post_loop_code) - Pattern7 + WithPostLoop(fn(&ASTNode, &[ASTNode], &[ASTNode]) -> Result, String>), +} + +/// Phase 287: Plan extractor routing table (SSOT) +/// +/// Order is important: more specific patterns first +/// - Pattern6/7: Need fn_body for capture analysis +/// - Pattern4: Continue loop (more specific than Pattern1) +/// - Pattern9: Accum const loop (2 carriers, more specific than Pattern1) +/// - Pattern1: Simple while (fallback) +static PLAN_EXTRACTORS: &[PlanExtractorEntry] = &[ + PlanExtractorEntry { + name: "Pattern6_ScanWithInit (Phase 273)", + extractor: PlanExtractorVariant::WithFnBody(super::pattern6_scan_with_init::extract_scan_with_init_plan), + }, + PlanExtractorEntry { + name: "Pattern7_SplitScan (Phase 273)", + extractor: PlanExtractorVariant::WithPostLoop(super::pattern7_split_scan::extract_split_scan_plan), + }, + PlanExtractorEntry { + name: "Pattern4_Continue (Phase 286 P2)", + extractor: PlanExtractorVariant::Simple(super::extractors::pattern4::extract_pattern4_plan), + }, + PlanExtractorEntry { + name: "Pattern9_AccumConstLoop (Phase 286 P2.3)", + extractor: PlanExtractorVariant::Simple(super::extractors::pattern9::extract_pattern9_plan), + }, + PlanExtractorEntry { + name: "Pattern1_SimpleWhile (Phase 286 P2.1)", + extractor: PlanExtractorVariant::Simple(super::extractors::pattern1::extract_pattern1_plan), + }, +]; + +/// Phase 287: Try all plan extractors in priority order +/// +/// Returns Ok(Some(value_id)) if any extractor succeeds +/// Returns Ok(None) if no extractor matches +/// Returns Err if extraction or lowering fails +fn try_plan_extractors( + builder: &mut MirBuilder, + ctx: &LoopPatternContext, +) -> Result, String> { + use super::super::trace; + + for entry in PLAN_EXTRACTORS { + // Try extraction based on variant + let plan_opt = match &entry.extractor { + PlanExtractorVariant::Simple(extractor) => { + extractor(ctx.condition, ctx.body)? + } + PlanExtractorVariant::WithFnBody(extractor) => { + // Pattern6: needs fn_body for capture analysis + extractor(ctx.condition, ctx.body, ctx.fn_body)? + } + PlanExtractorVariant::WithPostLoop(extractor) => { + // Pattern7: uses empty slice for post_loop_code (3rd param) + extractor(ctx.condition, ctx.body, &[])? + } + }; + + // If extraction succeeded, lower and return + if let Some(domain_plan) = plan_opt { + let log_msg = format!("route=plan strategy=extract pattern={}", entry.name); + trace::trace().pattern("route", &log_msg, true); + return lower_via_plan(builder, domain_plan, ctx); + } + + // Extraction returned None - try next extractor + if ctx.debug { + let debug_msg = format!("{} extraction returned None, trying next pattern", entry.name); + trace::trace().debug("route", &debug_msg); + } + } + + // No extractor matched + Ok(None) +} + /// Phase 272 P0.2 Refactoring: can_lower() strategy classification /// /// Clarifies the two main detection strategies used across patterns: @@ -318,98 +421,10 @@ pub(crate) fn route_loop_pattern( ) -> Result, String> { use super::super::trace; - // Phase 273 P1: Try Plan-based Pattern6 first (before table iteration) - // Flow: Extract → Normalize → Verify → Lower - match super::pattern6_scan_with_init::extract_scan_with_init_plan( - ctx.condition, - ctx.body, - ctx.fn_body, - )? { - Some(domain_plan) => { - // DomainPlan extracted successfully - trace::trace().pattern("route", "route=plan strategy=extract pattern=Pattern6_ScanWithInit (Phase 273)", true); - // Phase 286 P2.2: Use common helper - return lower_via_plan(builder, domain_plan, ctx); - } - None => { - // Not Pattern6 - continue to other patterns - if ctx.debug { - trace::trace().debug( - "route", - "Pattern6 Plan extraction returned None, trying other patterns", - ); - } - } - } - - // Phase 273 P2: Try Plan-based Pattern7 (SplitScan) - // Flow: Extract → Normalize → Verify → Lower - match super::pattern7_split_scan::extract_split_scan_plan( - ctx.condition, - ctx.body, - &[], - )? { - Some(domain_plan) => { - // DomainPlan extracted successfully - trace::trace().pattern("route", "route=plan strategy=extract pattern=Pattern7_SplitScan (Phase 273)", true); - // Phase 286 P2.2: Use common helper - return lower_via_plan(builder, domain_plan, ctx); - } - None => { - // Not Pattern7 - continue to other patterns - if ctx.debug { - trace::trace().debug( - "route", - "Pattern7 Plan extraction returned None, trying other patterns", - ); - } - } - } - - // Phase 286 P2: Try Plan-based Pattern4 (Loop with Continue) - // Flow: Extract → Normalize → Verify → Lower - match super::extractors::pattern4::extract_pattern4_plan( - ctx.condition, - ctx.body, - )? { - Some(domain_plan) => { - // DomainPlan extracted successfully - trace::trace().pattern("route", "route=plan strategy=extract pattern=Pattern4_Continue (Phase 286 P2)", true); - // Phase 286 P2.2: Use common helper - return lower_via_plan(builder, domain_plan, ctx); - } - None => { - // Not Pattern4 Plan - continue to legacy Pattern4 - if ctx.debug { - trace::trace().debug( - "route", - "Pattern4 Plan extraction returned None, trying legacy Pattern4", - ); - } - } - } - - // Phase 286 P2.1: Try Plan-based Pattern1 (Simple While Loop) - // Flow: Extract → Normalize → Verify → Lower - match super::extractors::pattern1::extract_pattern1_plan( - ctx.condition, - ctx.body, - )? { - Some(domain_plan) => { - // DomainPlan extracted successfully - trace::trace().pattern("route", "route=plan strategy=extract pattern=Pattern1_SimpleWhile (Phase 286 P2.1)", true); - // Phase 286 P2.2: Use common helper - return lower_via_plan(builder, domain_plan, ctx); - } - None => { - // Not Pattern1 Plan - continue to legacy Pattern1 - if ctx.debug { - trace::trace().debug( - "route", - "Pattern1 Plan extraction returned None, trying legacy Pattern1", - ); - } - } + // Phase 287: Try all Plan extractors in priority order (Pattern6/7/4/9/1) + // This replaces ~100 lines of repetitive extraction blocks + if let Some(value_id) = try_plan_extractors(builder, ctx)? { + return Ok(Some(value_id)); } // Phase 183: Route based on pre-classified pattern kind diff --git a/src/mir/builder/control_flow/plan/mod.rs b/src/mir/builder/control_flow/plan/mod.rs index 36295c04..92a57513 100644 --- a/src/mir/builder/control_flow/plan/mod.rs +++ b/src/mir/builder/control_flow/plan/mod.rs @@ -49,6 +49,8 @@ pub(in crate::mir::builder) enum DomainPlan { Pattern4Continue(Pattern4ContinuePlan), /// Pattern1: Simple While Loop (Phase 286 P2.1) Pattern1SimpleWhile(Pattern1SimpleWhilePlan), + /// Pattern9: Accumulator Const Loop (Phase 286 P2.3) + Pattern9AccumConstLoop(Pattern9AccumConstLoopPlan), } /// Phase 273 P0: Scan direction for forward/reverse scan @@ -134,6 +136,24 @@ pub(in crate::mir::builder) struct Pattern1SimpleWhilePlan { pub loop_increment: ASTNode, } +/// Phase 286 P2.3: Extracted structure for Pattern9 (Accumulator Const Loop) +/// +/// This structure contains all the information needed to lower an accumulator loop. +/// Pattern9 extends Pattern1 with an accumulator variable (e.g., sum = sum + 1). +#[derive(Debug, Clone)] +pub(in crate::mir::builder) struct Pattern9AccumConstLoopPlan { + /// Loop variable name (e.g., "i") + pub loop_var: String, + /// Accumulator variable name (e.g., "sum") + pub acc_var: String, + /// Loop condition AST (e.g., `i < 3`) + pub condition: ASTNode, + /// Accumulator update expression AST (e.g., `sum + 1` or `sum + i`) + pub acc_update: ASTNode, + /// Loop increment expression AST (e.g., `i + 1`) + pub loop_increment: ASTNode, +} + // ============================================================================ // CorePlan (固定語彙 - 構造ノードのみ) // ============================================================================ diff --git a/src/mir/builder/control_flow/plan/normalizer.rs b/src/mir/builder/control_flow/plan/normalizer.rs index 1c18935b..2a2fbf7d 100644 --- a/src/mir/builder/control_flow/plan/normalizer.rs +++ b/src/mir/builder/control_flow/plan/normalizer.rs @@ -13,7 +13,7 @@ use super::{ CoreEffectPlan, CoreLoopPlan, CorePhiInfo, CorePlan, DomainPlan, - ScanWithInitPlan, SplitScanPlan, Pattern1SimpleWhilePlan, + ScanWithInitPlan, SplitScanPlan, Pattern1SimpleWhilePlan, Pattern9AccumConstLoopPlan, }; use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext; use crate::mir::builder::MirBuilder; @@ -41,6 +41,7 @@ impl PlanNormalizer { DomainPlan::SplitScan(parts) => Self::normalize_split_scan(builder, parts, ctx), DomainPlan::Pattern4Continue(parts) => Self::normalize_pattern4_continue(builder, parts, ctx), DomainPlan::Pattern1SimpleWhile(parts) => Self::normalize_pattern1_simple_while(builder, parts, ctx), + DomainPlan::Pattern9AccumConstLoop(parts) => Self::normalize_pattern9_accum_const_loop(builder, parts, ctx), } } @@ -1464,4 +1465,224 @@ impl PlanNormalizer { Ok(CorePlan::Loop(loop_plan)) } + + /// Phase 286 P2.3: Pattern9AccumConstLoop → CorePlan 変換 + /// + /// Expands Pattern9 (Accumulator Const Loop) semantics into generic CorePlan: + /// - CFG structure: preheader → header → body → step → header (back-edge) + /// - 2 PHIs in header: loop variable (i) and accumulator (sum) + /// - Similar to Pattern1 but with an additional carrier + fn normalize_pattern9_accum_const_loop( + builder: &mut MirBuilder, + parts: Pattern9AccumConstLoopPlan, + 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/pattern9_accum_const_loop", + &format!( + "Phase 286 P2.3: Normalizing Pattern9AccumConstLoop for {} (loop_var: {}, acc_var: {})", + ctx.func_name, parts.loop_var, parts.acc_var + ), + ); + } + + // Step 1: Get host ValueIds for variables + 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 acc_var_init = builder + .variable_ctx + .variable_map + .get(&parts.acc_var) + .copied() + .ok_or_else(|| format!("[normalizer] Accumulator variable {} not found", parts.acc_var))?; + + // Step 2: Capture preheader block + let preheader_bb = builder + .current_block + .ok_or_else(|| "[normalizer] No current block for loop entry".to_string())?; + + // Step 3: Allocate BasicBlockIds for 5 blocks + let header_bb = builder.next_block_id(); + let body_bb = builder.next_block_id(); + let step_bb = builder.next_block_id(); + let after_bb = builder.next_block_id(); + + if debug { + trace_logger.debug( + "normalizer/pattern9_accum_const_loop", + &format!( + "Allocated: preheader={:?}, header={:?}, body={:?}, step={:?}, after={:?}", + preheader_bb, header_bb, body_bb, step_bb, after_bb + ), + ); + } + + // Step 4: Allocate ValueIds for loop control + let loop_var_current = builder.alloc_typed(MirType::Integer); // PHI destination for loop var + let acc_var_current = builder.alloc_typed(MirType::Integer); // PHI destination for accumulator + let cond_loop = builder.alloc_typed(MirType::Bool); // Loop condition + let acc_var_next = builder.alloc_typed(MirType::Integer); // Updated accumulator + let loop_var_next = builder.alloc_typed(MirType::Integer); // Incremented loop var + + // Step 5: Build phi_bindings - PHI dst takes precedence over variable_map + let mut phi_bindings: BTreeMap = BTreeMap::new(); + phi_bindings.insert(parts.loop_var.clone(), loop_var_current); + phi_bindings.insert(parts.acc_var.clone(), acc_var_current); + + // Step 6: Lower AST expressions + // Lower loop condition (e.g., `i < 3`) + let (loop_cond_lhs, loop_cond_op, loop_cond_rhs, loop_cond_consts) = + Self::lower_compare_ast(&parts.condition, builder, &phi_bindings)?; + + // Lower accumulator update (e.g., `sum + 1` or `sum + i`) + let (acc_update_lhs, acc_update_op, acc_update_rhs, acc_update_consts) = + Self::lower_binop_ast(&parts.acc_update, builder, &phi_bindings)?; + + // Lower loop increment (e.g., `i + 1`) + let (loop_inc_lhs, loop_inc_op, loop_inc_rhs, loop_inc_consts) = + Self::lower_binop_ast(&parts.loop_increment, builder, &phi_bindings)?; + + // Step 7: Build header_effects (const definitions + loop condition check) + 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, + }); + + // Step 8: Build step_effects (accumulator update + loop increment) + let mut step_effects = acc_update_consts; + step_effects.push(CoreEffectPlan::BinOp { + dst: acc_var_next, + lhs: acc_update_lhs, + op: acc_update_op, + rhs: acc_update_rhs, + }); + step_effects.extend(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 9: Build block_effects + let block_effects = vec![ + (preheader_bb, vec![]), + (header_bb, header_effects), + (body_bb, vec![]), // Body is empty (effects are in step) + (step_bb, step_effects), + ]; + + // Step 10: Build PHIs (2 PHIs: loop variable + accumulator) + let phis = vec![ + // Loop variable PHI + 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), + }, + // Accumulator PHI + CorePhiInfo { + block: header_bb, + dst: acc_var_current, + inputs: vec![ + (preheader_bb, acc_var_init), + (step_bb, acc_var_next), + ], + tag: format!("acc_var_{}", parts.acc_var), + }, + ]; + + // Step 11: Build Frag + 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![ + // body → step + EdgeStub { + from: body_bb, + kind: ExitKind::Normal, + target: Some(step_bb), + args: empty_args.clone(), + }, + // step → header (back-edge) + EdgeStub { + from: step_bb, + kind: ExitKind::Normal, + target: Some(header_bb), + args: empty_args.clone(), + }, + ]; + + let frag = Frag { + entry: header_bb, + exits: BTreeMap::new(), + wires, + branches, + }; + + // Step 12: Build final_values (both loop var and accumulator) + let final_values = vec![ + (parts.loop_var.clone(), loop_var_current), + (parts.acc_var.clone(), acc_var_current), + ]; + + // Step 13: Build CoreLoopPlan + // Note: found_bb = after_bb (no early exit in Pattern9) + // Note: cond_match unused but required by struct, use cond_loop as placeholder + let loop_plan = CoreLoopPlan { + preheader_bb, + header_bb, + body_bb, + step_bb, + after_bb, + found_bb: after_bb, // No early exit + body: vec![], // Body is empty (effects are in step) + cond_loop, + cond_match: cond_loop, // Placeholder (unused in Pattern9) + block_effects, + phis, + frag, + final_values, + }; + + if debug { + trace_logger.debug( + "normalizer/pattern9_accum_const_loop", + "CorePlan construction complete (4 blocks, 2 PHIs)", + ); + } + + Ok(CorePlan::Loop(loop_plan)) + } } diff --git a/tools/smokes/v2/profiles/integration/apps/phase286_pattern9_frag_poc.sh b/tools/smokes/v2/profiles/integration/apps/phase286_pattern9_frag_poc.sh new file mode 100644 index 00000000..251d06fc --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase286_pattern9_frag_poc.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Phase 286 P2.3: Pattern9 → Frag PoC test +# Tests: Pattern9 (AccumConstLoop) using Plan/Frag SSOT +# +# PoC Goal: +# Pattern9 → DomainPlan → CorePlan → Frag → emit_frag() +# (Skip: JoinIR → bridge → merge) +# +# Expected: return 3 (sum = 0 + 1 + 1 + 1 = 3) + +source "$(dirname "$0")/../../../lib/test_runner.sh" +export SMOKES_USE_PYVM=0 +require_env || exit 2 + +INPUT="$NYASH_ROOT/apps/tests/phase286_pattern9_frag_poc.hako" +RUN_TIMEOUT_SECS=${RUN_TIMEOUT_SECS:-10} + +set +e +OUTPUT=$(timeout "$RUN_TIMEOUT_SECS" env NYASH_DISABLE_PLUGINS=1 "$NYASH_BIN" "$INPUT" 2>&1) +EXIT_CODE=$? +set -e + +if [ "$EXIT_CODE" -eq 124 ]; then + test_fail "phase286_pattern9_frag_poc: hakorune timed out (>${RUN_TIMEOUT_SECS}s)" + exit 1 +elif [ "$EXIT_CODE" -eq 3 ]; then + # Expected: return 3 (exit code) + test_pass "phase286_pattern9_frag_poc: Pattern9 Frag PoC succeeded (return: 3)" + exit 0 +else + echo "[FAIL] Unexpected exit code (expected: 3, got: $EXIT_CODE)" + echo "[INFO] Output:" + echo "$OUTPUT" | head -n 20 || true + test_fail "phase286_pattern9_frag_poc: Unexpected exit code" + exit 1 +fi