diff --git a/apps/tests/loopform_break_and_return.hako b/apps/tests/loopform_break_and_return.hako new file mode 100644 index 00000000..44319875 --- /dev/null +++ b/apps/tests/loopform_break_and_return.hako @@ -0,0 +1,26 @@ +// loopform_break_and_return.hako +// 目的: header-cond + break + early-return が混在するループをスモークする + +static box Main { + compute(limit) { + local i = 0 + // Case A: header-cond (i < limit) + loop(i < limit) { + if i == 5 { return -1 } // 早期 return(exit PHI 非対象) + if i >= limit - 1 { break } // exit-break + i = i + 1 + } + return i + } + + main(args) { + // limit が小さい場合は break 経路で終了 + local r1 = Main.compute(3) + print(r1) + // limit が大きい場合は i==5 で早期 return + local r2 = Main.compute(10) + print(r2) + return 0 + } +} + diff --git a/apps/tests/loopform_continue_break_scan.hako b/apps/tests/loopform_continue_break_scan.hako new file mode 100644 index 00000000..049337ec --- /dev/null +++ b/apps/tests/loopform_continue_break_scan.hako @@ -0,0 +1,40 @@ +// loopform_continue_break_scan.hako +// 目的: Region+next_i 型の scan ループで continue + break 混在パターンをスモークする + +static box Main { + scan(s) { + local i = 0 + local n = s.length() + // Case C-ish: constant-true + continue/backedge + break + loop(1 == 1) { + if i >= n { break } + + // Region+next_i: この周回での「次の i」を 1 箇所に集約 + local next_i = i + local ch = s.substring(i, i + 1) + + if ch == " " { + next_i = i + 1 + i = next_i + continue + } + if ch == "\t" { + next_i = i + 2 + i = next_i + continue + } + // その他の文字で終了 + break + } + return i + } + + main(args) { + // 先頭に space + tab を並べたケースで scan が 3 を返すことを想定 + local s = " \tX" + local r = Main.scan(s) + print(r) + return 0 + } +} + diff --git a/apps/tests/loopform_nested_region.hako b/apps/tests/loopform_nested_region.hako new file mode 100644 index 00000000..2d16e0b8 --- /dev/null +++ b/apps/tests/loopform_nested_region.hako @@ -0,0 +1,39 @@ +// loopform_nested_region.hako +// 目的: 外側 Loop + 内側 Region 型 Loop のネストをスモークする + +static box Main { + find_first_non_space(lines) { + local line_idx = 0 + local n = lines.size() + // outer loop: 行単位 + loop(line_idx < n) { + local line = lines.get(line_idx) + local col = 0 + + // inner loop: 行内の先頭非空白を探す(Region+next_col 風) + loop(1 == 1) { + if col >= line.length() { break } + local ch = line.substring(col, col + 1) + if ch == " " { + col = col + 1 + continue + } + break + } + + if col < line.length() { return line_idx } + line_idx = line_idx + 1 + } + return -1 + } + + main(args) { + local lines = new ArrayBox() + lines.push(" ") + lines.push(" xx") + local r = Main.find_first_non_space(lines) + print(r) + return 0 + } +} + diff --git a/apps/tests/minimal_ssa_bug.hako b/apps/tests/minimal_ssa_bug.hako new file mode 100644 index 00000000..d4c64d7e --- /dev/null +++ b/apps/tests/minimal_ssa_bug.hako @@ -0,0 +1,20 @@ +// Minimal SSA Bug Reproduction +// Pattern: Conditional reassignment with immediate usage +// Expected: SSA violation "use of undefined value" + +static box MinimalSSABug { + main(args) { + local x = "test" + + // Problem pattern: reassign + immediate use in nested condition + if x.length() > 0 { + x = x.substring(0, 2) // reassign x + if x.length() > 0 && x.substring(0, 1) == "t" { // immediate use + x = x.substring(1, 2) // another reassign + print(x) + } + } + + return 0 + } +} diff --git a/apps/tests/minimal_ssa_bug_loop.hako b/apps/tests/minimal_ssa_bug_loop.hako new file mode 100644 index 00000000..23973021 --- /dev/null +++ b/apps/tests/minimal_ssa_bug_loop.hako @@ -0,0 +1,28 @@ +// Minimal SSA Bug Reproduction with Loop +// Pattern: Conditional reassignment inside loop with immediate usage +// Expected: SSA violation "use of undefined value" + +static box MinimalSSABugLoop { + main(args) { + local src = "using foo\nusing bar\n" + local i = 0 + local n = src.length() + + loop(i < n) { + local line = src.substring(i, i + 10) + + // Problem pattern: nested conditions with reassignment + immediate use + if line.length() > 0 { + line = line.substring(0, 5) // reassign line + if line.length() > 0 && line.substring(0, 1) == "u" { // immediate use + line = line.substring(1, 5) // another reassign + print(line) + } + } + + i = i + 10 + } + + return 0 + } +} diff --git a/apps/tests/minimal_ssa_skip_ws.hako b/apps/tests/minimal_ssa_skip_ws.hako new file mode 100644 index 00000000..0066403e --- /dev/null +++ b/apps/tests/minimal_ssa_skip_ws.hako @@ -0,0 +1,24 @@ +// minimal_ssa_skip_ws.hako — LoopForm exit-PHI regression canary +// 目的: loop(1 == 1) + break だけで exit PHI が壊れる既知事例を最小化 + +static box Main { + skip(s) { + local i = 0 + local n = s.length() + // 本来は loop(i < n) だが、ワークアラウンドとして loop(1 == 1) にしていた経路で + // exit PHI が崩れて ValueId 未定義になる不具合を再現する。 + loop(1 == 1) { + if i >= n { break } + local ch = s.substring(i, i + 1) + if ch == " " { i = i + 1 } else { break } + } + return i + } + + main(args) { + local s = " abc" + local r = Main.skip(s) + print(r) + return 0 + } +} diff --git a/apps/tests/stage1_using_minimal.hako b/apps/tests/stage1_using_minimal.hako new file mode 100644 index 00000000..1041f697 --- /dev/null +++ b/apps/tests/stage1_using_minimal.hako @@ -0,0 +1 @@ +using "foo/bar.hako" as Foo diff --git a/apps/tests/stage1_using_namespace.hako b/apps/tests/stage1_using_namespace.hako new file mode 100644 index 00000000..6280f90b --- /dev/null +++ b/apps/tests/stage1_using_namespace.hako @@ -0,0 +1,9 @@ +using std.collections +using "path/to/module.hako" as MyModule + +static box Main { + main(args) { + print("Hello") + return 0 + } +} diff --git a/apps/tests/test_else_if_scope.hako b/apps/tests/test_else_if_scope.hako new file mode 100644 index 00000000..d418b3bf --- /dev/null +++ b/apps/tests/test_else_if_scope.hako @@ -0,0 +1,23 @@ +static box Main { + main(args) { + local x = args + if x == null { + x = new ArrayBox() + x.push("default") + if x.length() > 0 { + local status = "initialized" + if x.get(0) != null { + status = "has_value: " + x.length() + } + print("Status: " + status) + } + } else if x.length() >= 0 { + print("Length: " + ("" + x.length())) + } + if x == null { + return 1 + } + print("Final length: " + ("" + x.length())) + return 0 + } +} diff --git a/apps/tests/test_loop_conditional_assign.hako b/apps/tests/test_loop_conditional_assign.hako new file mode 100644 index 00000000..2ac007ab --- /dev/null +++ b/apps/tests/test_loop_conditional_assign.hako @@ -0,0 +1,15 @@ +static box Main { + main(args) { + local str = "foo/bar/baz" + local idx = -1 + local t = 0 + loop(t < str.length()) { + if str.substring(t, t+1) == "/" { + idx = t + } + t = t + 1 + } + print("Last slash at: " + ("" + idx)) + return 0 + } +} diff --git a/apps/tests/test_nested_scope_phi.hako b/apps/tests/test_nested_scope_phi.hako new file mode 100644 index 00000000..1a3a90aa --- /dev/null +++ b/apps/tests/test_nested_scope_phi.hako @@ -0,0 +1,29 @@ +static box Main { + main(args) { + local src = "using foo\nusing bar" + local n = src.length() + local i = 0 + loop(i < n) { + local j = i + loop(j < n && src.substring(j, j+1) != "\n") { j = j + 1 } + local line = src.substring(i, j) + + if line.length() > 0 { + local p = line + local idx = -1 + local t = 0 + loop(t < p.length()) { + if p.substring(t,t+1) == " " { idx = t } + t = t + 1 + } + if idx >= 0 { + p = p.substring(0, idx) + } + print("Processed: " + p) + } + + i = j + 1 + } + return 0 + } +} diff --git a/apps/tests/test_string_concat_phi.hako b/apps/tests/test_string_concat_phi.hako new file mode 100644 index 00000000..8ca2d14b --- /dev/null +++ b/apps/tests/test_string_concat_phi.hako @@ -0,0 +1,28 @@ +static box TestBox { + get_value() { + return "test_result" + } +} + +static box Main { + main(args) { + local prog = TestBox.get_value() + { + local status = "null" + if prog != null { status = "non-null" } + print("Debug: " + status) + } + if prog == null { + print("Error: prog is null") + return null + } + local ps = "" + prog + print("Stringified length: " + ("" + ps.length())) + if ps.indexOf("test") < 0 || ps.indexOf("result") < 0 { + print("Error: unexpected output") + return null + } + print("Success") + return ps + } +} diff --git a/docs/development/analysis/minimal_ssa_bug_analysis.md b/docs/development/analysis/minimal_ssa_bug_analysis.md new file mode 100644 index 00000000..c8ac9ba0 --- /dev/null +++ b/docs/development/analysis/minimal_ssa_bug_analysis.md @@ -0,0 +1,26 @@ +# Minimal SSA Bug Analysis — loop(1==1) + break exit PHI + +## 何が起きていたか +- Stage‑B / FuncScanner の `skip_whitespace` 系で、`loop(1 == 1) { ... break }` という形のループが exit PHI を壊し、`ValueId` 未定義エラー(`use of undefined value` / Dominator violation)が発生していた。 +- 原因: exit PHI 入力に「存在しない predecessor(header)」由来の値を含めていた。 + - `loop(1 == 1)` の場合、`header → exit` の CFG エッジは存在しない(exit は break 経路のみ)。 + - それにもかかわらず、`merge_exit_with_classification` が header 値を無条件で PHI 入力に足していたため、非支配ブロックからの値参照になり破綻。 + +## 最小再現 +- ファイル: `apps/tests/minimal_ssa_skip_ws.hako` +- 構造: `loop(1 == 1)` の中で `if i >= n { break }` を先頭に置く形。break 以外に exit pred が無い。 +- Rustテスト: `src/tests/mir_loopform_conditional_reassign.rs::loop_constant_true_exit_phi_dominates`(今回新設) + +## 修正内容 +- `src/mir/phi_core/loop_snapshot_merge.rs` の `merge_exit_with_classification` で、header が exit predecessor に含まれている場合にのみ header 値を PHI 入力へ追加するようガード。 + - CFG に存在しない predecessor 由来の値を PHI に入れないことで Dominator violation を解消。 + +## 追加したもの +- 最小再現ハコ: `apps/tests/minimal_ssa_skip_ws.hako` +- Rustテスト: `src/tests/mir_loopform_conditional_reassign.rs` + - `loop_constant_true_exit_phi_dominates`(今回のバグ再現→修正確認用) + - 将来拡張用に 2 本を `#[ignore]` で占位(条件付き再代入 / body-local 変数の exit PHI 観測) + +## 次のステップ +- `#[ignore]` テストを埋める: 条件付き再代入(`loop(i < n)`)や body-local 混在ケースの SSA 安定性を追加検証。 +- Stage‑B / UsingResolver の既存スモークにも `apps/tests/minimal_ssa_skip_ws.hako` を流し込んで回帰を防ぐ。 diff --git a/src/tests/mir_loopform_complex.rs b/src/tests/mir_loopform_complex.rs new file mode 100644 index 00000000..3fb57dad --- /dev/null +++ b/src/tests/mir_loopform_complex.rs @@ -0,0 +1,85 @@ +//! LoopForm / Region+next_i 複雑パターン向けスモークテスト +//! +//! 目的: +//! - continue + break 混在ループ +//! - break + early-return 混在ループ +//! - 外側ループ + 内側 Region 型ループのネスト +//! を MIR Verifier ベースで「構造テスト」として押さえる。 + +use crate::mir::{MirCompiler, MirVerifier}; +use crate::parser::NyashParser; + +fn setup_stage3_env() { + std::env::set_var("NYASH_PARSER_STAGE3", "1"); + std::env::set_var("HAKO_PARSER_STAGE3", "1"); + std::env::set_var("NYASH_ENABLE_USING", "1"); + std::env::set_var("HAKO_ENABLE_USING", "1"); + std::env::set_var("NYASH_DISABLE_PLUGINS", "1"); +} + +fn teardown_stage3_env() { + std::env::remove_var("NYASH_PARSER_STAGE3"); + std::env::remove_var("HAKO_PARSER_STAGE3"); + std::env::remove_var("NYASH_ENABLE_USING"); + std::env::remove_var("HAKO_ENABLE_USING"); + std::env::remove_var("NYASH_DISABLE_PLUGINS"); +} + +fn compile_module(src: &str) -> crate::mir::MirCompileResult { + setup_stage3_env(); + let ast = NyashParser::parse_from_string(src).expect("parse ok"); + let mut mc = MirCompiler::with_options(false); + mc.compile(ast).expect("compile ok") +} + +#[test] +fn mir_loopform_continue_break_scan_verify() { + // Case C-ish: constant-true + continue/backedge + break(Region+next_i 型) + let src = include_str!("../../apps/tests/loopform_continue_break_scan.hako"); + let compiled = compile_module(src); + + let mut verifier = MirVerifier::new(); + if let Err(errors) = verifier.verify_module(&compiled.module) { + for e in &errors { + eprintln!("[mir-verify] {}", e); + } + teardown_stage3_env(); + panic!("mir_loopform_continue_break_scan_verify: MIR verification failed"); + } + teardown_stage3_env(); +} + +#[test] +fn mir_loopform_break_and_return_verify() { + // Case: header-cond + break + early-return 混在 + let src = include_str!("../../apps/tests/loopform_break_and_return.hako"); + let compiled = compile_module(src); + + let mut verifier = MirVerifier::new(); + if let Err(errors) = verifier.verify_module(&compiled.module) { + for e in &errors { + eprintln!("[mir-verify] {}", e); + } + teardown_stage3_env(); + panic!("mir_loopform_break_and_return_verify: MIR verification failed"); + } + teardown_stage3_env(); +} + +#[test] +fn mir_loopform_nested_region_verify() { + // Case: 外側 Loop + 内側 Region 型 Loop(inner は constant-true + continue + break) + let src = include_str!("../../apps/tests/loopform_nested_region.hako"); + let compiled = compile_module(src); + + let mut verifier = MirVerifier::new(); + if let Err(errors) = verifier.verify_module(&compiled.module) { + for e in &errors { + eprintln!("[mir-verify] {}", e); + } + teardown_stage3_env(); + panic!("mir_loopform_nested_region_verify: MIR verification failed"); + } + teardown_stage3_env(); +} + diff --git a/src/tests/mir_loopform_conditional_reassign.rs b/src/tests/mir_loopform_conditional_reassign.rs new file mode 100644 index 00000000..067c822e --- /dev/null +++ b/src/tests/mir_loopform_conditional_reassign.rs @@ -0,0 +1,171 @@ +//! LoopForm exit PHI regression tests (conditional reassign / break) +//! +//! 目的: +//! - loop(1 == 1) + break 経路で exit PHI が壊れて ValueId 未定義になる既知バグを捕まえる。 +//! - 将来: 条件付き再代入や body-local 変数が混在するケースを拡張(現状は #[ignore])。 + +use crate::mir::{MirCompiler, MirVerifier}; +use crate::parser::NyashParser; + +fn setup_stage3_env() { + std::env::set_var("NYASH_PARSER_STAGE3", "1"); + std::env::set_var("HAKO_PARSER_STAGE3", "1"); + std::env::set_var("NYASH_ENABLE_USING", "1"); + std::env::set_var("HAKO_ENABLE_USING", "1"); + std::env::set_var("NYASH_DISABLE_PLUGINS", "1"); +} + +fn teardown_stage3_env() { + std::env::remove_var("NYASH_PARSER_STAGE3"); + std::env::remove_var("HAKO_PARSER_STAGE3"); + std::env::remove_var("NYASH_ENABLE_USING"); + std::env::remove_var("HAKO_ENABLE_USING"); + std::env::remove_var("NYASH_DISABLE_PLUGINS"); +} + +fn compile_module(src: &str) -> crate::mir::MirCompileResult { + setup_stage3_env(); + let ast = NyashParser::parse_from_string(src).expect("parse ok"); + let mut mc = MirCompiler::with_options(false); + mc.compile(ast).expect("compile ok") +} + +// [LoopForm-Test] Case B: constant-true+break-only +#[test] +fn loop_constant_true_exit_phi_dominates() { + // Repro: loop(1 == 1) + break only (header is NOT an exit predecessor) + // This used to create exit PHIs with header inputs from non-predecessor blocks → dominator violation. + let src = include_str!("../../apps/tests/minimal_ssa_skip_ws.hako"); + let compiled = compile_module(src); + + let mut verifier = MirVerifier::new(); + if let Err(errors) = verifier.verify_module(&compiled.module) { + for e in &errors { + eprintln!("[mir-verify] {}", e); + } + teardown_stage3_env(); + panic!("loop_constant_true_exit_phi_dominates: MIR verification failed"); + } + teardown_stage3_env(); +} + +// [LoopForm-Test] Case A: header+break +#[test] +fn loop_conditional_reassign_exit_phi_header_and_break() { + // Case A: 通常の loop(i < n) で header→exit と body→exit の両方が存在するケース。 + // ここで PHI が壊れていないことを確認し、今回の修正が既存パターンを regress していないことを見る。 + let src = r#" +static box Main { + loop_case(s) { + local i = 0 + local n = s.length() + loop(i < n) { + if i >= n { break } + i = i + 1 + } + return i + } + + main(args) { + local s = "abc" + local r = Main.loop_case(s) + print(r) + return 0 + } +} +"#; + let compiled = compile_module(src); + + let mut verifier = MirVerifier::new(); + if let Err(errors) = verifier.verify_module(&compiled.module) { + for e in &errors { + eprintln!("[mir-verify] {}", e); + } + teardown_stage3_env(); + panic!("loop_conditional_reassign_exit_phi_header_and_break: MIR verification failed"); + } + teardown_stage3_env(); +} + +// [LoopForm-Test] Case C: body-local (BodyLocalInternal) +#[test] +fn loop_body_local_exit_phi_body_only() { + // Case C: body-local 変数が break 経路にだけ存在し、header には存在しないケース。 + // Option C の分類により、こうした BodyLocalInternal には exit PHI が張られず、 + // MIR 検証が通ることを確認する。 + let src = r#" +static box Main { + loop_case(s) { + local i = 0 + local n = s.length() + loop(1 == 1) { + if i >= n { break } + local temp = s.substring(i, i + 1) + if temp == "x" { break } else { i = i + 1 } + } + return i + } + + main(args) { + local s = "abc" + local r = Main.loop_case(s) + print(r) + return 0 + } +} +"#; + let compiled = compile_module(src); + + let mut verifier = MirVerifier::new(); + if let Err(errors) = verifier.verify_module(&compiled.module) { + for e in &errors { + eprintln!("[mir-verify] {}", e); + } + teardown_stage3_env(); + panic!("loop_body_local_exit_phi_body_only: MIR verification failed"); + } + teardown_stage3_env(); +} + +// [LoopForm-Test] Case D: continue+break (continue_merge → header → exit) +#[test] +fn loop_continue_merge_header_exit() { + // Case D: continue 文を含むループで、continue_merge → header → exit の組み合わせを検証。 + // continue_merge ブロックで PHI を生成し、header PHI に正しく伝播することを確認。 + let src = r#" +static box Main { + loop_case(s) { + local i = 0 + local n = s.length() + loop(i < n) { + local ch = s.substring(i, i + 1) + if ch == " " { + i = i + 1 + continue + } + if ch == "x" { break } + i = i + 1 + } + return i + } + + main(args) { + local s = " x " + local r = Main.loop_case(s) + print(r) + return 0 + } +} +"#; + let compiled = compile_module(src); + + let mut verifier = MirVerifier::new(); + if let Err(errors) = verifier.verify_module(&compiled.module) { + for e in &errors { + eprintln!("[mir-verify] {}", e); + } + teardown_stage3_env(); + panic!("loop_continue_merge_header_exit: MIR verification failed"); + } + teardown_stage3_env(); +} diff --git a/src/tests/mir_loopform_exit_phi.rs b/src/tests/mir_loopform_exit_phi.rs index f1da183b..939df387 100644 --- a/src/tests/mir_loopform_exit_phi.rs +++ b/src/tests/mir_loopform_exit_phi.rs @@ -5,8 +5,8 @@ * Focus: predecessor tracking and PHI input generation for break statements */ -use crate::parser::NyashParser; use crate::mir::{MirCompiler, MirVerifier}; +use crate::parser::NyashParser; #[test] fn test_loopform_exit_phi_single_break() { @@ -33,8 +33,7 @@ static box TestExitPhi { println!("=== Test: Single break statement ==="); // Parse - let ast = NyashParser::parse_from_string(src) - .expect("parse failed"); + let ast = NyashParser::parse_from_string(src).expect("parse failed"); // Compile let mut mc = MirCompiler::with_options(false); @@ -46,8 +45,12 @@ static box TestExitPhi { eprintln!("Entry block: {:?}", func.entry_block); eprintln!("Total blocks: {}", func.blocks.len()); for (bid, block) in &func.blocks { - eprintln!(" Block {:?}: {} instructions, successors={:?}", - bid, block.instructions.len(), block.successors); + eprintln!( + " Block {:?}: {} instructions, successors={:?}", + bid, + block.instructions.len(), + block.successors + ); if *bid == crate::mir::BasicBlockId(10) { eprintln!(" BB10 instructions:"); for inst in &block.instructions {