diff --git a/src/boxes/file/handle_box/metadata.rs b/src/boxes/file/handle_box/metadata.rs index 1cd91704..f777ab8c 100644 --- a/src/boxes/file/handle_box/metadata.rs +++ b/src/boxes/file/handle_box/metadata.rs @@ -6,7 +6,7 @@ impl FileHandleBox { /// Phase 114: Internal helper using FileIo::stat() /// /// Unified metadata access through FileIo trait. - fn metadata_internal(&self) -> Result { + pub(super) fn metadata_internal(&self) -> Result { let io = self .io .as_ref() diff --git a/src/mir/join_ir_runner.rs b/src/mir/join_ir_runner.rs deleted file mode 100644 index 82d1a8d7..00000000 --- a/src/mir/join_ir_runner.rs +++ /dev/null @@ -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 { - #[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 { - // 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, -) -> Result { - 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 = 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, -) -> 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 = args[1..] - .iter() - .map(|&var_id| read_var(locals, var_id).map(|jv| jv.to_vm_value())) - .collect::, _>>()?; - - // 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, var: VarId) -> Result { - locals - .get(&var) - .cloned() - .ok_or_else(|| JoinRuntimeError::new(format!("Variable {:?} not bound", var))) -} - -fn materialize_args( - args: &[VarId], - locals: &HashMap, -) -> Result, JoinRuntimeError> { - args.iter().map(|v| read_var(locals, *v)).collect() -} - -fn as_bool(value: &JoinValue) -> Result { - match value { - JoinValue::Bool(b) => Ok(*b), - JoinValue::Int(i) => Ok(*i != 0), - JoinValue::Unit => Ok(false), - other => Err(JoinRuntimeError::new(format!( - "Expected bool-compatible value, got {:?}", - other - ))), - } -} - -#[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 - } -} diff --git a/src/mir/join_ir_runner/api.rs b/src/mir/join_ir_runner/api.rs new file mode 100644 index 00000000..fe006ad6 --- /dev/null +++ b/src/mir/join_ir_runner/api.rs @@ -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 { + #[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 { + // 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) + }) +} diff --git a/src/mir/join_ir_runner/exec.rs b/src/mir/join_ir_runner/exec.rs new file mode 100644 index 00000000..1661e4f5 --- /dev/null +++ b/src/mir/join_ir_runner/exec.rs @@ -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, +) -> Result { + 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 = 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, +) -> 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 = args[1..] + .iter() + .map(|&var_id| read_var(locals, var_id).map(|jv| jv.to_vm_value())) + .collect::, _>>()?; + + // 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, var: VarId) -> Result { + locals + .get(&var) + .cloned() + .ok_or_else(|| JoinRuntimeError::new(format!("Variable {:?} not bound", var))) +} + +fn materialize_args( + args: &[VarId], + locals: &HashMap, +) -> Result, JoinRuntimeError> { + args.iter().map(|v| read_var(locals, *v)).collect() +} + +fn as_bool(value: &JoinValue) -> Result { + match value { + JoinValue::Bool(b) => Ok(*b), + JoinValue::Int(i) => Ok(*i != 0), + JoinValue::Unit => Ok(false), + other => Err(JoinRuntimeError::new(format!( + "Expected bool-compatible value, got {:?}", + other + ))), + } +} diff --git a/src/mir/join_ir_runner/mod.rs b/src/mir/join_ir_runner/mod.rs new file mode 100644 index 00000000..ee60a1ca --- /dev/null +++ b/src/mir/join_ir_runner/mod.rs @@ -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; diff --git a/src/mir/join_ir_runner/tests.rs b/src/mir/join_ir_runner/tests.rs new file mode 100644 index 00000000..29e03252 --- /dev/null +++ b/src/mir/join_ir_runner/tests.rs @@ -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 +} diff --git a/src/mir/loop_pattern_detection/function_scope_capture/analyzers/tests/mod.rs b/src/mir/loop_pattern_detection/function_scope_capture/analyzers/tests/mod.rs new file mode 100644 index 00000000..4260d356 --- /dev/null +++ b/src/mir/loop_pattern_detection/function_scope_capture/analyzers/tests/mod.rs @@ -0,0 +1,2 @@ +mod v1; +mod v2; diff --git a/src/mir/loop_pattern_detection/function_scope_capture/analyzers/tests.rs b/src/mir/loop_pattern_detection/function_scope_capture/analyzers/tests/v1.rs similarity index 50% rename from src/mir/loop_pattern_detection/function_scope_capture/analyzers/tests.rs rename to src/mir/loop_pattern_detection/function_scope_capture/analyzers/tests/v1.rs index 2adcf746..4ea5c1ab 100644 --- a/src/mir/loop_pattern_detection/function_scope_capture/analyzers/tests.rs +++ b/src/mir/loop_pattern_detection/function_scope_capture/analyzers/tests/v1.rs @@ -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()); -} diff --git a/src/mir/loop_pattern_detection/function_scope_capture/analyzers/tests/v2.rs b/src/mir/loop_pattern_detection/function_scope_capture/analyzers/tests/v2.rs new file mode 100644 index 00000000..c9a53af4 --- /dev/null +++ b/src/mir/loop_pattern_detection/function_scope_capture/analyzers/tests/v2.rs @@ -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()); +}