diff --git a/docs/development/analysis/LOOPFORM_PHI_SOLUTION_SUMMARY.md b/docs/development/analysis/LOOPFORM_PHI_SOLUTION_SUMMARY.md index cfcadb2d..166d67b3 100644 --- a/docs/development/analysis/LOOPFORM_PHI_SOLUTION_SUMMARY.md +++ b/docs/development/analysis/LOOPFORM_PHI_SOLUTION_SUMMARY.md @@ -52,8 +52,10 @@ builder.emit_header_phis(ops)?; // Result: %101 = phi [%100, bb0]; %103 = phi [%102, bb0]; ... // Pass 4: Seal PHIs after loop body -builder.seal_phis(ops, latch_id)?; -// Result: %101 = phi [%100, bb0], [%101, latch]; %103 = phi [%102, bb0], [%120, latch] +// (merge preheader + continue blocks + latch into header PHIs) +builder.seal_phis(ops, latch_id, &continue_snapshots)?; +// Result: %101 = phi [%100, bb0], [%200, bb_cont], [%101, latch]; +// %103 = phi [%102, bb0], [%201, bb_cont], [%120, latch] ``` **Critical Property**: All ValueIds allocated in Pass 1, **before** any MIR emission → **no circular dependencies possible**. diff --git a/docs/development/roadmap/phases/phase-25.1m/README.md b/docs/development/roadmap/phases/phase-25.1m/README.md new file mode 100644 index 00000000..0d9e4ecc --- /dev/null +++ b/docs/development/roadmap/phases/phase-25.1m/README.md @@ -0,0 +1,108 @@ +# Phase 25.1m — Static Method / VM Param Semantics Bugfix + +Status: planning(Rust 側の暗黙レシーバ問題を切り出したバグ修正フェーズ) + +## ゴール + +- Rust MIR/VM 層に残っている「静的メソッド呼び出し時の引数ずれ/暗黙レシーバ」問題を解消し、 + Stage‑B / Stage‑1 / selfhost / Dev トレース(TraceBox 系)がすべて **同じ呼び出し規約**で動くようにする。 +- 具体的には: + - `static box Foo { method bar(x){...} }` に対して + - 呼び出し: `Foo.bar("HELLO")` + - VM 内部: `params.len() == 1`、args 1 本 → `bar` の唯一の引数に `"HELLO"` が入る + - 「暗黙の receiver(仮想 me)」を静的メソッドにだけ特別扱いしない設計に戻す。 + +## 現状の症状(2025-11-18 時点) + +- 再現(簡易例): + ```hako + static box TraceTest { + method log(label){ + if label == null { print("label=NULL") } + else { print("label=\"\" + label") } + } + } + + static box Main { + method main(args){ + TraceTest.log("HELLO") + return 0 + } + } + ``` + - 期待: `label=HELLO` + - 実際: `label=NULL` +- 原因の一次切り分け: + - `MirFunction::new` が「名前に '.' を含み、かつ第 1 パラメータ型が Box でない関数」を「静的メソッド with 暗黙 receiver」とみなし、 + - `signature.params.len() = 1`(`label`)でも `total_value_ids = 2` を予約して `params = [%0, %1]` を組み立てている。 + - VM 側の `exec_function_inner` は `args` をそのまま `func.params` に 1:1 でバインドするため: + - `args = ["HELLO"]` + - `%0` ← `"HELLO"`(暗黙 receiver) + - `%1` ← `Void`(足りない分が Void 埋め) → `label` に null が入る。 + - その結果: + - 静的メソッドに文字列リテラルを直接渡すと label が null 化される。 + - 25.1d/e で扱っていた Stage‑1 UsingResolver 系テスト(`collect_entries/1`)でも、 + `%0` / `%1` の扱いに由来する SSA 破綻が見えていた(現在は LoopForm v2/Conservative PHI 側で多くを解消済みだが、根底の呼び出し規約はまだ歪なまま)。 + +## スコープ(25.1m でやること) + +1. 呼び出し規約の SSOT を決める + - 原則: + - **インスタンスメソッド**: `prepare_method_signature` 側で `me` を明示的に第 1 パラメータに含める。 + - **静的メソッド / Global 関数**: `signature.params` は「実引数と 1:1」のみ。暗黙レシーバを追加しない。 + - 影響範囲の調査: + - `MirFunction::new` の param reservation ロジック(暗黙 receiver 判定と `total_value_ids` 計算)。 + - `emit_unified_call` / `CalleeResolverBox` の Method/Global 判定と receiver 差し込み。 + - VM 側 `exec_function_inner` の args バインド(ここは既に「params と args を 1:1」としているので、なるべく触らない)。 + +2. 静的メソッドまわりの SSA/テストの洗い出し + - 代表ケース: + - `src/tests/mir_stage1_using_resolver_verify.rs` 内の + `mir_stage1_using_resolver_full_collect_entries_verifies`(`Stage1UsingResolverFull.collect_entries/1` を静的メソッドとして使うテスト)。 + - Dev 用トレース箱(今回の `StageBTraceBox` / 既存の TraceTest 相当)。 + - 25.1m では: + - まず `trace_param_bug.hako` 相当のミニテスト(静的メソッド + 1 引数)を Rust 側にユニットテストとして追加し、 + Bugfix 前後で「label に null が入らない」ことを固定する。 + - 次に `Stage1UsingResolverFull.collect_entries/1` を LoopForm v2 経路込みで通し、 + `%0` / `%1` の ValueId 割り当てと PHI が健全であることを `MirVerifier` のテストで確認する。 + +3. 実装方針(高レベル) + - `MirFunction::new`: + - 暗黙 receiver 判定を段階縮小し、最終的には「インスタンスメソッドのみ `MirType::Box(_)` を用いた明示的 receiver」に統一。 + - 静的メソッド(`static box Foo { method bar(x){...} }`)は `signature.params == [Unknown]` のみを予約対象にし、 + 追加の `receiver_count` を持たない設計に戻す。 + - `emit_unified_call`: + - Method call のみ receiver を args に足す (`args_local.insert(0, recv)`)、Global/static 呼び出しでは一切いじらない。 + - `exec_function_inner`: + - 現状の「params[] と args[] を 1:1 でバインドする」実装を前提として保ち、 + 呼び出し規約の側(MIR builder 側)で整合を取る。 + +## 非スコープ(25.1m でやらないこと) + +- 言語仕様の変更: + - Hako/Nyash の静的メソッド構文 (`static box` / `method`) 自体は変更しない。 +- Stage‑B / Stage‑1 CLI の構造タスク: + - Stage‑B body 抽出/bundle/using/RegionBox 観測は 25.1c のスコープに残す。 +- VM 命令や Box 実装の追加: + - 25 フェーズのポリシーに従い、新しい命令・Box 機能は追加しない(既存の呼び出し規約を整えるだけ)。 + +## 関連フェーズとの関係 + +- Phase 25.1d/e/g/k/l: + - Rust MIR 側の SSA/PHI(特に LoopForm v2 + Conservative PHI)と Region 観測レイヤは、静的メソッドを含む多くのケースで安定している。 + - 25.1m はその上に残った「呼び出し規約レベルの歪み」を片付けるフェーズ。 +- Phase 25.1c: + - Stage‑B / Stage‑1 CLI 側の構造デバッグ(RegionBox 的な観測、StageBArgs/BodyExtractor/Driver 分解)に専念。 + - StageBTraceBox は既にインスタンス box 化しており、静的メソッドのバグを踏まないようにしてある。 + - 25.1m で静的メソッド呼び出し規約が直れば、将来的に Trace 系 Box を static 化することも再検討できる。 + +## 受け入れ条件(25.1m) + +- 新規ユニットテスト: + - 簡易 TraceTest(静的メソッド + 文字列引数)の MIR/VM 実行が `label=HELLO` となること。 + - `mir_stage1_using_resolver_full_collect_entries_verifies` が LoopForm v2 経路で緑のまま(または改善)であること。 +- 既存の Stage‑B / selfhost / 数値系テスト: + - 25.1c までに整えた Stage‑B / selfhost ラインの canary(fib defs / CLI run)が、25.1m の変更で悪化していないこと。 +- 挙動の安定: + - 静的メソッド呼び出しに関する「引数が null になる」「ValueId %0 が未定義になる」といった VM エラーが、新規テスト群で再現しないこと。 + diff --git a/lang/src/compiler/parser/parser_box.hako b/lang/src/compiler/parser/parser_box.hako index 02a0fdf9..110b1724 100644 --- a/lang/src/compiler/parser/parser_box.hako +++ b/lang/src/compiler/parser/parser_box.hako @@ -235,6 +235,14 @@ box ParserBox { // === Top-level program parser === parse_program2(src) { + // Dev-only: guaranteed direct marker before any env operations + { + local marker_enabled = env.get("HAKO_STAGEB_TRACE") + if marker_enabled != null && ("" + marker_enabled) == "1" { + print("[stageb/trace] ParserBox.parse_program2:entry") + } + } + // Shallow recursion guard for Stage‑B / selfhost callers: // prevent accidental self-recursion in ParserBox.parse_program2. { @@ -247,11 +255,36 @@ box ParserBox { env.set("HAKO_STAGEB_PARSER_DEPTH", "1") } + // Dev-only: marker before trace_enabled() call + { + local marker_enabled = env.get("HAKO_STAGEB_TRACE") + if marker_enabled != null && ("" + marker_enabled) == "1" { + print("[stageb/trace] ParserBox.parse_program2:before_trace_enabled") + } + } + local trace = me.trace_enabled() + // Dev-only: marker after trace_enabled() call + { + local marker_enabled = env.get("HAKO_STAGEB_TRACE") + if marker_enabled != null && ("" + marker_enabled) == "1" { + print("[stageb/trace] ParserBox.parse_program2:after_trace_enabled trace=" + ("" + trace)) + } + } + // Inline skip_ws to avoid VM bug: method-with-loop called from within loop local i = 0 local n = src.length() + + // Dev-only: marker before src.length() operations + { + local marker_enabled = env.get("HAKO_STAGEB_TRACE") + if marker_enabled != null && ("" + marker_enabled) == "1" { + print("[stageb/trace] ParserBox.parse_program2:got_src_length n=" + ("" + n)) + } + } + if trace == 1 { print("[parser/trace:program2] start len=" + ("" + n) + " stage3=" + ("" + me.stage3)) } @@ -266,6 +299,14 @@ box ParserBox { } } + // Dev-only: marker before main loop + { + local marker_enabled = env.get("HAKO_STAGEB_TRACE") + if marker_enabled != null && ("" + marker_enabled) == "1" { + print("[stageb/trace] ParserBox.parse_program2:before_main_loop i=" + ("" + i)) + } + } + local body = "[" local first = 1 local cont_prog = 1 diff --git a/src/mir/builder/calls/lowering.rs b/src/mir/builder/calls/lowering.rs index 47988373..3fcfdf5e 100644 --- a/src/mir/builder/calls/lowering.rs +++ b/src/mir/builder/calls/lowering.rs @@ -78,7 +78,7 @@ impl MirBuilder { ctx: &mut LoweringContext, ) -> Result<(), String> { let signature = function_lowering::prepare_static_method_signature( - func_name, + func_name.clone(), params, body, ); @@ -89,6 +89,9 @@ impl MirBuilder { ctx.saved_function = self.current_function.take(); ctx.saved_block = self.current_block.take(); + eprintln!("[DEBUG/create_function_skeleton] Creating function: {}", func_name); + eprintln!("[DEBUG/create_function_skeleton] Entry block: {:?}", entry); + // 新しい関数に切り替え self.current_function = Some(function); self.current_block = Some(entry); @@ -98,7 +101,7 @@ impl MirBuilder { // Region 観測レイヤ: static 関数用の FunctionRegion を積むよ。 crate::mir::region::observer::observe_function_region(self); - + Ok(()) } @@ -145,8 +148,11 @@ impl MirBuilder { /// 🎯 箱理論: Step 4 - 本体lowering fn lower_function_body(&mut self, body: Vec) -> Result<(), String> { + eprintln!("[DEBUG/lower_function_body] body.len() = {}", body.len()); let program_ast = function_lowering::wrap_in_program(body); + eprintln!("[DEBUG/lower_function_body] About to call build_expression"); let _last = self.build_expression(program_ast)?; + eprintln!("[DEBUG/lower_function_body] build_expression completed"); Ok(()) } diff --git a/src/mir/builder/decls.rs b/src/mir/builder/decls.rs index e73192bd..db615b4b 100644 --- a/src/mir/builder/decls.rs +++ b/src/mir/builder/decls.rs @@ -29,15 +29,25 @@ impl super::MirBuilder { // Look for the main() method let out = if let Some(main_method) = methods.get("main") { if let ASTNode::FunctionDeclaration { params, body, .. } = main_method { - // Also materialize a callable function entry "BoxName.main/N" for harness/PyVM - let func_name = format!("{}.{}", box_name, "main"); - eprintln!("[DEBUG] build_static_main_box: Before lower_static_method_as_function"); - eprintln!("[DEBUG] variable_map = {:?}", self.variable_map); - // Note: Metadata clearing is now handled by BoxCompilationContext (箱理論) - // See lifecycle.rs and builder_calls.rs for context swap implementation - let _ = self.lower_static_method_as_function(func_name, params.clone(), body.clone()); - eprintln!("[DEBUG] build_static_main_box: After lower_static_method_as_function"); - eprintln!("[DEBUG] variable_map = {:?}", self.variable_map); + // Optional: materialize a callable function entry "BoxName.main/N" for harness/PyVM. + // This static entryは通常の VM 実行では使用されず、過去の Hotfix 4 絡みの loop/control-flow + // バグの温床になっていたため、Phase 25.1m では明示トグルが立っている場合だけ生成する。 + if std::env::var("NYASH_BUILD_STATIC_MAIN_ENTRY") + .ok() + .as_deref() + == Some("1") + { + let func_name = format!("{}.{}", box_name, "main"); + eprintln!("[DEBUG] build_static_main_box: Before lower_static_method_as_function"); + eprintln!("[DEBUG] params.len() = {}", params.len()); + eprintln!("[DEBUG] body.len() = {}", body.len()); + eprintln!("[DEBUG] variable_map = {:?}", self.variable_map); + // Note: Metadata clearing is now handled by BoxCompilationContext (箱理論) + // See lifecycle.rs and builder_calls.rs for context swap implementation + let _ = self.lower_static_method_as_function(func_name, params.clone(), body.clone()); + eprintln!("[DEBUG] build_static_main_box: After lower_static_method_as_function"); + eprintln!("[DEBUG] variable_map = {:?}", self.variable_map); + } // Initialize local variables for Main.main() parameters // Note: These are local variables in the wrapper main() function, NOT parameters let saved_var_map = std::mem::take(&mut self.variable_map); diff --git a/src/mir/builder/stmts.rs b/src/mir/builder/stmts.rs index a0f2702f..484cbe34 100644 --- a/src/mir/builder/stmts.rs +++ b/src/mir/builder/stmts.rs @@ -152,11 +152,17 @@ impl super::MirBuilder { let scope_id = self.current_block.map(|bb| bb.as_u32()).unwrap_or(0); self.hint_scope_enter(scope_id); let mut last_value = None; - for statement in statements { + let total = statements.len(); + eprintln!("[DEBUG/build_block] Processing {} statements", total); + for (idx, statement) in statements.into_iter().enumerate() { + eprintln!("[DEBUG/build_block] Statement {}/{} current_block={:?} current_function={}", + idx+1, total, self.current_block, + self.current_function.as_ref().map(|f| f.signature.name.as_str()).unwrap_or("none")); last_value = Some(self.build_statement(statement)?); // If the current block was terminated by this statement (e.g., return/throw), // do not emit any further instructions for this block. if is_current_block_terminated(self)? { + eprintln!("[DEBUG/build_block] Block terminated after statement {}", idx+1); break; } } @@ -168,6 +174,7 @@ impl super::MirBuilder { if !self.is_current_block_terminated() { self.hint_scope_leave(scope_id); } + eprintln!("[DEBUG/build_block] Completed, returning value {:?}", out); Ok(out) } diff --git a/src/mir/control_form.rs b/src/mir/control_form.rs index 60afe827..6ebcf599 100644 --- a/src/mir/control_form.rs +++ b/src/mir/control_form.rs @@ -14,13 +14,13 @@ use crate::mir::{BasicBlock, BasicBlockId, MirFunction}; /// ループ構造の形だけを表す箱だよ。 /// -/// - `preheader` : ループ直前のブロック(キャリア/ピン変数のコピー元) -/// - `header` : ループヘッダ(条件判定と header PHI が置かれる) -/// - `body` : 代表的なループ本体ブロック(最初の body など) -/// - `latch` : ヘッダへ戻るバックエッジを張るブロック -/// - `exit` : ループを抜けた先のブロック -/// - `continue_targets` : continue がジャンプするブロック群(通常は latch か header) -/// - `break_targets` : break がジャンプするブロック群(通常は exit) +/// - `preheader` : ループ直前のブロック(キャリア/ピン変数のコピー元) +/// - `header` : ループヘッダ(条件判定と header PHI が置かれる) +/// - `body` : 代表的なループ本体ブロック(最初の body など) +/// - `latch` : ヘッダへ戻るバックエッジを張るブロック +/// - `exit` : ループを抜けた先のブロック +/// - `continue_targets` : continue 文を含み、`header` へ遷移するブロック群(エッジの「出発点」) +/// - `break_targets` : break 文を含み、`exit` へ遷移するブロック群(エッジの「出発点」) #[derive(Debug, Clone)] pub struct LoopShape { pub preheader: BasicBlockId, @@ -172,6 +172,8 @@ impl LoopShape { /// /// - preheader → header にエッジがあること /// - latch → header にバックエッジがあること + /// - continue_targets の各ブロックから header へのエッジがあること + /// - break_targets の各ブロックから exit へのエッジがあること #[cfg(debug_assertions)] pub fn debug_validate(&self, cfg: &C) { debug_assert!( @@ -186,6 +188,24 @@ impl LoopShape { self.latch, self.header ); + + for ct in &self.continue_targets { + debug_assert!( + cfg.has_edge(*ct, self.header), + "LoopShape invalid: continue source block {:?} does not branch to header {:?}", + ct, + self.header + ); + } + + for bt in &self.break_targets { + debug_assert!( + cfg.has_edge(*bt, self.exit), + "LoopShape invalid: break source block {:?} does not branch to exit {:?}", + bt, + self.exit + ); + } } } diff --git a/src/mir/loop_builder.rs b/src/mir/loop_builder.rs index 7190bbc3..122a4c8a 100644 --- a/src/mir/loop_builder.rs +++ b/src/mir/loop_builder.rs @@ -318,8 +318,9 @@ impl<'a> LoopBuilder<'a> { // 📦 Hotfix 6: Add CFG predecessor for header from latch (same as legacy version) crate::mir::builder::loops::add_predecessor(self.parent_builder, header_id, latch_id)?; - // Pass 4: Seal PHIs with latch values - loopform.seal_phis(self, actual_latch_id)?; + // Pass 4: Seal PHIs with latch + continue values + let continue_snaps = self.continue_snapshots.clone(); + loopform.seal_phis(self, actual_latch_id, &continue_snaps)?; // Exit block self.set_current_block(exit_id)?; diff --git a/src/mir/phi_core/loopform_builder.rs b/src/mir/phi_core/loopform_builder.rs index f4097f17..025b23c1 100644 --- a/src/mir/phi_core/loopform_builder.rs +++ b/src/mir/phi_core/loopform_builder.rs @@ -259,33 +259,75 @@ impl LoopFormBuilder { /// Pass 4: Seal PHI nodes after loop body lowering /// - /// Completes PHI nodes with latch inputs, converting them from: + /// Completes PHI nodes with latch + continue inputs, converting them from: /// phi [preheader_val, preheader] /// to: - /// phi [preheader_val, preheader], [latch_val, latch] + /// phi [preheader_val, preheader], [continue_val, continue_bb]..., [latch_val, latch] + /// + /// # Parameters + /// - `latch_id`: The block that closes the canonical backedge to `header`. + /// - `continue_snapshots`: Per-`continue` block variable snapshots. + /// Each entry represents a predecessor of `header` created by `continue`. pub fn seal_phis( &mut self, ops: &mut O, latch_id: BasicBlockId, + continue_snapshots: &[(BasicBlockId, HashMap)], ) -> Result<(), String> { + let debug = std::env::var("NYASH_LOOPFORM_DEBUG").is_ok(); + + if debug { + eprintln!( + "[loopform/seal_phis] header={:?} preheader={:?} latch={:?} continue_snapshots={}", + self.header_id, + self.preheader_id, + latch_id, + continue_snapshots.len() + ); + } + // Seal pinned variable PHIs + // + // Pinned variables are loop-invariant parameters, but header has multiple + // predecessors (preheader + continue + latch). To keep SSA well-formed, + // we still materialize PHI inputs for all predecessors so that every edge + // into header has a corresponding value. for pinned in &self.pinned { - // Pinned variables are not modified in loop, so latch value = header phi + let mut inputs: Vec<(BasicBlockId, ValueId)> = + vec![(self.preheader_id, pinned.preheader_copy)]; + + // Add inputs from each continue snapshot that carries this variable. + for (cid, snapshot) in continue_snapshots { + if let Some(&value) = snapshot.get(&pinned.name) { + inputs.push((*cid, value)); + } + } + + // Pinned variables are not modified in loop, so latch value typically + // equals header PHI. Fallback to header_phi if lookup fails. let latch_value = ops .get_variable_at_block(&pinned.name, latch_id) .unwrap_or(pinned.header_phi); + inputs.push((latch_id, latch_value)); - ops.update_phi_inputs( - self.header_id, - pinned.header_phi, - vec![ - (self.preheader_id, pinned.preheader_copy), - (latch_id, latch_value), - ], - )?; + sanitize_phi_inputs(&mut inputs); + + if debug { + eprintln!( + "[loopform/seal_phis] pinned '{}' phi={:?} inputs={:?}", + pinned.name, pinned.header_phi, inputs + ); + } + + ops.update_phi_inputs(self.header_id, pinned.header_phi, inputs)?; } // Seal carrier variable PHIs + // + // Carriers are loop-variant locals. They must merge values from: + // - preheader (initial value before the loop), + // - each continue block (early jump to header), + // - latch (normal end-of-iteration backedge). for carrier in &mut self.carriers { carrier.latch_value = ops .get_variable_at_block(&carrier.name, latch_id) @@ -293,14 +335,27 @@ impl LoopFormBuilder { format!("Carrier variable '{}' not found at latch block", carrier.name) })?; - ops.update_phi_inputs( - self.header_id, - carrier.header_phi, - vec![ - (self.preheader_id, carrier.preheader_copy), - (latch_id, carrier.latch_value), - ], - )?; + let mut inputs: Vec<(BasicBlockId, ValueId)> = + vec![(self.preheader_id, carrier.preheader_copy)]; + + for (cid, snapshot) in continue_snapshots { + if let Some(&value) = snapshot.get(&carrier.name) { + inputs.push((*cid, value)); + } + } + + inputs.push((latch_id, carrier.latch_value)); + + sanitize_phi_inputs(&mut inputs); + + if debug { + eprintln!( + "[loopform/seal_phis] carrier '{}' phi={:?} inputs={:?}", + carrier.name, carrier.header_phi, inputs + ); + } + + ops.update_phi_inputs(self.header_id, carrier.header_phi, inputs)?; } Ok(()) @@ -546,6 +601,7 @@ pub fn build_exit_phis_for_control( #[cfg(test)] mod tests { use super::*; + use std::collections::HashMap; #[test] fn test_sanitize_phi_inputs() { @@ -687,4 +743,152 @@ mod tests { assert_eq!(builder.carriers[0].preheader_copy, ValueId::new(104)); // i copy assert_eq!(builder.carriers[0].header_phi, ValueId::new(105)); // i phi } + + #[test] + fn test_seal_phis_includes_continue_snapshots() { + let preheader = BasicBlockId::new(0); + let header = BasicBlockId::new(1); + let latch = BasicBlockId::new(2); + + // Prepare LoopFormBuilder with one pinned and one carrier variable + let mut builder = LoopFormBuilder::new(preheader, header); + builder.pinned.push(PinnedVariable { + name: "p".to_string(), + param_value: ValueId::new(1), + preheader_copy: ValueId::new(10), + header_phi: ValueId::new(20), + }); + builder.carriers.push(CarrierVariable { + name: "i".to_string(), + init_value: ValueId::new(2), + preheader_copy: ValueId::new(11), + header_phi: ValueId::new(21), + latch_value: ValueId::INVALID, + }); + + // Mock LoopFormOps that records PHI updates + struct MockSealOps { + vars_at_block: HashMap<(BasicBlockId, String), ValueId>, + phi_updates: Vec<(BasicBlockId, ValueId, Vec<(BasicBlockId, ValueId)>)>, + } + + impl MockSealOps { + fn new() -> Self { + Self { + vars_at_block: HashMap::new(), + phi_updates: Vec::new(), + } + } + } + + impl LoopFormOps for MockSealOps { + fn new_value(&mut self) -> ValueId { + // Not used by seal_phis in this test + ValueId::new(999) + } + + fn ensure_counter_after(&mut self, _max_id: u32) -> Result<(), String> { + Ok(()) + } + + fn block_exists(&self, _block: BasicBlockId) -> bool { + true + } + + fn get_block_predecessors( + &self, + _block: BasicBlockId, + ) -> std::collections::HashSet { + std::collections::HashSet::new() + } + + fn is_parameter(&self, _name: &str) -> bool { + false + } + + fn set_current_block(&mut self, _block: BasicBlockId) -> Result<(), String> { + Ok(()) + } + + fn emit_copy(&mut self, _dst: ValueId, _src: ValueId) -> Result<(), String> { + Ok(()) + } + + fn emit_jump(&mut self, _target: BasicBlockId) -> Result<(), String> { + Ok(()) + } + + fn emit_phi( + &mut self, + _dst: ValueId, + _inputs: Vec<(BasicBlockId, ValueId)>, + ) -> Result<(), String> { + Ok(()) + } + + fn update_phi_inputs( + &mut self, + block: BasicBlockId, + phi_id: ValueId, + inputs: Vec<(BasicBlockId, ValueId)>, + ) -> Result<(), String> { + self.phi_updates.push((block, phi_id, inputs)); + Ok(()) + } + + fn update_var(&mut self, _name: String, _value: ValueId) {} + + fn get_variable_at_block( + &self, + name: &str, + block: BasicBlockId, + ) -> Option { + self.vars_at_block + .get(&(block, name.to_string())) + .copied() + } + } + + let mut ops = MockSealOps::new(); + // Latch values for p and i + ops.vars_at_block + .insert((latch, "p".to_string()), ValueId::new(30)); + ops.vars_at_block + .insert((latch, "i".to_string()), ValueId::new(31)); + + // Continue snapshot from block 5: p and i have distinct values there + let cont_bb = BasicBlockId::new(5); + let mut cont_snapshot: HashMap = HashMap::new(); + cont_snapshot.insert("p".to_string(), ValueId::new(40)); + cont_snapshot.insert("i".to_string(), ValueId::new(41)); + let continue_snapshots = vec![(cont_bb, cont_snapshot)]; + + // Act: seal PHIs + builder + .seal_phis(&mut ops, latch, &continue_snapshots) + .expect("seal_phis should succeed"); + + // We expect PHI updates for both pinned (p) and carrier (i) + assert_eq!(ops.phi_updates.len(), 2); + + // Helper to find inputs for a given phi id + let find_inputs = |phi_id: ValueId, + updates: &[(BasicBlockId, ValueId, Vec<(BasicBlockId, ValueId)>)]| { + updates + .iter() + .find(|(_, id, _)| *id == phi_id) + .map(|(_, _, inputs)| inputs.clone()) + .expect("phi id not found in updates") + }; + + let pinned_inputs = find_inputs(ValueId::new(20), &ops.phi_updates); + assert!(pinned_inputs.contains(&(preheader, ValueId::new(10)))); + assert!(pinned_inputs.contains(&(cont_bb, ValueId::new(40)))); + assert!(pinned_inputs.contains(&(latch, ValueId::new(30)))); + + let carrier_inputs = find_inputs(ValueId::new(21), &ops.phi_updates); + assert!(carrier_inputs.contains(&(preheader, ValueId::new(11)))); + assert!(carrier_inputs.contains(&(cont_bb, ValueId::new(41)))); + assert!(carrier_inputs.contains(&(latch, ValueId::new(31)))); + } } diff --git a/tests/mir_static_main_args_loop.rs b/tests/mir_static_main_args_loop.rs new file mode 100644 index 00000000..0c38949e --- /dev/null +++ b/tests/mir_static_main_args_loop.rs @@ -0,0 +1,106 @@ +/// Bug A investigation: main(args) causes loops not to execute +/// This test reproduces the issue where adding a parameter to main() +/// causes the loop body to never execute (RC=0 instead of RC=3) + +use std::process::Command; +use std::fs; + +#[test] +fn mir_static_main_no_args_loop() { + // Working case: main() → RC=3 + let source = r#" +static box Main { + main() { + local i = 0 + local count = 0 + loop(i < 3) { + count = count + 1 + i = i + 1 + } + return count + } +} +"#; + + let temp_file = "/tmp/mir_test_no_args.hako"; + fs::write(temp_file, source).expect("Failed to write test file"); + + let output = Command::new("./target/release/hakorune") + .arg("--backend") + .arg("vm") + .arg(temp_file) + .env("NYASH_PARSER_STAGE3", "1") + .env("NYASH_DISABLE_PLUGINS", "1") + .output() + .expect("Failed to execute hakorune"); + + fs::remove_file(temp_file).ok(); + + let exit_code = output.status.code().unwrap_or(-1); + assert_eq!(exit_code, 3, "Expected RC=3 for main() with loop"); +} + +#[test] +fn mir_static_main_with_args_loop() { + // Broken case: main(args) → RC=0 (BUG: should be 3) + let source = r#" +static box Main { + main(args) { + local i = 0 + local count = 0 + loop(i < 3) { + count = count + 1 + i = i + 1 + } + return count + } +} +"#; + + let temp_file = "/tmp/mir_test_with_args.hako"; + fs::write(temp_file, source).expect("Failed to write test file"); + + let output = Command::new("./target/release/hakorune") + .arg("--backend") + .arg("vm") + .arg(temp_file) + .env("NYASH_PARSER_STAGE3", "1") + .env("NYASH_DISABLE_PLUGINS", "1") + .output() + .expect("Failed to execute hakorune"); + + fs::remove_file(temp_file).ok(); + + let exit_code = output.status.code().unwrap_or(-1); + // This will FAIL due to the bug - loop doesn't execute + assert_eq!(exit_code, 3, "Expected RC=3 for main(args) with loop (BUG: currently returns 0)"); +} + +#[test] +fn mir_static_main_args_without_loop() { + // Sanity check: main(args) works WITHOUT loop + let source = r#" +static box Main { + main(args) { + return 42 + } +} +"#; + + let temp_file = "/tmp/mir_test_args_no_loop.hako"; + fs::write(temp_file, source).expect("Failed to write test file"); + + let output = Command::new("./target/release/hakorune") + .arg("--backend") + .arg("vm") + .arg(temp_file) + .env("NYASH_PARSER_STAGE3", "1") + .env("NYASH_DISABLE_PLUGINS", "1") + .output() + .expect("Failed to execute hakorune"); + + fs::remove_file(temp_file).ok(); + + let exit_code = output.status.code().unwrap_or(-1); + assert_eq!(exit_code, 42, "Expected RC=42 for main(args) without loop"); +}