Fix MIR builder me-call recursion and add compile tracing

This commit is contained in:
nyash-codex
2025-11-17 19:53:44 +09:00
parent c551131941
commit f300b9f3c9
7 changed files with 68 additions and 15 deletions

View File

@ -1559,3 +1559,26 @@ Update (2025-11-15 — Stage1 CLI emit調査メモ)
- Rust 側 JSON v0 ブリッジに `ExprV0::Null` variant を追加し、StageB が吐く Null リテラルを受理。 - Rust 側 JSON v0 ブリッジに `ExprV0::Null` variant を追加し、StageB が吐く Null リテラルを受理。
- `launcher.hako` の `while` を `loop` 構文へ置換StageB パーサ互換)。 - `launcher.hako` の `while` を `loop` 構文へ置換StageB パーサ互換)。
- Stage1 CLI の Program(JSON) は attr付き defs まで含むようになったが、selfhost builder MirBuilderBox on VMがまだ HakoCli 全体を lowering できず stub MIR を返している。provider 経由では 62KB 超の MIR(JSON) が得られるので Phase 25.1a ではこちらを利用。 - Stage1 CLI の Program(JSON) は attr付き defs まで含むようになったが、selfhost builder MirBuilderBox on VMがまだ HakoCli 全体を lowering できず stub MIR を返している。provider 経由では 62KB 超の MIR(JSON) が得られるので Phase 25.1a ではこちらを利用。
Update (2025-11-17 — Phase 25.1d: Rust MIR me-call recursion fix & StageB stack overflow原因特定)
- 状況:
- `tools/test_stageb_min.sh` の Test2`compiler_stageb.hako -- --source stageb_min_sample.hako`)で発生していた stack overflow / Segfault は、Rust MIR builder 層の無限再帰が原因だったことが判明。
- backtrace および `logs/stageb_min_full.log` から、次の 3 関数がループしていることが確認できた:
- `try_build_me_method_call``src/mir/builder/calls/build.rs`
- `try_handle_me_direct_call``src/mir/builder/builder_calls.rs`
- `handle_me_method_call``src/mir/builder/method_call_handlers.rs`
→ `try_build_me_method_call` → `handle_me_method_call` → `try_handle_me_direct_call` → 再び `try_build_me_method_call` という三角ループで Rust スタックを消費していた。
- 対応Rust 層):
- `src/mir/builder/calls/build.rs` の `try_build_me_method_call` から「3-a) Static box fast path」として `handle_me_method_call` を即呼び出すパスを削除し、相互再帰を解消。
- これにより `try_build_me_method_call` は「enclosing box 名を見て、既に lower 済みの `<Box>.<method>/Arity` を Global call に差し替える」経路だけを担うようになった(インスタンス Box 向けの 1 パスのみ)。
- `src/mir/builder.rs` / `src/mir/builder/lifecycle.rs` に `NYASH_MIR_COMPILE_TRACE=1` 用の compile trace を追加し、
- `lower_root` の static box 降ろし時に `[mir-compile] lower static box <Name>` をログ出力できるようにした(デフォルト OFF
- 観測結果(修正後):
- `NYASH_MIR_COMPILE_TRACE=1` で Test2 を再実行したところ:
- `BundleResolver` / `ParserBox` 関連の static box 群 / `StageBArgsBox` / `StageBBodyExtractorBox` / `StageBDriverBox` / `ParserStub` 等、すべての static box が `[mir-compile] lower static box ...` ログ付きで正常に MIR に lower されることを確認。
- `Main` の `build_static_main_box` も `Before/After lower_static_method_as_function` のデバッグログ付きで完走している。
- stack overflow / Segfault は消え、最終的なエラーは:
- `❌ VM error: Invalid instruction: extern function: Unknown: env.set/2`
となっており、Rust VM/MIR 層ではなく Nyash側の `env.set` extern 定義不足StageB 再入ガードに env を使っている部分)だけが残っている状態まで収束した。
- 今後のタスクPhase 25.1d 続き):
- StageB の再入ガードを `env.set/2` ではなく Box 内 stateフィールドで持たせるか、あるいは `env.set/2` を StageB 実行環境向けに Rust 側 extern として提供するかを検討・実装する。
- `NYASH_MIR_COMPILE_TRACE` ログを活用して、今後も MIR builder 側の深い再帰や相互再帰を早期に検知できるようにする(特に StageB / ParserBox 周辺の me 呼び出し)。

View File

@ -136,6 +136,9 @@ required-features = ["gui-examples"]
thiserror = "2.0" thiserror = "2.0"
anyhow = "1.0" anyhow = "1.0"
# 開発時のスタックオーバーフロー診断用
backtrace-on-stack-overflow = "0.3"
# シリアライゼーション将来のAST永続化用 # シリアライゼーション将来のAST永続化用
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View File

@ -15,7 +15,7 @@ impl MirInterpreter {
) -> Result<VMValue, VMError> { ) -> Result<VMValue, VMError> {
// Safety valve: cap nested exec_function_inner depth to avoid Rust stack overflow // Safety valve: cap nested exec_function_inner depth to avoid Rust stack overflow
// on accidental infinite recursion in MIR (e.g., self-recursive call chains). // on accidental infinite recursion in MIR (e.g., self-recursive call chains).
const MAX_CALL_DEPTH: usize = 1024; const MAX_CALL_DEPTH: usize = 128;
self.call_depth = self.call_depth.saturating_add(1); self.call_depth = self.call_depth.saturating_add(1);
if self.call_depth > MAX_CALL_DEPTH { if self.call_depth > MAX_CALL_DEPTH {
eprintln!( eprintln!(

View File

@ -9,6 +9,18 @@ use nyash_rust::runner::NyashRunner;
/// Thin entry point - delegates to CLI parsing and runner execution /// Thin entry point - delegates to CLI parsing and runner execution
fn main() { fn main() {
// Optional: enable backtrace on stack overflow for deep debug runs.
// Guarded by env to keep default behavior unchanged.
if std::env::var("NYASH_DEBUG_STACK_OVERFLOW")
.ok()
.as_deref()
== Some("1")
{
unsafe {
let _ = backtrace_on_stack_overflow::enable();
}
}
// hv1 direct (primary): earliest possible check before any bootstrap/log init // hv1 direct (primary): earliest possible check before any bootstrap/log init
// If NYASH_VERIFY_JSON is present and route is requested, execute and exit. // If NYASH_VERIFY_JSON is present and route is requested, execute and exit.
// This avoids plugin host/registry initialization and keeps output minimal. // This avoids plugin host/registry initialization and keeps output minimal.

View File

@ -314,6 +314,24 @@ impl MirBuilder {
self.debug_scope_stack.last().cloned() self.debug_scope_stack.last().cloned()
} }
// ----------------------
// Compile trace helpers (dev only; env-gated)
// ----------------------
#[inline]
pub(super) fn compile_trace_enabled() -> bool {
std::env::var("NYASH_MIR_COMPILE_TRACE")
.ok()
.as_deref()
== Some("1")
}
#[inline]
pub(super) fn trace_compile<S: AsRef<str>>(&self, msg: S) {
if Self::compile_trace_enabled() {
eprintln!("[mir-compile] {}", msg.as_ref());
}
}
// ---------------------- // ----------------------
// Method tail index (performance helper) // Method tail index (performance helper)
// ---------------------- // ----------------------

View File

@ -400,12 +400,7 @@ impl MirBuilder {
method: &str, method: &str,
arguments: &[ASTNode], arguments: &[ASTNode],
) -> Result<Option<ValueId>, String> { ) -> Result<Option<ValueId>, String> {
// 3-a) Static box fast path // Instance box: prefer enclosing box method
if let Some(res) = self.handle_me_method_call(method, arguments)? {
return Ok(Some(res));
}
// 3-b) Instance box: prefer enclosing box method
let enclosing_cls: Option<String> = self let enclosing_cls: Option<String> = self
.current_function .current_function
.as_ref() .as_ref()

View File

@ -87,6 +87,8 @@ impl super::MirBuilder {
if name == "Main" { if name == "Main" {
main_static = Some((name.clone(), methods.clone())); main_static = Some((name.clone(), methods.clone()));
} else { } else {
// Dev: trace which static box is being lowered (env-gated)
self.trace_compile(format!("lower static box {}", name));
// 🎯 箱理論: 各static boxに専用のコンパイルコンテキストを作成 // 🎯 箱理論: 各static boxに専用のコンパイルコンテキストを作成
// これにより、using文や前のboxからのメタデータ汚染を構造的に防止 // これにより、using文や前のboxからのメタデータ汚染を構造的に防止
// スコープを抜けると自動的にコンテキストが破棄される // スコープを抜けると自動的にコンテキストが破棄される
@ -105,12 +107,12 @@ impl super::MirBuilder {
.push((name.clone(), params.len())); .push((name.clone(), params.len()));
} }
} }
}
// 🎯 箱理論: コンテキストをクリア(スコープ終了で自動破棄) // 🎯 箱理論: コンテキストをクリア(スコープ終了で自動破棄)
// これにより、次のstatic boxは汚染されていない状態から開始される // これにより、次のstatic boxは汚染されていない状態から開始される
self.compilation_context = None; self.compilation_context = None;
} }
}
} else { } else {
// Instance box: register type and lower instance methods/ctors as functions // Instance box: register type and lower instance methods/ctors as functions
self.user_defined_boxes.insert(name.clone()); self.user_defined_boxes.insert(name.clone());