From bff223eff99b5afaa185cfc899baadc40ac2d274 Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Mon, 24 Nov 2025 07:24:42 +0900 Subject: [PATCH] =?UTF-8?q?feat(joinir):=20Phase=2027-shortterm=20S-4=20?= =?UTF-8?q?=E5=AE=8C=E4=BA=86=20-=20JoinIR=20=E2=86=92=20Rust=20VM=20?= =?UTF-8?q?=E3=83=96=E3=83=AA=E3=83=83=E3=82=B8=E5=8F=82=E8=80=83=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 実装内容 ### S-4.1: VM インターフェース調査 - VMValue 定義調査(Integer/Float/Bool/String/Future/Void/BoxRef) - execute_module() API 調査 ### S-4.2: JoinValue ↔ VMValue 変換関数実装 - src/mir/join_ir_ops.rs に変換関数追加(+121行) - to_vm_value()/from_vm_value() 実装 - Float/Future/BoxRef は非対応エラー ### S-4.3: join_ir_vm_bridge.rs 基本構造実装 - src/mir/join_ir_vm_bridge.rs 新規作成(362行) - run_joinir_via_vm() API 実装 - convert_join_function_to_mir() 実装 - 7つのコンパイルエラー修正完了 ### S-4.4-A: Call/Jump 命令実装(skip_ws パターンのみ) - Call: 末尾呼び出しのみサポート - Jump: Multi-block CFG 生成(Branch + Return) - 非対応パターンは unimplemented!() で落とす ### S-4.4-B: A/B テスト作成 - src/tests/joinir_vm_bridge_skip_ws.rs 作成(104行) - Route A: 直接 VM 実行 - Route C: JoinIR → VM bridge 経由 - skip_ws(" abc") → 3 の A/B 比較実装 ## 戦略転換 ChatGPT 先生 + Task 先生の分析により、**Approach 2 (JoinIR Runner 本線化 + ガードレール)** への移行が決定。 本ブリッジ実装は参考実装として保持し、これ以上は太らせない方針。次フェーズで JoinIR Runner を method_router 経由で Rust VM と統合する。 --- src/mir/join_ir_vm_bridge.rs | 148 +++++++++++++++++++++----- src/tests/joinir_vm_bridge_skip_ws.rs | 105 ++++++++++++++++++ src/tests/mod.rs | 1 + 3 files changed, 229 insertions(+), 25 deletions(-) create mode 100644 src/tests/joinir_vm_bridge_skip_ws.rs diff --git a/src/mir/join_ir_vm_bridge.rs b/src/mir/join_ir_vm_bridge.rs index fcfba1c3..56163876 100644 --- a/src/mir/join_ir_vm_bridge.rs +++ b/src/mir/join_ir_vm_bridge.rs @@ -151,8 +151,9 @@ fn convert_joinir_to_mir( fn convert_join_function_to_mir( join_func: &crate::mir::join_ir::JoinFunction, ) -> Result { - // Phase 27-shortterm S-4.3 最小実装: - // 1つの basic block に全命令を配置 + // Phase 27-shortterm S-4.4: skip_ws パターン対応版 + // - Call (tail call): MIR Call に変換 + // - Jump (conditional exit): Branch + Return に変換 let entry_block = BasicBlockId(0); // Create minimal FunctionSignature for JoinIR function @@ -170,44 +171,141 @@ fn convert_join_function_to_mir( let mut mir_func = MirFunction::new(signature, entry_block); - let mut instructions = Vec::new(); + // Phase 27-shortterm S-4.4: Multi-block conversion for Jump instructions + // Strategy: + // - Accumulate Compute instructions in current block + // - On Jump: emit Branch + create exit block with Return + // - On Call: emit Call in current block + let mut current_block_id = entry_block; + let mut current_instructions = Vec::new(); + let mut next_block_id = 1u32; // for creating new blocks - // JoinInst → MirInstruction 変換 for join_inst in &join_func.body { match join_inst { JoinInst::Compute(mir_like) => { let mir_inst = convert_mir_like_inst(mir_like)?; - instructions.push(mir_inst); + current_instructions.push(mir_inst); } - JoinInst::Call { func, args, dst, .. } => { - // TODO: S-4.4 で関数呼び出し実装 - eprintln!( - "[joinir_vm_bridge] WARNING: Call instruction not yet implemented (func={:?}, args={:?}, dst={:?})", - func, args, dst - ); + JoinInst::Call { func, args, 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() { + return Err(JoinIrVmBridgeError::new(format!( + "Non-tail Call not supported (dst={:?}, k_next={:?}). Only skip_ws tail call pattern is supported.", + dst, 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) + }; + + 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); + + // Add Const instruction for function name + current_instructions.push(MirInstruction::Const { + dst: func_name_id, + value: MirConstValue::String(func_name.clone()), + }); + + // Create Call instruction (legacy string-based) + // Phase 27-shortterm: No Callee yet, use func field with string ValueId + current_instructions.push(MirInstruction::Call { + dst: None, // tail call, no return value captured + func: func_name_id, + callee: None, // Phase 27-shortterm: no Callee resolution + args: args.clone(), + effects: EffectMask::PURE, + }); } JoinInst::Jump { cont, args, cond } => { - // TODO: S-4.4 で継続実装 - eprintln!( - "[joinir_vm_bridge] WARNING: Jump instruction not yet implemented (cont={:?}, args={:?}, cond={:?})", - 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!("[joinir_vm_bridge] Converting Jump to cont={:?}, args={:?}, cond={:?}", cont, args, cond); + + match cond { + Some(cond_var) => { + // Conditional jump: Branch to exit block + let exit_block_id = BasicBlockId(next_block_id); + next_block_id += 1; + let continue_block_id = BasicBlockId(next_block_id); + next_block_id += 1; + + // Emit Branch in current block + current_instructions.push(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(¤t_block_id) { + block.instructions = current_instructions; + } + + // Create exit block with Return + 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, + }); + mir_func.blocks.insert(exit_block_id, exit_block); + + // Create continue block (will be populated by subsequent instructions) + let continue_block = crate::mir::BasicBlock::new(continue_block_id); + mir_func.blocks.insert(continue_block_id, continue_block); + + // Continue in the next block + current_block_id = continue_block_id; + current_instructions = Vec::new(); + } + None => { + // Unconditional jump: direct Return + let exit_value = args.first().copied(); + current_instructions.push(MirInstruction::Return { + value: exit_value, + }); + + // Finalize current block + if let Some(block) = mir_func.blocks.get_mut(¤t_block_id) { + block.instructions = current_instructions; + } + + // No continuation after unconditional return + current_instructions = Vec::new(); + } + } } JoinInst::Ret { value } => { - if let Some(val_id) = value { - instructions.push(MirInstruction::Return { - value: Some(*val_id), - }); - } else { - instructions.push(MirInstruction::Return { value: None }); + current_instructions.push(MirInstruction::Return { + value: *value, + }); + + // Finalize current block + if let Some(block) = mir_func.blocks.get_mut(¤t_block_id) { + block.instructions = current_instructions; } + + current_instructions = Vec::new(); } } } - // Entry block に命令を登録 - if let Some(block) = mir_func.blocks.get_mut(&entry_block) { - block.instructions = instructions; + // Finalize any remaining instructions in the last block + if !current_instructions.is_empty() { + if let Some(block) = mir_func.blocks.get_mut(¤t_block_id) { + block.instructions = current_instructions; + } } Ok(mir_func) diff --git a/src/tests/joinir_vm_bridge_skip_ws.rs b/src/tests/joinir_vm_bridge_skip_ws.rs new file mode 100644 index 00000000..8a69b028 --- /dev/null +++ b/src/tests/joinir_vm_bridge_skip_ws.rs @@ -0,0 +1,105 @@ +// Phase 27-shortterm S-4.4: JoinIR → Rust VM Bridge A/B Test +// +// 目的: +// - JoinIR を VM ブリッジ経由で実行し、直接 VM 実行の結果と一致することを確認する +// - Route A (AST→MIR→VM) と Route C (AST→MIR→JoinIR→MIR'→VM) の比較 +// +// Test Pattern: +// - skip_ws(" abc") → 3 (leading spaces count) +// +// Implementation Status: +// - S-4.3: Basic bridge structure ✅ +// - S-4.4-A: Call/Jump instructions for skip_ws pattern ✅ +// - S-4.4-B: A/B test (this file) ✅ + +use crate::ast::ASTNode; +use crate::backend::VM; +use crate::mir::join_ir::{lower_skip_ws_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 +} + +#[test] +#[ignore] +fn joinir_vm_bridge_skip_ws_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 src = std::fs::read_to_string("apps/tests/minimal_ssa_skip_ws.hako") + .expect("failed to read minimal_ssa_skip_ws.hako"); + let runner = r#" +static box Runner { + main(args) { + return Main.skip(" abc") + } +} +"#; + let full_src = format!("{src}\n{runner}"); + + let ast: ASTNode = + NyashParser::parse_from_string(&full_src).expect("skip_ws: parse failed"); + let mut mc = MirCompiler::with_options(false); + let compiled = mc.compile(ast).expect("skip_ws: MIR compile failed"); + + // Route A: AST → MIR → VM (direct) + eprintln!("[joinir_vm_bridge_test] 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("skip_ws: VM execution failed"); + let vm_result = vm_out.to_string_box().value; + std::env::remove_var("NYASH_ENTRY"); + + eprintln!("[joinir_vm_bridge_test] Route A result: {}", vm_result); + + // Route C: AST → MIR → JoinIR → MIR' → VM (via bridge) + eprintln!("[joinir_vm_bridge_test] Route C: JoinIR → VM bridge execution"); + let join_module = + lower_skip_ws_to_joinir(&compiled.module).expect("lower_skip_ws_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 skip_ws"); + + eprintln!("[joinir_vm_bridge_test] Route C result: {:?}", bridge_result); + + // Assertions: Both routes should produce the same result + assert_eq!(vm_result, "3", "Route A (VM) expected to skip 3 leading spaces"); + match bridge_result { + JoinValue::Int(v) => { + assert_eq!(v, 3, "Route C (JoinIR→VM bridge) skip_ws result mismatch"); + eprintln!("[joinir_vm_bridge_test] ✅ A/B test passed: both routes returned 3"); + } + other => panic!("JoinIR VM bridge returned non-int 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"); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index fd799c8b..79a69452 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -18,6 +18,7 @@ pub mod mir_joinir_stage1_using_resolver_min; // Phase 27.12: Stage1UsingResolve pub mod mir_joinir_funcscanner_append_defs; // Phase 27.14: FuncScannerBox._append_defs JoinIR変換 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 mir_locals_ssa; pub mod mir_loopform_conditional_reassign; pub mod mir_loopform_exit_phi;