feat(stageb): Phase 25.1c - Stage-B トレース追加(dev-only)

追加内容:
- StageBTraceBox: dev トレース用 Box 追加(HAKO_STAGEB_TRACE=1 で有効)
- トレースポイント:
  - StageBArgsBox.resolve_src: enter/return_len
  - StageBBodyExtractorBox.build_body_src: enter_len/return_len
  - StageBDriverBox.main: enter/after_resolve_src/after_build_body_src/
    after_parse_program2/func_scan methods/exit rc=0

Phase 25.1c 目標:
- Stage-B / Stage-1 CLI 構造デバッグ
- fib canary / selfhost CLI canary の rc=1 原因特定

ポリシー:
- dev env でガード(挙動不変)
- 既定挙動は変更せず、観測のみ追加

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-11-19 04:49:25 +09:00
parent 5c5e1bd099
commit fa571a656e
5 changed files with 193 additions and 7 deletions

View File

@ -31,6 +31,60 @@ Update (2025-11-18 — Phase 25.1k: LoopSSA v2 (.hako) & StageB harness 追
- KC: PhiInjectorBox に Carrier/Pinned ベースの v2 入口を追加(既存 `_collect_phi_vars` は当面維持)。
- KD: StageB Test3 の `%0` SSA 問題が悪化していないことを確認しつつ、LoopSSA 有効/無効の切り分けを残す。
Update (2025-11-18 — Phase 25.1b: StageB/Ny selfhost parser depth guards hotfix)
- Context:
- StageB / Ny selfhost ラインでは、`ParserBox.parse_program2``ParserStmtBox.parse``ParserControlBox.*` の再帰ガードに `HAKO_STAGEB_*_DEPTH` 環境変数を使っており、誤ループや自己再帰を検出する設計になっている。
- ただしこれまでの実装では、正常終了パスで depth を 0 に戻していない箇所があり、一度ガードが立つとその後のすべての呼び出しが「再帰検出扱い」になってしまい、空文列 `""` やダミー Break/Continue を返すことで **進捗ガード頼みの疑似無限ループ** を引き起こしていた。
- 特に `.ny` の大きなソースを inline selfhost 経路(`ny_selfhost_inline.sh` / `inline_selfhost_emit.hako` 相当)で処理した場合に、`ParserBox.parse_program2` が 60s 付近まで進んでから `Undefined variable: local` を出す背景の一つになっていた疑いがある。
- Done:
- `lang/src/compiler/parser/stmt/parser_stmt_box.hako`:
- `ParserStmtBox.parse` の再帰ガード:
- 入口で `HAKO_STAGEB_STMT_DEPTH``"1"` にセットし、すでに `"!=0"` のときは `[stageb/recursion] ParserStmtBox.parse recursion detected` を出して空文 `""` を返す構造は維持。
- そのうえで「正常に 1 文パースして return する」すべての分岐で `env.set("HAKO_STAGEB_STMT_DEPTH", "0")` を明示的に呼ぶように修正し、**1 文1 ガードサイクル** として閉じるようにした。
- 対象: `@extern_c` アノテーション、`using` 文(`parse_using`)、代入文(`Local` 生成)、`return` 文、`local` 宣言Stage3`if/loop/break/continue/throw/try` を各 Box に委譲する分岐、および既存の Expr fallback。
- これにより、1 回目の parse 成功後に `HAKO_STAGEB_STMT_DEPTH` が 1 のまま残り、2 回目以降がすべて再帰検出ブランチに落ちるバグを解消StageB/Stage1/自前 Ny selfhost での multistmt ソースを安定して扱えるようにした)。
- `lang/src/compiler/parser/stmt/parser_control_box.hako`:
- `parse_break` / `parse_continue` の Stage3 パスで、正常終了時にもそれぞれ `HAKO_STAGEB_BREAK_DEPTH` / `HAKO_STAGEB_CONTINUE_DEPTH``"0"` に戻すように修正。
- これにより、break/continue を含むループを Stage3 構文でパースした後も、次の `parse_stmt2` 呼び出しからは再び通常パスに戻れるようになったbreak/continue 自体は既存どおり `{type:"Break"}` / `{type:"Continue"}` を返す)。
- Quick verification:
- `tools/ny_selfhost_inline.sh local_tests/test_large_method_break.nyash target/release/hakorune`:
- 実行時に Rust VM の `Invalid value: use of undefined value ValueId(..)` や無限ループは発生せず、inline selfhost パーサ経路は正常終了することを確認。
- これにより Ny selfhost パイプラインの **「Rust VM の SSA/PHI 破綻で即死」フェーズは抜けており、残る課題は .hako 側 StageB コンパイラ(構文/using/Stage3 周辺)の rc=1 に集約されている** ことがはっきりした。
- Current status of Phase 25.1 selfhost canaries:
- `tools/smokes/v2/profiles/quick/core/phase251/stageb_fib_program_defs_canary_vm.sh`:
- 依然として StageB rc=1 で FAILProgram(JSON v0) の抽出以前で落ちている)。ログ上は GC/SSA ではなく StageB 本体側のエラーとして見えている。
- `tools/smokes/v2/profiles/quick/core/phase251/selfhost_cli_run_basic_vm.sh`:
- `HAKO_SELFHOST_BUILDER_FIRST=1` + `HAKO_MIR_BUILDER_CLI_RUN=1` で selfhost builder を優先した場合に、StageB / direct MIR emit 双方 rc=1 のまま(`HakoCli.run` の MIR(JSON) が生成されない)。
- 代表的な Rust MIR/SSA スモーク(`mir_stage1_using_resolver_*` / `mir_loopform_exit_phi` / `mir_stageb_loop_break_continue`)および Ny selfhost inline テストでは、Rust VM の `Undefined value` / `%0` 二重定義は再現しておらず、25.1 の Rust 側 SSA/PHI 修正は安定したとみなす。
→ 今後の 25.1b/25.1c では、Ny selfhost/Numeric core の前に **StageB (`compiler_stageb.hako`) と Stage1 CLI (`HakoCli.run`) の .hako 側構文/using/Stage3 ロジックを構造的に整理し、rc=1 の原因を 1 関数1 テスト単位で切り出して潰していく** フェーズに入る。
Update (2025-11-18 — Phase 25.1c: StageB / Stage1 CLI 構造デバッグプラン)
- Context:
- Phase 25.1c は `env.*` / `hostbridge.*` / `env.box_introspect.*` の責務整理に加えて、StageB Main (`compiler_stageb.hako`) と Stage1 CLI (`HakoCli.run`) の構造を「Region/スコープが追いやすい形」に整えるフェーズとして位置づけた。
- Rust 側ではすでに `Region / RefSlotKind / FunctionSlotRegistry + ControlForm` により Loop/If のスコープが安定しており、`NYASH_REGION_TRACE=1` で StageB 由来関数の Region/slot を観測できる状態。これを .hako 側 StageB 実装の「正解ビュー」として使う。
- 一方、25.1b の depth guard hotfix により Ny selfhost パーサStageB の `local` 系バグのうち「疑似再帰で進捗ガード頼みになる」ものは排除され、残る問題は主に StageB 本体compiler_stageb.hakoの rc=1 と CLI 経路の構造に集約されている。
- Plan (25.1c, initial steps):
1) StageB rc=1 の発生箇所を箱単位へ縮小:
- 代表 canary を 2 本ターゲットにする:
- `tools/smokes/v2/profiles/quick/core/phase251/stageb_fib_program_defs_canary_vm.sh`fib 風 defs + Loop の presence を確認する canary
- `tools/smokes/v2/profiles/quick/core/phase251/selfhost_cli_run_basic_vm.sh`HakoCli.run の selfhost MIR 生成 canary
- `compiler_stageb.hako` の StageB Main を「箱理論」に沿って分割した設計(`StageBArgsBox` / `StageBBodyExtractorBox` / `StageBDriverBox`)に合わせて、各箱入口/出口に `[stageb/trace:<box>.<method>:enter|leave]` を付与。
→ どの箱のどのメソッドで rc=1 になっているかをまずログだけで特定する(挙動は変えない)。
2) Rust Region ログと .hako 側のスコープ観測を揃える:
- Rust 側の `src/mir/region/*` + `NYASH_REGION_TRACE=1` をオンにし、`StageBBodyExtractorBox.*` 周辺ループ/if の Region 情報entry/exit/slotsをログとして取得する。
- .hako 側 StageB 実装に観測専用 Box案: `StageBRegionObserverBox`)を追加し、`enter_region(kind, name, slots_json)` / `leave_region()` などの API で「箱名+制御構造+生きているローカル名」のみ JSON で出力する。
- StageB 本体ではこの Box を外側ループ・内側 if などの境界で呼び出し、Rust の `[region/observe]` ログと .hako ログを突き合わせて、「どのスコープでどのローカル(例: `body_src` / `bundle_srcs` / `require_mods`)が欠損しているか」を構造レベルで炙り出す。
3) StageB 用ミニマムハーネスを Rust / .hako 両方に用意:
- 100〜200 行規模の最小 Hako サンプル(`using` + 1 box + 1 loop 程度)を `lang/src/compiler/tests/stageb_min_sample.hako` 相当として固定し、Rust 側では AST→MIR→`NYASH_VM_VERIFY_MIR=1` を通すテストを維持。
- .hako 側では同じサンプルを入力とする mini driver`StageBDriverMiniBox` のようなもの)を用意し、`StageBArgsBox.resolve_src``StageBBodyExtractorBox.build_body_src``ParserBox.parse_program2` だけを見る canary を追加。
→ fib / HakoCli.run より軽いケースで StageB の構造バグを素早く再現・修正できるようにする。
4) Stage1 CLI selfhost ラインは StageB 修正後に再アタック:
- 上記 1)〜3) で StageB canaryfib defs / stageb_min / mini driverの rc=1 を解消したあとに、25.1b 側の selfhost builder / `HakoCli.run` MIR 生成 canary を再度実行し、Program(JSON v0)→MIR(JSON) の差分を Rust ラインと比較していく。
- Goal for 25.1c:
- StageB (`compiler_stageb.hako`) と Stage1 CLI (`HakoCli.run`) に関する「rc=1 / StageB fail」の原因を、Region/スコープ構造まで落とし込んだうえで 1 箱単位で潰せる状態にすること。
- Rust Region レイヤを正としつつ、.hako 側でも同等の Region 観測レイヤRegionBox 的な構造)を持つことで、今後の GC/寿命設計や selfhost 側 LoopSSA v2 への追従を見通し良く行えるようにしておく。
Update (2025-11-18 — Phase 25.1l: Region/GC 観測レイヤー導入Rust 側のみ) — completed)
- Context:
- LoopForm v2 / ControlForm / Conservative PHI Box により、If/Loop の SSA/PHI は Rust 側で安定しているが、

View File

@ -85,3 +85,38 @@ Status: planning構造整理フェーズ・挙動は変えない
- Phase 25.1b: selfhost builder / multicarrier / BoxTypeInspector 実装フェーズ(機能側)。
- Phase 25.1c: そのうち「env.* / hostbridge.* / BoxIntrospect」に加えて、StageB Main / LoopBuilder / builder 観測レイヤの構造と責務も整理するメタフェーズ(構造側)。
- 挙動を変えないことFailFast / default path は現状維持)を前提に、小さな差分で進める。
### デバッグ方針25.1c で踏みたい順番)
- 1) StageB rc=1 の発生箇所を箱単位まで特定する
- 代表 canary:
- `tools/smokes/v2/profiles/quick/core/phase251/stageb_fib_program_defs_canary_vm.sh`
- `tools/smokes/v2/profiles/quick/core/phase251/selfhost_cli_run_basic_vm.sh`
- まずは `compiler_stageb.hako` の流れを箱ごとに分解してログする:
- `StageBArgsBox.resolve_src`
- `StageBBodyExtractorBox.build_body_src`
- `ParserBox.parse_program2`
- `FuncScannerBox.scan_all_boxes`
- 各箱の入口/出口に `[stageb/trace:<box>.<method>:enter|leave]` のような軽いタグを置き、どの箱が rc=1 の直前で止まっているかを特定する(挙動は変えない)。
- 2) Rust Region レイヤを「正解ビュー」として .hako 側を寄せる
- Rust 側にはすでに `Region / RefSlotKind / FunctionSlotRegistry + ControlForm` があり、`StageBBodyExtractorBox.*` 周辺のスロット(`src/body_src/bundle_*` など)がどの Loop/If Region に属しているかを `NYASH_REGION_TRACE=1` で観測できる。
- 25.1c では .hako 側に観測専用 Box案: `StageBRegionObserverBox`)を追加し、
- `enter_region(kind, name, slots_json)`
- `leave_region()`
のような API で「箱名・構造名・スロット名の集合」だけを JSON で print する。
- `StageBBodyExtractorBox` の外側ループ・内側 if など、問題になりやすい箇所でこの Box を呼び出し、Rust の `[region/observe]` ログと「木構造+スロット名」が対応しているかを確認する。
→ ずれている Region例: `body_src` などが早期に欠落するスコープ)から優先的に修正する。
- 3) StageB 用の極小ハーネスを Rust / .hako 両方に用意する
- fib canary はやや重いため、100〜200 行程度の「using + 1 box + 1 loop」だけの最小サンプルを `lang/src/compiler/tests/stageb_min_sample.hako` のようなファイルとして固定する。
- Rust 側:
- そのサンプルを AST→MIR に通し、`NYASH_VM_VERIFY_MIR=1``Undefined value` が出ないことを確認する小さなテストを用意(既存の StageB 向け MIR テスト群に揃える)。
- .hako 側:
- 同じサンプルを入力として `StageBDriverBox` の簡易版mini driverを作り、`StageBArgsBox.resolve_src``StageBBodyExtractorBox.build_body_src``ParserBox.parse_program2` だけを通す driver を追加する。
- これにより、StageB/LoopBuilder に対する修正を「本番 compiler_stageb.hako 全体」ではなく「ミニマムな Hako 断片」で検証できるようにする。
- 4) Stage1 CLI (`HakoCli.run`) の selfhost ラインは StageB が緑になってから扱う
- `selfhost_cli_run_basic_vm.sh` の現状の失敗は、StageB が rc=1 で Program(JSON) を 1 行も返していないことが原因であり、HakoCli.run の MIR 生成まで到達していない。
- 25.1c ではまず StageB 側fib defs / stageb_min / mini driverを rc=0 に戻し、
その Program(JSON v0) を固定入力にして Phase 25.1b 側の selfhost builder / HakoCli.run MIR を Rust ラインと diff する、という順番で進める。

View File

@ -17,9 +17,28 @@ using lang.compiler.entry.func_scanner as FuncScannerBox
using lang.compiler.entry.using_resolver as Stage1UsingResolverBox
using lang.compiler.builder.mod as CompilerBuilder
// Dev-only trace helper (Phase 25.1c)
// - Enabled when HAKO_STAGEB_TRACE=1
// - Keeps Stage-B behavior unchangedログのみ追加
static box StageBTraceBox {
log(label) {
local flag = env.get("HAKO_STAGEB_TRACE")
if flag == null { return 0 }
if ("" + flag) != "1" { return 0 }
// label が null/Void でも落ちないように守るdev専用
local msg = "[stageb/trace]"
if label != null {
msg = msg + " " + ("" + label)
}
print(msg)
return 0
}
}
// Phase 25.1c: CLI argument → source resolution
static box StageBArgsBox {
resolve_src(args) {
StageBTraceBox.log("StageBArgsBox.resolve_src:enter")
// 1) Collect source from args or env
local src = null
local src_file = null
@ -43,6 +62,13 @@ static box StageBArgsBox {
// Original: if src == null { src = env.local.get("HAKO_SOURCE") }
if src == null { src = "return 0" }
{
// Trace final source lengthdev専用
local l = 0
if src != null { l = ("" + src).length() }
StageBTraceBox.log("StageBArgsBox.resolve_src:return_len=" + ("" + l))
}
return src
}
}
@ -50,6 +76,12 @@ static box StageBArgsBox {
// Phase 25.1c: Body extraction + bundle + using + trim
static box StageBBodyExtractorBox {
build_body_src(src, args) {
{
//入口トレース: 入力ソース長と引数有無
local l = 0
if src != null { l = ("" + src).length() }
StageBTraceBox.log("StageBBodyExtractorBox.build_body_src:enter len=" + ("" + l))
}
// ============================================================================
// Depth guard: prevent accidental recursion inside StageB body extractor
// ============================================================================
@ -609,6 +641,13 @@ static box StageBBodyExtractorBox {
if e > b { body_src = s.substring(b, e) } else { body_src = "" }
}
{
//出口トレース: 抽出後 body_src 長
local l2 = 0
if body_src != null { l2 = ("" + body_src).length() }
StageBTraceBox.log("StageBBodyExtractorBox.build_body_src:return_len=" + ("" + l2))
}
// Clear depth guard before returning
env.set("HAKO_STAGEB_BODY_DEPTH", "0")
return body_src
@ -630,7 +669,14 @@ static box StageBDriverBox {
env.set("HAKO_STAGEB_DRIVER_DEPTH", "1")
}
StageBTraceBox.log("StageBDriverBox.main:enter")
local src = StageBArgsBox.resolve_src(args)
{
local l = 0
if src != null { l = ("" + src).length() }
StageBTraceBox.log("StageBDriverBox.main:after_resolve_src len=" + ("" + l))
}
// 2) Stage3 acceptance default ON for selfhost (env may turn off; keep tolerant here)
local p = new ParserBox()
@ -643,11 +689,22 @@ static box StageBDriverBox {
// local externs_json = p.get_externs_json()
local body_src = StageBBodyExtractorBox.build_body_src(src, args)
{
local l2 = 0
if body_src != null { l2 = ("" + body_src).length() }
StageBTraceBox.log("StageBDriverBox.main:after_build_body_src len=" + ("" + l2))
}
// 6) Parse and emit Stage1 JSON v0 (Program)
// Bridge(JSON v0) が Program v0 を受け取り MIR に lowering するため、ここでは AST(JSON v0) を出力する。
// 既定で MIR 直出力は行わない(重い経路を避け、一行出力を保証)。
local ast_json = p.parse_program2(body_src)
{
// AST(JSON v0) の長さを軽く観測
local la = 0
if ast_json != null { la = ("" + ast_json).length() }
StageBTraceBox.log("StageBDriverBox.main:after_parse_program2 len=" + ("" + la))
}
// 6.3) Apply SSA transformations (CompilerBuilder pipeline)
{
@ -686,6 +743,12 @@ static box StageBDriverBox {
// Use FuncScannerBox to extract method definitions from all boxes
local methods = FuncScannerBox.scan_all_boxes(src)
{
local cnt = 0
if methods != null { cnt = methods.length() }
StageBTraceBox.log("StageBDriverBox.main:func_scan methods=" + ("" + cnt))
}
// Build defs JSON array
if methods.length() > 0 {
defs_json = ",\"defs\":["
@ -734,6 +797,7 @@ static box StageBDriverBox {
}
print(ast_json)
StageBTraceBox.log("StageBDriverBox.main:exit rc=0")
// Clear depth guard before returning
env.set("HAKO_STAGEB_DRIVER_DEPTH", "0")
return 0

View File

@ -176,6 +176,8 @@ static box ParserControlBox {
if j < src.length() { j = j + 1 } else { j = src.length() }
}
ctx.gpos_set(j)
// Reset recursion guard before returning (Stage-3 path)
env.set("HAKO_STAGEB_BREAK_DEPTH", "0")
return "{\"type\":\"Break\"}"
}
@ -205,6 +207,8 @@ static box ParserControlBox {
if j < src.length() { j = j + 1 } else { j = src.length() }
}
ctx.gpos_set(j)
// Reset recursion guard before returning (Stage-3 path)
env.set("HAKO_STAGEB_CONTINUE_DEPTH", "0")
return "{\"type\":\"Continue\"}"
}

View File

@ -53,12 +53,17 @@ static box ParserStmtBox {
ctx.gpos_set(j)
// Record annotation in parser context and emit no statement
ctx.add_extern_c(sym, func_name)
// Reset recursion guard before returning1回の parse 呼び出しごとに depth をクリアする)
env.set("HAKO_STAGEB_STMT_DEPTH", "0")
return ""
}
// using statement
if ctx.starts_with_kw(src, j, "using") == 1 {
return me.parse_using(src, j, stmt_start, ctx)
local out_using = me.parse_using(src, j, stmt_start, ctx)
// Reset recursion guard before returning
env.set("HAKO_STAGEB_STMT_DEPTH", "0")
return out_using
}
// assignment: IDENT '=' expr
@ -90,6 +95,8 @@ static box ParserStmtBox {
if k0 < src.length() { k0 = k0 + 1 } else { k0 = src.length() }
}
ctx.gpos_set(k0)
// Reset recursion guard before returning
env.set("HAKO_STAGEB_STMT_DEPTH", "0")
return "{\"type\":\"Local\",\"name\":\"" + name0 + "\",\"expr\":" + expr_json0 + "}"
}
}
@ -115,6 +122,8 @@ static box ParserStmtBox {
if j < src.length() { j = j + 1 } else { j = src.length() }
}
ctx.gpos_set(j)
// Reset recursion guard before returning
env.set("HAKO_STAGEB_STMT_DEPTH", "0")
return "{\"type\":\"Return\",\"expr\":" + expr_json_ret + "}"
}
@ -157,32 +166,52 @@ static box ParserStmtBox {
if j < src.length() { j = j + 1 } else { j = src.length() }
}
ctx.gpos_set(j)
// Reset recursion guard before returning
env.set("HAKO_STAGEB_STMT_DEPTH", "0")
return "{\"type\":\"Local\",\"name\":\"" + name + "\",\"expr\":" + expr_json_local + "}"
}
// Delegate to specialized boxes
if ctx.starts_with_kw(src, j, "if") == 1 {
return ParserControlBox.parse_if(src, j, stmt_start, ctx)
local out_if = ParserControlBox.parse_if(src, j, stmt_start, ctx)
// Reset recursion guard before returning
env.set("HAKO_STAGEB_STMT_DEPTH", "0")
return out_if
}
if ctx.starts_with_kw(src, j, "loop") == 1 {
return ParserControlBox.parse_loop(src, j, stmt_start, ctx)
local out_loop = ParserControlBox.parse_loop(src, j, stmt_start, ctx)
// Reset recursion guard before returning
env.set("HAKO_STAGEB_STMT_DEPTH", "0")
return out_loop
}
if ctx.starts_with_kw(src, j, "break") == 1 {
return ParserControlBox.parse_break(src, j, stmt_start, ctx)
local out_break = ParserControlBox.parse_break(src, j, stmt_start, ctx)
// Reset recursion guard before returning
env.set("HAKO_STAGEB_STMT_DEPTH", "0")
return out_break
}
if ctx.starts_with_kw(src, j, "continue") == 1 {
return ParserControlBox.parse_continue(src, j, stmt_start, ctx)
local out_cont = ParserControlBox.parse_continue(src, j, stmt_start, ctx)
// Reset recursion guard before returning
env.set("HAKO_STAGEB_STMT_DEPTH", "0")
return out_cont
}
if ctx.starts_with_kw(src, j, "throw") == 1 {
return ParserExceptionBox.parse_throw(src, j, stmt_start, ctx)
local out_throw = ParserExceptionBox.parse_throw(src, j, stmt_start, ctx)
// Reset recursion guard before returning
env.set("HAKO_STAGEB_STMT_DEPTH", "0")
return out_throw
}
if ctx.starts_with_kw(src, j, "try") == 1 {
return ParserExceptionBox.parse_try(src, j, stmt_start, ctx)
local out_try = ParserExceptionBox.parse_try(src, j, stmt_start, ctx)
// Reset recursion guard before returning
env.set("HAKO_STAGEB_STMT_DEPTH", "0")
return out_try
}
// Fallback: expression or unknown token