refactor(join_ir_runner): split runner and analyzer tests
This commit is contained in:
@ -6,7 +6,7 @@ impl FileHandleBox {
|
||||
/// Phase 114: Internal helper using FileIo::stat()
|
||||
///
|
||||
/// Unified metadata access through FileIo trait.
|
||||
fn metadata_internal(&self) -> Result<crate::boxes::file::provider::FileStat, String> {
|
||||
pub(super) fn metadata_internal(&self) -> Result<crate::boxes::file::provider::FileStat, String> {
|
||||
let io = self
|
||||
.io
|
||||
.as_ref()
|
||||
|
||||
@ -1,952 +0,0 @@
|
||||
//! JoinIR Runner - Development Harness (Structure Validation Only)
|
||||
//!
|
||||
//! # Two Routes
|
||||
//!
|
||||
//! ## Route A: JoinIR→MIR→VM (Recommended SSOT)
|
||||
//! - Full semantic validation via MIR lowering pipeline
|
||||
//! - Tests should use `JoinIrFrontendTestRunner` or `run_joinir_via_vm`
|
||||
//! - Examples: Phase 34 tests (IfSelect, Loop, Break, Continue)
|
||||
//! - **Use this route for ALL semantic tests**
|
||||
//!
|
||||
//! ## Route B: Direct JoinIR Runner (Structure Validation)
|
||||
//! - For structure-only validation of JoinIR constructs
|
||||
//! - Use `run_joinir_function` only when Route A is insufficient
|
||||
//! - Examples: Handwritten JoinIR module tests, low-level instruction tests
|
||||
//! - Note: Some operations (e.g., MethodCall) may be unimplemented in Runner
|
||||
//!
|
||||
//! # Phase 35-4 Unification Strategy
|
||||
//! All semantic tests migrated to Route A. Route B kept only for fundamental
|
||||
//! structure validation that cannot be verified through MIR→VM path.
|
||||
//!
|
||||
//! # Original Purpose (Phase 27.2)
|
||||
//! hand-written / minimal JoinIR を VM と A/B 比較するための軽量ランナー。
|
||||
//! - 対応値: i64 / bool / String / Unit
|
||||
//! - 対応命令: Const / BinOp / Compare / BoxCall(StringBox: length, substring) /
|
||||
//! Call / Jump / Ret
|
||||
//!
|
||||
//! Phase 27.8: ops box 統合
|
||||
//! - JoinValue / JoinIrOpError は join_ir_ops から再エクスポート
|
||||
//! - eval_binop() / eval_compare() を使用(実装を一箇所に集約)
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
use crate::config::env::joinir_dev::{current_joinir_mode, JoinIrMode};
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
use crate::mir::join_ir::normalized::{dev_env, normalized_dev_roundtrip_structured, shape_guard};
|
||||
use crate::mir::join_ir::{ConstValue, JoinFuncId, JoinInst, JoinModule, MirLikeInst, VarId};
|
||||
|
||||
// Phase 27.8: ops box からの再エクスポート
|
||||
pub use crate::mir::join_ir_ops::{JoinIrOpError, JoinValue};
|
||||
|
||||
// Phase 27.8: 互換性のため JoinRuntimeError を JoinIrOpError の別名として保持
|
||||
pub type JoinRuntimeError = JoinIrOpError;
|
||||
|
||||
pub fn run_joinir_function(
|
||||
vm: &mut crate::backend::mir_interpreter::MirInterpreter,
|
||||
module: &JoinModule,
|
||||
entry: JoinFuncId,
|
||||
args: &[JoinValue],
|
||||
) -> Result<JoinValue, JoinRuntimeError> {
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
{
|
||||
// Canonical shapes always go through Normalized roundtrip regardless of mode/env.
|
||||
let canonical_shapes = shape_guard::canonical_shapes(module);
|
||||
if !canonical_shapes.is_empty() {
|
||||
let args_vec = args.to_vec();
|
||||
return dev_env::with_dev_env_if_unset(|| {
|
||||
let structured = normalized_dev_roundtrip_structured(module).map_err(|msg| {
|
||||
JoinRuntimeError::new(format!(
|
||||
"[joinir/normalized-dev/runner] canonical roundtrip failed: {}",
|
||||
msg
|
||||
))
|
||||
})?;
|
||||
if dev_env::normalized_dev_logs_enabled() {
|
||||
eprintln!(
|
||||
"[joinir/normalized-dev/runner] canonical normalized roundtrip (shapes={:?}, functions={})",
|
||||
canonical_shapes,
|
||||
structured.functions.len()
|
||||
);
|
||||
}
|
||||
execute_function(vm, &structured, entry, args_vec)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
match current_joinir_mode() {
|
||||
JoinIrMode::NormalizedDev => {
|
||||
return run_joinir_function_normalized_dev(vm, module, entry, args);
|
||||
}
|
||||
_ => {
|
||||
// Structured-only path (default)
|
||||
}
|
||||
}
|
||||
|
||||
execute_function(vm, module, entry, args.to_vec())
|
||||
}
|
||||
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
fn run_joinir_function_normalized_dev(
|
||||
vm: &mut crate::backend::mir_interpreter::MirInterpreter,
|
||||
module: &JoinModule,
|
||||
entry: JoinFuncId,
|
||||
args: &[JoinValue],
|
||||
) -> Result<JoinValue, JoinRuntimeError> {
|
||||
// JoinIrMode::NormalizedDev path: Structured→Normalized→Structured roundtrip
|
||||
// Keep dev path opt-in and fail-fast: only Structured P1/P2 minis are supported.
|
||||
dev_env::with_dev_env_if_unset(|| {
|
||||
let debug = dev_env::normalized_dev_logs_enabled();
|
||||
let args_vec = args.to_vec();
|
||||
|
||||
let shapes = shape_guard::supported_shapes(module);
|
||||
if shapes.is_empty() {
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[joinir/normalized-dev/runner] shape unsupported; staying on Structured path"
|
||||
);
|
||||
}
|
||||
return execute_function(vm, module, entry, args_vec);
|
||||
}
|
||||
|
||||
let structured_roundtrip = normalized_dev_roundtrip_structured(module).map_err(|msg| {
|
||||
JoinRuntimeError::new(format!("[joinir/normalized-dev/runner] {}", msg))
|
||||
})?;
|
||||
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[joinir/normalized-dev/runner] normalized roundtrip succeeded (shapes={:?}, functions={})",
|
||||
shapes,
|
||||
structured_roundtrip.functions.len()
|
||||
);
|
||||
}
|
||||
|
||||
execute_function(vm, &structured_roundtrip, entry, args_vec)
|
||||
})
|
||||
}
|
||||
|
||||
fn execute_function(
|
||||
vm: &mut crate::backend::mir_interpreter::MirInterpreter,
|
||||
module: &JoinModule,
|
||||
mut current_func: JoinFuncId,
|
||||
mut current_args: Vec<JoinValue>,
|
||||
) -> Result<JoinValue, JoinRuntimeError> {
|
||||
let verbose = crate::config::env::joinir_dev_enabled();
|
||||
|
||||
'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<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(vm, 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(vm, 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);
|
||||
}
|
||||
// Phase 33: Select instruction execution
|
||||
JoinInst::Select {
|
||||
dst,
|
||||
cond,
|
||||
then_val,
|
||||
else_val,
|
||||
type_hint: _, // Phase 63-3: 実行時は未使用
|
||||
} => {
|
||||
// 1. Evaluate cond (Bool or Int)
|
||||
let cond_value = read_var(&locals, *cond)?;
|
||||
if verbose {
|
||||
eprintln!(
|
||||
"[joinir/runner/select] cond={:?}, cond_value={:?}",
|
||||
cond, cond_value
|
||||
);
|
||||
}
|
||||
let cond_bool = match cond_value {
|
||||
JoinValue::Bool(b) => b,
|
||||
JoinValue::Int(i) => i != 0, // Int も許す(0=false, それ以外=true)
|
||||
_ => {
|
||||
return Err(JoinRuntimeError::new(format!(
|
||||
"Select: cond must be Bool or Int, got {:?}",
|
||||
cond_value
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Select then_val or else_val
|
||||
let then_value = read_var(&locals, *then_val)?;
|
||||
let else_value = read_var(&locals, *else_val)?;
|
||||
if verbose {
|
||||
eprintln!(
|
||||
"[joinir/runner/select] cond_bool={}, then_val={:?}={:?}, else_val={:?}={:?}",
|
||||
cond_bool, then_val, then_value, else_val, else_value
|
||||
);
|
||||
}
|
||||
|
||||
let selected_id = if cond_bool { *then_val } else { *else_val };
|
||||
let selected_value = read_var(&locals, selected_id)?;
|
||||
if verbose {
|
||||
eprintln!(
|
||||
"[joinir/runner/select] selected_id={:?}, selected_value={:?}",
|
||||
selected_id, selected_value
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Write to dst
|
||||
locals.insert(*dst, selected_value);
|
||||
ip += 1;
|
||||
}
|
||||
// Phase 33-6: IfMerge instruction execution (複数変数 PHI)
|
||||
JoinInst::IfMerge {
|
||||
cond,
|
||||
merges,
|
||||
k_next,
|
||||
} => {
|
||||
// Phase 33-6 最小実装: k_next は None のみサポート
|
||||
if k_next.is_some() {
|
||||
return Err(JoinRuntimeError::new(
|
||||
"IfMerge: k_next continuation is not yet supported (Phase 33-6 minimal)",
|
||||
));
|
||||
}
|
||||
|
||||
// 1. Evaluate cond (Bool or Int)
|
||||
let cond_value = read_var(&locals, *cond)?;
|
||||
let cond_bool = match cond_value {
|
||||
JoinValue::Bool(b) => b,
|
||||
JoinValue::Int(i) => i != 0,
|
||||
_ => {
|
||||
return Err(JoinRuntimeError::new(format!(
|
||||
"IfMerge: cond must be Bool or Int, got {:?}",
|
||||
cond_value
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 各 merge ペアについて、cond に応じて値を選択して代入
|
||||
for merge in merges {
|
||||
let selected_id = if cond_bool {
|
||||
merge.then_val
|
||||
} else {
|
||||
merge.else_val
|
||||
};
|
||||
let selected_value = read_var(&locals, selected_id)?;
|
||||
locals.insert(merge.dst, selected_value);
|
||||
}
|
||||
|
||||
ip += 1;
|
||||
}
|
||||
// Phase 34-6: MethodCall instruction execution
|
||||
JoinInst::MethodCall { .. } => {
|
||||
// Phase 34-6: MethodCall は JoinIR Runner では未対応
|
||||
// JoinIR → MIR 変換経由で VM が実行する
|
||||
return Err(JoinRuntimeError::new(
|
||||
"MethodCall is not supported in JoinIR Runner (use JoinIR→MIR→VM bridge instead)"
|
||||
));
|
||||
}
|
||||
// Phase 56: ConditionalMethodCall instruction execution
|
||||
JoinInst::ConditionalMethodCall { .. } => {
|
||||
// Phase 56: ConditionalMethodCall は JoinIR Runner では未対応
|
||||
// JoinIR → MIR 変換経由で VM が実行する
|
||||
return Err(JoinRuntimeError::new(
|
||||
"ConditionalMethodCall is not supported in JoinIR Runner (use JoinIR→MIR→VM bridge instead)"
|
||||
));
|
||||
}
|
||||
// Phase 41-4: NestedIfMerge instruction execution
|
||||
JoinInst::NestedIfMerge { .. } => {
|
||||
// Phase 41-4: NestedIfMerge は JoinIR Runner では未対応
|
||||
// JoinIR → MIR 変換経由で VM が実行する
|
||||
return Err(JoinRuntimeError::new(
|
||||
"NestedIfMerge is not supported in JoinIR Runner (use JoinIR→MIR→VM bridge instead)"
|
||||
));
|
||||
}
|
||||
// Phase 51: FieldAccess instruction execution
|
||||
JoinInst::FieldAccess { .. } => {
|
||||
// Phase 51: FieldAccess は JoinIR Runner では未対応
|
||||
// JoinIR → MIR 変換経由で VM が実行する
|
||||
return Err(JoinRuntimeError::new(
|
||||
"FieldAccess is not supported in JoinIR Runner (use JoinIR→MIR→VM bridge instead)"
|
||||
));
|
||||
}
|
||||
// Phase 51: NewBox instruction execution
|
||||
JoinInst::NewBox { .. } => {
|
||||
// Phase 51: NewBox は JoinIR Runner では未対応
|
||||
// JoinIR → MIR 変換経由で VM が実行する
|
||||
return Err(JoinRuntimeError::new(
|
||||
"NewBox is not supported in JoinIR Runner (use JoinIR→MIR→VM bridge instead)"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallthrough without explicit return
|
||||
return Ok(JoinValue::Unit);
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_compute(
|
||||
vm: &mut crate::backend::mir_interpreter::MirInterpreter,
|
||||
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 } => {
|
||||
// Phase 27.8: ops box の eval_binop() を使用
|
||||
let l = read_var(locals, *lhs)?;
|
||||
let r = read_var(locals, *rhs)?;
|
||||
let v = crate::mir::join_ir_ops::eval_binop(*op, &l, &r)?;
|
||||
locals.insert(*dst, v);
|
||||
}
|
||||
MirLikeInst::Compare { dst, op, lhs, rhs } => {
|
||||
// Phase 27.8: ops box の eval_compare() を使用
|
||||
let l = read_var(locals, *lhs)?;
|
||||
let r = read_var(locals, *rhs)?;
|
||||
let v = crate::mir::join_ir_ops::eval_compare(*op, &l, &r)?;
|
||||
locals.insert(*dst, v);
|
||||
}
|
||||
// S-5.2-improved: BoxCall → VM execute_box_call ラッパー経由
|
||||
// - 制御フロー: JoinIR Runner が担当
|
||||
// - Box/Plugin 実装: Rust VM に完全委譲(VM 2号機を避ける)
|
||||
// - VM の完全な BoxCall 意味論を使用:
|
||||
// * Void guards (Void.length() → 0)
|
||||
// * PluginBox サポート (FileBox, NetBox)
|
||||
// * InstanceBox policy checks
|
||||
// * object_fields handling
|
||||
// * Method re-routing (toString→str)
|
||||
MirLikeInst::BoxCall {
|
||||
dst,
|
||||
box_name: _, // box_name は VM が内部で判定するため不要
|
||||
method,
|
||||
args,
|
||||
} => {
|
||||
// First argument is the receiver (box instance)
|
||||
if args.is_empty() {
|
||||
return Err(JoinRuntimeError::new(
|
||||
"BoxCall requires at least a receiver argument",
|
||||
));
|
||||
}
|
||||
|
||||
// Convert receiver to VMValue
|
||||
let receiver_jv = read_var(locals, args[0])?;
|
||||
let receiver_vm = receiver_jv.to_vm_value();
|
||||
|
||||
// Convert remaining arguments to VMValue
|
||||
let method_args_vm: Vec<crate::backend::VMValue> = args[1..]
|
||||
.iter()
|
||||
.map(|&var_id| read_var(locals, var_id).map(|jv| jv.to_vm_value()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// Invoke VM's execute_box_call for complete semantics
|
||||
let result_vm = vm
|
||||
.execute_box_call(receiver_vm, method, method_args_vm)
|
||||
.map_err(|e| JoinRuntimeError::new(format!("BoxCall failed: {}", e)))?;
|
||||
|
||||
// Convert result back to JoinValue
|
||||
let result_jv = crate::mir::join_ir_ops::JoinValue::from_vm_value(&result_vm)?;
|
||||
|
||||
// Store result if destination is specified
|
||||
if let Some(dst_var) = dst {
|
||||
locals.insert(*dst_var, result_jv);
|
||||
}
|
||||
}
|
||||
// Phase 56: UnaryOp
|
||||
MirLikeInst::UnaryOp { dst, op, operand } => {
|
||||
let operand_val = read_var(locals, *operand)?;
|
||||
let result = match op {
|
||||
crate::mir::join_ir::UnaryOp::Not => match operand_val {
|
||||
JoinValue::Bool(b) => JoinValue::Bool(!b),
|
||||
JoinValue::Int(i) => JoinValue::Bool(i == 0),
|
||||
_ => {
|
||||
return Err(JoinRuntimeError::new(format!(
|
||||
"Cannot apply 'not' to {:?}",
|
||||
operand_val
|
||||
)))
|
||||
}
|
||||
},
|
||||
crate::mir::join_ir::UnaryOp::Neg => match operand_val {
|
||||
JoinValue::Int(i) => JoinValue::Int(-i),
|
||||
_ => {
|
||||
return Err(JoinRuntimeError::new(format!(
|
||||
"Cannot apply '-' to {:?}",
|
||||
operand_val
|
||||
)))
|
||||
}
|
||||
},
|
||||
};
|
||||
locals.insert(*dst, result);
|
||||
}
|
||||
// Phase 188: Print
|
||||
MirLikeInst::Print { value } => {
|
||||
let val = read_var(locals, *value)?;
|
||||
// Print to stdout (convert to string representation)
|
||||
let output = match val {
|
||||
JoinValue::Int(i) => i.to_string(),
|
||||
JoinValue::Bool(b) => b.to_string(),
|
||||
JoinValue::Str(s) => s,
|
||||
JoinValue::Unit => "null".to_string(),
|
||||
JoinValue::BoxRef(_) => "[BoxRef]".to_string(),
|
||||
};
|
||||
println!("{}", output);
|
||||
}
|
||||
// Phase 188-Impl-3: Select
|
||||
MirLikeInst::Select {
|
||||
dst,
|
||||
cond,
|
||||
then_val,
|
||||
else_val,
|
||||
} => {
|
||||
let cond_value = read_var(locals, *cond)?;
|
||||
let is_true = match cond_value {
|
||||
JoinValue::Bool(b) => b,
|
||||
JoinValue::Int(i) => i != 0,
|
||||
_ => {
|
||||
return Err(JoinRuntimeError::new(format!(
|
||||
"Select condition must be Bool or Int, got {:?}",
|
||||
cond_value
|
||||
)))
|
||||
}
|
||||
};
|
||||
let result = if is_true {
|
||||
read_var(locals, *then_val)?
|
||||
} else {
|
||||
read_var(locals, *else_val)?
|
||||
};
|
||||
locals.insert(*dst, result);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::backend::mir_interpreter::MirInterpreter;
|
||||
use crate::mir::join_ir::{ConstValue, JoinFunction, JoinModule};
|
||||
use crate::mir::ValueId;
|
||||
|
||||
#[test]
|
||||
fn test_select_true() {
|
||||
// let result = if true { 1 } else { 2 }
|
||||
// expected: result == 1
|
||||
let mut module = JoinModule::new();
|
||||
let mut func = JoinFunction::new(JoinFuncId::new(0), "test_func".to_string(), vec![]);
|
||||
|
||||
let v_cond = ValueId(1);
|
||||
let v_then = ValueId(2);
|
||||
let v_else = ValueId(3);
|
||||
let v_result = ValueId(4);
|
||||
|
||||
// const v1 = true
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_cond,
|
||||
value: ConstValue::Bool(true),
|
||||
}));
|
||||
|
||||
// const v2 = 1
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then,
|
||||
value: ConstValue::Integer(1),
|
||||
}));
|
||||
|
||||
// const v3 = 2
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else,
|
||||
value: ConstValue::Integer(2),
|
||||
}));
|
||||
|
||||
// select v4 = v1 ? v2 : v3
|
||||
func.body.push(JoinInst::Select {
|
||||
dst: v_result,
|
||||
cond: v_cond,
|
||||
then_val: v_then,
|
||||
else_val: v_else,
|
||||
type_hint: None, // Phase 63-3
|
||||
});
|
||||
|
||||
// return v4
|
||||
func.body.push(JoinInst::Ret {
|
||||
value: Some(v_result),
|
||||
});
|
||||
|
||||
module.add_function(func);
|
||||
|
||||
let mut vm = MirInterpreter::new();
|
||||
let result = run_joinir_function(&mut vm, &module, JoinFuncId::new(0), &[]).unwrap();
|
||||
|
||||
assert_eq!(result, JoinValue::Int(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_false() {
|
||||
// let result = if false { 1 } else { 2 }
|
||||
// expected: result == 2
|
||||
let mut module = JoinModule::new();
|
||||
let mut func = JoinFunction::new(JoinFuncId::new(0), "test_func".to_string(), vec![]);
|
||||
|
||||
let v_cond = ValueId(1);
|
||||
let v_then = ValueId(2);
|
||||
let v_else = ValueId(3);
|
||||
let v_result = ValueId(4);
|
||||
|
||||
// const v1 = false
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_cond,
|
||||
value: ConstValue::Bool(false),
|
||||
}));
|
||||
|
||||
// const v2 = 1
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then,
|
||||
value: ConstValue::Integer(1),
|
||||
}));
|
||||
|
||||
// const v3 = 2
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else,
|
||||
value: ConstValue::Integer(2),
|
||||
}));
|
||||
|
||||
// select v4 = v1 ? v2 : v3
|
||||
func.body.push(JoinInst::Select {
|
||||
dst: v_result,
|
||||
cond: v_cond,
|
||||
then_val: v_then,
|
||||
else_val: v_else,
|
||||
type_hint: None, // Phase 63-3
|
||||
});
|
||||
|
||||
// return v4
|
||||
func.body.push(JoinInst::Ret {
|
||||
value: Some(v_result),
|
||||
});
|
||||
|
||||
module.add_function(func);
|
||||
|
||||
let mut vm = MirInterpreter::new();
|
||||
let result = run_joinir_function(&mut vm, &module, JoinFuncId::new(0), &[]).unwrap();
|
||||
|
||||
assert_eq!(result, JoinValue::Int(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_int_cond() {
|
||||
// cond=Int(0) → false、Int(1) → true
|
||||
let mut module = JoinModule::new();
|
||||
let mut func = JoinFunction::new(JoinFuncId::new(0), "test_func".to_string(), vec![]);
|
||||
|
||||
let v_cond = ValueId(1);
|
||||
let v_then = ValueId(2);
|
||||
let v_else = ValueId(3);
|
||||
let v_result = ValueId(4);
|
||||
|
||||
// const v1 = 0 (treated as false)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_cond,
|
||||
value: ConstValue::Integer(0),
|
||||
}));
|
||||
|
||||
// const v2 = 100
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then,
|
||||
value: ConstValue::Integer(100),
|
||||
}));
|
||||
|
||||
// const v3 = 200
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else,
|
||||
value: ConstValue::Integer(200),
|
||||
}));
|
||||
|
||||
// select v4 = v1 ? v2 : v3
|
||||
func.body.push(JoinInst::Select {
|
||||
dst: v_result,
|
||||
cond: v_cond,
|
||||
then_val: v_then,
|
||||
else_val: v_else,
|
||||
type_hint: None, // Phase 63-3
|
||||
});
|
||||
|
||||
// return v4
|
||||
func.body.push(JoinInst::Ret {
|
||||
value: Some(v_result),
|
||||
});
|
||||
|
||||
module.add_function(func);
|
||||
|
||||
let mut vm = MirInterpreter::new();
|
||||
let result = run_joinir_function(&mut vm, &module, JoinFuncId::new(0), &[]).unwrap();
|
||||
|
||||
assert_eq!(result, JoinValue::Int(200)); // 0 is false, so should select else
|
||||
}
|
||||
|
||||
// Phase 33-6: IfMerge instruction tests
|
||||
#[test]
|
||||
fn test_if_merge_true() {
|
||||
// if true { x=1; y=2 } else { x=3; y=4 }
|
||||
// expected: x=1, y=2
|
||||
let mut module = JoinModule::new();
|
||||
let mut func = JoinFunction::new(JoinFuncId::new(0), "test_func".to_string(), vec![]);
|
||||
|
||||
let v_cond = ValueId(1);
|
||||
let v_then_x = ValueId(2);
|
||||
let v_then_y = ValueId(3);
|
||||
let v_else_x = ValueId(4);
|
||||
let v_else_y = ValueId(5);
|
||||
let v_result_x = ValueId(6);
|
||||
let v_result_y = ValueId(7);
|
||||
|
||||
// const v1 = true
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_cond,
|
||||
value: ConstValue::Bool(true),
|
||||
}));
|
||||
|
||||
// const v2 = 1 (then x)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then_x,
|
||||
value: ConstValue::Integer(1),
|
||||
}));
|
||||
|
||||
// const v3 = 2 (then y)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then_y,
|
||||
value: ConstValue::Integer(2),
|
||||
}));
|
||||
|
||||
// const v4 = 3 (else x)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else_x,
|
||||
value: ConstValue::Integer(3),
|
||||
}));
|
||||
|
||||
// const v5 = 4 (else y)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else_y,
|
||||
value: ConstValue::Integer(4),
|
||||
}));
|
||||
|
||||
// if_merge v1 { v6=v2; v7=v3 } else { v6=v4; v7=v5 }
|
||||
func.body.push(JoinInst::IfMerge {
|
||||
cond: v_cond,
|
||||
merges: vec![
|
||||
crate::mir::join_ir::MergePair {
|
||||
dst: v_result_x,
|
||||
then_val: v_then_x,
|
||||
else_val: v_else_x,
|
||||
type_hint: None, // Phase 63-3
|
||||
},
|
||||
crate::mir::join_ir::MergePair {
|
||||
dst: v_result_y,
|
||||
then_val: v_then_y,
|
||||
else_val: v_else_y,
|
||||
type_hint: None, // Phase 63-3
|
||||
},
|
||||
],
|
||||
k_next: None,
|
||||
});
|
||||
|
||||
// return v6 + v7
|
||||
let v_sum = ValueId(8);
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::BinOp {
|
||||
dst: v_sum,
|
||||
op: crate::mir::join_ir::BinOpKind::Add,
|
||||
lhs: v_result_x,
|
||||
rhs: v_result_y,
|
||||
}));
|
||||
|
||||
func.body.push(JoinInst::Ret { value: Some(v_sum) });
|
||||
|
||||
module.add_function(func);
|
||||
|
||||
let mut vm = MirInterpreter::new();
|
||||
let result = run_joinir_function(&mut vm, &module, JoinFuncId::new(0), &[]).unwrap();
|
||||
|
||||
assert_eq!(result, JoinValue::Int(3)); // 1 + 2 = 3
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_if_merge_false() {
|
||||
// if false { x=1; y=2 } else { x=3; y=4 }
|
||||
// expected: x=3, y=4
|
||||
let mut module = JoinModule::new();
|
||||
let mut func = JoinFunction::new(JoinFuncId::new(0), "test_func".to_string(), vec![]);
|
||||
|
||||
let v_cond = ValueId(1);
|
||||
let v_then_x = ValueId(2);
|
||||
let v_then_y = ValueId(3);
|
||||
let v_else_x = ValueId(4);
|
||||
let v_else_y = ValueId(5);
|
||||
let v_result_x = ValueId(6);
|
||||
let v_result_y = ValueId(7);
|
||||
|
||||
// const v1 = false
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_cond,
|
||||
value: ConstValue::Bool(false),
|
||||
}));
|
||||
|
||||
// const v2 = 1 (then x)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then_x,
|
||||
value: ConstValue::Integer(1),
|
||||
}));
|
||||
|
||||
// const v3 = 2 (then y)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then_y,
|
||||
value: ConstValue::Integer(2),
|
||||
}));
|
||||
|
||||
// const v4 = 3 (else x)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else_x,
|
||||
value: ConstValue::Integer(3),
|
||||
}));
|
||||
|
||||
// const v5 = 4 (else y)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else_y,
|
||||
value: ConstValue::Integer(4),
|
||||
}));
|
||||
|
||||
// if_merge v1 { v6=v2; v7=v3 } else { v6=v4; v7=v5 }
|
||||
func.body.push(JoinInst::IfMerge {
|
||||
cond: v_cond,
|
||||
merges: vec![
|
||||
crate::mir::join_ir::MergePair {
|
||||
dst: v_result_x,
|
||||
then_val: v_then_x,
|
||||
else_val: v_else_x,
|
||||
type_hint: None, // Phase 63-3
|
||||
},
|
||||
crate::mir::join_ir::MergePair {
|
||||
dst: v_result_y,
|
||||
then_val: v_then_y,
|
||||
else_val: v_else_y,
|
||||
type_hint: None, // Phase 63-3
|
||||
},
|
||||
],
|
||||
k_next: None,
|
||||
});
|
||||
|
||||
// return v6 + v7
|
||||
let v_sum = ValueId(8);
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::BinOp {
|
||||
dst: v_sum,
|
||||
op: crate::mir::join_ir::BinOpKind::Add,
|
||||
lhs: v_result_x,
|
||||
rhs: v_result_y,
|
||||
}));
|
||||
|
||||
func.body.push(JoinInst::Ret { value: Some(v_sum) });
|
||||
|
||||
module.add_function(func);
|
||||
|
||||
let mut vm = MirInterpreter::new();
|
||||
let result = run_joinir_function(&mut vm, &module, JoinFuncId::new(0), &[]).unwrap();
|
||||
|
||||
assert_eq!(result, JoinValue::Int(7)); // 3 + 4 = 7
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_if_merge_multiple() {
|
||||
// if true { x=10; y=20; z=30 } else { x=1; y=2; z=3 }
|
||||
// expected: x=10, y=20, z=30 → sum=60
|
||||
let mut module = JoinModule::new();
|
||||
let mut func = JoinFunction::new(JoinFuncId::new(0), "test_func".to_string(), vec![]);
|
||||
|
||||
let v_cond = ValueId(1);
|
||||
let v_then_x = ValueId(2);
|
||||
let v_then_y = ValueId(3);
|
||||
let v_then_z = ValueId(4);
|
||||
let v_else_x = ValueId(5);
|
||||
let v_else_y = ValueId(6);
|
||||
let v_else_z = ValueId(7);
|
||||
let v_result_x = ValueId(8);
|
||||
let v_result_y = ValueId(9);
|
||||
let v_result_z = ValueId(10);
|
||||
|
||||
// const v1 = true
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_cond,
|
||||
value: ConstValue::Bool(true),
|
||||
}));
|
||||
|
||||
// then values
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then_x,
|
||||
value: ConstValue::Integer(10),
|
||||
}));
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then_y,
|
||||
value: ConstValue::Integer(20),
|
||||
}));
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then_z,
|
||||
value: ConstValue::Integer(30),
|
||||
}));
|
||||
|
||||
// else values
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else_x,
|
||||
value: ConstValue::Integer(1),
|
||||
}));
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else_y,
|
||||
value: ConstValue::Integer(2),
|
||||
}));
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else_z,
|
||||
value: ConstValue::Integer(3),
|
||||
}));
|
||||
|
||||
// if_merge with 3 variables
|
||||
func.body.push(JoinInst::IfMerge {
|
||||
cond: v_cond,
|
||||
merges: vec![
|
||||
crate::mir::join_ir::MergePair {
|
||||
dst: v_result_x,
|
||||
then_val: v_then_x,
|
||||
else_val: v_else_x,
|
||||
type_hint: None, // Phase 63-3
|
||||
},
|
||||
crate::mir::join_ir::MergePair {
|
||||
dst: v_result_y,
|
||||
then_val: v_then_y,
|
||||
else_val: v_else_y,
|
||||
type_hint: None, // Phase 63-3
|
||||
},
|
||||
crate::mir::join_ir::MergePair {
|
||||
dst: v_result_z,
|
||||
then_val: v_then_z,
|
||||
else_val: v_else_z,
|
||||
type_hint: None, // Phase 63-3
|
||||
},
|
||||
],
|
||||
k_next: None,
|
||||
});
|
||||
|
||||
// return x + y + z
|
||||
let v_sum_xy = ValueId(11);
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::BinOp {
|
||||
dst: v_sum_xy,
|
||||
op: crate::mir::join_ir::BinOpKind::Add,
|
||||
lhs: v_result_x,
|
||||
rhs: v_result_y,
|
||||
}));
|
||||
|
||||
let v_sum_xyz = ValueId(12);
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::BinOp {
|
||||
dst: v_sum_xyz,
|
||||
op: crate::mir::join_ir::BinOpKind::Add,
|
||||
lhs: v_sum_xy,
|
||||
rhs: v_result_z,
|
||||
}));
|
||||
|
||||
func.body.push(JoinInst::Ret {
|
||||
value: Some(v_sum_xyz),
|
||||
});
|
||||
|
||||
module.add_function(func);
|
||||
|
||||
let mut vm = MirInterpreter::new();
|
||||
let result = run_joinir_function(&mut vm, &module, JoinFuncId::new(0), &[]).unwrap();
|
||||
|
||||
assert_eq!(result, JoinValue::Int(60)); // 10 + 20 + 30 = 60
|
||||
}
|
||||
}
|
||||
93
src/mir/join_ir_runner/api.rs
Normal file
93
src/mir/join_ir_runner/api.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use crate::backend::mir_interpreter::MirInterpreter;
|
||||
use crate::mir::join_ir::{JoinFuncId, JoinModule};
|
||||
|
||||
use super::exec::execute_function;
|
||||
use super::{JoinRuntimeError, JoinValue};
|
||||
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
use crate::config::env::joinir_dev::{current_joinir_mode, JoinIrMode};
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
use crate::mir::join_ir::normalized::{dev_env, normalized_dev_roundtrip_structured, shape_guard};
|
||||
|
||||
pub fn run_joinir_function(
|
||||
vm: &mut MirInterpreter,
|
||||
module: &JoinModule,
|
||||
entry: JoinFuncId,
|
||||
args: &[JoinValue],
|
||||
) -> Result<JoinValue, JoinRuntimeError> {
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
{
|
||||
// Canonical shapes always go through Normalized roundtrip regardless of mode/env.
|
||||
let canonical_shapes = shape_guard::canonical_shapes(module);
|
||||
if !canonical_shapes.is_empty() {
|
||||
let args_vec = args.to_vec();
|
||||
return dev_env::with_dev_env_if_unset(|| {
|
||||
let structured = normalized_dev_roundtrip_structured(module).map_err(|msg| {
|
||||
JoinRuntimeError::new(format!(
|
||||
"[joinir/normalized-dev/runner] canonical roundtrip failed: {}",
|
||||
msg
|
||||
))
|
||||
})?;
|
||||
if dev_env::normalized_dev_logs_enabled() {
|
||||
eprintln!(
|
||||
"[joinir/normalized-dev/runner] canonical normalized roundtrip (shapes={:?}, functions={})",
|
||||
canonical_shapes,
|
||||
structured.functions.len()
|
||||
);
|
||||
}
|
||||
execute_function(vm, &structured, entry, args_vec)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
match current_joinir_mode() {
|
||||
JoinIrMode::NormalizedDev => {
|
||||
return run_joinir_function_normalized_dev(vm, module, entry, args);
|
||||
}
|
||||
_ => {
|
||||
// Structured-only path (default)
|
||||
}
|
||||
}
|
||||
|
||||
execute_function(vm, module, entry, args.to_vec())
|
||||
}
|
||||
|
||||
#[cfg(feature = "normalized_dev")]
|
||||
fn run_joinir_function_normalized_dev(
|
||||
vm: &mut MirInterpreter,
|
||||
module: &JoinModule,
|
||||
entry: JoinFuncId,
|
||||
args: &[JoinValue],
|
||||
) -> Result<JoinValue, JoinRuntimeError> {
|
||||
// JoinIrMode::NormalizedDev path: Structured→Normalized→Structured roundtrip
|
||||
// Keep dev path opt-in and fail-fast: only Structured P1/P2 minis are supported.
|
||||
dev_env::with_dev_env_if_unset(|| {
|
||||
let debug = dev_env::normalized_dev_logs_enabled();
|
||||
let args_vec = args.to_vec();
|
||||
|
||||
let shapes = shape_guard::supported_shapes(module);
|
||||
if shapes.is_empty() {
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[joinir/normalized-dev/runner] shape unsupported; staying on Structured path"
|
||||
);
|
||||
}
|
||||
return execute_function(vm, module, entry, args_vec);
|
||||
}
|
||||
|
||||
let structured_roundtrip = normalized_dev_roundtrip_structured(module).map_err(|msg| {
|
||||
JoinRuntimeError::new(format!("[joinir/normalized-dev/runner] {}", msg))
|
||||
})?;
|
||||
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[joinir/normalized-dev/runner] normalized roundtrip succeeded (shapes={:?}, functions={})",
|
||||
shapes,
|
||||
structured_roundtrip.functions.len()
|
||||
);
|
||||
}
|
||||
|
||||
execute_function(vm, &structured_roundtrip, entry, args_vec)
|
||||
})
|
||||
}
|
||||
393
src/mir/join_ir_runner/exec.rs
Normal file
393
src/mir/join_ir_runner/exec.rs
Normal file
@ -0,0 +1,393 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::backend::mir_interpreter::MirInterpreter;
|
||||
use crate::mir::join_ir::{ConstValue, JoinFuncId, JoinInst, JoinModule, MirLikeInst, VarId};
|
||||
|
||||
use super::{JoinRuntimeError, JoinValue};
|
||||
|
||||
pub(super) fn execute_function(
|
||||
vm: &mut MirInterpreter,
|
||||
module: &JoinModule,
|
||||
mut current_func: JoinFuncId,
|
||||
mut current_args: Vec<JoinValue>,
|
||||
) -> Result<JoinValue, JoinRuntimeError> {
|
||||
let verbose = crate::config::env::joinir_dev_enabled();
|
||||
|
||||
'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<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(vm, 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(vm, 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);
|
||||
}
|
||||
// Phase 33: Select instruction execution
|
||||
JoinInst::Select {
|
||||
dst,
|
||||
cond,
|
||||
then_val,
|
||||
else_val,
|
||||
type_hint: _, // Phase 63-3: 実行時は未使用
|
||||
} => {
|
||||
// 1. Evaluate cond (Bool or Int)
|
||||
let cond_value = read_var(&locals, *cond)?;
|
||||
if verbose {
|
||||
eprintln!(
|
||||
"[joinir/runner/select] cond={:?}, cond_value={:?}",
|
||||
cond, cond_value
|
||||
);
|
||||
}
|
||||
let cond_bool = match cond_value {
|
||||
JoinValue::Bool(b) => b,
|
||||
JoinValue::Int(i) => i != 0, // Int も許す(0=false, それ以外=true)
|
||||
_ => {
|
||||
return Err(JoinRuntimeError::new(format!(
|
||||
"Select: cond must be Bool or Int, got {:?}",
|
||||
cond_value
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Select then_val or else_val
|
||||
let then_value = read_var(&locals, *then_val)?;
|
||||
let else_value = read_var(&locals, *else_val)?;
|
||||
if verbose {
|
||||
eprintln!(
|
||||
"[joinir/runner/select] cond_bool={}, then_val={:?}={:?}, else_val={:?}={:?}",
|
||||
cond_bool, then_val, then_value, else_val, else_value
|
||||
);
|
||||
}
|
||||
|
||||
let selected_id = if cond_bool { *then_val } else { *else_val };
|
||||
let selected_value = read_var(&locals, selected_id)?;
|
||||
if verbose {
|
||||
eprintln!(
|
||||
"[joinir/runner/select] selected_id={:?}, selected_value={:?}",
|
||||
selected_id, selected_value
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Write to dst
|
||||
locals.insert(*dst, selected_value);
|
||||
ip += 1;
|
||||
}
|
||||
// Phase 33-6: IfMerge instruction execution (複数変数 PHI)
|
||||
JoinInst::IfMerge {
|
||||
cond,
|
||||
merges,
|
||||
k_next,
|
||||
} => {
|
||||
// Phase 33-6 最小実装: k_next は None のみサポート
|
||||
if k_next.is_some() {
|
||||
return Err(JoinRuntimeError::new(
|
||||
"IfMerge: k_next continuation is not yet supported (Phase 33-6 minimal)",
|
||||
));
|
||||
}
|
||||
|
||||
// 1. Evaluate cond (Bool or Int)
|
||||
let cond_value = read_var(&locals, *cond)?;
|
||||
let cond_bool = match cond_value {
|
||||
JoinValue::Bool(b) => b,
|
||||
JoinValue::Int(i) => i != 0,
|
||||
_ => {
|
||||
return Err(JoinRuntimeError::new(format!(
|
||||
"IfMerge: cond must be Bool or Int, got {:?}",
|
||||
cond_value
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
// 2. 各 merge ペアについて、cond に応じて値を選択して代入
|
||||
for merge in merges {
|
||||
let selected_id = if cond_bool {
|
||||
merge.then_val
|
||||
} else {
|
||||
merge.else_val
|
||||
};
|
||||
let selected_value = read_var(&locals, selected_id)?;
|
||||
locals.insert(merge.dst, selected_value);
|
||||
}
|
||||
|
||||
ip += 1;
|
||||
}
|
||||
// Phase 34-6: MethodCall instruction execution
|
||||
JoinInst::MethodCall { .. } => {
|
||||
// Phase 34-6: MethodCall は JoinIR Runner では未対応
|
||||
// JoinIR → MIR 変換経由で VM が実行する
|
||||
return Err(JoinRuntimeError::new(
|
||||
"MethodCall is not supported in JoinIR Runner (use JoinIR→MIR→VM bridge instead)",
|
||||
));
|
||||
}
|
||||
// Phase 56: ConditionalMethodCall instruction execution
|
||||
JoinInst::ConditionalMethodCall { .. } => {
|
||||
// Phase 56: ConditionalMethodCall は JoinIR Runner では未対応
|
||||
// JoinIR → MIR 変換経由で VM が実行する
|
||||
return Err(JoinRuntimeError::new(
|
||||
"ConditionalMethodCall is not supported in JoinIR Runner (use JoinIR→MIR→VM bridge instead)",
|
||||
));
|
||||
}
|
||||
// Phase 41-4: NestedIfMerge instruction execution
|
||||
JoinInst::NestedIfMerge { .. } => {
|
||||
// Phase 41-4: NestedIfMerge は JoinIR Runner では未対応
|
||||
// JoinIR → MIR 変換経由で VM が実行する
|
||||
return Err(JoinRuntimeError::new(
|
||||
"NestedIfMerge is not supported in JoinIR Runner (use JoinIR→MIR→VM bridge instead)",
|
||||
));
|
||||
}
|
||||
// Phase 51: FieldAccess instruction execution
|
||||
JoinInst::FieldAccess { .. } => {
|
||||
// Phase 51: FieldAccess は JoinIR Runner では未対応
|
||||
// JoinIR → MIR 変換経由で VM が実行する
|
||||
return Err(JoinRuntimeError::new(
|
||||
"FieldAccess is not supported in JoinIR Runner (use JoinIR→MIR→VM bridge instead)",
|
||||
));
|
||||
}
|
||||
// Phase 51: NewBox instruction execution
|
||||
JoinInst::NewBox { .. } => {
|
||||
// Phase 51: NewBox は JoinIR Runner では未対応
|
||||
// JoinIR → MIR 変換経由で VM が実行する
|
||||
return Err(JoinRuntimeError::new(
|
||||
"NewBox is not supported in JoinIR Runner (use JoinIR→MIR→VM bridge instead)",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallthrough without explicit return
|
||||
return Ok(JoinValue::Unit);
|
||||
}
|
||||
}
|
||||
|
||||
fn eval_compute(
|
||||
vm: &mut MirInterpreter,
|
||||
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 } => {
|
||||
// Phase 27.8: ops box の eval_binop() を使用
|
||||
let l = read_var(locals, *lhs)?;
|
||||
let r = read_var(locals, *rhs)?;
|
||||
let v = crate::mir::join_ir_ops::eval_binop(*op, &l, &r)?;
|
||||
locals.insert(*dst, v);
|
||||
}
|
||||
MirLikeInst::Compare { dst, op, lhs, rhs } => {
|
||||
// Phase 27.8: ops box の eval_compare() を使用
|
||||
let l = read_var(locals, *lhs)?;
|
||||
let r = read_var(locals, *rhs)?;
|
||||
let v = crate::mir::join_ir_ops::eval_compare(*op, &l, &r)?;
|
||||
locals.insert(*dst, v);
|
||||
}
|
||||
// S-5.2-improved: BoxCall → VM execute_box_call ラッパー経由
|
||||
// - 制御フロー: JoinIR Runner が担当
|
||||
// - Box/Plugin 実装: Rust VM に完全委譲(VM 2号機を避ける)
|
||||
// - VM の完全な BoxCall 意味論を使用:
|
||||
// * Void guards (Void.length() → 0)
|
||||
// * PluginBox サポート (FileBox, NetBox)
|
||||
// * InstanceBox policy checks
|
||||
// * object_fields handling
|
||||
// * Method re-routing (toString→str)
|
||||
MirLikeInst::BoxCall {
|
||||
dst,
|
||||
box_name: _, // box_name は VM が内部で判定するため不要
|
||||
method,
|
||||
args,
|
||||
} => {
|
||||
// First argument is the receiver (box instance)
|
||||
if args.is_empty() {
|
||||
return Err(JoinRuntimeError::new(
|
||||
"BoxCall requires at least a receiver argument",
|
||||
));
|
||||
}
|
||||
|
||||
// Convert receiver to VMValue
|
||||
let receiver_jv = read_var(locals, args[0])?;
|
||||
let receiver_vm = receiver_jv.to_vm_value();
|
||||
|
||||
// Convert remaining arguments to VMValue
|
||||
let method_args_vm: Vec<crate::backend::VMValue> = args[1..]
|
||||
.iter()
|
||||
.map(|&var_id| read_var(locals, var_id).map(|jv| jv.to_vm_value()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// Invoke VM's execute_box_call for complete semantics
|
||||
let result_vm = vm
|
||||
.execute_box_call(receiver_vm, method, method_args_vm)
|
||||
.map_err(|e| JoinRuntimeError::new(format!("BoxCall failed: {}", e)))?;
|
||||
|
||||
// Convert result back to JoinValue
|
||||
let result_jv = crate::mir::join_ir_ops::JoinValue::from_vm_value(&result_vm)?;
|
||||
|
||||
// Store result if destination is specified
|
||||
if let Some(dst_var) = dst {
|
||||
locals.insert(*dst_var, result_jv);
|
||||
}
|
||||
}
|
||||
// Phase 56: UnaryOp
|
||||
MirLikeInst::UnaryOp { dst, op, operand } => {
|
||||
let operand_val = read_var(locals, *operand)?;
|
||||
let result = match op {
|
||||
crate::mir::join_ir::UnaryOp::Not => match operand_val {
|
||||
JoinValue::Bool(b) => JoinValue::Bool(!b),
|
||||
JoinValue::Int(i) => JoinValue::Bool(i == 0),
|
||||
_ => {
|
||||
return Err(JoinRuntimeError::new(format!(
|
||||
"Cannot apply 'not' to {:?}",
|
||||
operand_val
|
||||
)))
|
||||
}
|
||||
},
|
||||
crate::mir::join_ir::UnaryOp::Neg => match operand_val {
|
||||
JoinValue::Int(i) => JoinValue::Int(-i),
|
||||
_ => {
|
||||
return Err(JoinRuntimeError::new(format!(
|
||||
"Cannot apply '-' to {:?}",
|
||||
operand_val
|
||||
)))
|
||||
}
|
||||
},
|
||||
};
|
||||
locals.insert(*dst, result);
|
||||
}
|
||||
// Phase 188: Print
|
||||
MirLikeInst::Print { value } => {
|
||||
let val = read_var(locals, *value)?;
|
||||
// Print to stdout (convert to string representation)
|
||||
let output = match val {
|
||||
JoinValue::Int(i) => i.to_string(),
|
||||
JoinValue::Bool(b) => b.to_string(),
|
||||
JoinValue::Str(s) => s,
|
||||
JoinValue::Unit => "null".to_string(),
|
||||
JoinValue::BoxRef(_) => "[BoxRef]".to_string(),
|
||||
};
|
||||
println!("{}", output);
|
||||
}
|
||||
// Phase 188-Impl-3: Select
|
||||
MirLikeInst::Select {
|
||||
dst,
|
||||
cond,
|
||||
then_val,
|
||||
else_val,
|
||||
} => {
|
||||
let cond_value = read_var(locals, *cond)?;
|
||||
let is_true = match cond_value {
|
||||
JoinValue::Bool(b) => b,
|
||||
JoinValue::Int(i) => i != 0,
|
||||
_ => {
|
||||
return Err(JoinRuntimeError::new(format!(
|
||||
"Select condition must be Bool or Int, got {:?}",
|
||||
cond_value
|
||||
)))
|
||||
}
|
||||
};
|
||||
let result = if is_true {
|
||||
read_var(locals, *then_val)?
|
||||
} else {
|
||||
read_var(locals, *else_val)?
|
||||
};
|
||||
locals.insert(*dst, result);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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
|
||||
))),
|
||||
}
|
||||
}
|
||||
42
src/mir/join_ir_runner/mod.rs
Normal file
42
src/mir/join_ir_runner/mod.rs
Normal file
@ -0,0 +1,42 @@
|
||||
//! JoinIR Runner - Development Harness (Structure Validation Only)
|
||||
//!
|
||||
//! # Two Routes
|
||||
//!
|
||||
//! ## Route A: JoinIR→MIR→VM (Recommended SSOT)
|
||||
//! - Full semantic validation via MIR lowering pipeline
|
||||
//! - Tests should use `JoinIrFrontendTestRunner` or `run_joinir_via_vm`
|
||||
//! - Examples: Phase 34 tests (IfSelect, Loop, Break, Continue)
|
||||
//! - **Use this route for ALL semantic tests**
|
||||
//!
|
||||
//! ## Route B: Direct JoinIR Runner (Structure Validation)
|
||||
//! - For structure-only validation of JoinIR constructs
|
||||
//! - Use `run_joinir_function` only when Route A is insufficient
|
||||
//! - Examples: Handwritten JoinIR module tests, low-level instruction tests
|
||||
//! - Note: Some operations (e.g., MethodCall) may be unimplemented in Runner
|
||||
//!
|
||||
//! # Phase 35-4 Unification Strategy
|
||||
//! All semantic tests migrated to Route A. Route B kept only for fundamental
|
||||
//! structure validation that cannot be verified through MIR→VM path.
|
||||
//!
|
||||
//! # Original Purpose (Phase 27.2)
|
||||
//! hand-written / minimal JoinIR を VM と A/B 比較するための軽量ランナー。
|
||||
//! - 対応値: i64 / bool / String / Unit
|
||||
//! - 対応命令: Const / BinOp / Compare / BoxCall(StringBox: length, substring) /
|
||||
//! Call / Jump / Ret
|
||||
//!
|
||||
//! Phase 27.8: ops box 統合
|
||||
//! - JoinValue / JoinIrOpError は join_ir_ops から再エクスポート
|
||||
//! - eval_binop() / eval_compare() を使用(実装を一箇所に集約)
|
||||
|
||||
mod api;
|
||||
mod exec;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
// Phase 27.8: ops box からの再エクスポート
|
||||
pub use crate::mir::join_ir_ops::{JoinIrOpError, JoinValue};
|
||||
|
||||
// Phase 27.8: 互換性のため JoinRuntimeError を JoinIrOpError の別名として保持
|
||||
pub type JoinRuntimeError = JoinIrOpError;
|
||||
|
||||
pub use api::run_joinir_function;
|
||||
435
src/mir/join_ir_runner/tests.rs
Normal file
435
src/mir/join_ir_runner/tests.rs
Normal file
@ -0,0 +1,435 @@
|
||||
use super::{run_joinir_function, JoinValue};
|
||||
use crate::backend::mir_interpreter::MirInterpreter;
|
||||
use crate::mir::join_ir::{ConstValue, JoinFunction, JoinFuncId, JoinInst, JoinModule, MirLikeInst};
|
||||
use crate::mir::ValueId;
|
||||
|
||||
#[test]
|
||||
fn test_select_true() {
|
||||
// let result = if true { 1 } else { 2 }
|
||||
// expected: result == 1
|
||||
let mut module = JoinModule::new();
|
||||
let mut func = JoinFunction::new(JoinFuncId::new(0), "test_func".to_string(), vec![]);
|
||||
|
||||
let v_cond = ValueId(1);
|
||||
let v_then = ValueId(2);
|
||||
let v_else = ValueId(3);
|
||||
let v_result = ValueId(4);
|
||||
|
||||
// const v1 = true
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_cond,
|
||||
value: ConstValue::Bool(true),
|
||||
}));
|
||||
|
||||
// const v2 = 1
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then,
|
||||
value: ConstValue::Integer(1),
|
||||
}));
|
||||
|
||||
// const v3 = 2
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else,
|
||||
value: ConstValue::Integer(2),
|
||||
}));
|
||||
|
||||
// select v4 = v1 ? v2 : v3
|
||||
func.body.push(JoinInst::Select {
|
||||
dst: v_result,
|
||||
cond: v_cond,
|
||||
then_val: v_then,
|
||||
else_val: v_else,
|
||||
type_hint: None, // Phase 63-3
|
||||
});
|
||||
|
||||
// return v4
|
||||
func.body.push(JoinInst::Ret {
|
||||
value: Some(v_result),
|
||||
});
|
||||
|
||||
module.add_function(func);
|
||||
|
||||
let mut vm = MirInterpreter::new();
|
||||
let result = run_joinir_function(&mut vm, &module, JoinFuncId::new(0), &[]).unwrap();
|
||||
|
||||
assert_eq!(result, JoinValue::Int(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_false() {
|
||||
// let result = if false { 1 } else { 2 }
|
||||
// expected: result == 2
|
||||
let mut module = JoinModule::new();
|
||||
let mut func = JoinFunction::new(JoinFuncId::new(0), "test_func".to_string(), vec![]);
|
||||
|
||||
let v_cond = ValueId(1);
|
||||
let v_then = ValueId(2);
|
||||
let v_else = ValueId(3);
|
||||
let v_result = ValueId(4);
|
||||
|
||||
// const v1 = false
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_cond,
|
||||
value: ConstValue::Bool(false),
|
||||
}));
|
||||
|
||||
// const v2 = 1
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then,
|
||||
value: ConstValue::Integer(1),
|
||||
}));
|
||||
|
||||
// const v3 = 2
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else,
|
||||
value: ConstValue::Integer(2),
|
||||
}));
|
||||
|
||||
// select v4 = v1 ? v2 : v3
|
||||
func.body.push(JoinInst::Select {
|
||||
dst: v_result,
|
||||
cond: v_cond,
|
||||
then_val: v_then,
|
||||
else_val: v_else,
|
||||
type_hint: None, // Phase 63-3
|
||||
});
|
||||
|
||||
// return v4
|
||||
func.body.push(JoinInst::Ret {
|
||||
value: Some(v_result),
|
||||
});
|
||||
|
||||
module.add_function(func);
|
||||
|
||||
let mut vm = MirInterpreter::new();
|
||||
let result = run_joinir_function(&mut vm, &module, JoinFuncId::new(0), &[]).unwrap();
|
||||
|
||||
assert_eq!(result, JoinValue::Int(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_select_int_cond() {
|
||||
// cond=Int(0) → false、Int(1) → true
|
||||
let mut module = JoinModule::new();
|
||||
let mut func = JoinFunction::new(JoinFuncId::new(0), "test_func".to_string(), vec![]);
|
||||
|
||||
let v_cond = ValueId(1);
|
||||
let v_then = ValueId(2);
|
||||
let v_else = ValueId(3);
|
||||
let v_result = ValueId(4);
|
||||
|
||||
// const v1 = 0 (treated as false)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_cond,
|
||||
value: ConstValue::Integer(0),
|
||||
}));
|
||||
|
||||
// const v2 = 100
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then,
|
||||
value: ConstValue::Integer(100),
|
||||
}));
|
||||
|
||||
// const v3 = 200
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else,
|
||||
value: ConstValue::Integer(200),
|
||||
}));
|
||||
|
||||
// select v4 = v1 ? v2 : v3
|
||||
func.body.push(JoinInst::Select {
|
||||
dst: v_result,
|
||||
cond: v_cond,
|
||||
then_val: v_then,
|
||||
else_val: v_else,
|
||||
type_hint: None, // Phase 63-3
|
||||
});
|
||||
|
||||
// return v4
|
||||
func.body.push(JoinInst::Ret {
|
||||
value: Some(v_result),
|
||||
});
|
||||
|
||||
module.add_function(func);
|
||||
|
||||
let mut vm = MirInterpreter::new();
|
||||
let result = run_joinir_function(&mut vm, &module, JoinFuncId::new(0), &[]).unwrap();
|
||||
|
||||
assert_eq!(result, JoinValue::Int(200)); // 0 is false, so should select else
|
||||
}
|
||||
|
||||
// Phase 33-6: IfMerge instruction tests
|
||||
#[test]
|
||||
fn test_if_merge_true() {
|
||||
// if true { x=1; y=2 } else { x=3; y=4 }
|
||||
// expected: x=1, y=2
|
||||
let mut module = JoinModule::new();
|
||||
let mut func = JoinFunction::new(JoinFuncId::new(0), "test_func".to_string(), vec![]);
|
||||
|
||||
let v_cond = ValueId(1);
|
||||
let v_then_x = ValueId(2);
|
||||
let v_then_y = ValueId(3);
|
||||
let v_else_x = ValueId(4);
|
||||
let v_else_y = ValueId(5);
|
||||
let v_result_x = ValueId(6);
|
||||
let v_result_y = ValueId(7);
|
||||
|
||||
// const v1 = true
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_cond,
|
||||
value: ConstValue::Bool(true),
|
||||
}));
|
||||
|
||||
// const v2 = 1 (then x)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then_x,
|
||||
value: ConstValue::Integer(1),
|
||||
}));
|
||||
|
||||
// const v3 = 2 (then y)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then_y,
|
||||
value: ConstValue::Integer(2),
|
||||
}));
|
||||
|
||||
// const v4 = 3 (else x)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else_x,
|
||||
value: ConstValue::Integer(3),
|
||||
}));
|
||||
|
||||
// const v5 = 4 (else y)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else_y,
|
||||
value: ConstValue::Integer(4),
|
||||
}));
|
||||
|
||||
// if_merge v1 { v6=v2; v7=v3 } else { v6=v4; v7=v5 }
|
||||
func.body.push(JoinInst::IfMerge {
|
||||
cond: v_cond,
|
||||
merges: vec![
|
||||
crate::mir::join_ir::MergePair {
|
||||
dst: v_result_x,
|
||||
then_val: v_then_x,
|
||||
else_val: v_else_x,
|
||||
type_hint: None, // Phase 63-3
|
||||
},
|
||||
crate::mir::join_ir::MergePair {
|
||||
dst: v_result_y,
|
||||
then_val: v_then_y,
|
||||
else_val: v_else_y,
|
||||
type_hint: None, // Phase 63-3
|
||||
},
|
||||
],
|
||||
k_next: None,
|
||||
});
|
||||
|
||||
// return v6 + v7
|
||||
let v_sum = ValueId(8);
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::BinOp {
|
||||
dst: v_sum,
|
||||
op: crate::mir::join_ir::BinOpKind::Add,
|
||||
lhs: v_result_x,
|
||||
rhs: v_result_y,
|
||||
}));
|
||||
|
||||
func.body.push(JoinInst::Ret { value: Some(v_sum) });
|
||||
|
||||
module.add_function(func);
|
||||
|
||||
let mut vm = MirInterpreter::new();
|
||||
let result = run_joinir_function(&mut vm, &module, JoinFuncId::new(0), &[]).unwrap();
|
||||
|
||||
assert_eq!(result, JoinValue::Int(3)); // 1 + 2 = 3
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_if_merge_false() {
|
||||
// if false { x=1; y=2 } else { x=3; y=4 }
|
||||
// expected: x=3, y=4
|
||||
let mut module = JoinModule::new();
|
||||
let mut func = JoinFunction::new(JoinFuncId::new(0), "test_func".to_string(), vec![]);
|
||||
|
||||
let v_cond = ValueId(1);
|
||||
let v_then_x = ValueId(2);
|
||||
let v_then_y = ValueId(3);
|
||||
let v_else_x = ValueId(4);
|
||||
let v_else_y = ValueId(5);
|
||||
let v_result_x = ValueId(6);
|
||||
let v_result_y = ValueId(7);
|
||||
|
||||
// const v1 = false
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_cond,
|
||||
value: ConstValue::Bool(false),
|
||||
}));
|
||||
|
||||
// const v2 = 1 (then x)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then_x,
|
||||
value: ConstValue::Integer(1),
|
||||
}));
|
||||
|
||||
// const v3 = 2 (then y)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then_y,
|
||||
value: ConstValue::Integer(2),
|
||||
}));
|
||||
|
||||
// const v4 = 3 (else x)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else_x,
|
||||
value: ConstValue::Integer(3),
|
||||
}));
|
||||
|
||||
// const v5 = 4 (else y)
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else_y,
|
||||
value: ConstValue::Integer(4),
|
||||
}));
|
||||
|
||||
// if_merge v1 { v6=v2; v7=v3 } else { v6=v4; v7=v5 }
|
||||
func.body.push(JoinInst::IfMerge {
|
||||
cond: v_cond,
|
||||
merges: vec![
|
||||
crate::mir::join_ir::MergePair {
|
||||
dst: v_result_x,
|
||||
then_val: v_then_x,
|
||||
else_val: v_else_x,
|
||||
type_hint: None, // Phase 63-3
|
||||
},
|
||||
crate::mir::join_ir::MergePair {
|
||||
dst: v_result_y,
|
||||
then_val: v_then_y,
|
||||
else_val: v_else_y,
|
||||
type_hint: None, // Phase 63-3
|
||||
},
|
||||
],
|
||||
k_next: None,
|
||||
});
|
||||
|
||||
// return v6 + v7
|
||||
let v_sum = ValueId(8);
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::BinOp {
|
||||
dst: v_sum,
|
||||
op: crate::mir::join_ir::BinOpKind::Add,
|
||||
lhs: v_result_x,
|
||||
rhs: v_result_y,
|
||||
}));
|
||||
|
||||
func.body.push(JoinInst::Ret { value: Some(v_sum) });
|
||||
|
||||
module.add_function(func);
|
||||
|
||||
let mut vm = MirInterpreter::new();
|
||||
let result = run_joinir_function(&mut vm, &module, JoinFuncId::new(0), &[]).unwrap();
|
||||
|
||||
assert_eq!(result, JoinValue::Int(7)); // 3 + 4 = 7
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_if_merge_multiple() {
|
||||
// if true { x=10; y=20; z=30 } else { x=1; y=2; z=3 }
|
||||
// expected: x=10, y=20, z=30 → sum=60
|
||||
let mut module = JoinModule::new();
|
||||
let mut func = JoinFunction::new(JoinFuncId::new(0), "test_func".to_string(), vec![]);
|
||||
|
||||
let v_cond = ValueId(1);
|
||||
let v_then_x = ValueId(2);
|
||||
let v_then_y = ValueId(3);
|
||||
let v_then_z = ValueId(4);
|
||||
let v_else_x = ValueId(5);
|
||||
let v_else_y = ValueId(6);
|
||||
let v_else_z = ValueId(7);
|
||||
let v_result_x = ValueId(8);
|
||||
let v_result_y = ValueId(9);
|
||||
let v_result_z = ValueId(10);
|
||||
|
||||
// const v1 = true
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_cond,
|
||||
value: ConstValue::Bool(true),
|
||||
}));
|
||||
|
||||
// then values
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then_x,
|
||||
value: ConstValue::Integer(10),
|
||||
}));
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then_y,
|
||||
value: ConstValue::Integer(20),
|
||||
}));
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_then_z,
|
||||
value: ConstValue::Integer(30),
|
||||
}));
|
||||
|
||||
// else values
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else_x,
|
||||
value: ConstValue::Integer(1),
|
||||
}));
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else_y,
|
||||
value: ConstValue::Integer(2),
|
||||
}));
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::Const {
|
||||
dst: v_else_z,
|
||||
value: ConstValue::Integer(3),
|
||||
}));
|
||||
|
||||
// if_merge with 3 variables
|
||||
func.body.push(JoinInst::IfMerge {
|
||||
cond: v_cond,
|
||||
merges: vec![
|
||||
crate::mir::join_ir::MergePair {
|
||||
dst: v_result_x,
|
||||
then_val: v_then_x,
|
||||
else_val: v_else_x,
|
||||
type_hint: None, // Phase 63-3
|
||||
},
|
||||
crate::mir::join_ir::MergePair {
|
||||
dst: v_result_y,
|
||||
then_val: v_then_y,
|
||||
else_val: v_else_y,
|
||||
type_hint: None, // Phase 63-3
|
||||
},
|
||||
crate::mir::join_ir::MergePair {
|
||||
dst: v_result_z,
|
||||
then_val: v_then_z,
|
||||
else_val: v_else_z,
|
||||
type_hint: None, // Phase 63-3
|
||||
},
|
||||
],
|
||||
k_next: None,
|
||||
});
|
||||
|
||||
// return x + y + z
|
||||
let v_sum_xy = ValueId(11);
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::BinOp {
|
||||
dst: v_sum_xy,
|
||||
op: crate::mir::join_ir::BinOpKind::Add,
|
||||
lhs: v_result_x,
|
||||
rhs: v_result_y,
|
||||
}));
|
||||
|
||||
let v_sum_xyz = ValueId(12);
|
||||
func.body.push(JoinInst::Compute(MirLikeInst::BinOp {
|
||||
dst: v_sum_xyz,
|
||||
op: crate::mir::join_ir::BinOpKind::Add,
|
||||
lhs: v_sum_xy,
|
||||
rhs: v_result_z,
|
||||
}));
|
||||
|
||||
func.body.push(JoinInst::Ret {
|
||||
value: Some(v_sum_xyz),
|
||||
});
|
||||
|
||||
module.add_function(func);
|
||||
|
||||
let mut vm = MirInterpreter::new();
|
||||
let result = run_joinir_function(&mut vm, &module, JoinFuncId::new(0), &[]).unwrap();
|
||||
|
||||
assert_eq!(result, JoinValue::Int(60)); // 10 + 20 + 30 = 60
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
mod v1;
|
||||
mod v2;
|
||||
@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
|
||||
use super::super::analyze_captured_vars;
|
||||
use crate::ast::{ASTNode, LiteralValue, Span};
|
||||
use crate::mir::BasicBlockId;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
@ -345,309 +345,3 @@ fn test_capture_unused_in_loop_rejected() {
|
||||
// Should reject because digits is not used in loop
|
||||
assert_eq!(env.vars.len(), 0);
|
||||
}
|
||||
|
||||
// Phase 245C: Function parameter capture tests
|
||||
|
||||
#[test]
|
||||
fn test_capture_function_param_used_in_condition() {
|
||||
// Simulate: fn parse_number(s, p, len) { loop(p < len) { ... } }
|
||||
// Expected: 'len' should be captured (used in condition, not reassigned)
|
||||
|
||||
let condition = Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Variable {
|
||||
name: "len".to_string(), // function parameter
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
});
|
||||
|
||||
let body = vec![ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(1),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}];
|
||||
|
||||
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
|
||||
header: BasicBlockId(0),
|
||||
body: BasicBlockId(1),
|
||||
latch: BasicBlockId(2),
|
||||
exit: BasicBlockId(3),
|
||||
pinned: BTreeSet::from(["p".to_string()]), // p is loop param
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::new(),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
// Use analyze_captured_vars_v2 with structural matching
|
||||
let env = analyze_captured_vars_v2(&[], condition.as_ref(), &body, &scope);
|
||||
|
||||
// Should capture 'len' (function parameter used in condition)
|
||||
assert_eq!(env.vars.len(), 1);
|
||||
assert!(env.get("len").is_some());
|
||||
let var = env.get("len").unwrap();
|
||||
assert_eq!(var.name, "len");
|
||||
assert!(var.is_immutable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_function_param_used_in_method_call() {
|
||||
// Simulate: fn parse_number(s, p) { loop(p < s.length()) { ch = s.charAt(p) } }
|
||||
// Expected: 's' should be captured (used in condition and body, not reassigned)
|
||||
|
||||
let condition = Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s".to_string(), // function parameter
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "length".to_string(),
|
||||
arguments: vec![],
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
});
|
||||
|
||||
let body = vec![
|
||||
ASTNode::Local {
|
||||
variables: vec!["ch".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s".to_string(), // function parameter
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "charAt".to_string(),
|
||||
arguments: vec![ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(1),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
];
|
||||
|
||||
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
|
||||
header: BasicBlockId(0),
|
||||
body: BasicBlockId(1),
|
||||
latch: BasicBlockId(2),
|
||||
exit: BasicBlockId(3),
|
||||
pinned: BTreeSet::from(["p".to_string()]), // p is loop param
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::from(["ch".to_string()]),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
// Use analyze_captured_vars_v2 with structural matching
|
||||
let env = analyze_captured_vars_v2(&[], condition.as_ref(), &body, &scope);
|
||||
|
||||
// Should capture 's' (function parameter used in condition and body)
|
||||
assert_eq!(env.vars.len(), 1);
|
||||
assert!(env.get("s").is_some());
|
||||
let var = env.get("s").unwrap();
|
||||
assert_eq!(var.name, "s");
|
||||
assert!(var.is_immutable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_function_param_reassigned_rejected() {
|
||||
// Simulate: fn bad_func(x) { x = 5; loop(x < 10) { x = x + 1 } }
|
||||
// Expected: 'x' should NOT be captured (reassigned in function)
|
||||
|
||||
let condition = Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(10),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
});
|
||||
|
||||
let body = vec![ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(1),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}];
|
||||
|
||||
// fn_body includes reassignment before loop
|
||||
let fn_body = vec![ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
value: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(5),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}];
|
||||
|
||||
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
|
||||
header: BasicBlockId(0),
|
||||
body: BasicBlockId(1),
|
||||
latch: BasicBlockId(2),
|
||||
exit: BasicBlockId(3),
|
||||
pinned: BTreeSet::from(["x".to_string()]), // x is loop param
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::new(),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
// Use analyze_captured_vars_v2 with structural matching
|
||||
let env = analyze_captured_vars_v2(&fn_body, condition.as_ref(), &body, &scope);
|
||||
|
||||
// Should NOT capture 'x' (reassigned in fn_body)
|
||||
assert_eq!(env.vars.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_mixed_locals_and_params() {
|
||||
// Simulate: fn parse(s, len) { local digits = "0123"; loop(p < len) { ch = digits.indexOf(...); s.charAt(...) } }
|
||||
// Expected: 'len', 's', and 'digits' should all be captured
|
||||
|
||||
let condition = Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Variable {
|
||||
name: "len".to_string(), // function parameter
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
});
|
||||
|
||||
let body = vec![
|
||||
ASTNode::Local {
|
||||
variables: vec!["ch".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s".to_string(), // function parameter
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "charAt".to_string(),
|
||||
arguments: vec![ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Local {
|
||||
variables: vec!["digit".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "digits".to_string(), // pre-loop local
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "indexOf".to_string(),
|
||||
arguments: vec![ASTNode::Variable {
|
||||
name: "ch".to_string(),
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
];
|
||||
|
||||
// fn_body includes local declaration before loop
|
||||
let fn_body = vec![ASTNode::Local {
|
||||
variables: vec!["digits".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::String("0123".to_string()),
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
}];
|
||||
|
||||
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
|
||||
header: BasicBlockId(0),
|
||||
body: BasicBlockId(1),
|
||||
latch: BasicBlockId(2),
|
||||
exit: BasicBlockId(3),
|
||||
pinned: BTreeSet::from(["p".to_string()]), // p is loop param
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::from(["ch".to_string(), "digit".to_string()]),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
// Use analyze_captured_vars_v2 with structural matching
|
||||
let env = analyze_captured_vars_v2(&fn_body, condition.as_ref(), &body, &scope);
|
||||
|
||||
// Should capture all three: 'len' (param), 's' (param), 'digits' (pre-loop local)
|
||||
assert_eq!(env.vars.len(), 3);
|
||||
assert!(env.get("len").is_some());
|
||||
assert!(env.get("s").is_some());
|
||||
assert!(env.get("digits").is_some());
|
||||
}
|
||||
@ -0,0 +1,310 @@
|
||||
use super::super::analyze_captured_vars_v2;
|
||||
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
|
||||
use crate::mir::BasicBlockId;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
// Phase 245C: Function parameter capture tests
|
||||
|
||||
#[test]
|
||||
fn test_capture_function_param_used_in_condition() {
|
||||
// Simulate: fn parse_number(s, p, len) { loop(p < len) { ... } }
|
||||
// Expected: 'len' should be captured (used in condition, not reassigned)
|
||||
|
||||
let condition = Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Variable {
|
||||
name: "len".to_string(), // function parameter
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
});
|
||||
|
||||
let body = vec![ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(1),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}];
|
||||
|
||||
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
|
||||
header: BasicBlockId(0),
|
||||
body: BasicBlockId(1),
|
||||
latch: BasicBlockId(2),
|
||||
exit: BasicBlockId(3),
|
||||
pinned: BTreeSet::from(["p".to_string()]), // p is loop param
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::new(),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
// Use analyze_captured_vars_v2 with structural matching
|
||||
let env = analyze_captured_vars_v2(&[], condition.as_ref(), &body, &scope);
|
||||
|
||||
// Should capture 'len' (function parameter used in condition)
|
||||
assert_eq!(env.vars.len(), 1);
|
||||
assert!(env.get("len").is_some());
|
||||
let var = env.get("len").unwrap();
|
||||
assert_eq!(var.name, "len");
|
||||
assert!(var.is_immutable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_function_param_used_in_method_call() {
|
||||
// Simulate: fn parse_number(s, p) { loop(p < s.length()) { ch = s.charAt(p) } }
|
||||
// Expected: 's' should be captured (used in condition and body, not reassigned)
|
||||
|
||||
let condition = Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s".to_string(), // function parameter
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "length".to_string(),
|
||||
arguments: vec![],
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
});
|
||||
|
||||
let body = vec![
|
||||
ASTNode::Local {
|
||||
variables: vec!["ch".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s".to_string(), // function parameter
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "charAt".to_string(),
|
||||
arguments: vec![ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(1),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
},
|
||||
];
|
||||
|
||||
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
|
||||
header: BasicBlockId(0),
|
||||
body: BasicBlockId(1),
|
||||
latch: BasicBlockId(2),
|
||||
exit: BasicBlockId(3),
|
||||
pinned: BTreeSet::from(["p".to_string()]), // p is loop param
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::from(["ch".to_string()]),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
// Use analyze_captured_vars_v2 with structural matching
|
||||
let env = analyze_captured_vars_v2(&[], condition.as_ref(), &body, &scope);
|
||||
|
||||
// Should capture 's' (function parameter used in condition and body)
|
||||
assert_eq!(env.vars.len(), 1);
|
||||
assert!(env.get("s").is_some());
|
||||
let var = env.get("s").unwrap();
|
||||
assert_eq!(var.name, "s");
|
||||
assert!(var.is_immutable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_function_param_reassigned_rejected() {
|
||||
// Simulate: fn bad_func(x) { x = 5; loop(x < 10) { x = x + 1 } }
|
||||
// Expected: 'x' should NOT be captured (reassigned in function)
|
||||
|
||||
let condition = Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(10),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
});
|
||||
|
||||
let body = vec![ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
value: Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Add,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(1),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}];
|
||||
|
||||
// fn_body includes reassignment before loop
|
||||
let fn_body = vec![ASTNode::Assignment {
|
||||
target: Box::new(ASTNode::Variable {
|
||||
name: "x".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
value: Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::Integer(5),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
}];
|
||||
|
||||
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
|
||||
header: BasicBlockId(0),
|
||||
body: BasicBlockId(1),
|
||||
latch: BasicBlockId(2),
|
||||
exit: BasicBlockId(3),
|
||||
pinned: BTreeSet::from(["x".to_string()]), // x is loop param
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::new(),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
// Use analyze_captured_vars_v2 with structural matching
|
||||
let env = analyze_captured_vars_v2(&fn_body, condition.as_ref(), &body, &scope);
|
||||
|
||||
// Should NOT capture 'x' (reassigned in fn_body)
|
||||
assert_eq!(env.vars.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_mixed_locals_and_params() {
|
||||
// Simulate: fn parse(s, len) { local digits = "0123"; loop(p < len) { ch = digits.indexOf(...); s.charAt(...) } }
|
||||
// Expected: 'len', 's', and 'digits' should all be captured
|
||||
|
||||
let condition = Box::new(ASTNode::BinaryOp {
|
||||
operator: BinaryOperator::Less,
|
||||
left: Box::new(ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
right: Box::new(ASTNode::Variable {
|
||||
name: "len".to_string(), // function parameter
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
span: Span::unknown(),
|
||||
});
|
||||
|
||||
let body = vec![
|
||||
ASTNode::Local {
|
||||
variables: vec!["ch".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "s".to_string(), // function parameter
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "charAt".to_string(),
|
||||
arguments: vec![ASTNode::Variable {
|
||||
name: "p".to_string(),
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
ASTNode::Local {
|
||||
variables: vec!["digit".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::MethodCall {
|
||||
object: Box::new(ASTNode::Variable {
|
||||
name: "digits".to_string(), // pre-loop local
|
||||
span: Span::unknown(),
|
||||
}),
|
||||
method: "indexOf".to_string(),
|
||||
arguments: vec![ASTNode::Variable {
|
||||
name: "ch".to_string(),
|
||||
span: Span::unknown(),
|
||||
}],
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
},
|
||||
];
|
||||
|
||||
// fn_body includes local declaration before loop
|
||||
let fn_body = vec![ASTNode::Local {
|
||||
variables: vec!["digits".to_string()],
|
||||
initial_values: vec![Some(Box::new(ASTNode::Literal {
|
||||
value: LiteralValue::String("0123".to_string()),
|
||||
span: Span::unknown(),
|
||||
}))],
|
||||
span: Span::unknown(),
|
||||
}];
|
||||
|
||||
let scope = crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape {
|
||||
header: BasicBlockId(0),
|
||||
body: BasicBlockId(1),
|
||||
latch: BasicBlockId(2),
|
||||
exit: BasicBlockId(3),
|
||||
pinned: BTreeSet::from(["p".to_string()]), // p is loop param
|
||||
carriers: BTreeSet::new(),
|
||||
body_locals: BTreeSet::from(["ch".to_string(), "digit".to_string()]),
|
||||
exit_live: BTreeSet::new(),
|
||||
progress_carrier: None,
|
||||
variable_definitions: BTreeMap::new(),
|
||||
};
|
||||
|
||||
// Use analyze_captured_vars_v2 with structural matching
|
||||
let env = analyze_captured_vars_v2(&fn_body, condition.as_ref(), &body, &scope);
|
||||
|
||||
// Should capture all three: 'len' (param), 's' (param), 'digits' (pre-loop local)
|
||||
assert_eq!(env.vars.len(), 3);
|
||||
assert!(env.get("len").is_some());
|
||||
assert!(env.get("s").is_some());
|
||||
assert!(env.get("digits").is_some());
|
||||
}
|
||||
Reference in New Issue
Block a user