Files
hakorune/docs/development/current/main/phase200-B-capture-impl.md

530 lines
16 KiB
Markdown
Raw Normal View History

# Phase 200-B: FunctionScopeCaptureAnalyzer 実装 & ConditionEnv 統合
**Date**: 2025-12-09
**Status**: Ready for Implementation
**Prerequisite**: Phase 200-A complete
---
## ゴール
1. **CapturedEnv に「安全にキャプチャできる関数ローカル」を実際に埋める**
2. **ConditionEnv / JoinInlineBoundaryBuilder に統合して、`digits` みたいな変数を JoinIR 側から参照できるようにする**
3. **影響範囲は `_parse_number` / `_atoi` の最小ケースに限定、挙動は Fail-Fast を維持**
**スコープ制限**:
- ✅ ConditionEnv に digits を見せられるようにする
-`digits.indexOf(ch)` の E2E 動作は Phase 200-CComplexAddendNormalizer 連携)
---
## Task 200-B-1: capture 判定ロジック実装
### 対象ファイル
`src/mir/loop_pattern_detection/function_scope_capture.rs`Phase 200-A で作ったスケルトン)
### 実装内容
`analyze_captured_vars(fn_body, loop_ast, scope) -> CapturedEnv` を実装する。
**判定条件(全部満たしたものだけ許可)**:
1. **関数トップレベルで `local name = <expr>;` として 1 回だけ定義されている**
- ループより前の位置で定義
- 複数回定義されていない
2. **その変数 `name` はループ本体(条件含む)で読み取りのみ(再代入なし)**
- ループ内で `name = ...` が存在しない
3. **`<expr>` は「安全な初期式」だけ**:
- 文字列リテラル `"0123456789"`
- 整数リテラル `123`
- 将来拡張を見越して Const 系だけにしておくMethodCall 等はまだ対象外)
### 実装アルゴリズム
```rust
pub fn analyze_captured_vars(
fn_body: &[Stmt],
loop_ast: &Stmt,
scope: &LoopScopeShape,
) -> CapturedEnv {
let mut env = CapturedEnv::new();
// Step 1: Find loop position in fn_body
let loop_index = find_stmt_index(fn_body, loop_ast);
// Step 2: Collect local declarations BEFORE the loop
let pre_loop_locals = collect_local_declarations(&fn_body[..loop_index]);
// Step 3: For each pre-loop local, check:
for local in pre_loop_locals {
// 3a: Is init expression a safe constant?
if !is_safe_const_init(&local.init) {
continue;
}
// 3b: Is this variable reassigned anywhere in fn_body?
if is_reassigned_in_fn(fn_body, &local.name) {
continue;
}
// 3c: Is this variable used in loop (condition or body)?
if !is_used_in_loop(loop_ast, &local.name) {
continue;
}
// 3d: Skip if already in LoopParam or LoopBodyLocal
if scope.loop_params.contains(&local.name) || scope.body_locals.contains(&local.name) {
continue;
}
// All checks passed: add to CapturedEnv
env.add_var(CapturedVar {
name: local.name.clone(),
host_id: local.value_id, // From scope/variable_map
is_immutable: true,
});
}
env
}
/// Check if expression is a safe constant (string/integer literal)
fn is_safe_const_init(expr: &Option<Expr>) -> bool {
match expr {
Some(Expr::StringLiteral(_)) => true,
Some(Expr::IntegerLiteral(_)) => true,
_ => false,
}
}
/// Check if variable is reassigned anywhere in function body
fn is_reassigned_in_fn(fn_body: &[Stmt], name: &str) -> bool {
// Walk all statements, check for `name = ...` (excluding initial declaration)
// Implementation uses AST visitor pattern
}
/// Check if variable is referenced in loop condition or body
fn is_used_in_loop(loop_ast: &Stmt, name: &str) -> bool {
// Walk loop AST, check for Identifier(name) references
}
```
### ユニットテスト
```rust
#[test]
fn test_capture_simple_digits() {
// local digits = "0123456789"
// loop(i < 10) { digits.indexOf(ch) }
// → 1 var captured (digits)
}
#[test]
fn test_capture_reassigned_rejected() {
// local digits = "0123456789"
// digits = "abc" // reassignment
// loop(i < 10) { digits.indexOf(ch) }
// → 0 vars captured
}
#[test]
fn test_capture_after_loop_rejected() {
// loop(i < 10) { ... }
// local digits = "0123456789" // defined AFTER loop
// → 0 vars captured
}
#[test]
fn test_capture_method_call_init_rejected() {
// local result = someBox.getValue() // MethodCall init
// loop(i < 10) { result.indexOf(ch) }
// → 0 vars captured (not safe const)
}
```
### 成果物
- [x] `analyze_captured_vars` 本実装
- [x] ヘルパ関数(`is_safe_const_init`, `is_reassigned_in_fn`, `is_used_in_loop`
- [x] 4+ unit tests
---
## Task 200-B-2: ConditionEnvBuilder v2 実装
### 対象ファイル
`src/mir/builder/control_flow/joinir/patterns/condition_env_builder.rs`
### 実装内容
`build_with_captures(loop_var_name, captured, boundary) -> ConditionEnv` を本実装にする。
```rust
pub fn build_with_captures(
loop_var_name: &str,
captured: &CapturedEnv,
boundary: &mut JoinInlineBoundaryBuilder,
) -> ConditionEnv {
// Step 1: Build base ConditionEnv with loop params (existing logic)
let mut env = build_loop_param_only(loop_var_name, boundary);
// Step 2: Add captured vars as ParamRole::Condition
for var in &captured.vars {
// 2a: Add to boundary with Condition role
boundary.add_param_with_role(&var.name, var.host_id, ParamRole::Condition);
// 2b: Add to ConditionEnv.captured map
// Need JoinIR ValueId from boundary/remapper
let join_id = boundary.get_condition_binding(&var.name)
.expect("captured var should be in boundary");
env.captured.insert(var.name.clone(), join_id);
}
// Step 3: Debug guard - Condition params must NOT be in PHI candidates
#[cfg(debug_assertions)]
for var in &captured.vars {
assert!(
!env.params.contains_key(&var.name),
"Captured var '{}' must not be in loop params (ParamRole conflict)",
var.name
);
}
env
}
```
### ConditionEnv 拡張
```rust
pub struct ConditionEnv {
pub params: BTreeMap<String, ValueId>, // LoopParam (existing)
pub captured: BTreeMap<String, ValueId>, // NEW: Captured vars (ParamRole::Condition)
}
impl ConditionEnv {
/// Look up a variable (params first, then captured)
pub fn get(&self, name: &str) -> Option<ValueId> {
self.params.get(name).copied()
.or_else(|| self.captured.get(name).copied())
}
/// Check if variable is a captured (Condition role) var
pub fn is_captured(&self, name: &str) -> bool {
self.captured.contains_key(name)
}
}
```
### JoinInlineBoundaryBuilder 更新
```rust
impl JoinInlineBoundaryBuilder {
pub fn add_param_with_role(&mut self, name: &str, host_id: ValueId, role: ParamRole) {
match role {
ParamRole::LoopParam | ParamRole::Carrier => {
// Existing: add to join_inputs
self.add_input(name, host_id);
}
ParamRole::Condition => {
// NEW: Add to condition_bindings only (no PHI, no ExitLine)
let join_id = self.alloc_value(); // Allocate JoinIR ValueId
self.condition_bindings.push(ConditionBinding {
name: name.to_string(),
host_id,
join_id,
role: ParamRole::Condition,
});
}
ParamRole::ExprResult => {
// Handled by set_expr_result
}
}
}
pub fn get_condition_binding(&self, name: &str) -> Option<ValueId> {
self.condition_bindings.iter()
.find(|b| b.name == name)
.map(|b| b.join_id)
}
}
```
### ユニットテスト
```rust
#[test]
fn test_build_with_empty_captures() {
// CapturedEnv empty → same as existing build
let captured = CapturedEnv::new();
let env = build_with_captures("i", &captured, &mut builder);
assert!(env.captured.is_empty());
}
#[test]
fn test_build_with_digits_capture() {
// CapturedEnv with "digits"
let mut captured = CapturedEnv::new();
captured.add_var(CapturedVar {
name: "digits".to_string(),
host_id: ValueId(42),
is_immutable: true,
});
let env = build_with_captures("i", &captured, &mut builder);
// Verify captured var is in ConditionEnv
assert!(env.captured.contains_key("digits"));
// Verify boundary has Condition role
let binding = builder.get_condition_binding("digits").unwrap();
// binding should exist with ParamRole::Condition
}
```
### 成果物
- [x] `build_with_captures` 本実装
- [x] `ConditionEnv.captured` フィールド追加
- [x] `add_param_with_role` の Condition ブランチ実装
- [x] 2+ unit tests
---
## Task 200-B-3: パイプライン組み込み
### 対象
PatternPipelineContext / Pattern lowerer の「前処理パス」
### 実装内容
Pattern 決定後、JoinIR lowering に入る前の箇所で capture 解析を挿入。
```rust
// In pattern lowerer (e.g., pattern2_with_break.rs)
// Step 1: Existing - build PatternPipelineContext
let pipeline_ctx = PatternPipelineContext::new(/* ... */);
// Step 2: NEW - Analyze captured vars
let captured = analyze_captured_vars(
&fn_body, // Function body statements
&loop_ast, // Loop AST
&pipeline_ctx.loop_scope,
);
// Step 3: Build ConditionEnv with captures
let cond_env = build_with_captures(
&pipeline_ctx.loop_var_name,
&captured,
&mut boundary_builder,
);
// Step 4: Proceed with JoinIR lowering using cond_env
```
### 段階適用(今フェーズ)
- **Pattern 2 のみに適用**`_parse_number` / `_atoi` 向け)
- 他パターンP1/P3/P4は既存 ConditionEnv のまま(影響なし)
### テストファイル whitelist
```rust
// routing.rs に追加(必要な場合)
// Phase 200-B: digits capture test cases
"phase200_digits_atoi_min",
"phase200_digits_parse_number_min",
```
### 成果物
- [x] Pattern 2 に capture 解析パス追加
- [x] 必要に応じて whitelist 更新
---
## Task 200-B-4: digits ケース検証
### テストファイル作成
#### `apps/tests/phase200_digits_atoi_min.hako`
```nyash
// Phase 200-B: Minimal atoi with digits capture
static box Main {
main() {
local s = "123"
local digits = "0123456789" // ← Captured var
local i = 0
local v = 0
local n = s.length()
loop(i < n) {
local ch = s.substring(i, i+1)
local pos = digits.indexOf(ch) // ← Uses captured digits
if pos < 0 {
break
}
v = v * 10 + pos
i = i + 1
}
print(v) // Expected: 123
}
}
```
#### `apps/tests/phase200_digits_parse_number_min.hako`
```nyash
// Phase 200-B: Minimal parse_number with digits capture
static box Main {
main() {
local s = "42abc"
local digits = "0123456789" // ← Captured var
local p = 0
local num_str = ""
local n = s.length()
loop(p < n) {
local ch = s.substring(p, p+1)
local digit_pos = digits.indexOf(ch) // ← Uses captured digits
if digit_pos < 0 {
break
}
num_str = num_str + ch
p = p + 1
}
print(num_str) // Expected: "42"
}
}
```
### 検証手順
```bash
# Step 1: 構造トレースPattern 選択確認)
NYASH_JOINIR_CORE=1 NYASH_JOINIR_STRUCTURE_ONLY=1 ./target/release/hakorune \
apps/tests/phase200_digits_atoi_min.hako 2>&1 | head -30
# Expected: Pattern 2 selected, NO [joinir/freeze]
# Step 2: Capture tracedigits が捕捉されているか)
NYASH_JOINIR_CORE=1 NYASH_CAPTURE_DEBUG=1 ./target/release/hakorune \
apps/tests/phase200_digits_atoi_min.hako 2>&1 | grep -i "capture"
# Expected: [capture] Found: digits (host_id=XX, is_immutable=true)
# Step 3: E2E 実行
NYASH_JOINIR_CORE=1 ./target/release/hakorune apps/tests/phase200_digits_atoi_min.hako
# Phase 200-B Goal: digits がConditionEnv に見えていることを確認
# E2E 動作は Phase 200-CComplexAddendNormalizer + digits.indexOf 連携)
```
### 期待される結果
**Phase 200-B のゴール達成**:
-`digits` が CapturedEnv に捕捉される
-`digits` が ConditionEnv.captured に存在する
- ✅ boundary に ParamRole::Condition として登録される
**Phase 200-C への引き継ぎ**:
- ⚠️ `digits.indexOf(ch)` の E2E 動作はまだ Fail-Fast の可能性あり
- → ComplexAddendNormalizer が MethodCall を扱えるようにする必要あり
### 成果物
- [x] `phase200_digits_atoi_min.hako` テストファイル
- [x] `phase200_digits_parse_number_min.hako` テストファイル
- [x] 構造トレース確認
- [x] Capture debug 確認
---
## Task 200-B-5: ドキュメント更新
### 1. joinir-architecture-overview.md
**Section 2.3 に追記**:
```markdown
- **FunctionScopeCaptureAnalyzer** (Phase 200-B 実装完了)
- 責務: 関数スコープの「実質定数」を検出
- 判定条件:
1. 関数トップレベルで 1 回だけ定義
2. ループ内で再代入なし
3. 安全な初期式(文字列/整数リテラル)のみ
- 結果: CapturedEnv に name, host_id, is_immutable を格納
- **ConditionEnvBuilder v2** (Phase 200-B 実装完了)
- 責務: CapturedEnv から ParamRole::Condition として ConditionEnv に追加
- 経路: analyze_captured_vars → build_with_captures → ConditionEnv.captured
- 不変条件: Condition role は Header PHI / ExitLine の対象にならない
```
### 2. CURRENT_TASK.md
```markdown
- [x] **Phase 200-B: FunctionScopeCaptureAnalyzer 実装 & ConditionEnv 統合** ✅ (完了: 2025-12-XX)
- **目的**: digits 等の関数ローカルを ConditionEnv から参照可能に
- **実装内容**:
- 200-B-1: capture 判定ロジック実装 ✅
- 200-B-2: ConditionEnvBuilder v2 実装 ✅
- 200-B-3: パイプライン組み込みPattern 2
- 200-B-4: digits ケース検証 ✅
- 200-B-5: ドキュメント更新 ✅
- **成果**:
- digits が CapturedEnv に捕捉される ✅
- ConditionEnv.captured に登録される ✅
- ParamRole::Condition として boundary に追加される ✅
- **制約**:
- digits.indexOf(ch) の E2E 動作は Phase 200-C
- ComplexAddendNormalizer の MethodCall 対応が必要
- **次フェーズ**: Phase 200-Cdigits.indexOf E2E 連携)
```
---
## 成功基準
- [x] `analyze_captured_vars` が digits を正しく検出
- [x] `build_with_captures` が ConditionEnv.captured に追加
- [x] boundary に ParamRole::Condition として登録
- [x] 既存テストが退行しない
- [x] Unit tests (6+ 件) が PASS
- [x] phase200_digits_*.hako で capture が確認できる
---
## 設計原則Phase 200-B
1. **スコープ限定**: digits 系の最小ケースのみ
2. **Fail-Fast 維持**: 安全でないパターンは即座に拒否
3. **段階適用**: Pattern 2 のみに適用、他パターンは影響なし
4. **E2E 分離**: ConditionEnv への統合と、MethodCall 連携は別フェーズ
---
## 関連ファイル
### 修正対象
- `src/mir/loop_pattern_detection/function_scope_capture.rs`
- `src/mir/builder/control_flow/joinir/patterns/condition_env_builder.rs`
- `src/mir/join_ir/lowering/inline_boundary_builder.rs`
- `src/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs`
### 新規作成
- `apps/tests/phase200_digits_atoi_min.hako`
- `apps/tests/phase200_digits_parse_number_min.hako`
### ドキュメント
- `docs/development/current/main/joinir-architecture-overview.md`
- `CURRENT_TASK.md`