diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 3cca0eaa..a8b9b70e 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -389,6 +389,21 @@ - **次候補**: - Phase 200+: ConditionEnv 拡張(_parse_number, _atoi) - Phase 198+: JsonParser 残りループ個別対応 + - [x] **Phase 200-A: ConditionEnv 拡張インフラ(型と箱のみ)** ✅ (完了: 2025-12-09) + - **目的**: ConditionEnv を壊さずに関数スコープ "実質定数" を扱う基盤 + - **実施内容**: + - 200-A-1: CapturedVar / CapturedEnv 型導入 ✅ + - 200-A-2: FunctionScopeCaptureAnalyzer スケルトン ✅ + - 200-A-3: ConditionEnvBuilder v2 入口 ✅ + - 200-A-4: ParamRole enum 追加 ✅ + - 200-A-5: ドキュメント更新 ✅ + - **スコープ**: Infra only / Integration pending + - **成果**: + - 型と箱の定義完了 ✅ + - 既存ループの挙動変更なし ✅ + - Phase 200-B への準備完了 ✅ + - **次フェーズ**: Phase 200-B(digits 系ループへの適用) + - **詳細**: phase200-A-conditionenv-infra.md --- diff --git a/docs/development/current/main/joinir-architecture-overview.md b/docs/development/current/main/joinir-architecture-overview.md index 00245f96..d1c2c22e 100644 --- a/docs/development/current/main/joinir-architecture-overview.md +++ b/docs/development/current/main/joinir-architecture-overview.md @@ -49,10 +49,23 @@ JoinIR ラインで守るべきルールを先に書いておくよ: - 現状は ConditionBinding/ExitMeta/JoinFragmentMeta で役割を区別しており、将来 ParamRole enum として明示する予定。 8. **LoopHeader PHI dst は予約領域(上書き禁止)** - - LoopHeaderPhiBuilder が生成したヘッダ PHI の dst ValueId は「現在のループ値」の SSOT として扱い、 + - LoopHeaderPhiBuilder が生成したヘッダ PHI の dst ValueId は「現在のループ値」の SSOT として扱い、 BoundaryInjector や InstructionRewriter が `Copy` などで二度書きすることを禁止する。 - merge ラインでは「ヘッダ PHI dst に対する新しい定義が出てきたら debug モードで panic する」ことで契約違反を早期検出する。 +9. **ParamRole の不変条件(Phase 200-A 追加)** + - **Condition 役のパラメータは「PHI dst にしてはいけない」** + - 理由: 条件専用変数はループ内で更新されない(例: `digits` in `_atoi()`) + - LoopHeaderPhiBuilder は Condition 役の変数に対して header PHI を生成しない + - **Condition 役のパラメータは「ExitLine の対象にも入れない」** + - 理由: 条件専用変数はループ外で使われない(ループ内でのみ参照) + - ExitLineReconnector は Condition 役の変数を exit_bindings から除外 + - **ParamRole の分類**: + - `LoopParam`: ループ制御変数(例: `i` in `loop(i < len)`)→ header PHI + exit_bindings + - `Condition`: 条件専用変数(例: `digits` in `digits.indexOf(ch)`)→ condition_bindings のみ + - `Carrier`: 状態更新変数(例: `sum`, `count`)→ header PHI + exit_bindings + - `ExprResult`: ループ戻り値 → exit_phi_builder で処理 + --- ## 2. 主な箱と責務 @@ -262,6 +275,35 @@ JoinIR ラインで守るべきルールを先に書いておくよ: - `verify_joinir_contracts()`: merge_joinir_mir_blocks() の最後で全契約を一括チェック。 - release ビルドでは完全に除去される(`#[cfg(debug_assertions)]`)。 +- **FunctionScopeCaptureAnalyzer / CapturedEnv(Phase 200-A 追加)** + - ファイル: `src/mir/loop_pattern_detection/function_scope_capture.rs` + - 責務: + - 関数スコープで宣言され、ループ内で不変な変数("実質定数")を検出。 + - 例: `local digits = "0123456789"` in `JsonParser._atoi()` + - CapturedVar: `{ name, host_id, is_immutable }` + - CapturedEnv: 検出された変数のコレクション + - **Phase 200-A**: 型と空実装のみ(skeleton)。 + - **Phase 200-B**: 実際の検出ロジックを実装予定(AST スキャン + 再代入チェック)。 + +- **ParamRole enum(Phase 200-A 追加)** + - ファイル: `src/mir/join_ir/lowering/inline_boundary_builder.rs` + - 責務: + - JoinInlineBoundary のパラメータ役割を明示的に区別。 + - LoopParam / Condition / Carrier / ExprResult の 4 種類。 + - **不変条件**: + - **Condition 役**: PHI dst にしてはいけない(ループ内で更新されない)。 + - **Condition 役**: ExitLine の対象にも入れない(ループ外で使われない)。 + - 理由: 条件専用変数(例: `digits`)はループ内でのみ参照され、不変。 + - **Phase 200-A**: enum 定義のみ。 + - **Phase 200-B**: ルーティングロジック実装予定(CapturedEnv 統合時)。 + +- **ConditionEnvBuilder::build_with_captures(Phase 200-A 追加)** + - ファイル: `src/mir/builder/control_flow/joinir/patterns/condition_env_builder.rs` + - 責務: + - 将来 CapturedEnv を受け取り、ConditionEnv に統合する v2 入口。 + - **Phase 200-A**: 既存実装に委譲する skeleton。 + - **Phase 200-B**: CapturedEnv の変数を condition_bindings に追加する実装予定。 + ### 2.4 expr result ライン(式としての戻り値) - **exit_phi_builder** diff --git a/docs/development/current/main/phase200-A-conditionenv-infra.md b/docs/development/current/main/phase200-A-conditionenv-infra.md new file mode 100644 index 00000000..d5395efd --- /dev/null +++ b/docs/development/current/main/phase200-A-conditionenv-infra.md @@ -0,0 +1,446 @@ +# Phase 200-A: ConditionEnv 拡張インフラ(型と箱のみ) + +**Date**: 2025-12-09 +**Status**: Ready for Implementation +**Prerequisite**: Phase 197 complete + +--- + +## 目的 + +**ConditionEnv を壊さずに、関数スコープの "実質定数" ローカル(例: `local digits = "0123456789"`)を安全に扱う基盤を作る。** + +**このフェーズでは**: +- ✅ 型と解析箱だけを導入 +- ✅ 既存ループの挙動は変えない +- ❌ 具体ループ(`_parse_number` / `_atoi` 等)にはまだ手を出さない + +**設計原則**: +- ConditionEnv の「芯」(= boundary に載ったものだけを見る)を保持 +- 2-tier 設計(ConditionEnv / LoopBodyLocalEnv)は維持 +- 新しい箱は「関数スコープの実質定数」を捕捉するレイヤーとして追加 + +--- + +## アーキテクチャ概要 + +### 現在の設計(Phase 193 まで) + +``` +┌─────────────────────────────────────────────────────────┐ +│ ConditionEnv │ +│ - LoopParam (i, len, etc.) │ +│ - OuterLocal (function params: s, pos) │ +│ - LoopBodyLocal (body-local vars: ch, digit) │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ JoinInlineBoundary │ +│ - join_inputs / host_inputs │ +│ - condition_bindings │ +│ - exit_bindings │ +└─────────────────────────────────────────────────────────┘ +``` + +**問題**: `digits` のような「関数スコープで宣言され、ループ内で不変なローカル」が捕捉できない + +### 新設計(Phase 200+) + +``` +┌─────────────────────────────────────────────────────────┐ +│ FunctionScopeCaptureAnalyzer │ +│ - 関数スコープの "実質定数" を検出 │ +│ - CapturedEnv を生成 │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ CapturedEnv │ +│ - CapturedVar[] { name, host_id, is_immutable } │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ ConditionEnvBuilder v2 │ +│ - LoopParam + CapturedEnv → ConditionEnv に合成 │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ JoinInlineBoundary (拡張) │ +│ - ParamRole: LoopParam / Condition / Carrier / Expr │ +│ - CapturedEnv の変数も condition_bindings に載せる │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## Task 200-A-1: CapturedVar / CapturedEnv 型の導入 + +### 目標 +関数スコープの「実質定数」を表現する型を定義する。 + +### ファイル +**新規**: `src/mir/loop_pattern_detection/function_scope_capture.rs` + +### 実装内容 + +```rust +//! Phase 200-A: Function scope capture infrastructure +//! +//! This module provides types for capturing function-scoped variables +//! that are effectively immutable within a loop context. + +/// A variable captured from function scope for use in loop conditions. +/// +/// Example: `local digits = "0123456789"` in JsonParser._atoi() +#[derive(Debug, Clone)] +pub struct CapturedVar { + /// Variable name (e.g., "digits", "table") + pub name: String, + + /// MIR ValueId of the original definition + pub host_id: ValueId, + + /// Whether this variable is never reassigned in the function + pub is_immutable: bool, +} + +/// Environment containing function-scoped captured variables. +/// +/// Phase 200-A: Type definition only, not yet integrated with ConditionEnv. +#[derive(Debug, Clone, Default)] +pub struct CapturedEnv { + pub vars: Vec, +} + +impl CapturedEnv { + pub fn new() -> Self { + Self { vars: Vec::new() } + } + + pub fn is_empty(&self) -> bool { + self.vars.is_empty() + } + + pub fn add_var(&mut self, var: CapturedVar) { + self.vars.push(var); + } + + /// Look up a captured variable by name + pub fn get(&self, name: &str) -> Option<&CapturedVar> { + self.vars.iter().find(|v| v.name == name) + } +} +``` + +### テスト + +```rust +#[cfg(test)] +mod tests { + use super::*; + use crate::mir::ValueId; + + #[test] + fn test_captured_env_empty() { + let env = CapturedEnv::new(); + assert!(env.is_empty()); + assert!(env.get("digits").is_none()); + } + + #[test] + fn test_captured_env_add_and_get() { + let mut env = CapturedEnv::new(); + env.add_var(CapturedVar { + name: "digits".to_string(), + host_id: ValueId(42), + is_immutable: true, + }); + + assert!(!env.is_empty()); + let var = env.get("digits").unwrap(); + assert_eq!(var.name, "digits"); + assert_eq!(var.host_id, ValueId(42)); + assert!(var.is_immutable); + } +} +``` + +### 成果物 +- [x] `CapturedVar` 構造体定義 +- [x] `CapturedEnv` 構造体定義 +- [x] 基本的なアクセサメソッド +- [x] Unit tests (2件) + +--- + +## Task 200-A-2: FunctionScopeCaptureAnalyzer スケルトン + +### 目標 +関数スコープの実質定数を検出する解析関数の枠を作る。 + +### ファイル +同じ `function_scope_capture.rs` に追加 + +### 実装内容 + +```rust +/// Analyzes function-scoped variables that can be safely captured for loop conditions. +/// +/// # Phase 200-A Status +/// Currently returns empty CapturedEnv (skeleton implementation). +/// Actual capture detection will be implemented in Phase 200-B. +/// +/// # Future Detection Criteria (Phase 200-B+) +/// - Variable is declared before the loop +/// - Variable is never reassigned within the function +/// - Variable is referenced in loop condition or body +pub fn analyze_captured_vars( + _fn_body: &[Stmt], + _loop_ast: &Stmt, + _scope: &LoopScopeShape, +) -> CapturedEnv { + // Phase 200-A: Skeleton implementation + // TODO(Phase 200-B): Implement actual capture detection + // + // Detection algorithm: + // 1. Find all `local` declarations before the loop + // 2. Check if each is never reassigned (is_immutable = true) + // 3. Check if referenced in loop condition/body + // 4. Exclude loop parameters and body-local variables + + CapturedEnv::new() +} +``` + +### 設計メモ + +**Phase 200-B で実装する検出アルゴリズム**: +1. ループの前にある `local` 宣言を全て収集 +2. 関数全体で再代入されていないかチェック(`is_immutable`) +3. ループ条件/本体で参照されているかチェック +4. LoopParam / LoopBodyLocal は除外(既に ConditionEnv で扱われている) + +### 成果物 +- [x] `analyze_captured_vars` 関数シグネチャ +- [x] ドキュメントコメント(将来の検出基準を記載) +- [x] 空実装(Phase 200-B で中身を実装) + +--- + +## Task 200-A-3: ConditionEnvBuilder v2 入口 + +### 目標 +将来 CapturedEnv を受け取るフックだけ用意する。 + +### ファイル +`src/mir/join_ir/lowering/condition_env_builder.rs` または既存の ConditionEnv 関連ファイル + +### 実装内容 + +```rust +/// Build ConditionEnv with optional captured variables. +/// +/// # Phase 200-A Status +/// Currently ignores `captured` parameter and calls existing implementation. +/// Integration with CapturedEnv will be implemented in Phase 200-B. +/// +/// # Future Behavior (Phase 200-B+) +/// - Add captured variables to condition_bindings +/// - Generate Copy instructions for captured vars in entry block +/// - Track captured vars separately from loop params +pub fn build_condition_env_with_captures( + loop_params: &[ParamInfo], + _captured: &CapturedEnv, + boundary: &mut JoinInlineBoundaryBuilder, +) -> ConditionEnv { + // Phase 200-A: Delegate to existing implementation + // TODO(Phase 200-B): Integrate captured vars into ConditionEnv + // + // Integration steps: + // 1. For each captured var, add to boundary.condition_bindings + // 2. Mark as ParamRole::Condition (not Carrier or LoopParam) + // 3. Ensure captured vars are NOT included in exit_bindings + + build_condition_env_from_params(loop_params, boundary) +} +``` + +### 成果物 +- [x] `build_condition_env_with_captures` 関数(既存実装に委譲) +- [x] ドキュメントコメント(Phase 200-B の統合手順を記載) + +--- + +## Task 200-A-4: ParamRole enum 追加 + +### 目標 +JoinInlineBoundary のパラメータ役割を明示的に区別する。 + +### ファイル +`src/mir/join_ir/lowering/inline_boundary.rs` または関連ファイル + +### 実装内容 + +```rust +/// Role of a parameter in JoinIR lowering. +/// +/// # Invariants +/// - **LoopParam**: Participates in header PHI, updated in loop body +/// - **Condition**: Used in condition only, NOT in header PHI, NOT in ExitLine +/// - **Carrier**: Updated in loop body, participates in header PHI and ExitLine +/// - **ExprResult**: Return value of the loop expression, handled by exit_phi_builder +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParamRole { + /// Loop iteration variable (e.g., `i` in `loop(i < len)`) + LoopParam, + + /// Condition-only parameter (e.g., `digits` in `digits.indexOf(ch)`) + /// NOT included in header PHI or ExitLine + Condition, + + /// State carried across iterations (e.g., `sum`, `count`) + Carrier, + + /// Expression result returned by the loop + ExprResult, +} + +impl JoinInlineBoundaryBuilder { + /// Add a parameter with explicit role. + /// + /// # Phase 200-A Status + /// Currently stores role but does not use it for routing. + /// Role-based routing will be implemented in Phase 200-B. + pub fn add_param_with_role(&mut self, name: &str, host_id: ValueId, role: ParamRole) { + // Phase 200-A: Store role for future use + // TODO(Phase 200-B): Route based on role + // + // Routing rules: + // - LoopParam: join_inputs + host_inputs + // - Condition: condition_bindings only (no PHI, no ExitLine) + // - Carrier: join_inputs + host_inputs + exit_bindings + // - ExprResult: handled by exit_phi_builder + + match role { + ParamRole::LoopParam | ParamRole::Carrier => { + // Existing behavior: add to join_inputs + self.add_input(name, host_id); + } + ParamRole::Condition => { + // Phase 200-A: Just log for now + // TODO(Phase 200-B): Add to condition_bindings without PHI + } + ParamRole::ExprResult => { + // Handled separately by set_expr_result + } + } + } +} +``` + +### 不変条件(joinir-architecture-overview.md に追記) + +```markdown +9. **ParamRole の不変条件** + - **Condition 役のパラメータは「PHI dst にしてはいけない」** + - 理由: 条件専用変数はループ内で更新されない + - 例: `digits` は header PHI に参加しない + - **Condition 役のパラメータは「ExitLine の対象にも入れない」** + - 理由: 条件専用変数はループ外で使われない(ループ内でのみ参照) + - 例: `digits.indexOf(ch)` の結果は exit_bindings に載らない +``` + +### 成果物 +- [x] `ParamRole` enum 定義 +- [x] `add_param_with_role` メソッド(スケルトン) +- [x] 不変条件のドキュメント + +--- + +## Task 200-A-5: ドキュメント更新 + +### 1. joinir-architecture-overview.md + +**Section 2.3 (Boundary / Carrier ライン) に追記**: + +```markdown +- **FunctionScopeCaptureAnalyzer / CapturedEnv** (Phase 200-A) + - ファイル: `src/mir/loop_pattern_detection/function_scope_capture.rs` + - 責務: + - 関数スコープで宣言され、ループ内で不変な変数を検出 + - CapturedVar: { name, host_id, is_immutable } + - CapturedEnv: 検出された変数のコレクション + - **Phase 200-A**: 型と空実装のみ(中身は Phase 200-B で実装) + +- **ParamRole enum** (Phase 200-A) + - ファイル: `src/mir/join_ir/lowering/inline_boundary.rs` + - 責務: + - JoinInlineBoundary のパラメータ役割を明示的に区別 + - LoopParam / Condition / Carrier / ExprResult + - **Phase 200-A**: enum 定義のみ(ルーティングは Phase 200-B で実装) +``` + +**Section 1 (不変条件) に追記**: + +```markdown +9. **ParamRole の不変条件** + - Condition 役のパラメータは「PHI dst にしてはいけない」 + - Condition 役のパラメータは「ExitLine の対象にも入れない」 + - 理由: 条件専用変数はループ内で更新されず、ループ外で使われない +``` + +### 2. CURRENT_TASK.md + +**Phase 200-A セクション追加**: + +```markdown + - [x] **Phase 200-A: ConditionEnv 拡張インフラ(型と箱のみ)** ✅ (完了: 2025-12-XX) + - **目的**: ConditionEnv を壊さずに関数スコープ "実質定数" を扱う基盤 + - **実装内容**: + - 200-A-1: CapturedVar / CapturedEnv 型導入 ✅ + - 200-A-2: FunctionScopeCaptureAnalyzer スケルトン ✅ + - 200-A-3: ConditionEnvBuilder v2 入口 ✅ + - 200-A-4: ParamRole enum 追加 ✅ + - 200-A-5: ドキュメント更新 ✅ + - **スコープ**: Infra only / Integration pending + - **成果**: + - 型と箱の定義完了 ✅ + - 既存ループの挙動変更なし ✅ + - Phase 200-B への準備完了 ✅ + - **次フェーズ**: Phase 200-B(digits 系ループへの適用) +``` + +--- + +## 成功基準 + +- [x] `CapturedVar` / `CapturedEnv` 型が定義されている +- [x] `analyze_captured_vars` 関数スケルトンが存在する +- [x] `build_condition_env_with_captures` 入口が存在する +- [x] `ParamRole` enum が定義されている +- [x] 既存テストが全て PASS(退行なし) +- [x] ドキュメント更新(overview + CURRENT_TASK) + +--- + +## 設計原則(Phase 200-A) + +1. **Infra only**: 型と箱の定義のみ、具体ループへの適用は Phase 200-B +2. **既存挙動維持**: 現在のループは全く同じように動作する +3. **箱化原則**: 新しい責務は新しい箱(FunctionScopeCaptureAnalyzer)に集約 +4. **ドキュメント駆動**: 将来の実装方針をコメントとして残す + +--- + +## 関連ファイル + +### 新規作成 +- `src/mir/loop_pattern_detection/function_scope_capture.rs` + +### 修正対象 +- `src/mir/loop_pattern_detection/mod.rs`(モジュール追加) +- `src/mir/join_ir/lowering/condition_env_builder.rs`(v2 入口) +- `src/mir/join_ir/lowering/inline_boundary.rs`(ParamRole) + +### ドキュメント +- `docs/development/current/main/joinir-architecture-overview.md` +- `CURRENT_TASK.md` diff --git a/src/mir/builder/control_flow/joinir/patterns/condition_env_builder.rs b/src/mir/builder/control_flow/joinir/patterns/condition_env_builder.rs index 3d1fd7c4..1096deb0 100644 --- a/src/mir/builder/control_flow/joinir/patterns/condition_env_builder.rs +++ b/src/mir/builder/control_flow/joinir/patterns/condition_env_builder.rs @@ -27,6 +27,8 @@ use crate::ast::ASTNode; use crate::mir::join_ir::lowering::condition_env::{ConditionBinding, ConditionEnv}; use crate::mir::join_ir::lowering::condition_to_joinir::extract_condition_variables; +use crate::mir::join_ir::lowering::inline_boundary_builder::JoinInlineBoundaryBuilder; +use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv; use crate::mir::ValueId; use std::collections::BTreeMap; @@ -122,6 +124,64 @@ impl ConditionEnvBuilder { env.insert(loop_var_name.to_string(), ValueId(0)); env } + + /// Build ConditionEnv with optional captured variables (Phase 200-A v2 entry point) + /// + /// # Phase 200-A Status + /// + /// Currently ignores `captured` parameter and delegates to existing implementation. + /// Integration with CapturedEnv will be implemented in Phase 200-B. + /// + /// # Future Behavior (Phase 200-B+) + /// + /// - Add captured variables to ConditionEnv + /// - Generate condition_bindings for captured vars in boundary + /// - Track captured vars separately from loop params + /// - Ensure captured vars do NOT participate in header PHI or exit_bindings + /// + /// # Arguments + /// + /// * `loop_var_name` - Loop parameter name (e.g., "i", "pos") + /// * `_captured` - Function-scoped captured variables (Phase 200-B+) + /// * `_boundary` - Boundary builder for adding condition_bindings (Phase 200-B+) + /// + /// # Returns + /// + /// ConditionEnv with loop parameter mapping (Phase 200-A: same as build_loop_param_only) + /// + /// # Example (Future Phase 200-B) + /// + /// ```ignore + /// let captured = analyze_captured_vars(fn_body, loop_ast, scope); + /// let mut boundary = JoinInlineBoundaryBuilder::new(); + /// let env = ConditionEnvBuilder::build_with_captures( + /// "pos", + /// &captured, // Contains "digits" with host ValueId(42) + /// &mut boundary, + /// ); + /// // Phase 200-B: env will contain "pos" → ValueId(0), "digits" → ValueId(1) + /// // Phase 200-B: boundary.condition_bindings will have entry for "digits" + /// ``` + pub fn build_with_captures( + loop_var_name: &str, + _captured: &CapturedEnv, + _boundary: &mut JoinInlineBoundaryBuilder, + ) -> ConditionEnv { + // Phase 200-A: Delegate to existing implementation + // TODO(Phase 200-B): Integrate captured vars into ConditionEnv + // + // Integration steps: + // 1. Start with loop parameter in env (ValueId(0)) + // 2. For each captured var: + // a. Allocate JoinIR-local ValueId (starting from 1) + // b. Add to ConditionEnv (var.name → join_id) + // c. Add to boundary.condition_bindings (host_id ↔ join_id) + // d. Mark as ParamRole::Condition (not Carrier or LoopParam) + // 3. Ensure captured vars are NOT in exit_bindings (condition-only) + // 4. Return populated ConditionEnv + + Self::build_loop_param_only(loop_var_name) + } } #[cfg(test)] diff --git a/src/mir/join_ir/lowering/inline_boundary_builder.rs b/src/mir/join_ir/lowering/inline_boundary_builder.rs index 0efce242..49a72f54 100644 --- a/src/mir/join_ir/lowering/inline_boundary_builder.rs +++ b/src/mir/join_ir/lowering/inline_boundary_builder.rs @@ -28,6 +28,50 @@ use crate::mir::ValueId; use super::inline_boundary::{JoinInlineBoundary, LoopExitBinding}; use super::condition_to_joinir::ConditionBinding; +/// Role of a parameter in JoinIR lowering (Phase 200-A) +/// +/// This enum explicitly classifies parameters to ensure correct routing +/// during JoinIR → MIR lowering and boundary construction. +/// +/// # Invariants +/// +/// - **LoopParam**: Participates in header PHI, updated in loop body +/// - Example: `i` in `loop(i < len)` - iteration variable +/// - Routing: join_inputs + host_inputs + header PHI + exit_bindings +/// +/// - **Condition**: Used in condition only, NOT in header PHI, NOT in ExitLine +/// - Example: `digits` in `digits.indexOf(ch)` - function-scoped constant +/// - Routing: condition_bindings ONLY (no PHI, no exit_bindings) +/// - Rationale: Condition-only vars are immutable and not updated in loop +/// +/// - **Carrier**: Updated in loop body, participates in header PHI and ExitLine +/// - Example: `sum`, `count` in accumulation loops +/// - Routing: join_inputs + host_inputs + header PHI + exit_bindings +/// +/// - **ExprResult**: Return value of the loop expression +/// - Example: Loop result in `return loop(...)` +/// - Routing: Handled by exit_phi_builder (set_expr_result) +/// +/// # Phase 200-A Status +/// +/// Enum is defined but not yet used for routing. Routing implementation +/// will be added in Phase 200-B when CapturedEnv integration is complete. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParamRole { + /// Loop iteration variable (e.g., `i` in `loop(i < len)`) + LoopParam, + + /// Condition-only parameter (e.g., `digits` in `digits.indexOf(ch)`) + /// NOT included in header PHI or ExitLine + Condition, + + /// State carried across iterations (e.g., `sum`, `count`) + Carrier, + + /// Expression result returned by the loop + ExprResult, +} + /// Builder for constructing JoinInlineBoundary objects /// /// Provides a fluent API to set boundary fields without direct field manipulation. @@ -128,6 +172,70 @@ impl JoinInlineBoundaryBuilder { pub fn build(self) -> JoinInlineBoundary { self.boundary } + + /// Add a parameter with explicit role (Phase 200-A) + /// + /// This method allows adding parameters with explicit role classification, + /// ensuring correct routing during JoinIR → MIR lowering. + /// + /// # Phase 200-A Status + /// + /// Currently stores parameters based on role but does not use role for advanced routing. + /// Full role-based routing will be implemented in Phase 200-B. + /// + /// # Arguments + /// + /// * `name` - Variable name (e.g., "i", "digits", "sum") + /// * `host_id` - Host function's ValueId for this variable + /// * `role` - Parameter role (LoopParam / Condition / Carrier / ExprResult) + /// + /// # Routing Rules (Phase 200-B+) + /// + /// - **LoopParam**: add_input (join_inputs + host_inputs) + /// - **Condition**: add to condition_bindings (no PHI, no exit_bindings) + /// - **Carrier**: add_input + exit_bindings + /// - **ExprResult**: set_expr_result (handled separately) + /// + /// # Example (Future Phase 200-B) + /// + /// ```ignore + /// builder.add_param_with_role("i", ValueId(100), ParamRole::LoopParam); + /// builder.add_param_with_role("digits", ValueId(42), ParamRole::Condition); + /// builder.add_param_with_role("sum", ValueId(101), ParamRole::Carrier); + /// ``` + pub fn add_param_with_role(&mut self, _name: &str, host_id: ValueId, role: ParamRole) { + // Phase 200-A: Basic routing only + // TODO(Phase 200-B): Implement full role-based routing + // + // Routing implementation: + // - LoopParam: join_inputs + host_inputs + // - Condition: condition_bindings (with JoinIR-local ValueId allocation) + // - Carrier: join_inputs + host_inputs + exit_bindings + // - ExprResult: Handled by set_expr_result + + match role { + ParamRole::LoopParam | ParamRole::Carrier => { + // Existing behavior: add to join_inputs + // Note: In Phase 200-A, we don't have a simple add_input method + // that takes a name. This is a skeleton implementation. + // In Phase 200-B, we'll need to allocate JoinIR-local ValueIds. + let join_id = ValueId(self.boundary.join_inputs.len() as u32); + self.boundary.join_inputs.push(join_id); + self.boundary.host_inputs.push(host_id); + } + ParamRole::Condition => { + // Phase 200-A: Log only + // TODO(Phase 200-B): Add to condition_bindings without PHI + // 1. Allocate JoinIR-local ValueId + // 2. Create ConditionBinding { name, host_id, join_id } + // 3. Add to self.boundary.condition_bindings + } + ParamRole::ExprResult => { + // Handled separately by set_expr_result + // No action needed here + } + } + } } impl Default for JoinInlineBoundaryBuilder { @@ -255,4 +363,40 @@ mod tests { assert_eq!(boundary.join_inputs.len(), 3); assert_eq!(boundary.host_inputs.len(), 3); } + + // Phase 200-A: ParamRole tests + #[test] + fn test_param_role_loop_param() { + let mut builder = JoinInlineBoundaryBuilder::new(); + builder.add_param_with_role("i", ValueId(100), ParamRole::LoopParam); + + let boundary = builder.build(); + assert_eq!(boundary.join_inputs.len(), 1); + assert_eq!(boundary.host_inputs.len(), 1); + assert_eq!(boundary.host_inputs[0], ValueId(100)); + } + + #[test] + fn test_param_role_condition() { + let mut builder = JoinInlineBoundaryBuilder::new(); + // Phase 200-A: Condition role is logged but not yet routed + builder.add_param_with_role("digits", ValueId(42), ParamRole::Condition); + + let boundary = builder.build(); + // Phase 200-A: No action for Condition role yet + // Phase 200-B: This will add to condition_bindings + assert_eq!(boundary.join_inputs.len(), 0); + assert_eq!(boundary.condition_bindings.len(), 0); + } + + #[test] + fn test_param_role_carrier() { + let mut builder = JoinInlineBoundaryBuilder::new(); + builder.add_param_with_role("sum", ValueId(101), ParamRole::Carrier); + + let boundary = builder.build(); + assert_eq!(boundary.join_inputs.len(), 1); + assert_eq!(boundary.host_inputs.len(), 1); + assert_eq!(boundary.host_inputs[0], ValueId(101)); + } } diff --git a/src/mir/loop_pattern_detection/function_scope_capture.rs b/src/mir/loop_pattern_detection/function_scope_capture.rs new file mode 100644 index 00000000..4b185895 --- /dev/null +++ b/src/mir/loop_pattern_detection/function_scope_capture.rs @@ -0,0 +1,203 @@ +//! Phase 200-A: Function scope capture infrastructure +//! +//! This module provides types for capturing function-scoped variables +//! that are effectively immutable within a loop context. +//! +//! # Example +//! +//! For a function like JsonParser._atoi(): +//! +//! ```nyash +//! method _atoi(s, pos, len) { +//! local digits = "0123456789" // <-- Captured variable +//! local value = 0 +//! loop(pos < len) { +//! local ch = s.charAt(pos) +//! local digit = digits.indexOf(ch) // Uses captured 'digits' +//! if (digit < 0) { break } +//! value = value * 10 + digit +//! pos = pos + 1 +//! } +//! return value +//! } +//! ``` +//! +//! Here, `digits` is: +//! - Declared in function scope (before the loop) +//! - Never reassigned (effectively immutable) +//! - Referenced in loop body (digits.indexOf(ch)) +//! +//! Phase 200-A creates the infrastructure to capture such variables. +//! Phase 200-B will implement the actual detection logic. + +use crate::mir::ValueId; +use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape; +use crate::ast::ASTNode; + +/// A variable captured from function scope for use in loop conditions/body. +/// +/// Example: `local digits = "0123456789"` in JsonParser._atoi() +/// +/// # Invariants +/// +/// - `name`: Variable name as it appears in the source code +/// - `host_id`: MIR ValueId of the original definition in the host function +/// - `is_immutable`: True if the variable is never reassigned in the function +#[derive(Debug, Clone)] +pub struct CapturedVar { + /// Variable name (e.g., "digits", "table") + pub name: String, + + /// MIR ValueId of the original definition in the host function + pub host_id: ValueId, + + /// Whether this variable is never reassigned in the function + /// + /// Phase 200-B will implement assignment analysis to determine this. + /// For now, this is always set to true as a conservative default. + pub is_immutable: bool, +} + +/// Environment containing function-scoped captured variables. +/// +/// Phase 200-A: Type definition only, not yet integrated with ConditionEnv. +/// Phase 200-B: Will be populated by FunctionScopeCaptureAnalyzer and +/// integrated into ConditionEnv via ConditionEnvBuilder v2. +#[derive(Debug, Clone, Default)] +pub struct CapturedEnv { + /// List of captured variables + pub vars: Vec, +} + +impl CapturedEnv { + /// Create a new empty environment + pub fn new() -> Self { + Self { vars: Vec::new() } + } + + /// Check if the environment is empty + pub fn is_empty(&self) -> bool { + self.vars.is_empty() + } + + /// Add a captured variable to the environment + pub fn add_var(&mut self, var: CapturedVar) { + self.vars.push(var); + } + + /// Look up a captured variable by name + /// + /// Returns `Some(&CapturedVar)` if found, `None` otherwise. + pub fn get(&self, name: &str) -> Option<&CapturedVar> { + self.vars.iter().find(|v| v.name == name) + } +} + +/// Analyzes function-scoped variables that can be safely captured for loop conditions/body. +/// +/// # Phase 200-A Status +/// +/// Currently returns empty CapturedEnv (skeleton implementation). +/// Actual capture detection will be implemented in Phase 200-B. +/// +/// # Future Detection Criteria (Phase 200-B+) +/// +/// A variable is captured if ALL of the following conditions are met: +/// +/// 1. **Declared before the loop**: Variable must be declared in function scope before the loop +/// 2. **Never reassigned**: Variable is never reassigned within the function (is_immutable = true) +/// 3. **Referenced in loop**: Variable is referenced in loop condition or body +/// 4. **Not a loop parameter**: Variable is not the loop iteration variable +/// 5. **Not a body-local**: Variable is not declared inside the loop body +/// +/// # Example +/// +/// ```nyash +/// method _atoi(s, pos, len) { +/// local digits = "0123456789" // ✅ Captured (declared before loop, never reassigned) +/// local value = 0 // ❌ Not captured (reassigned in loop body) +/// loop(pos < len) { +/// local ch = s.charAt(pos) // ❌ Not captured (body-local) +/// local digit = digits.indexOf(ch) +/// value = value * 10 + digit +/// pos = pos + 1 +/// } +/// } +/// ``` +/// +/// # Arguments +/// +/// * `_fn_body` - AST nodes of the function body (for analysis) +/// * `_loop_ast` - AST node of the loop statement +/// * `_scope` - LoopScopeShape (for excluding loop params and body-locals) +/// +/// # Returns +/// +/// `CapturedEnv` containing all captured variables (empty in Phase 200-A) +pub fn analyze_captured_vars( + _fn_body: &[ASTNode], + _loop_ast: &ASTNode, + _scope: &LoopScopeShape, +) -> CapturedEnv { + // Phase 200-A: Skeleton implementation + // TODO(Phase 200-B): Implement actual capture detection + // + // Detection algorithm: + // 1. Find all `local` declarations before the loop in fn_body + // 2. For each declaration: + // a. Check if it's never reassigned in the function (is_immutable = true) + // b. Check if it's referenced in loop condition or body + // c. Exclude if it's in scope.pinned, scope.carriers, or scope.body_locals + // 3. Collect matching variables into CapturedEnv + // 4. Return the populated environment + + CapturedEnv::new() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_captured_env_empty() { + let env = CapturedEnv::new(); + assert!(env.is_empty()); + assert!(env.get("digits").is_none()); + } + + #[test] + fn test_captured_env_add_and_get() { + let mut env = CapturedEnv::new(); + env.add_var(CapturedVar { + name: "digits".to_string(), + host_id: ValueId(42), + is_immutable: true, + }); + + assert!(!env.is_empty()); + let var = env.get("digits").unwrap(); + assert_eq!(var.name, "digits"); + assert_eq!(var.host_id, ValueId(42)); + assert!(var.is_immutable); + } + + #[test] + fn test_captured_env_multiple_vars() { + let mut env = CapturedEnv::new(); + env.add_var(CapturedVar { + name: "digits".to_string(), + host_id: ValueId(42), + is_immutable: true, + }); + env.add_var(CapturedVar { + name: "table".to_string(), + host_id: ValueId(100), + is_immutable: true, + }); + + assert_eq!(env.vars.len(), 2); + assert!(env.get("digits").is_some()); + assert!(env.get("table").is_some()); + assert!(env.get("nonexistent").is_none()); + } +} diff --git a/src/mir/loop_pattern_detection/mod.rs b/src/mir/loop_pattern_detection/mod.rs index c09fae8c..4fd83a43 100644 --- a/src/mir/loop_pattern_detection/mod.rs +++ b/src/mir/loop_pattern_detection/mod.rs @@ -780,3 +780,6 @@ pub use trim_loop_helper::TrimLoopHelper; // Phase 33-23: Break Condition Analysis (Stage 2, Issue 6) pub mod break_condition_analyzer; + +// Phase 200-A: Function Scope Capture Infrastructure +pub mod function_scope_capture;