feat(joinir): FuncScannerBox.trim/1 JoinIR VM Bridge A/B test

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>
This commit is contained in:
nyash-codex
2025-11-25 12:22:08 +09:00
parent de16ff9b7f
commit ee2b3115ee
5 changed files with 565 additions and 79 deletions

View File

@ -189,6 +189,26 @@ pub fn fail_fast() -> bool {
env_bool_default("NYASH_FAIL_FAST", true)
}
// ---- Phase 29/30 JoinIR toggles ----
/// JoinIR experiment mode. Required for JoinIR-related experimental paths.
/// Set NYASH_JOINIR_EXPERIMENT=1 to enable.
pub fn joinir_experiment_enabled() -> bool {
env_bool("NYASH_JOINIR_EXPERIMENT")
}
/// JoinIR VM bridge mode. When enabled with NYASH_JOINIR_EXPERIMENT=1,
/// specific functions can be executed via JoinIR → VM bridge instead of direct MIR → VM.
/// Set NYASH_JOINIR_VM_BRIDGE=1 to enable.
pub fn joinir_vm_bridge_enabled() -> bool {
env_bool("NYASH_JOINIR_VM_BRIDGE")
}
/// JoinIR VM bridge debug output. Enables verbose logging of JoinIR→MIR conversion.
/// Set NYASH_JOINIR_VM_BRIDGE_DEBUG=1 to enable.
pub fn joinir_vm_bridge_debug() -> bool {
env_bool("NYASH_JOINIR_VM_BRIDGE_DEBUG")
}
// VM legacy by-name call fallback was removed (Phase 2 complete).
// ---- Phase 11.8 MIR cleanup toggles ----

View File

@ -23,15 +23,26 @@
//! Phase 27-shortterm scope: skip_ws で green 化できれば成功
use crate::backend::{MirInterpreter, VMError, VMValue};
use crate::config::env::joinir_vm_bridge_debug;
use crate::mir::join_ir::{
BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinInst, JoinModule, MirLikeInst,
};
use crate::mir::join_ir_ops::JoinValue;
use crate::ast::Span;
use crate::mir::{
BasicBlockId, BinaryOp, CompareOp as MirCompareOp, ConstValue as MirConstValue, EffectMask,
FunctionSignature, MirFunction, MirInstruction, MirModule, MirType, ValueId,
};
/// 条件付きデバッグ出力マクロNYASH_JOINIR_VM_BRIDGE_DEBUG=1 で有効)
macro_rules! debug_log {
($($arg:tt)*) => {
if joinir_vm_bridge_debug() {
eprintln!($($arg)*);
}
};
}
/// Phase 27-shortterm S-4 エラー型
#[derive(Debug, Clone)]
pub struct JoinIrVmBridgeError {
@ -52,6 +63,40 @@ impl From<VMError> for JoinIrVmBridgeError {
}
}
/// ブロックを確定するinstructions + spans + terminator を設定)
fn finalize_block(
mir_func: &mut MirFunction,
block_id: BasicBlockId,
instructions: Vec<MirInstruction>,
terminator: MirInstruction,
) {
if let Some(block) = mir_func.blocks.get_mut(&block_id) {
let inst_count = instructions.len();
block.instructions = instructions;
block.instruction_spans = vec![Span::unknown(); inst_count];
block.terminator = Some(terminator);
}
}
/// JoinFuncId から MIR 用の関数名を生成
fn join_func_name(id: JoinFuncId) -> String {
format!("join_func_{}", id.0)
}
/// JoinValue → MirConstValue 変換
fn join_value_to_mir_const(value: &JoinValue) -> Result<MirConstValue, JoinIrVmBridgeError> {
match value {
JoinValue::Int(v) => Ok(MirConstValue::Integer(*v)),
JoinValue::Bool(b) => Ok(MirConstValue::Bool(*b)),
JoinValue::Str(s) => Ok(MirConstValue::String(s.clone())),
JoinValue::Unit => Ok(MirConstValue::Null),
_ => Err(JoinIrVmBridgeError::new(format!(
"Unsupported JoinValue type: {:?}",
value
))),
}
}
/// Phase 27-shortterm S-4.3: JoinIR → VM 実行のエントリーポイント
///
/// ## Arguments
@ -78,23 +123,21 @@ pub fn run_joinir_via_vm(
entry_func: JoinFuncId,
args: &[JoinValue],
) -> Result<JoinValue, JoinIrVmBridgeError> {
eprintln!("[joinir_vm_bridge] Phase 27-shortterm S-4.3");
eprintln!("[joinir_vm_bridge] Converting JoinIR to MIR for VM execution");
debug_log!("[joinir_vm_bridge] Phase 27-shortterm S-4.3");
debug_log!("[joinir_vm_bridge] Converting JoinIR to MIR for VM execution");
// Step 1: JoinIR → MIR 変換
let mir_module = convert_joinir_to_mir(join_module, entry_func)?;
// Step 1: JoinIR → MIR 変換 (with argument wrapper)
let mir_module = convert_joinir_to_mir_with_args(join_module, entry_func, args)?;
eprintln!(
"[joinir_vm_bridge] Converted {} JoinIR functions to MIR",
debug_log!(
"[joinir_vm_bridge] Converted {} JoinIR functions to MIR (+ entry wrapper)",
join_module.functions.len()
);
// Step 2: VM 実行
let mut vm = MirInterpreter::new();
// 初期引数を VM に設定(暫定実装: 環境変数経由)
// TODO: S-4.4 で引数渡し機構を追加
eprintln!(
debug_log!(
"[joinir_vm_bridge] Executing via VM with {} arguments",
args.len()
);
@ -106,44 +149,106 @@ pub fn run_joinir_via_vm(
let join_result = JoinValue::from_vm_value(&vm_value)
.map_err(|e| JoinIrVmBridgeError::new(format!("Result conversion error: {}", e.message)))?;
eprintln!("[joinir_vm_bridge] Execution succeeded: {:?}", join_result);
debug_log!("[joinir_vm_bridge] Execution succeeded: {:?}", join_result);
Ok(join_result)
}
/// Phase 27-shortterm S-4.3: JoinIR → MIR 変換器
/// Phase 30.x: JoinIR → MIR 変換器 (with entry arguments)
///
/// JoinIR の関数群を MIR モジュールに変換する。
/// JoinIR の正規化構造Pinned/Carrier、Exit φ)は関数引数として表現されているため、
/// MIR に変換しても構造的意味は保持される。
fn convert_joinir_to_mir(
/// Creates a wrapper "main" function that:
/// 1. Creates constant values for the entry arguments
/// 2. Calls the actual entry function with those arguments
/// 3. Returns the result
fn convert_joinir_to_mir_with_args(
join_module: &JoinModule,
entry_func: JoinFuncId,
args: &[JoinValue],
) -> Result<MirModule, JoinIrVmBridgeError> {
let mut mir_module = MirModule::new("joinir_bridge".to_string());
// すべての JoinIR 関数を MIR 関数に変換
// Convert all JoinIR functions to MIR (entry function becomes "skip" or similar)
for (func_id, join_func) in &join_module.functions {
eprintln!(
debug_log!(
"[joinir_vm_bridge] Converting JoinFunction {} ({})",
func_id.0, join_func.name
);
let mir_func = convert_join_function_to_mir(join_func)?;
// Entry function を "main" として登録
let func_name = if *func_id == entry_func {
"main".to_string()
} else {
format!("join_func_{}", func_id.0)
};
mir_module.functions.insert(func_name, mir_func);
// Use actual function name (not "main") since we'll create a wrapper
mir_module.functions.insert(join_func_name(*func_id), mir_func);
}
// Create a wrapper "main" function that calls the entry function with args
let wrapper = create_entry_wrapper(entry_func, args)?;
mir_module.functions.insert("main".to_string(), wrapper);
Ok(mir_module)
}
/// Create a wrapper "main" function that calls the entry function with constant arguments
fn create_entry_wrapper(
entry_func: JoinFuncId,
args: &[JoinValue],
) -> Result<MirFunction, JoinIrVmBridgeError> {
let entry_block = BasicBlockId(0);
let signature = FunctionSignature {
name: "main".to_string(),
params: vec![], // No parameters - args are hardcoded
return_type: MirType::Unknown,
effects: EffectMask::PURE,
};
let mut mir_func = MirFunction::new(signature, entry_block);
// Generate instructions to create constant arguments and call entry function
let mut instructions = Vec::new();
// Create constant values for each argument
let mut arg_value_ids = Vec::new();
for (i, arg) in args.iter().enumerate() {
let dst = ValueId(50000 + i as u32); // Use high ValueIds to avoid conflicts
arg_value_ids.push(dst);
let const_value = join_value_to_mir_const(arg)?;
instructions.push(MirInstruction::Const { dst, value: const_value });
}
// Create function name constant
let func_name_id = ValueId(59990);
instructions.push(MirInstruction::Const {
dst: func_name_id,
value: MirConstValue::String(join_func_name(entry_func)),
});
// Create Call instruction
let result_id = ValueId(59991);
instructions.push(MirInstruction::Call {
dst: Some(result_id),
func: func_name_id,
callee: None,
args: arg_value_ids,
effects: EffectMask::PURE,
});
// Set up entry block
finalize_block(
&mut mir_func,
entry_block,
instructions,
MirInstruction::Return { value: Some(result_id) },
);
debug_log!(
"[joinir_vm_bridge] Created entry wrapper with {} args",
args.len()
);
Ok(mir_func)
}
/// JoinFunction → MirFunction 変換
fn convert_join_function_to_mir(
join_func: &crate::mir::join_ir::JoinFunction,
@ -170,6 +275,10 @@ fn convert_join_function_to_mir(
let mut mir_func = MirFunction::new(signature, entry_block);
// Phase 30.x: Set parameter ValueIds from JoinIR function
// JoinIR's VarId is an alias for ValueId, so direct copy works
mir_func.params = join_func.params.clone();
// Phase 27-shortterm S-4.4: Multi-block conversion for Jump instructions
// Strategy:
// - Accumulate Compute instructions in current block
@ -191,55 +300,66 @@ fn convert_join_function_to_mir(
dst,
k_next,
} => {
// Phase 27-shortterm S-4.4-A: skip_ws pattern only (tail calls)
// Validate: dst=None, k_next=None (tail call)
if dst.is_some() || k_next.is_some() {
// Phase 30.x: Support both tail calls and non-tail calls
// - dst=None, k_next=None: Tail call → call + return
// - dst=Some(id), k_next=None: Non-tail call → call + store + continue
// - k_next=Some: Not yet supported
if k_next.is_some() {
return Err(JoinIrVmBridgeError::new(format!(
"Non-tail Call not supported (dst={:?}, k_next={:?}). Only skip_ws tail call pattern is supported.",
dst, k_next
"Call with k_next is not yet supported (k_next={:?})",
k_next
)));
}
// Convert JoinFuncId to function name
// For skip_ws: func=0 → "main", func=1 → "join_func_1"
let func_name = if func.0 == 0 {
"main".to_string()
} else {
format!("join_func_{}", func.0)
};
let func_name = join_func_name(*func);
eprintln!(
"[joinir_vm_bridge] Converting Call to function '{}' with {} args",
func_name,
args.len()
);
// Create a temporary ValueId to hold the function name (string constant)
// This is a workaround for MIR's string-based Call resolution
// In Phase 27-shortterm, we use a high ValueId to avoid conflicts
let func_name_id = ValueId(9999);
// Create temporary ValueId for function name
let func_name_id = ValueId(99990 + next_block_id);
next_block_id += 1;
// Add Const instruction for function name
current_instructions.push(MirInstruction::Const {
dst: func_name_id,
value: MirConstValue::String(func_name.clone()),
value: MirConstValue::String(func_name),
});
// Create Call instruction (legacy string-based)
// Phase 27-shortterm: No Callee yet, use func field with string ValueId
match dst {
Some(result_dst) => {
// Non-tail call: store result in dst and continue
current_instructions.push(MirInstruction::Call {
dst: None, // tail call, no return value captured
dst: Some(*result_dst),
func: func_name_id,
callee: None, // Phase 27-shortterm: no Callee resolution
callee: None,
args: args.clone(),
effects: EffectMask::PURE,
});
// Continue to next instruction (no block termination)
}
None => {
// Tail call: call + return result
let call_result_id = ValueId(99991);
current_instructions.push(MirInstruction::Call {
dst: Some(call_result_id),
func: func_name_id,
callee: None,
args: args.clone(),
effects: EffectMask::PURE,
});
// Return the result of the tail call
let terminator = MirInstruction::Return { value: Some(call_result_id) };
finalize_block(&mut mir_func, current_block_id, current_instructions, terminator);
current_instructions = Vec::new();
}
}
}
JoinInst::Jump { cont, args, cond } => {
// Phase 27-shortterm S-4.4-A: Jump with condition → Branch + Return
// Jump represents an exit continuation (k_exit) in skip_ws pattern
eprintln!(
debug_log!(
"[joinir_vm_bridge] Converting Jump to cont={:?}, args={:?}, cond={:?}",
cont, args, cond
);
@ -252,24 +372,20 @@ fn convert_join_function_to_mir(
let continue_block_id = BasicBlockId(next_block_id);
next_block_id += 1;
// Emit Branch in current block
current_instructions.push(MirInstruction::Branch {
// Phase 30.x: Branch terminator (separate from instructions)
let branch_terminator = MirInstruction::Branch {
condition: *cond_var,
then_bb: exit_block_id,
else_bb: continue_block_id,
});
};
// Finalize current block
if let Some(block) = mir_func.blocks.get_mut(&current_block_id) {
block.instructions = current_instructions;
}
// Finalize current block with Branch terminator
finalize_block(&mut mir_func, current_block_id, current_instructions, branch_terminator);
// Create exit block with Return
// Create exit block with Return terminator
let exit_value = args.first().copied();
let mut exit_block = crate::mir::BasicBlock::new(exit_block_id);
exit_block
.instructions
.push(MirInstruction::Return { value: exit_value });
exit_block.terminator = Some(MirInstruction::Return { value: exit_value });
mir_func.blocks.insert(exit_block_id, exit_block);
// Create continue block (will be populated by subsequent instructions)
@ -281,14 +397,12 @@ fn convert_join_function_to_mir(
current_instructions = Vec::new();
}
None => {
// Unconditional jump: direct Return
// Unconditional jump: direct Return terminator
let exit_value = args.first().copied();
current_instructions.push(MirInstruction::Return { value: exit_value });
let return_terminator = MirInstruction::Return { value: exit_value };
// Finalize current block
if let Some(block) = mir_func.blocks.get_mut(&current_block_id) {
block.instructions = current_instructions;
}
// Finalize current block with Return terminator
finalize_block(&mut mir_func, current_block_id, current_instructions, return_terminator);
// No continuation after unconditional return
current_instructions = Vec::new();
@ -296,12 +410,11 @@ fn convert_join_function_to_mir(
}
}
JoinInst::Ret { value } => {
current_instructions.push(MirInstruction::Return { value: *value });
// Phase 30.x: Return terminator (separate from instructions)
let return_terminator = MirInstruction::Return { value: *value };
// Finalize current block
if let Some(block) = mir_func.blocks.get_mut(&current_block_id) {
block.instructions = current_instructions;
}
// Finalize current block with Return terminator
finalize_block(&mut mir_func, current_block_id, current_instructions, return_terminator);
current_instructions = Vec::new();
}
@ -310,11 +423,34 @@ fn convert_join_function_to_mir(
// Finalize any remaining instructions in the last block
if !current_instructions.is_empty() {
debug_log!(
"[joinir_vm_bridge] Final block {:?} has {} remaining instructions",
current_block_id,
current_instructions.len()
);
if let Some(block) = mir_func.blocks.get_mut(&current_block_id) {
// Phase 30.x: VM requires instruction_spans to match instructions length
let inst_count = current_instructions.len();
block.instructions = current_instructions;
block.instruction_spans = vec![Span::unknown(); inst_count];
}
}
// Debug: print all blocks and their instruction counts + terminators
debug_log!(
"[joinir_vm_bridge] Function '{}' has {} blocks:",
mir_func.signature.name,
mir_func.blocks.len()
);
for (block_id, block) in &mir_func.blocks {
debug_log!(
" Block {:?}: {} instructions, terminator={:?}",
block_id,
block.instructions.len(),
block.terminator
);
}
Ok(mir_func)
}

View File

@ -2,6 +2,13 @@ use super::super::NyashRunner;
use nyash_rust::{ast::ASTNode, mir::MirCompiler, parser::NyashParser};
use std::{fs, process};
// Phase 30.x: JoinIR VM Bridge integration (experimental)
// Used only when NYASH_JOINIR_EXPERIMENT=1 AND NYASH_JOINIR_VM_BRIDGE=1
use crate::config::env::{joinir_experiment_enabled, joinir_vm_bridge_enabled};
use crate::mir::join_ir::{lower_funcscanner_trim_to_joinir, lower_skip_ws_to_joinir, JoinFuncId};
use crate::mir::join_ir_ops::JoinValue;
use crate::mir::join_ir_vm_bridge::run_joinir_via_vm;
impl NyashRunner {
/// Execute VM mode with full plugin initialization and AST prelude merge
pub(crate) fn execute_vm_mode(&self, filename: &str) {
@ -495,6 +502,106 @@ impl NyashRunner {
}
}
// Phase 30.x: JoinIR VM Bridge experimental path
// Activated when NYASH_JOINIR_EXPERIMENT=1 AND NYASH_JOINIR_VM_BRIDGE=1
// Currently only supports minimal_ssa_skip_ws.hako (Main.skip function)
let joinir_path_attempted = if joinir_experiment_enabled() && joinir_vm_bridge_enabled() {
// Check if this module contains Main.skip/1 (minimal_ssa_skip_ws target)
// Note: function names include arity suffix like "Main.skip/1"
let has_main_skip = module_vm.functions.contains_key("Main.skip/1");
let has_trim = module_vm.functions.contains_key("FuncScannerBox.trim/1");
if has_main_skip {
eprintln!("[joinir/vm_bridge] Attempting JoinIR path for Main.skip");
match lower_skip_ws_to_joinir(&module_vm) {
Some(join_module) => {
// Get input argument from NYASH_JOINIR_INPUT or use default
let input = std::env::var("NYASH_JOINIR_INPUT")
.unwrap_or_else(|_| " abc".to_string());
eprintln!("[joinir/vm_bridge] Input: {:?}", input);
match run_joinir_via_vm(
&join_module,
JoinFuncId::new(0),
&[JoinValue::Str(input)],
) {
Ok(result) => {
let exit_code = match &result {
JoinValue::Int(v) => *v as i32,
JoinValue::Bool(b) => if *b { 1 } else { 0 },
_ => 0,
};
eprintln!("[joinir/vm_bridge] ✅ JoinIR result: {:?}", result);
if !quiet_pipe {
println!("RC: {}", exit_code);
}
process::exit(exit_code);
}
Err(e) => {
eprintln!("[joinir/vm_bridge] ❌ JoinIR execution failed: {:?}", e);
eprintln!("[joinir/vm_bridge] Falling back to normal VM path");
false // Continue to normal VM execution
}
}
}
None => {
eprintln!("[joinir/vm_bridge] lower_skip_ws_to_joinir returned None");
eprintln!("[joinir/vm_bridge] Falling back to normal VM path");
false
}
}
} else if has_trim {
// Phase 30.x: FuncScannerBox.trim/1 JoinIR path
eprintln!("[joinir/vm_bridge] Attempting JoinIR path for FuncScannerBox.trim");
match lower_funcscanner_trim_to_joinir(&module_vm) {
Some(join_module) => {
// Get input argument from NYASH_JOINIR_INPUT or use default
let input = std::env::var("NYASH_JOINIR_INPUT")
.unwrap_or_else(|_| " abc ".to_string());
eprintln!("[joinir/vm_bridge] Input: {:?}", input);
match run_joinir_via_vm(
&join_module,
JoinFuncId::new(0),
&[JoinValue::Str(input)],
) {
Ok(result) => {
// trim returns a string, print it and exit with 0
eprintln!("[joinir/vm_bridge] ✅ JoinIR trim result: {:?}", result);
if !quiet_pipe {
match &result {
JoinValue::Str(s) => println!("{}", s),
_ => println!("{:?}", result),
}
}
process::exit(0);
}
Err(e) => {
eprintln!("[joinir/vm_bridge] ❌ JoinIR trim failed: {:?}", e);
eprintln!("[joinir/vm_bridge] Falling back to normal VM path");
false
}
}
}
None => {
eprintln!("[joinir/vm_bridge] lower_funcscanner_trim_to_joinir returned None");
eprintln!("[joinir/vm_bridge] Falling back to normal VM path");
false
}
}
} else {
false // No supported JoinIR target function
}
} else {
false
};
// Normal VM execution path (fallback or default)
if joinir_path_attempted {
// This branch is never reached because successful JoinIR path calls process::exit()
unreachable!("JoinIR path should have exited");
}
match vm.execute_module(&module_vm) {
Ok(ret) => {
use crate::box_trait::{BoolBox, IntegerBox};

View File

@ -0,0 +1,222 @@
// 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");
}

View File

@ -10,6 +10,7 @@ pub mod joinir_json_min; // Phase 30.x: JoinIR JSON シリアライズテスト
pub mod joinir_runner_min; // Phase 27.2: JoinIR 実行器 A/B 比較テスト
pub mod joinir_runner_standalone; // Phase 27-shortterm S-3.2: JoinIR Runner 単体テスト
pub mod joinir_vm_bridge_skip_ws; // Phase 27-shortterm S-4.4: JoinIR → Rust VM Bridge A/B Test
pub mod joinir_vm_bridge_trim; // Phase 30.x: JoinIR → Rust VM Bridge A/B Test for trim
pub mod json_lint_stringutils_min_vm; // Phase 21.7++: using StringUtils alias resolution fix
pub mod mir_breakfinder_ssa;
pub mod mir_funcscanner_parse_params_trim_min;