Files
hakorune/docs/development/current/main/phase200-B-capture-impl.md
nyash-codex d4f90976da refactor(joinir): Phase 244 - ConditionLoweringBox trait unification
Unify condition lowering logic across Pattern 2/4 with trait-based API.

New infrastructure:
- condition_lowering_box.rs: ConditionLoweringBox trait + ConditionContext (293 lines)
- ExprLowerer implements ConditionLoweringBox trait (+51 lines)

Pattern migrations:
- Pattern 2 (loop_with_break_minimal.rs): Use trait API
- Pattern 4 (loop_with_continue_minimal.rs): Use trait API

Benefits:
- Unified condition lowering interface
- Extensible for future lowering strategies
- Clean API boundary between patterns and lowering logic
- Zero code duplication

Test results: 911/911 PASS (+2 new tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-11 02:35:31 +09:00

16 KiB
Raw Blame 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.rsPhase 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 等はまだ対象外)

実装アルゴリズム

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 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 を扱えるようにする必要あり

成果物

  • 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-Cdigits.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

  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 Status: Active
    Scope: ConditionEnv capture 実装メモJoinIR v2 / selfhost 深度2 用)