From 0852a397d91fca3e84063b9a496f5fa9071bb7fe Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Sun, 23 Nov 2025 08:38:15 +0900 Subject: [PATCH] Add experimental JoinIR runner and tests --- docs/reference/environment-variables.md | 4 + src/mir/join_ir.rs | 402 ++++++++++++++++++------ src/mir/join_ir_runner.rs | 327 +++++++++++++++++++ src/mir/mod.rs | 2 + src/tests/joinir_runner_min.rs | 148 +++++++++ src/tests/mir_joinir_min.rs | 2 + src/tests/mod.rs | 1 + 7 files changed, 798 insertions(+), 88 deletions(-) create mode 100644 src/mir/join_ir_runner.rs create mode 100644 src/tests/joinir_runner_min.rs diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md index acca3f8b..15d88f78 100644 --- a/docs/reference/environment-variables.md +++ b/docs/reference/environment-variables.md @@ -106,6 +106,7 @@ NYASH_PARSER_STAGE3=1 NYASH_ENABLE_USING=1 \ | `NYASH_STAGE1_SCAN_GE=1` | OFF | Compare Ge命令スキャン | Phase 25.x | | `NYASH_TO_I64_DEBUG=1` | OFF | to_i64変換デバッグ | Phase 25.x | | `NYASH_FUNCSCANNER_DEBUG=1` | OFF | FuncScanner詳細ログ | Phase 25.3 | +| `NYASH_JOINIR_EXPERIMENT=1` | OFF | JoinIR実験モード(MIR→JoinIR変換テストを有効化) | Phase 26-H/27 | ### 使用例 @@ -120,6 +121,9 @@ NYASH_VM_DUMP_MIR=1 ./target/release/hakorune program.hako # Stage-1 CLI + MIRダンプ NYASH_STAGE1_MIR_DUMP=1 cargo test mir_stage1_cli_emit_program_min + +# JoinIR実験(テスト限定) +NYASH_JOINIR_EXPERIMENT=1 cargo test --release mir_joinir_funcscanner_trim_auto_lowering -- --ignored ``` --- diff --git a/src/mir/join_ir.rs b/src/mir/join_ir.rs index 4391dbcb..31997398 100644 --- a/src/mir/join_ir.rs +++ b/src/mir/join_ir.rs @@ -82,12 +82,16 @@ pub enum JoinInst { func: JoinFuncId, args: Vec, k_next: Option, + /// 呼び出し結果を書き込む変数(None の場合は末尾呼び出しとして扱う) + dst: Option, }, /// 継続呼び出し(join / exit 継続など) Jump { cont: JoinContId, args: Vec, + /// None のときは無条件ジャンプ、Some(var) のときは var が truthy のときだけ実行 + cond: Option, }, /// ルート関数 or 上位への戻り @@ -259,6 +263,7 @@ pub fn lower_min_loop_to_joinir(module: &crate::mir::MirModule) -> Option Option Option= n { k_exit(i) } else { ... nested if ... } + // loop_step 関数: if i >= n { return i } else if ch == " " { loop_step(i + 1) } else { return i } let s_loop = ValueId(4000); let i_loop = ValueId(4001); let n_loop = ValueId(4002); @@ -423,9 +431,9 @@ pub fn lower_skip_ws_to_joinir(module: &crate::mir::MirModule) -> Option= n) loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Compare { @@ -435,41 +443,33 @@ pub fn lower_skip_ws_to_joinir(module: &crate::mir::MirModule) -> Option= n { return i } + loop_step_func.body.push(JoinInst::Jump { + cont: JoinContId::new(0), + args: vec![i_loop], + cond: Some(cmp1_result), }); - // ch = s.substring(i, i + 1) - // i_start = i - loop_step_func.body.push(JoinInst::Compute(MirLikeInst::BinOp { - dst: i_start, - op: BinOpKind::Add, - lhs: i_loop, - rhs: ValueId(4011), // const 0 - })); - // const 1 loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Const { dst: const_1, value: ConstValue::Integer(1), })); - // i_end = i + 1 + // i_plus_1 = i + 1 (再利用: substring end / continue path) loop_step_func.body.push(JoinInst::Compute(MirLikeInst::BinOp { - dst: i_end, + dst: i_plus_1, op: BinOpKind::Add, lhs: i_loop, rhs: const_1, })); - // ch = s.substring(i_start, i_end) + // ch = s.substring(i, i + 1) loop_step_func.body.push(JoinInst::Compute(MirLikeInst::BoxCall { dst: Some(ch), box_name: "StringBox".to_string(), method: "substring".to_string(), - args: vec![s_loop, i_start, i_end], + args: vec![s_loop, i_loop, i_plus_1], })); // const " " (space) @@ -486,23 +486,33 @@ pub fn lower_skip_ws_to_joinir(module: &crate::mir::MirModule) -> Option Optio let mut join_module = JoinModule::new(); - // trim_main 関数: 前処理 + loop_step 呼び出し + // trim_main 関数: 前処理 + 先頭/末尾の空白を除去 let trim_main_id = JoinFuncId::new(0); let s_param = ValueId(5000); let mut trim_main_func = JoinFunction::new(trim_main_id, "trim_main".to_string(), vec![s_param]); - // 変数定義(固定 ValueId 割り当て) let str_val = ValueId(5001); let n_val = ValueId(5002); let b_val = ValueId(5003); let e_init = ValueId(5004); + let const_empty = ValueId(5005); + let const_zero = ValueId(5006); // str = "" + s_param (文字列化) + trim_main_func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: const_empty, + value: ConstValue::String("".to_string()), + })); trim_main_func.body.push(JoinInst::Compute(MirLikeInst::BinOp { dst: str_val, - lhs: ValueId(5005), // empty string const + lhs: const_empty, rhs: s_param, op: BinOpKind::Add, })); - // 空文字列定数 - trim_main_func.body.insert(trim_main_func.body.len() - 1, JoinInst::Compute(MirLikeInst::Const { - dst: ValueId(5005), - value: ConstValue::String("".to_string()), - })); - // n = str.length() trim_main_func.body.push(JoinInst::Compute(MirLikeInst::BoxCall { dst: Some(n_val), @@ -599,29 +608,42 @@ pub fn lower_funcscanner_trim_to_joinir(module: &crate::mir::MirModule) -> Optio args: vec![str_val], })); - // b = skip_whitespace(str, 0) - 簡略化のため const 0 で代用 + // const 0 trim_main_func.body.push(JoinInst::Compute(MirLikeInst::Const { - dst: b_val, + dst: const_zero, value: ConstValue::Integer(0), })); - // e_init = n - trim_main_func.body.push(JoinInst::Compute(MirLikeInst::Const { + // b = skip_leading_whitespace(str, 0, n) + let skip_leading_id = JoinFuncId::new(2); + trim_main_func.body.push(JoinInst::Call { + func: skip_leading_id, + args: vec![str_val, const_zero, n_val], + k_next: None, + dst: Some(b_val), + }); + + // e_init = n (コピー) + trim_main_func.body.push(JoinInst::Compute(MirLikeInst::BinOp { dst: e_init, - value: ConstValue::Integer(0), // placeholder - 実際は n のコピー + op: BinOpKind::Add, + lhs: n_val, + rhs: const_zero, })); - // loop_step(str, b, e_init, k_exit) + // loop_step(str, b, e_init) -> 戻り値をそのまま返す let loop_step_id = JoinFuncId::new(1); trim_main_func.body.push(JoinInst::Call { func: loop_step_id, args: vec![str_val, b_val, e_init], k_next: None, + dst: None, }); + join_module.entry = Some(trim_main_id); join_module.add_function(trim_main_func); - // loop_step 関数: ループボディ + // loop_step 関数: 末尾の空白を削り、最終的に substring(b, e) を返す let str_loop = ValueId(6000); let b_loop = ValueId(6001); let e_loop = ValueId(6002); @@ -640,21 +662,55 @@ pub fn lower_funcscanner_trim_to_joinir(module: &crate::mir::MirModule) -> Optio op: CompareOp::Gt, })); - // ch = str.substring(e - 1, e) - let e_minus_1 = ValueId(6004); - loop_step_func.body.push(JoinInst::Compute(MirLikeInst::BinOp { - dst: e_minus_1, - lhs: e_loop, - rhs: ValueId(6005), // const 1 - op: BinOpKind::Sub, + // bool false (共通) + let bool_false = ValueId(6019); + loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: bool_false, + value: ConstValue::Bool(false), })); + // trimmed_base = str.substring(b, e) + let trimmed_base = ValueId(6004); + loop_step_func.body.push(JoinInst::Compute(MirLikeInst::BoxCall { + dst: Some(trimmed_base), + box_name: "StringBox".to_string(), + method: "substring".to_string(), + args: vec![str_loop, b_loop, e_loop], + })); + + // cond_is_false = (cond == false) + let cond_is_false = ValueId(6020); + loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Compare { + dst: cond_is_false, + lhs: cond, + rhs: bool_false, + op: CompareOp::Eq, + })); + + // if !(e > b) { return substring(b, e) } + loop_step_func.body.push(JoinInst::Jump { + cont: JoinContId::new(0), + args: vec![trimmed_base], + cond: Some(cond_is_false), + }); + + // const 1 + let const_1 = ValueId(6005); loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Const { - dst: ValueId(6005), + dst: const_1, value: ConstValue::Integer(1), })); - let ch = ValueId(6006); + // e_minus_1 = e - 1 + let e_minus_1 = ValueId(6006); + loop_step_func.body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: e_minus_1, + lhs: e_loop, + rhs: const_1, + op: BinOpKind::Sub, + })); + + let ch = ValueId(6007); loop_step_func.body.push(JoinInst::Compute(MirLikeInst::BoxCall { dst: Some(ch), box_name: "StringBox".to_string(), @@ -662,61 +718,65 @@ pub fn lower_funcscanner_trim_to_joinir(module: &crate::mir::MirModule) -> Optio args: vec![str_loop, e_minus_1, e_loop], })); - // is_space = (ch == " " || ch == "\t" || ch == "\n" || ch == "\r") - // 4つの比較を OR でつなぐ - let cmp_space = ValueId(6007); - let cmp_tab = ValueId(6008); - let cmp_newline = ValueId(6009); - let cmp_cr = ValueId(6010); + // is_space = (ch == " " || ch == "\\t" || ch == "\\n" || ch == "\\r") + let cmp_space = ValueId(6008); + let cmp_tab = ValueId(6009); + let cmp_newline = ValueId(6010); + let cmp_cr = ValueId(6011); + + let const_space = ValueId(6012); + let const_tab = ValueId(6013); + let const_newline = ValueId(6014); + let const_cr = ValueId(6015); loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Const { - dst: ValueId(6011), + dst: const_space, value: ConstValue::String(" ".to_string()), })); loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Compare { dst: cmp_space, lhs: ch, - rhs: ValueId(6011), + rhs: const_space, op: CompareOp::Eq, })); loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Const { - dst: ValueId(6012), - value: ConstValue::String("\t".to_string()), + dst: const_tab, + value: ConstValue::String("\\t".to_string()), })); loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Compare { dst: cmp_tab, lhs: ch, - rhs: ValueId(6012), + rhs: const_tab, op: CompareOp::Eq, })); loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Const { - dst: ValueId(6013), - value: ConstValue::String("\n".to_string()), + dst: const_newline, + value: ConstValue::String("\\n".to_string()), })); loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Compare { dst: cmp_newline, lhs: ch, - rhs: ValueId(6013), + rhs: const_newline, op: CompareOp::Eq, })); loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Const { - dst: ValueId(6014), - value: ConstValue::String("\r".to_string()), + dst: const_cr, + value: ConstValue::String("\\r".to_string()), })); loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Compare { dst: cmp_cr, lhs: ch, - rhs: ValueId(6014), + rhs: const_cr, op: CompareOp::Eq, })); // OR chain: (cmp_space || cmp_tab) || cmp_newline || cmp_cr - let or1 = ValueId(6015); - let or2 = ValueId(6016); - let is_space = ValueId(6017); + let or1 = ValueId(6016); + let or2 = ValueId(6017); + let is_space = ValueId(6018); loop_step_func.body.push(JoinInst::Compute(MirLikeInst::BinOp { dst: or1, @@ -739,31 +799,197 @@ pub fn lower_funcscanner_trim_to_joinir(module: &crate::mir::MirModule) -> Optio op: BinOpKind::Or, })); - // if is_space { e_next = e - 1; loop_step(...) } else { k_exit(e) } - let e_next = ValueId(6018); + // is_space_false = (is_space == false) + let is_space_false = ValueId(6021); + loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Compare { + dst: is_space_false, + lhs: is_space, + rhs: bool_false, + op: CompareOp::Eq, + })); + + // if !is_space { return substring(b, e) } + loop_step_func.body.push(JoinInst::Jump { + cont: JoinContId::new(1), + args: vec![trimmed_base], + cond: Some(is_space_false), + }); + + // continue path: e_next = e - 1; loop_step(str, b, e_next) + let e_next = ValueId(6022); loop_step_func.body.push(JoinInst::Compute(MirLikeInst::BinOp { dst: e_next, lhs: e_loop, - rhs: ValueId(6005), // const 1 (already defined) + rhs: const_1, op: BinOpKind::Sub, })); - // Branch on is_space - // then: loop_step(str, b, e_next, k_exit) - // else: k_exit(e) - // 簡略化: 直接 Ret で終了(実際の分岐は MIR から推測が必要) loop_step_func.body.push(JoinInst::Call { func: loop_step_id, // 再帰呼び出し args: vec![str_loop, b_loop, e_next], k_next: None, - }); - - // Break path: k_exit(e) - loop_step_func.body.push(JoinInst::Ret { - value: Some(e_loop), + dst: None, }); join_module.add_function(loop_step_func); + + // skip_leading 関数: 先頭の空白をスキップして位置を返す + let mut skip_func = JoinFunction::new( + skip_leading_id, + "skip_leading".to_string(), + vec![ValueId(7000), ValueId(7001), ValueId(7002)], // (s, i, n) + ); + let s_skip = ValueId(7000); + let i_skip = ValueId(7001); + let n_skip = ValueId(7002); + let cmp_len = ValueId(7003); + let const_1_skip = ValueId(7004); + let i_plus_1_skip = ValueId(7005); + let ch_skip = ValueId(7006); + let cmp_space_skip = ValueId(7007); + let cmp_tab_skip = ValueId(7008); + let cmp_newline_skip = ValueId(7009); + let cmp_cr_skip = ValueId(7010); + let const_space_skip = ValueId(7011); + let const_tab_skip = ValueId(7012); + let const_newline_skip = ValueId(7013); + let const_cr_skip = ValueId(7014); + let or1_skip = ValueId(7015); + let or2_skip = ValueId(7016); + let is_space_skip = ValueId(7017); + let bool_false_skip = ValueId(7018); + let is_space_false_skip = ValueId(7019); + + // cmp_len = (i >= n) + skip_func.body.push(JoinInst::Compute(MirLikeInst::Compare { + dst: cmp_len, + lhs: i_skip, + rhs: n_skip, + op: CompareOp::Ge, + })); + + // if i >= n { return i } + skip_func.body.push(JoinInst::Jump { + cont: JoinContId::new(2), + args: vec![i_skip], + cond: Some(cmp_len), + }); + + // const 1 + skip_func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: const_1_skip, + value: ConstValue::Integer(1), + })); + + // i_plus_1 = i + 1 + skip_func.body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: i_plus_1_skip, + lhs: i_skip, + rhs: const_1_skip, + op: BinOpKind::Add, + })); + + // ch = s.substring(i, i + 1) + skip_func.body.push(JoinInst::Compute(MirLikeInst::BoxCall { + dst: Some(ch_skip), + box_name: "StringBox".to_string(), + method: "substring".to_string(), + args: vec![s_skip, i_skip, i_plus_1_skip], + })); + + // whitespace constants + comparisons + skip_func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: const_space_skip, + value: ConstValue::String(" ".to_string()), + })); + skip_func.body.push(JoinInst::Compute(MirLikeInst::Compare { + dst: cmp_space_skip, + lhs: ch_skip, + rhs: const_space_skip, + op: CompareOp::Eq, + })); + + skip_func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: const_tab_skip, + value: ConstValue::String("\\t".to_string()), + })); + skip_func.body.push(JoinInst::Compute(MirLikeInst::Compare { + dst: cmp_tab_skip, + lhs: ch_skip, + rhs: const_tab_skip, + op: CompareOp::Eq, + })); + + skip_func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: const_newline_skip, + value: ConstValue::String("\\n".to_string()), + })); + skip_func.body.push(JoinInst::Compute(MirLikeInst::Compare { + dst: cmp_newline_skip, + lhs: ch_skip, + rhs: const_newline_skip, + op: CompareOp::Eq, + })); + + skip_func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: const_cr_skip, + value: ConstValue::String("\\r".to_string()), + })); + skip_func.body.push(JoinInst::Compute(MirLikeInst::Compare { + dst: cmp_cr_skip, + lhs: ch_skip, + rhs: const_cr_skip, + op: CompareOp::Eq, + })); + + // is_space_skip = OR chain + skip_func.body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: or1_skip, + lhs: cmp_space_skip, + rhs: cmp_tab_skip, + op: BinOpKind::Or, + })); + skip_func.body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: or2_skip, + lhs: or1_skip, + rhs: cmp_newline_skip, + op: BinOpKind::Or, + })); + skip_func.body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: is_space_skip, + lhs: or2_skip, + rhs: cmp_cr_skip, + op: BinOpKind::Or, + })); + + // bool false + negation + skip_func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: bool_false_skip, + value: ConstValue::Bool(false), + })); + skip_func.body.push(JoinInst::Compute(MirLikeInst::Compare { + dst: is_space_false_skip, + lhs: is_space_skip, + rhs: bool_false_skip, + op: CompareOp::Eq, + })); + + // if not space -> return i + skip_func.body.push(JoinInst::Jump { + cont: JoinContId::new(3), + args: vec![i_skip], + cond: Some(is_space_false_skip), + }); + + // continue path: skip_leading(s, i + 1, n) + skip_func.body.push(JoinInst::Call { + func: skip_leading_id, + args: vec![s_skip, i_plus_1_skip, n_skip], + k_next: None, + dst: None, + }); + + join_module.add_function(skip_func); eprintln!("[joinir/trim] Generated {} JoinIR functions", join_module.functions.len()); Some(join_module) diff --git a/src/mir/join_ir_runner.rs b/src/mir/join_ir_runner.rs new file mode 100644 index 00000000..90fb6ca9 --- /dev/null +++ b/src/mir/join_ir_runner.rs @@ -0,0 +1,327 @@ +//! JoinIR 実験用のミニ実行器(Phase 27.2) +//! +//! 目的: hand-written / minimal JoinIR を VM と A/B 比較するための軽量ランナー。 +//! - 対応値: i64 / bool / String / Unit +//! - 対応命令: Const / BinOp / Compare / BoxCall(StringBox: length, substring) / +//! Call / Jump / Ret + +use std::collections::HashMap; + +use crate::mir::join_ir::{ + BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinInst, JoinModule, MirLikeInst, VarId, +}; + +#[derive(Debug, Clone, PartialEq)] +pub enum JoinValue { + Int(i64), + Bool(bool), + Str(String), + Unit, +} + +#[derive(Debug, Clone)] +pub struct JoinRuntimeError { + pub message: String, +} + +impl JoinRuntimeError { + fn new(msg: impl Into) -> Self { + Self { + message: msg.into(), + } + } +} + +impl std::fmt::Display for JoinRuntimeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for JoinRuntimeError {} + +pub fn run_joinir_function( + module: &JoinModule, + entry: JoinFuncId, + args: &[JoinValue], +) -> Result { + execute_function(module, entry, args.to_vec()) +} + +fn execute_function( + module: &JoinModule, + mut current_func: JoinFuncId, + mut current_args: Vec, +) -> Result { + 'exec: loop { + let func = module + .functions + .get(¤t_func) + .ok_or_else(|| JoinRuntimeError::new(format!("Function {:?} not found", current_func)))?; + + if func.params.len() != current_args.len() { + return Err(JoinRuntimeError::new(format!( + "Arity mismatch for {:?}: expected {}, got {}", + func.id, + func.params.len(), + current_args.len() + ))); + } + + let mut locals: HashMap = HashMap::new(); + for (param, arg) in func.params.iter().zip(current_args.iter()) { + locals.insert(*param, arg.clone()); + } + + let mut ip = 0usize; + while ip < func.body.len() { + match &func.body[ip] { + JoinInst::Compute(inst) => { + eval_compute(inst, &mut locals)?; + ip += 1; + } + JoinInst::Call { + func: target, + args, + k_next, + dst, + } => { + if k_next.is_some() { + return Err(JoinRuntimeError::new( + "Join continuation (k_next) is not supported in the experimental runner", + )); + } + let resolved_args = materialize_args(args, &locals)?; + if let Some(dst_var) = dst { + let value = execute_function(module, *target, resolved_args)?; + locals.insert(*dst_var, value); + ip += 1; + } else { + current_func = *target; + current_args = resolved_args; + continue 'exec; + } + } + JoinInst::Jump { cont: _, args, cond } => { + let should_jump = match cond { + Some(var) => as_bool(&read_var(&locals, *var)?)?, + None => true, + }; + if should_jump { + let ret = if let Some(first) = args.first() { + read_var(&locals, *first)? + } else { + JoinValue::Unit + }; + return Ok(ret); + } + ip += 1; + } + JoinInst::Ret { value } => { + let ret = match value { + Some(var) => read_var(&locals, *var)?, + None => JoinValue::Unit, + }; + return Ok(ret); + } + } + } + + // fallthrough without explicit return + return Ok(JoinValue::Unit); + } +} + +fn eval_compute(inst: &MirLikeInst, locals: &mut HashMap) -> Result<(), JoinRuntimeError> { + match inst { + MirLikeInst::Const { dst, value } => { + let v = match value { + ConstValue::Integer(i) => JoinValue::Int(*i), + ConstValue::Bool(b) => JoinValue::Bool(*b), + ConstValue::String(s) => JoinValue::Str(s.clone()), + ConstValue::Null => JoinValue::Unit, + }; + locals.insert(*dst, v); + } + MirLikeInst::BinOp { dst, op, lhs, rhs } => { + let l = read_var(locals, *lhs)?; + let r = read_var(locals, *rhs)?; + let v = match op { + BinOpKind::Add => match (l, r) { + (JoinValue::Int(a), JoinValue::Int(b)) => JoinValue::Int(a + b), + (JoinValue::Str(a), JoinValue::Str(b)) => JoinValue::Str(format!("{a}{b}")), + _ => { + return Err(JoinRuntimeError::new( + "Add supported only for (int,int) or (str,str)", + )) + } + }, + BinOpKind::Sub => match (l, r) { + (JoinValue::Int(a), JoinValue::Int(b)) => JoinValue::Int(a - b), + _ => return Err(JoinRuntimeError::new("Sub supported only for integers")), + }, + BinOpKind::Mul => match (l, r) { + (JoinValue::Int(a), JoinValue::Int(b)) => JoinValue::Int(a * b), + _ => return Err(JoinRuntimeError::new("Mul supported only for integers")), + }, + BinOpKind::Div => match (l, r) { + (JoinValue::Int(_), JoinValue::Int(0)) => { + return Err(JoinRuntimeError::new("Division by zero")) + } + (JoinValue::Int(a), JoinValue::Int(b)) => JoinValue::Int(a / b), + _ => return Err(JoinRuntimeError::new("Div supported only for integers")), + }, + BinOpKind::Or => match (l, r) { + (JoinValue::Bool(a), JoinValue::Bool(b)) => JoinValue::Bool(a || b), + _ => return Err(JoinRuntimeError::new("Or supported only for bools")), + }, + BinOpKind::And => match (l, r) { + (JoinValue::Bool(a), JoinValue::Bool(b)) => JoinValue::Bool(a && b), + _ => return Err(JoinRuntimeError::new("And supported only for bools")), + }, + }; + locals.insert(*dst, v); + } + MirLikeInst::Compare { dst, op, lhs, rhs } => { + let l = read_var(locals, *lhs)?; + let r = read_var(locals, *rhs)?; + let v = match (l, r) { + (JoinValue::Int(a), JoinValue::Int(b)) => match op { + CompareOp::Lt => a < b, + CompareOp::Le => a <= b, + CompareOp::Gt => a > b, + CompareOp::Ge => a >= b, + CompareOp::Eq => a == b, + CompareOp::Ne => a != b, + }, + (JoinValue::Bool(a), JoinValue::Bool(b)) => match op { + CompareOp::Eq => a == b, + CompareOp::Ne => a != b, + _ => { + return Err(JoinRuntimeError::new( + "Bool comparison only supports Eq/Ne in the JoinIR runner", + )) + } + }, + (JoinValue::Str(a), JoinValue::Str(b)) => match op { + CompareOp::Eq => a == b, + CompareOp::Ne => a != b, + _ => { + return Err(JoinRuntimeError::new( + "String comparison only supports Eq/Ne in the JoinIR runner", + )) + } + }, + _ => { + return Err(JoinRuntimeError::new( + "Type mismatch in Compare (expected homogeneous operands)", + )) + } + }; + locals.insert(*dst, JoinValue::Bool(v)); + } + MirLikeInst::BoxCall { + dst, + box_name, + method, + args, + } => { + if box_name != "StringBox" { + return Err(JoinRuntimeError::new(format!( + "Unsupported box call target: {}", + box_name + ))); + } + match method.as_str() { + "length" => { + let arg = expect_str(&read_var(locals, args[0])?)?; + locals.insert(*dst.as_ref().ok_or_else(|| { + JoinRuntimeError::new("length call requires destination") + })?, JoinValue::Int(arg.len() as i64)); + } + "substring" => { + if args.len() != 3 { + return Err(JoinRuntimeError::new( + "substring expects 3 arguments (s, start, end)", + )); + } + let s = expect_str(&read_var(locals, args[0])?)?; + let start = expect_int(&read_var(locals, args[1])?)?; + let end = expect_int(&read_var(locals, args[2])?)?; + let slice = safe_substring(&s, start, end)?; + let dst_var = dst.ok_or_else(|| { + JoinRuntimeError::new("substring call requires destination") + })?; + locals.insert(dst_var, JoinValue::Str(slice)); + } + _ => { + return Err(JoinRuntimeError::new(format!( + "Unsupported StringBox method: {}", + method + ))) + } + } + } + } + Ok(()) +} + +fn safe_substring(s: &str, start: i64, end: i64) -> Result { + if start < 0 || end < 0 { + return Err(JoinRuntimeError::new("substring indices must be non-negative")); + } + let (start_usize, end_usize) = (start as usize, end as usize); + if start_usize > end_usize { + return Err(JoinRuntimeError::new("substring start > end")); + } + if start_usize > s.len() || end_usize > s.len() { + return Err(JoinRuntimeError::new("substring indices out of bounds")); + } + Ok(s[start_usize..end_usize].to_string()) +} + +fn read_var(locals: &HashMap, var: VarId) -> Result { + locals + .get(&var) + .cloned() + .ok_or_else(|| JoinRuntimeError::new(format!("Variable {:?} not bound", var))) +} + +fn materialize_args( + args: &[VarId], + locals: &HashMap, +) -> Result, JoinRuntimeError> { + args.iter().map(|v| read_var(locals, *v)).collect() +} + +fn as_bool(value: &JoinValue) -> Result { + match value { + JoinValue::Bool(b) => Ok(*b), + JoinValue::Int(i) => Ok(*i != 0), + JoinValue::Unit => Ok(false), + other => Err(JoinRuntimeError::new(format!( + "Expected bool-compatible value, got {:?}", + other + ))), + } +} + +fn expect_int(value: &JoinValue) -> Result { + match value { + JoinValue::Int(i) => Ok(*i), + other => Err(JoinRuntimeError::new(format!( + "Expected int, got {:?}", + other + ))), + } +} + +fn expect_str(value: &JoinValue) -> Result { + match value { + JoinValue::Str(s) => Ok(s.clone()), + other => Err(JoinRuntimeError::new(format!( + "Expected string, got {:?}", + other + ))), + } +} diff --git a/src/mir/mod.rs b/src/mir/mod.rs index 2ebd1f49..1cae5f5f 100644 --- a/src/mir/mod.rs +++ b/src/mir/mod.rs @@ -38,6 +38,7 @@ pub mod value_id; pub mod value_kind; // Phase 26-A: ValueId型安全化 pub mod query; // Phase 26-G: MIR read/write/CFGビュー (MirQuery) pub mod join_ir; // Phase 26-H: 関数正規化IR(JoinIR) +pub mod join_ir_runner; // Phase 27.2: JoinIR 実行器(実験用) pub mod verification; pub mod verification_types; // extracted error types // Optimization subpasses (e.g., type_hints) // Phase 25.1f: Loop/If 共通ビュー(ControlForm) @@ -59,6 +60,7 @@ pub use value_id::{LocalId, ValueId, ValueIdGenerator}; pub use value_kind::{MirValueKind, TypedValueId}; // Phase 26-A: ValueId型安全化 pub use verification::MirVerifier; pub use verification_types::VerificationError; +pub use join_ir_runner::{run_joinir_function, JoinRuntimeError, JoinValue}; // Phase 15 control flow utilities (段階的根治戦略) pub use utils::{ capture_actual_predecessor_and_jump, collect_phi_incoming_if_reachable, diff --git a/src/tests/joinir_runner_min.rs b/src/tests/joinir_runner_min.rs new file mode 100644 index 00000000..e1ae4782 --- /dev/null +++ b/src/tests/joinir_runner_min.rs @@ -0,0 +1,148 @@ +// JoinIR 実験ランナーの A/B 比較テスト(skip_ws / trim_min) +// +// 目的: +// - JoinIR を実際に実行し、既存 VM の結果と一致することを確認する +// - Phase 27.2 のブリッジ実装を env トグル付きで検証する + +use crate::ast::ASTNode; +use crate::backend::VM; +use crate::mir::join_ir::{lower_funcscanner_trim_to_joinir, lower_skip_ws_to_joinir, JoinFuncId}; +use crate::mir::join_ir_runner::{run_joinir_function, JoinValue}; +use crate::mir::MirCompiler; +use crate::parser::NyashParser; + +fn require_experiment_toggle() -> bool { + if std::env::var("NYASH_JOINIR_EXPERIMENT") + .ok() + .as_deref() + != Some("1") + { + eprintln!( + "[joinir/runner] NYASH_JOINIR_EXPERIMENT=1 not set, skipping experimental runner test" + ); + return false; + } + true +} + +#[test] +#[ignore] +fn joinir_runner_minimal_skip_ws_executes() { + 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"); + // 無限ループ検出のため、実験テストではステップ上限を小さめに設定しておく。 + // 0 は「上限なし」なので、ここでは明示的な上限を使う。 + 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"); + + 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"); + + let join_module = + lower_skip_ws_to_joinir(&compiled.module).expect("lower_skip_ws_to_joinir failed"); + let join_result = run_joinir_function( + &join_module, + JoinFuncId::new(0), + &[JoinValue::Str(" abc".to_string())], + ) + .expect("JoinIR runner failed for skip_ws"); + + assert_eq!(vm_result, "3", "VM expected to skip 3 leading spaces"); + match join_result { + JoinValue::Int(v) => assert_eq!(v, 3, "JoinIR runner skip_ws result mismatch"), + other => panic!("JoinIR runner 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"); +} + +#[test] +#[ignore] +fn joinir_runner_funcscanner_trim_executes() { + 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_ENABLE_USING", "1"); + std::env::set_var("HAKO_ENABLE_USING", "1"); + std::env::set_var("NYASH_DISABLE_PLUGINS", "1"); + // 上と同様、無限ループ検出用にステップ上限を明示しておく。 + std::env::set_var("NYASH_VM_MAX_STEPS", "100000"); + + let func_scanner_src = include_str!("../../lang/src/compiler/entry/func_scanner.hako"); + let test_src = std::fs::read_to_string("lang/src/compiler/tests/funcscanner_trim_min.hako") + .expect("failed to read funcscanner_trim_min.hako"); + let runner = r#" +static box Runner { + main(args) { + return FuncScannerBox.trim(" abc ") + } +} +"#; + let full_src = format!("{func_scanner_src}\n{test_src}\n{runner}"); + + let ast: ASTNode = + NyashParser::parse_from_string(&full_src).expect("trim_min: parse failed"); + let mut mc = MirCompiler::with_options(false); + let compiled = mc.compile(ast).expect("trim_min: MIR compile failed"); + + std::env::set_var("NYASH_ENTRY", "Runner.main"); + let mut vm = VM::new(); + let vm_out = vm + .execute_module(&compiled.module) + .expect("trim_min: VM execution failed"); + let vm_result = vm_out.to_string_box().value; + std::env::remove_var("NYASH_ENTRY"); + + let join_module = lower_funcscanner_trim_to_joinir(&compiled.module) + .expect("lower_funcscanner_trim_to_joinir failed"); + let join_result = run_joinir_function( + &join_module, + JoinFuncId::new(0), + &[JoinValue::Str(" abc ".to_string())], + ) + .expect("JoinIR runner failed for trim"); + + assert_eq!(vm_result, "abc", "VM trim_min should return stripped text"); + match join_result { + JoinValue::Str(s) => assert_eq!(s, "abc", "JoinIR runner trim result mismatch"), + other => panic!("JoinIR runner returned non-string value: {:?}", other), + } + + 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"); + std::env::remove_var("NYASH_VM_MAX_STEPS"); +} diff --git a/src/tests/mir_joinir_min.rs b/src/tests/mir_joinir_min.rs index ac830a67..c5dff16f 100644 --- a/src/tests/mir_joinir_min.rs +++ b/src/tests/mir_joinir_min.rs @@ -68,6 +68,7 @@ fn mir_joinir_min_manual_construction() { func: loop_step_id, args: vec![i_init], k_next: Some(k_exit_id), + dst: None, }); join_module.add_function(main_func); @@ -102,6 +103,7 @@ fn mir_joinir_min_manual_construction() { loop_step_func.body.push(JoinInst::Jump { cont: k_exit_id, args: vec![i_param], + cond: Some(cmp_result), }); // i_plus_1 = i + 1 diff --git a/src/tests/mod.rs b/src/tests/mod.rs index cafad6cf..2bacd68d 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -14,6 +14,7 @@ pub mod mir_funcscanner_ssa; pub mod mir_joinir_min; // Phase 26-H: JoinIR型定義妥当性確認 pub mod mir_joinir_skip_ws; // Phase 27.0: minimal_ssa_skip_ws JoinIR変換 pub mod mir_joinir_funcscanner_trim; // Phase 27.1: FuncScannerBox.trim JoinIR変換 +pub mod joinir_runner_min; // Phase 27.2: JoinIR 実行器 A/B 比較テスト pub mod mir_locals_ssa; pub mod mir_loopform_conditional_reassign; pub mod mir_loopform_exit_phi;