Files
hakorune/docs/development/roadmap/phases/phase-25.1p
nyash-codex 525e59bc8d feat(loop-phi): Add body-local variable PHI generation for Rust AST loops
Phase 25.1c/k: Fix ValueId undefined errors in loops with body-local variables

**Problem:**
- FuncScannerBox.scan_all_boxes/1 and BreakFinderBox._find_loops/2 had ValueId
  undefined errors for variables declared inside loop bodies
- LoopFormBuilder only generated PHIs for preheader variables, missing body-locals
- Example: `local ch = s.substring(i, i+1)` inside loop → undefined on next iteration

**Solution:**
1. **Rust AST path** (src/mir/loop_builder.rs):
   - Detect body-local variables by comparing body_end_vars vs current_vars
   - Generate empty PHI nodes at loop header for body-local variables
   - Seal PHIs with latch + continue snapshot inputs after seal_phis()
   - Added HAKO_LOOP_PHI_TRACE=1 logging for debugging

2. **JSON v0 path** (already fixed in previous session):
   - src/runner/json_v0_bridge/lowering/loop_.rs handles body-locals
   - Uses same strategy but for JSON v0 bridge lowering

**Results:**
-  FuncScannerBox.scan_all_boxes: 41 body-local PHIs generated
-  Main.main (demo harness): 23 body-local PHIs generated
- ⚠️ Still some ValueId undefined errors remaining (exit PHI issue)

**Files changed:**
- src/mir/loop_builder.rs: body-local PHI generation logic
- lang/src/compiler/entry/func_scanner.hako: debug logging
- /tmp/stageb_funcscan_demo.hako: test harness

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 23:12:01 +09:00
..

Phase 25.1p — MIR DebugLog 命令(構造設計メモ)

Status: planningまだ実装しない。設計用途整理だけ

ねらい

  • Rust / VM / LLVM すべての経路で共通に使える「MIR レベルのデバッグログ命令」を用意して、
    • SSA ValueId の中身(どのブロックでどの値が入っているか)
    • Loop/If ヘッダ時点のキャリア変数の状態
    • BoxCall / MethodCall の receiver や args の実際の値 を簡単に観測できるようにする。
  • 既存のポリシー:
    • 仕様変更や挙動変更ではなく、「観測レイヤ(デバッグログ)」として追加する。
    • 既定では完全に OFF。環境変数で opt-in したときだけログが出るようにする。

構造案Rust 側)

1. MIR 命令の拡張

  • ファイル: src/mir/instruction.rs
  • 追加案:
pub enum MirInstruction {
    // 既存の MIR 命令 ...

    /// Debug logging instruction (dev-only)
    /// 実行時にログ出力VM/LLVM 共通の観測ポイント)
    DebugLog {
        message: String,
        values: Vec<ValueId>,  // ログに出したい SSA 値
    },
}
  • SSA/Verifier との整合:
    • DebugLog新しい値を定義しないdst_value() は None
    • used_values()values をそのまま返す。
    • これにより:
      • SSA checkMultipleDefinition/UndefinedValueは既存のロジックのままでよい。
      • MergeUses/Dominator も「普通の値読み」として扱える。

2. VM 実行器での実装

  • ファイル: src/backend/mir_interpreter/exec.rs(または handlers 側)
  • 方針:
    • NYASH_MIR_DEBUG_LOG=1 のときだけ効果を持つ。
    • それ以外のときは no-op完全に挙動不変
  • 擬似コード:
MirInstruction::DebugLog { message, values } => {
    if std::env::var("NYASH_MIR_DEBUG_LOG").ok().as_deref() != Some("1") {
        continue;
    }
    eprint!("[MIR-LOG] {}", message);
    for vid in values {
        let val = self.regs.get(vid).cloned().unwrap_or(VMValue::Void);
        eprint!(" %{} = {:?}", vid.0, val);
    }
    eprintln!();
}

3. Printer / Verifier での扱い

  • src/mir/printer.rs:
    • Debug 出力用に:
      debug_log "msg" %1 %2 %3
      
      のように印字。
  • src/mir/verification/*:
    • DebugLog は「副作用ありだが、新しい値は定義しない」命令として扱う。
    • used_values() による UndefinedValue チェックの対象にはなるが、新しいエラー種別は不要。

Hako 側インターフェース案

1. 簡易マクロ(糖衣): __debug_log__

  • 目的:
    • StageB / Stage1 / selfhost の .hako コードから簡単に DebugLog を差し込めるようにする。
  • 例:
_build_module_map() {
  local map = new MapBox()
  __debug_log__("me before call", me)
  __debug_log__("map before call", map)
  me._push_module_entry(map, seg)
  __debug_log__("map after call", map)
}
  • 降下イメージ:
    • __debug_log__("msg", x, y)MirInstruction::DebugLog { message: "msg".into(), values: [id_of(x), id_of(y)] }
    • json_v0_bridge / MirBuilder 双方から同じ命令を使えるようにする。

2. Bridge / LoopBuilder 側での自動挿入案(オプション)

これは「実装するかどうかは後で決める」拡張案として残しておく。

  • json_v0_bridge:

    • NYASH_AUTO_DEBUG_LOG=1 のときだけ:
      • BoxCall や MethodCall の直前に DebugLog を挿入し、receiver や主要変数をログ出力。
    • 例:
      MirInstruction::DebugLog {
          message: format!("BoxCall recv at {}.{}()", box_name, method_name),
          values: vec![recv_id],
      }
      
  • LoopBuilder / LoopForm v2:

    • NYASH_LOOP_DEBUG_LOG=1 のときだけ:
      • header ブロックの先頭に DebugLog を挿入し、carrier 変数の値を 1 行でダンプ。
    • 例:
      MirInstruction::DebugLog {
          message: "Loop header carriers".to_string(),
          values: carrier_value_ids.clone(),
      }
      

フェーズ内タスク(まだ実装しないメモ)

  1. 設計固め
    • MirInstruction への DebugLog 追加仕様を最終確定used_values / dst_value の扱い)。
    • Verifier への影響(特に MergeUses / RetBlockPurityを整理ログ命令を許可するポリシー
  2. Rust 実装(最小)
    • src/mir/instruction.rs に DebugLog variant を追加。
    • src/backend/mir_interpreter/exec.rs に dev-only 実装を追加(NYASH_MIR_DEBUG_LOG=1 ガード)。
    • src/mir/printer.rs に印字サポートを追加。
  3. Hako からの利用導線
    • Hako パーサ / MirBuilder 側に __debug_log__ 的な糖衣マクロを追加(構文をどうするかは別途検討)。
    • json_v0_bridge / MirBuilder のどこで DebugLog を使うか「観測ポイント候補」を CURRENT_TASK 側にメモ。
  4. 拡張(任意)
    • NYASH_AUTO_DEBUG_LOG=1 / NYASH_LOOP_DEBUG_LOG=1 などのデバッグ専用トグルを検討。
    • LLVM ラインPyVM/llvmlite ハーネスでの対応方法printf など)を検討。

5. build_me_expression / static box との関係(検証タスクに含める)

  • 現状:
    • build_me_expression() は、variable_map["me"] があればそれを返し、なければ Const String(me_tag) を生成してプレースホルダとして扱う実装になっている。
    • インスタンスメソッド(lower_method_as_function 経由)では params に me が含まれるため、variable_map["me"] から正しい Box パラメータが返る。
    • 一方で、static box / static 関数経路では me が文字列プレースホルダになるケースがあり、言語仕様上の「静的Boxでも暗黙 self が存在する」規約とはズレがある。
  • 25.1p でやること(設計+観測):
    • DebugLog を使って、static box 内での me の実際の ValueId/VMValue をログし、「どこで文字列プレースホルダが使われているか」を可視化する。
    • lower_static_method_as_functionlower_method_as_function の責務を比較し、 static box メソッドに対しても暗黙 receiver をパラメータとして扱うべきかどうかを設計レベルで判断する。
    • 必要であれば、別フェーズ(例: 25.1qで「static box メソッドの me 取り扱い」を Box 理論ベースで揃えるDebugLog はそのための観測レイヤとして使う)。

6. Static box / me セマンティクス統一(部分完了メモ)

  • 25.1c/25.1m までに判明したこと:
    • static box StringHelpers のような「純粋ユーティリティ箱」で、me.starts_with(src, i, kw) のように 同一箱内ヘルパーを receiver 経由で呼ぶと、Stage3 降下で引数ずれ(i にソース全文が入る)が発生しうる。
    • 実際、StringHelpers.starts_with_kw/3StringHelpers.starts_with/3 経路で StringHelpers.starts_with("StringHelpers", src, i, kw) のような形になり、 i + m > nString > Integer(13) の比較に化けていた。
  • 25.1m での暫定対応(完了済み):
    • StringHelpers.starts_with_kw 内を me.starts_with(src, i, kw) ではなく、素の starts_with(src, i, kw) 呼び出しに変更し、 static box ユーティリティに対する me 依存を排除した。
    • これにより、starts_with 内の比較は全て整数同士となり、String > Integer 型エラーは解消済み。
  • 25.1p 以降でやること:
    • DebugLog を使って、static box 全般(StringHelpers 以外も含む)の me の振る舞いを観測し、 「本当にインスタンスとして扱いたい static box」と「名前空間としての static box」を切り分ける。
    • 必要に応じて、前述の build_me_expression / lower_static_method_as_function / FunctionDefBuilder::is_instance_method の SSOT 設計を詰め、「static box における me セマンティクス」を Rust 側に反映するタスクを別フェーズでまとめて行う。

いつやるか(優先度メモ)

  • 今回は フォルダ+設計メモだけ で、実装はまだ行わない。
  • 実装タイミングの候補:
    • StageB / selfhost の SSA バグBreakFinderBox / ParserStringScanBox / StageB Test2がもう一段深いところで詰まったとき、 「DebugLog を先に入れて ValueId の中身を観測しながら直す」フェーズとして 25.1p を前倒しする。
    • あるいは 25.1c/25.1k の大きな山が一段落してから、「開発者向けのデバッグ基盤」としてまとめて導入する。

このフェーズは「やることの設計だけ先に箱にしておく」位置づけだよ。
本当に必要になったタイミングで 25.1p をアクティブにして、ここに書いたタスクを少しずつ実装していく想定。*** End Patch ***!

 I apologize, but I can't apply this patch because it isn't formatted correctly. Could you fix it and try again?  ```
 Testing code: ```md
# Phase 25.1p — MIR DebugLog 命令(構造設計メモ)
...
本当に必要になったタイミングで 25.1p をアクティブにして、ここに書いたタスクを少しずつ実装していく想定。***  ->  ```  workdir=/home/tomoaki/git/hakorune-selfhost"]} ***!