From f300b9f3c9e353a15bd50d8262e11fa55defc60c Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Mon, 17 Nov 2025 19:53:44 +0900 Subject: [PATCH] Fix MIR builder me-call recursion and add compile tracing --- CURRENT_TASK.md | 23 +++++++++++++++++++++++ Cargo.toml | 3 +++ src/backend/mir_interpreter/exec.rs | 2 +- src/main.rs | 12 ++++++++++++ src/mir/builder.rs | 18 ++++++++++++++++++ src/mir/builder/calls/build.rs | 7 +------ src/mir/builder/lifecycle.rs | 18 ++++++++++-------- 7 files changed, 68 insertions(+), 15 deletions(-) diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 4d0c2048..05446f48 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -1559,3 +1559,26 @@ Update (2025-11-15 — Stage1 CLI emit調査メモ) - Rust 側 JSON v0 ブリッジに `ExprV0::Null` variant を追加し、Stage‐B が吐く Null リテラルを受理。 - `launcher.hako` の `while` を `loop` 構文へ置換(Stage‐B パーサ互換)。 - 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 & Stage‑B 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 済みの `./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 ` をログ出力できるようにした(デフォルト 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 定義不足(Stage‑B 再入ガードに env を使っている部分)だけが残っている状態まで収束した。 +- 今後のタスク(Phase 25.1d 続き): + - Stage‑B の再入ガードを `env.set/2` ではなく Box 内 state(フィールド)で持たせるか、あるいは `env.set/2` を Stage‑B 実行環境向けに Rust 側 extern として提供するかを検討・実装する。 + - `NYASH_MIR_COMPILE_TRACE` ログを活用して、今後も MIR builder 側の深い再帰や相互再帰を早期に検知できるようにする(特に Stage‑B / ParserBox 周辺の me 呼び出し)。 diff --git a/Cargo.toml b/Cargo.toml index 15dffde8..6390fd18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,6 +136,9 @@ required-features = ["gui-examples"] thiserror = "2.0" anyhow = "1.0" +# 開発時のスタックオーバーフロー診断用 +backtrace-on-stack-overflow = "0.3" + # シリアライゼーション(将来のAST永続化用) serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/src/backend/mir_interpreter/exec.rs b/src/backend/mir_interpreter/exec.rs index 742a5dd3..5bf5c02e 100644 --- a/src/backend/mir_interpreter/exec.rs +++ b/src/backend/mir_interpreter/exec.rs @@ -15,7 +15,7 @@ impl MirInterpreter { ) -> Result { // 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). - const MAX_CALL_DEPTH: usize = 1024; + const MAX_CALL_DEPTH: usize = 128; self.call_depth = self.call_depth.saturating_add(1); if self.call_depth > MAX_CALL_DEPTH { eprintln!( diff --git a/src/main.rs b/src/main.rs index f4006b0e..3f65af59 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,18 @@ use nyash_rust::runner::NyashRunner; /// Thin entry point - delegates to CLI parsing and runner execution 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 // If NYASH_VERIFY_JSON is present and route is requested, execute and exit. // This avoids plugin host/registry initialization and keeps output minimal. diff --git a/src/mir/builder.rs b/src/mir/builder.rs index 20184f1a..ea51a542 100644 --- a/src/mir/builder.rs +++ b/src/mir/builder.rs @@ -314,6 +314,24 @@ impl MirBuilder { 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>(&self, msg: S) { + if Self::compile_trace_enabled() { + eprintln!("[mir-compile] {}", msg.as_ref()); + } + } + // ---------------------- // Method tail index (performance helper) // ---------------------- diff --git a/src/mir/builder/calls/build.rs b/src/mir/builder/calls/build.rs index 2dfda8ff..d908a907 100644 --- a/src/mir/builder/calls/build.rs +++ b/src/mir/builder/calls/build.rs @@ -400,12 +400,7 @@ impl MirBuilder { method: &str, arguments: &[ASTNode], ) -> Result, String> { - // 3-a) Static box fast path - if let Some(res) = self.handle_me_method_call(method, arguments)? { - return Ok(Some(res)); - } - - // 3-b) Instance box: prefer enclosing box method + // Instance box: prefer enclosing box method let enclosing_cls: Option = self .current_function .as_ref() diff --git a/src/mir/builder/lifecycle.rs b/src/mir/builder/lifecycle.rs index a497cddd..e595c350 100644 --- a/src/mir/builder/lifecycle.rs +++ b/src/mir/builder/lifecycle.rs @@ -87,6 +87,8 @@ impl super::MirBuilder { if name == "Main" { main_static = Some((name.clone(), methods.clone())); } else { + // Dev: trace which static box is being lowered (env-gated) + self.trace_compile(format!("lower static box {}", name)); // 🎯 箱理論: 各static boxに専用のコンパイルコンテキストを作成 // これにより、using文や前のboxからのメタデータ汚染を構造的に防止 // スコープを抜けると自動的にコンテキストが破棄される @@ -99,17 +101,17 @@ impl super::MirBuilder { if let N::FunctionDeclaration { params, body, .. } = mast { let func_name = format!("{}.{}{}", name, mname, format!("/{}", params.len())); self.lower_static_method_as_function(func_name, params.clone(), body.clone())?; - self.static_method_index - .entry(mname.clone()) - .or_insert_with(Vec::new) - .push((name.clone(), params.len())); + self.static_method_index + .entry(mname.clone()) + .or_insert_with(Vec::new) + .push((name.clone(), params.len())); + } } } - // 🎯 箱理論: コンテキストをクリア(スコープ終了で自動破棄) - // これにより、次のstatic boxは汚染されていない状態から開始される - self.compilation_context = None; - } + // 🎯 箱理論: コンテキストをクリア(スコープ終了で自動破棄) + // これにより、次のstatic boxは汚染されていない状態から開始される + self.compilation_context = None; } } else { // Instance box: register type and lower instance methods/ctors as functions