Phase 30.x: JoinIR VM Bridge でPHI問題を設計で解決できることを実証 変更内容: - src/runner/modes/vm.rs: FuncScannerBox.trim/1 検出時にJoinIR経由実行 - src/mir/join_ir_vm_bridge.rs: Non-tail call サポート追加 - dst=Some(id) の場合、結果を格納して次の命令へ継続 - src/tests/joinir_vm_bridge_trim.rs: A/Bテスト新規作成 - メインテスト: Route A (VM) → "void" (PHI bug), Route C (JoinIR) → "abc" ✅ - エッジケース: 5パターン全てPASS - src/config/env.rs: joinir_vm_bridge_debug() 追加 - docs: Phase 30 TASKS.md に L-0.4 追加 テスト結果: Route A (MIR→VM直接): "void" ← PHI バグ Route C (MIR→JoinIR→VM): "abc" ← 正解 ✅ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
223 lines
7.3 KiB
Rust
223 lines
7.3 KiB
Rust
// Phase 30.x: JoinIR → Rust VM Bridge A/B Test for FuncScannerBox.trim/1
|
|
//
|
|
// 目的:
|
|
// - JoinIR を VM ブリッジ経由で実行し、直接 VM 実行の結果と一致することを確認する
|
|
// - Route A (AST→MIR→VM) と Route C (AST→MIR→JoinIR→MIR'→VM) の比較
|
|
//
|
|
// Test Pattern:
|
|
// - trim(" abc ") → "abc" (leading + trailing whitespace removed)
|
|
//
|
|
// Implementation Status:
|
|
// - Phase 30.x: Non-tail call support in VM bridge ✅
|
|
// - A/B test (this file) ✅
|
|
|
|
use crate::ast::ASTNode;
|
|
use crate::backend::VM;
|
|
use crate::mir::join_ir::{lower_funcscanner_trim_to_joinir, JoinFuncId};
|
|
use crate::mir::join_ir_ops::JoinValue;
|
|
use crate::mir::join_ir_vm_bridge::run_joinir_via_vm;
|
|
use crate::mir::MirCompiler;
|
|
use crate::parser::NyashParser;
|
|
|
|
fn require_experiment_toggle() -> bool {
|
|
if std::env::var("NYASH_JOINIR_VM_BRIDGE").ok().as_deref() != Some("1") {
|
|
eprintln!("[joinir/vm_bridge] NYASH_JOINIR_VM_BRIDGE=1 not set, skipping VM bridge test");
|
|
return false;
|
|
}
|
|
true
|
|
}
|
|
|
|
/// Minimal FuncScannerBox.trim/1 implementation for testing
|
|
const TRIM_SOURCE: &str = r#"
|
|
static box FuncScannerBox {
|
|
trim(s) {
|
|
local str = "" + s
|
|
local n = str.length()
|
|
local b = me._skip_leading(str, 0, n)
|
|
local e = n + 0
|
|
return me._trim_trailing(str, b, e)
|
|
}
|
|
|
|
_skip_leading(str, i, n) {
|
|
loop(i < n) {
|
|
local ch = str.substring(i, i + 1)
|
|
local is_ws = (ch == " ") or (ch == "\t") or (ch == "\n") or (ch == "\r")
|
|
if (not is_ws) {
|
|
return i
|
|
}
|
|
i = i + 1
|
|
}
|
|
return i
|
|
}
|
|
|
|
_trim_trailing(str, b, e) {
|
|
loop(e > b) {
|
|
local ch = str.substring(e - 1, e)
|
|
local is_ws = (ch == " ") or (ch == "\t") or (ch == "\n") or (ch == "\r")
|
|
if (not is_ws) {
|
|
return str.substring(b, e)
|
|
}
|
|
e = e - 1
|
|
}
|
|
return str.substring(b, e)
|
|
}
|
|
}
|
|
"#;
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn joinir_vm_bridge_trim_matches_direct_vm() {
|
|
if !require_experiment_toggle() {
|
|
return;
|
|
}
|
|
|
|
std::env::set_var("NYASH_PARSER_STAGE3", "1");
|
|
std::env::set_var("HAKO_PARSER_STAGE3", "1");
|
|
std::env::set_var("NYASH_DISABLE_PLUGINS", "1");
|
|
std::env::set_var("NYASH_VM_MAX_STEPS", "100000");
|
|
|
|
let runner = r#"
|
|
static box Runner {
|
|
main(args) {
|
|
return FuncScannerBox.trim(" abc ")
|
|
}
|
|
}
|
|
"#;
|
|
let full_src = format!("{TRIM_SOURCE}\n{runner}");
|
|
|
|
let ast: ASTNode = NyashParser::parse_from_string(&full_src).expect("trim: parse failed");
|
|
let mut mc = MirCompiler::with_options(false);
|
|
let compiled = mc.compile(ast).expect("trim: MIR compile failed");
|
|
|
|
// Route A: AST → MIR → VM (direct)
|
|
eprintln!("[joinir_vm_bridge_test/trim] Route A: Direct VM execution");
|
|
std::env::set_var("NYASH_ENTRY", "Runner.main");
|
|
let mut vm = VM::new();
|
|
let vm_out = vm
|
|
.execute_module(&compiled.module)
|
|
.expect("trim: VM execution failed");
|
|
let vm_result = vm_out.to_string_box().value;
|
|
std::env::remove_var("NYASH_ENTRY");
|
|
|
|
eprintln!("[joinir_vm_bridge_test/trim] Route A result: {}", vm_result);
|
|
|
|
// Route C: AST → MIR → JoinIR → MIR' → VM (via bridge)
|
|
eprintln!("[joinir_vm_bridge_test/trim] Route C: JoinIR → VM bridge execution");
|
|
let join_module = lower_funcscanner_trim_to_joinir(&compiled.module)
|
|
.expect("lower_funcscanner_trim_to_joinir failed");
|
|
|
|
let bridge_result = run_joinir_via_vm(
|
|
&join_module,
|
|
JoinFuncId::new(0),
|
|
&[JoinValue::Str(" abc ".to_string())],
|
|
)
|
|
.expect("JoinIR VM bridge failed for trim");
|
|
|
|
eprintln!(
|
|
"[joinir_vm_bridge_test/trim] Route C result: {:?}",
|
|
bridge_result
|
|
);
|
|
|
|
// Assertions:
|
|
// Note: Route A (direct VM) may return incorrect result due to PHI bugs.
|
|
// This is expected and is exactly what JoinIR is designed to fix.
|
|
// We verify that JoinIR returns the correct result.
|
|
match bridge_result {
|
|
JoinValue::Str(s) => {
|
|
assert_eq!(s, "abc", "Route C (JoinIR→VM bridge) trim result mismatch");
|
|
if vm_result == "abc" {
|
|
eprintln!("[joinir_vm_bridge_test/trim] ✅ A/B test passed: both routes returned 'abc'");
|
|
} else {
|
|
eprintln!("[joinir_vm_bridge_test/trim] ⚠️ Route A (VM) returned '{}' (PHI bug), Route C (JoinIR) returned 'abc' (correct)", vm_result);
|
|
eprintln!("[joinir_vm_bridge_test/trim] ✅ JoinIR correctly handles PHI issues that affect direct VM path");
|
|
}
|
|
}
|
|
other => panic!("JoinIR VM bridge returned non-string value: {:?}", other),
|
|
}
|
|
|
|
std::env::remove_var("NYASH_PARSER_STAGE3");
|
|
std::env::remove_var("HAKO_PARSER_STAGE3");
|
|
std::env::remove_var("NYASH_DISABLE_PLUGINS");
|
|
std::env::remove_var("NYASH_VM_MAX_STEPS");
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn joinir_vm_bridge_trim_edge_cases() {
|
|
if !require_experiment_toggle() {
|
|
return;
|
|
}
|
|
|
|
std::env::set_var("NYASH_PARSER_STAGE3", "1");
|
|
std::env::set_var("HAKO_PARSER_STAGE3", "1");
|
|
std::env::set_var("NYASH_DISABLE_PLUGINS", "1");
|
|
|
|
// Test cases: (input, expected_output)
|
|
// Note: Escape characters like \t\n\r are not tested here because
|
|
// Nyash string literal parsing differs from Rust escape handling.
|
|
let test_cases = [
|
|
(" abc ", "abc"),
|
|
("hello", "hello"),
|
|
(" ", ""),
|
|
(" x ", "x"),
|
|
(" hello world ", "hello world"),
|
|
];
|
|
|
|
for (input, expected) in test_cases {
|
|
// Re-set environment variables at each iteration to avoid race conditions
|
|
// with parallel test execution
|
|
std::env::set_var("NYASH_PARSER_STAGE3", "1");
|
|
std::env::set_var("HAKO_PARSER_STAGE3", "1");
|
|
std::env::set_var("NYASH_DISABLE_PLUGINS", "1");
|
|
|
|
let runner = format!(
|
|
r#"
|
|
static box Runner {{
|
|
main(args) {{
|
|
return FuncScannerBox.trim("{input}")
|
|
}}
|
|
}}
|
|
"#
|
|
);
|
|
let full_src = format!("{TRIM_SOURCE}\n{runner}");
|
|
|
|
let ast: ASTNode =
|
|
NyashParser::parse_from_string(&full_src).expect("trim edge case: parse failed");
|
|
let mut mc = MirCompiler::with_options(false);
|
|
let compiled = mc.compile(ast).expect("trim edge case: MIR compile failed");
|
|
|
|
// JoinIR path only
|
|
let join_module = lower_funcscanner_trim_to_joinir(&compiled.module)
|
|
.expect("lower_funcscanner_trim_to_joinir failed");
|
|
|
|
let bridge_result = run_joinir_via_vm(
|
|
&join_module,
|
|
JoinFuncId::new(0),
|
|
&[JoinValue::Str(input.to_string())],
|
|
)
|
|
.expect("JoinIR VM bridge failed for trim edge case");
|
|
|
|
match bridge_result {
|
|
JoinValue::Str(s) => {
|
|
assert_eq!(
|
|
s, expected,
|
|
"trim({:?}) expected {:?} but got {:?}",
|
|
input, expected, s
|
|
);
|
|
eprintln!(
|
|
"[joinir_vm_bridge_test/trim] ✅ trim({:?}) = {:?}",
|
|
input, s
|
|
);
|
|
}
|
|
other => panic!(
|
|
"trim({:?}) returned non-string value: {:?}",
|
|
input, other
|
|
),
|
|
}
|
|
}
|
|
|
|
std::env::remove_var("NYASH_PARSER_STAGE3");
|
|
std::env::remove_var("HAKO_PARSER_STAGE3");
|
|
std::env::remove_var("NYASH_DISABLE_PLUGINS");
|
|
}
|