Phase 200-B: FunctionScopeCaptureAnalyzer implementation - analyze_captured_vars_v2() with structural loop matching - CapturedEnv for immutable function-scope variables - ParamRole::Condition for condition-only variables Phase 200-C: ConditionEnvBuilder extension - build_with_captures() integrates CapturedEnv into ConditionEnv - fn_body propagation through LoopPatternContext to Pattern 2 Phase 200-D: E2E verification - capture detection working for base, limit, n etc. - Test files: phase200d_capture_minimal.hako, phase200d_capture_in_condition.hako Phase 201-A: MirBuilder reserved_value_ids infrastructure - reserved_value_ids: HashSet<ValueId> field in MirBuilder - next_value_id() skips reserved IDs - merge/mod.rs sets/clears reserved IDs around JoinIR merge Phase 201: JoinValueSpace design document - Param/Local/PHI disjoint regions design - API: alloc_param(), alloc_local(), reserve_phi() - Migration plan for Pattern 1-4 lowerers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
16 KiB
16 KiB
Phase 200-B: FunctionScopeCaptureAnalyzer 実装 & ConditionEnv 統合
Date: 2025-12-09 Status: Ready for Implementation Prerequisite: Phase 200-A complete
ゴール
- CapturedEnv に「安全にキャプチャできる関数ローカル」を実際に埋める
- ConditionEnv / JoinInlineBoundaryBuilder に統合して、
digitsみたいな変数を JoinIR 側から参照できるようにする - 影響範囲は
_parse_number/_atoiの最小ケースに限定、挙動は Fail-Fast を維持
スコープ制限:
- ✅ ConditionEnv に digits を見せられるようにする
- ❌
digits.indexOf(ch)の E2E 動作は Phase 200-C(ComplexAddendNormalizer 連携)
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 を実装する。
判定条件(全部満たしたものだけ許可):
-
関数トップレベルで
local name = <expr>;として 1 回だけ定義されている- ループより前の位置で定義
- 複数回定義されていない
-
その変数
nameはループ本体(条件含む)で読み取りのみ(再代入なし)- ループ内で
name = ...が存在しない
- ループ内で
-
<expr>は「安全な初期式」だけ:- 文字列リテラル
"0123456789" - 整数リテラル
123 - 将来拡張を見越して Const 系だけにしておく(MethodCall 等はまだ対象外)
- 文字列リテラル
実装アルゴリズム
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
}
ユニットテスト
#[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)
}
成果物
analyze_captured_vars本実装- ヘルパ関数(
is_safe_const_init,is_reassigned_in_fn,is_used_in_loop) - 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 を本実装にする。
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 拡張
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 更新
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)
}
}
ユニットテスト
#[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
}
成果物
build_with_captures本実装ConditionEnv.capturedフィールド追加add_param_with_roleの Condition ブランチ実装- 2+ unit tests
Task 200-B-3: パイプライン組み込み
対象
PatternPipelineContext / Pattern lowerer の「前処理パス」
実装内容
Pattern 決定後、JoinIR lowering に入る前の箇所で capture 解析を挿入。
// 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
// routing.rs に追加(必要な場合)
// Phase 200-B: digits capture test cases
"phase200_digits_atoi_min",
"phase200_digits_parse_number_min",
成果物
- Pattern 2 に capture 解析パス追加
- 必要に応じて whitelist 更新
Task 200-B-4: digits ケース検証
テストファイル作成
apps/tests/phase200_digits_atoi_min.hako
// 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
// 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"
}
}
検証手順
# 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 trace(digits が捕捉されているか)
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-C(ComplexAddendNormalizer + digits.indexOf 連携)
期待される結果
Phase 200-B のゴール達成:
- ✅
digitsが CapturedEnv に捕捉される - ✅
digitsが ConditionEnv.captured に存在する - ✅ boundary に ParamRole::Condition として登録される
Phase 200-C への引き継ぎ:
- ⚠️
digits.indexOf(ch)の E2E 動作はまだ Fail-Fast の可能性あり - → ComplexAddendNormalizer が MethodCall を扱えるようにする必要あり
成果物
phase200_digits_atoi_min.hakoテストファイルphase200_digits_parse_number_min.hakoテストファイル- 構造トレース確認
- Capture debug 確認
Task 200-B-5: ドキュメント更新
1. joinir-architecture-overview.md
Section 2.3 に追記:
- **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
- [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-C(digits.indexOf E2E 連携)
成功基準
analyze_captured_varsが digits を正しく検出build_with_capturesが ConditionEnv.captured に追加- boundary に ParamRole::Condition として登録
- 既存テストが退行しない
- Unit tests (6+ 件) が PASS
- phase200_digits_*.hako で capture が確認できる
設計原則(Phase 200-B)
- スコープ限定: digits 系の最小ケースのみ
- Fail-Fast 維持: 安全でないパターンは即座に拒否
- 段階適用: Pattern 2 のみに適用、他パターンは影響なし
- E2E 分離: ConditionEnv への統合と、MethodCall 連携は別フェーズ
関連ファイル
修正対象
src/mir/loop_pattern_detection/function_scope_capture.rssrc/mir/builder/control_flow/joinir/patterns/condition_env_builder.rssrc/mir/join_ir/lowering/inline_boundary_builder.rssrc/mir/builder/control_flow/joinir/patterns/pattern2_with_break.rs
新規作成
apps/tests/phase200_digits_atoi_min.hakoapps/tests/phase200_digits_parse_number_min.hako
ドキュメント
docs/development/current/main/joinir-architecture-overview.mdCURRENT_TASK.md