Add experimental JoinIR runner and tests

This commit is contained in:
nyash-codex
2025-11-23 08:38:15 +09:00
parent e283e4681b
commit 0852a397d9
7 changed files with 798 additions and 88 deletions

View File

@ -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
```
---

View File

@ -82,12 +82,16 @@ pub enum JoinInst {
func: JoinFuncId,
args: Vec<VarId>,
k_next: Option<JoinContId>,
/// 呼び出し結果を書き込む変数None の場合は末尾呼び出しとして扱う)
dst: Option<VarId>,
},
/// 継続呼び出しjoin / exit 継続など)
Jump {
cont: JoinContId,
args: Vec<VarId>,
/// None のときは無条件ジャンプ、Some(var) のときは var が truthy のときだけ実行
cond: Option<VarId>,
},
/// ルート関数 or 上位への戻り
@ -259,6 +263,7 @@ pub fn lower_min_loop_to_joinir(module: &crate::mir::MirModule) -> Option<JoinMo
func: loop_step_id,
args: vec![i_init],
k_next: None, // main は直接 loop_step を呼ぶ
dst: None,
});
join_module.add_function(main_func);
@ -315,6 +320,7 @@ pub fn lower_min_loop_to_joinir(module: &crate::mir::MirModule) -> Option<JoinMo
func: loop_step_id,
args: vec![i_plus_1],
k_next: None,
dst: None,
});
join_module.add_function(loop_step_func);
@ -404,11 +410,13 @@ pub fn lower_skip_ws_to_joinir(module: &crate::mir::MirModule) -> Option<JoinMod
func: loop_step_id,
args: vec![s_param, i_init, n],
k_next: None,
dst: None,
});
join_module.entry = Some(skip_id);
join_module.add_function(skip_func);
// loop_step 関数: if i >= 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<JoinMod
let cmp2_result = ValueId(4005);
let i_plus_1 = ValueId(4006);
let const_1 = ValueId(4007);
let i_start = ValueId(4008);
let i_end = ValueId(4009);
let const_space = ValueId(4010);
let bool_false = ValueId(4011);
let cmp2_is_false = ValueId(4012);
// cmp1_result = (i >= 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<JoinMod
rhs: n_loop,
}));
// if cmp1_result { k_exit(i) } - break path 1
// Phase 27.1 簡略化: 分岐なしで両方のパスを列挙
loop_step_func.body.push(JoinInst::Ret {
value: Some(i_loop),
// if i >= 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<JoinMod
rhs: const_space,
}));
// if ch == " " { loop_step(s, i + 1, n, k_exit) }
loop_step_func.body.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: i_plus_1,
op: BinOpKind::Add,
lhs: i_loop,
rhs: const_1,
// bool false (for negation)
loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Const {
dst: bool_false,
value: ConstValue::Bool(false),
}));
// cmp2_is_false = (cmp2_result == false)
loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Compare {
dst: cmp2_is_false,
op: CompareOp::Eq,
lhs: cmp2_result,
rhs: bool_false,
}));
// if ch != " " { return i }
loop_step_func.body.push(JoinInst::Jump {
cont: JoinContId::new(1),
args: vec![i_loop],
cond: Some(cmp2_is_false),
});
// continue path: loop_step(s, i + 1, n)
loop_step_func.body.push(JoinInst::Call {
func: loop_step_id,
args: vec![s_loop, i_plus_1, n_loop],
k_next: None,
});
// else { k_exit(i) } - break path 2
loop_step_func.body.push(JoinInst::Ret {
value: Some(i_loop),
dst: None,
});
join_module.add_function(loop_step_func);
@ -566,31 +576,30 @@ pub fn lower_funcscanner_trim_to_joinir(module: &crate::mir::MirModule) -> 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)

327
src/mir/join_ir_runner.rs Normal file
View File

@ -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<String>) -> 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<JoinValue, JoinRuntimeError> {
execute_function(module, entry, args.to_vec())
}
fn execute_function(
module: &JoinModule,
mut current_func: JoinFuncId,
mut current_args: Vec<JoinValue>,
) -> Result<JoinValue, JoinRuntimeError> {
'exec: loop {
let func = module
.functions
.get(&current_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<VarId, JoinValue> = 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<VarId, JoinValue>) -> 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<String, JoinRuntimeError> {
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<VarId, JoinValue>, var: VarId) -> Result<JoinValue, JoinRuntimeError> {
locals
.get(&var)
.cloned()
.ok_or_else(|| JoinRuntimeError::new(format!("Variable {:?} not bound", var)))
}
fn materialize_args(
args: &[VarId],
locals: &HashMap<VarId, JoinValue>,
) -> Result<Vec<JoinValue>, JoinRuntimeError> {
args.iter().map(|v| read_var(locals, *v)).collect()
}
fn as_bool(value: &JoinValue) -> Result<bool, JoinRuntimeError> {
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<i64, JoinRuntimeError> {
match value {
JoinValue::Int(i) => Ok(*i),
other => Err(JoinRuntimeError::new(format!(
"Expected int, got {:?}",
other
))),
}
}
fn expect_str(value: &JoinValue) -> Result<String, JoinRuntimeError> {
match value {
JoinValue::Str(s) => Ok(s.clone()),
other => Err(JoinRuntimeError::new(format!(
"Expected string, got {:?}",
other
))),
}
}

View File

@ -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: 関数正規化IRJoinIR
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,

View File

@ -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");
}

View File

@ -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

View File

@ -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;