diff --git a/tests/normalized_joinir_min.rs b/tests/normalized_joinir_min.rs index b8d1a0f6..805783a6 100644 --- a/tests/normalized_joinir_min.rs +++ b/tests/normalized_joinir_min.rs @@ -1,17 +1,12 @@ #![cfg(all(feature = "normalized_dev", debug_assertions))] use nyash_rust::backend::{mir_interpreter::MirInterpreter, VMValue}; -use nyash_rust::mir::join_ir::{ - normalize_pattern1_minimal, normalize_pattern2_minimal, normalized_pattern1_to_structured, - normalized_pattern2_to_structured, BinOpKind, ConstValue, JoinContId, JoinFuncId, - JoinFunction, JoinInst, JoinIrPhase, JoinModule, MirLikeInst, -}; use nyash_rust::mir::join_ir::normalized::dev_env::{ normalized_dev_enabled, test_ctx, NormalizedDevEnvGuard, NormalizedTestContext, }; use nyash_rust::mir::join_ir::normalized::fixtures::{ - build_jsonparser_atoi_structured_for_normalized_dev, build_jsonparser_atoi_real_structured_for_normalized_dev, + build_jsonparser_atoi_structured_for_normalized_dev, build_jsonparser_parse_array_continue_skip_ws_structured_for_normalized_dev, build_jsonparser_parse_number_real_structured_for_normalized_dev, build_jsonparser_parse_object_continue_skip_ws_structured_for_normalized_dev, @@ -29,13 +24,25 @@ use nyash_rust::mir::join_ir::normalized::fixtures::{ build_selfhost_token_scan_p2_accum_structured_for_normalized_dev, build_selfhost_token_scan_p2_structured_for_normalized_dev, }; -use nyash_rust::mir::join_ir_runner::run_joinir_function; -use nyash_rust::mir::join_ir_ops::JoinValue; -use nyash_rust::mir::join_ir_vm_bridge::{ - convert_join_module_to_mir_with_meta, run_joinir_via_vm, +use nyash_rust::mir::join_ir::{ + normalize_pattern1_minimal, normalize_pattern2_minimal, normalized_pattern1_to_structured, + normalized_pattern2_to_structured, BinOpKind, ConstValue, JoinContId, JoinFuncId, JoinFunction, + JoinInst, JoinIrPhase, JoinModule, MirLikeInst, }; +use nyash_rust::mir::join_ir_ops::JoinValue; +use nyash_rust::mir::join_ir_runner::run_joinir_function; +use nyash_rust::mir::join_ir_vm_bridge::{convert_join_module_to_mir_with_meta, run_joinir_via_vm}; use nyash_rust::mir::ValueId; use std::collections::BTreeMap; + +#[path = "normalized_joinir_min/basic.rs"] +mod basic; +#[path = "normalized_joinir_min/ownership.rs"] +mod ownership; +#[path = "normalized_joinir_min/selfhost.rs"] +mod selfhost; +#[path = "normalized_joinir_min/shapes.rs"] +mod shapes; fn normalized_dev_test_ctx() -> NormalizedTestContext<'static> { let ctx = test_ctx(); assert!( @@ -95,2227 +102,10 @@ fn run_joinir_vm_bridge_structured_only( JoinValue::from_vm_value(&result).expect("result conversion") } -fn build_structured_pattern1() -> JoinModule { - let mut module = JoinModule::new(); - let mut loop_fn = JoinFunction::new( - JoinFuncId::new(1), - "loop_step".to_string(), - vec![ValueId(10)], - ); +// moved into tests/normalized_joinir_min/basic.rs - loop_fn.body.push(JoinInst::Compute(MirLikeInst::Const { - dst: ValueId(11), - value: ConstValue::Integer(0), - })); - loop_fn.body.push(JoinInst::Compute(MirLikeInst::BinOp { - dst: ValueId(12), - op: BinOpKind::Add, - lhs: ValueId(10), - rhs: ValueId(11), - })); - loop_fn.body.push(JoinInst::Jump { - cont: JoinContId(2), - args: vec![ValueId(12)], - cond: None, // 単純経路: 無条件で k_exit に渡して終了 - }); +// moved into tests/normalized_joinir_min/{selfhost.rs,shapes.rs} - let mut k_exit = JoinFunction::new(JoinFuncId::new(2), "k_exit".to_string(), vec![ValueId(12)]); - k_exit.body.push(JoinInst::Ret { - value: Some(ValueId(12)), - }); +// moved into tests/normalized_joinir_min/shapes.rs - module.entry = Some(loop_fn.id); - module.add_function(loop_fn); - module.add_function(k_exit); - module -} - -#[test] -fn normalized_pattern1_minimal_smoke() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_structured_pattern1(); - let normalized = normalize_pattern1_minimal(&structured); - assert_eq!(normalized.phase, JoinIrPhase::Normalized); - assert!(!normalized.env_layouts.is_empty()); - assert!(!normalized.functions.is_empty()); - - // Structured バックアップが保持されていることを確認 - let restored = normalized - .to_structured() - .expect("structured backup should exist"); - assert!(restored.is_structured()); - assert_eq!(restored.functions.len(), structured.functions.len()); -} - -#[test] -fn normalized_pattern1_roundtrip_structured_equivalent() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_structured_pattern1(); - let normalized = normalize_pattern1_minimal(&structured); - let reconstructed = normalized_pattern1_to_structured(&normalized); - - assert!(reconstructed.is_structured()); - assert_eq!(reconstructed.functions.len(), structured.functions.len()); - - for (fid, func) in &structured.functions { - let recon = reconstructed - .functions - .get(fid) - .expect("function missing after reconstruction"); - assert_eq!(recon.params.len(), func.params.len()); - } -} - -#[test] -fn normalized_pattern1_exec_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_structured_pattern1(); - let normalized = normalize_pattern1_minimal(&structured); - let reconstructed = normalized_pattern1_to_structured(&normalized); - - let entry = structured.entry.unwrap_or(JoinFuncId::new(1)); - let input = [JoinValue::Int(0)]; - - let result_structured = run_joinir_vm_bridge(&structured, entry, &input, false); - let result_norm = run_joinir_vm_bridge(&reconstructed, entry, &input, false); - - assert_eq!(result_structured, result_norm); -} - -#[test] -fn normalized_pattern1_exec_matches_structured_roundtrip_backup() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_structured_pattern1(); - let normalized = normalize_pattern1_minimal(&structured); - let reconstructed = normalized_pattern1_to_structured(&normalized); - let restored_backup = normalized - .to_structured() - .expect("structured backup should be present"); - - let entry = structured.entry.unwrap_or(JoinFuncId::new(1)); - let input = [JoinValue::Int(0)]; - - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let recon = run_joinir_vm_bridge(&reconstructed, entry, &input, false); - let restored = run_joinir_vm_bridge(&restored_backup, entry, &input, false); - - assert_eq!(base, recon); - assert_eq!(base, restored); -} - -#[test] -fn normalized_pattern2_roundtrip_structure() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_pattern2_minimal_structured(); - let normalized = normalize_pattern2_minimal(&structured); - assert_eq!(normalized.phase, JoinIrPhase::Normalized); - - let reconstructed = normalized_pattern2_to_structured(&normalized); - assert!(reconstructed.is_structured()); - assert_eq!(reconstructed.functions.len(), structured.functions.len()); - - for name in ["main", "loop_step", "k_exit"] { - let original_has = structured.functions.values().any(|f| f.name == name); - let reconstructed_has = reconstructed.functions.values().any(|f| f.name == name); - assert!( - original_has && reconstructed_has, - "expected function '{}' to exist in both modules", - name - ); - } -} - -#[test] -fn normalized_pattern2_jsonparser_parse_number_real_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_jsonparser_parse_number_real_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - let cases = [ - ("42", 0, "42"), - ("123abc", 0, "123"), - ("9", 0, "9"), - ("abc", 0, ""), - ("xx7yy", 2, "7"), - ("007", 0, "007"), - ]; - - for (s, pos, expected) in cases { - let input = [JoinValue::Str(s.to_string()), JoinValue::Int(pos)]; - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s); - assert_eq!( - dev, - JoinValue::Str(expected.to_string()), - "unexpected result for input '{}' (pos={}) (expected num_str)", - s, - pos - ); - } -} - -#[test] -fn normalized_pattern2_exec_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_pattern2_minimal_structured(); - let normalized = normalize_pattern2_minimal(&structured); - let reconstructed = normalized_pattern2_to_structured(&normalized); - - let entry = structured.entry.unwrap_or(JoinFuncId::new(0)); - let input = [JoinValue::Int(0)]; - - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let recon = run_joinir_vm_bridge(&reconstructed, entry, &input, false); - - assert_eq!(base, recon); -} - -#[test] -#[should_panic(expected = "normalize_pattern2_minimal")] -fn normalized_pattern2_rejects_non_pattern2_structured() { - let _ctx = normalized_dev_test_ctx(); - // Pattern1 Structured module should be rejected by Pattern2 normalizer. - let structured = build_structured_pattern1(); - let _ = normalize_pattern2_minimal(&structured); -} - -#[test] -fn normalized_pattern2_real_loop_roundtrip_structure() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_pattern2_break_fixture_structured(); - let normalized = normalize_pattern2_minimal(&structured); - let reconstructed = normalized_pattern2_to_structured(&normalized); - - assert!(reconstructed.is_structured()); - assert_eq!(structured.functions.len(), reconstructed.functions.len()); - assert_eq!(structured.entry, reconstructed.entry); - - let original_names: Vec<_> = structured - .functions - .values() - .map(|f| f.name.clone()) - .collect(); - for name in original_names { - let reconstructed_has = reconstructed.functions.values().any(|f| f.name == name); - assert!(reconstructed_has, "function '{}' missing after roundtrip", name); - } -} - -#[test] -fn normalized_pattern2_real_loop_exec_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_pattern2_break_fixture_structured(); - let normalized = normalize_pattern2_minimal(&structured); - let reconstructed = normalized_pattern2_to_structured(&normalized); - - let entry = structured.entry.expect("structured entry required"); - let cases = [0, 1, 3, 5]; - - for n in cases { - let input = [JoinValue::Int(n)]; - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let recon = run_joinir_vm_bridge(&reconstructed, entry, &input, false); - - assert_eq!(base, recon, "mismatch at n={}", n); - let expected_sum = n * (n.saturating_sub(1)) / 2; - assert_eq!( - base, - JoinValue::Int(expected_sum), - "unexpected loop result at n={}", - n - ); - } -} - -#[test] -fn normalized_pattern1_runner_dev_switch_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_structured_pattern1(); - let entry = structured.entry.expect("structured entry required"); - let input = [JoinValue::Int(7)]; - - let base = run_joinir_runner(&structured, entry, &input, false); - let dev = run_joinir_runner(&structured, entry, &input, true); - - assert_eq!(base, dev); - assert_eq!(base, JoinValue::Int(7)); -} - -#[test] -fn normalized_pattern2_runner_dev_switch_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_pattern2_break_fixture_structured(); - let entry = structured.entry.expect("structured entry required"); - let cases = [0, 1, 3, 5]; - - for n in cases { - let input = [JoinValue::Int(n)]; - let base = run_joinir_runner(&structured, entry, &input, false); - let dev = run_joinir_runner(&structured, entry, &input, true); - - assert_eq!(base, dev, "runner mismatch at n={}", n); - let expected_sum = n * (n.saturating_sub(1)) / 2; - assert_eq!( - dev, - JoinValue::Int(expected_sum), - "runner result mismatch at n={}", - n - ); - } -} - -#[test] -fn normalized_pattern2_jsonparser_runner_dev_switch_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_jsonparser_skip_ws_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - let cases = [0, 1, 2, 5]; - - for len in cases { - let input = [JoinValue::Int(len)]; - let base = run_joinir_runner(&structured, entry, &input, false); - let dev = run_joinir_runner(&structured, entry, &input, true); - - assert_eq!(base, dev, "runner mismatch at len={}", len); - assert_eq!(dev, JoinValue::Int(len), "unexpected result at len={}", len); - } -} - -#[test] -fn normalized_pattern2_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_pattern2_break_fixture_structured(); - let entry = structured.entry.expect("structured entry required"); - let cases = [0, 1, 3, 5]; - - for n in cases { - let input = [JoinValue::Int(n)]; - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch at n={}", n); - let expected_sum = n * (n.saturating_sub(1)) / 2; - assert_eq!( - dev, - JoinValue::Int(expected_sum), - "vm bridge result mismatch at n={}", - n - ); - } -} - -#[test] -fn normalized_pattern1_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_structured_pattern1(); - let entry = structured.entry.expect("structured entry required"); - let cases = [0, 5, 7]; - - for n in cases { - let input = [JoinValue::Int(n)]; - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch at n={}", n); - assert_eq!(dev, JoinValue::Int(n), "unexpected result at n={}", n); - } -} - -#[test] -fn normalized_pattern2_jsonparser_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_jsonparser_skip_ws_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - let cases = [0, 1, 2, 5]; - - for len in cases { - let input = [JoinValue::Int(len)]; - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch at len={}", len); - assert_eq!(dev, JoinValue::Int(len), "unexpected result at len={}", len); - } -} - -#[test] -fn normalized_pattern2_jsonparser_skip_ws_real_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_jsonparser_skip_ws_real_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - let cases = [ - (" abc", 0, 3), - ("abc", 0, 0), - (" \t\nx", 0, 3), - (" \t\nx", 2, 3), - ]; - - for (s, pos, expected) in cases { - let input = [JoinValue::Str(s.to_string()), JoinValue::Int(pos)]; - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s); - assert_eq!( - dev, - JoinValue::Int(expected), - "unexpected result for input '{}' (pos={})", - s, - pos - ); - } -} - -#[test] -fn normalized_pattern2_jsonparser_atoi_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_jsonparser_atoi_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - let cases = [ - ("42", 2, 42), - ("123abc", 6, 123), - ("007", 3, 7), - ("", 0, 0), - ]; - - for (s, len, expected) in cases { - let input = [JoinValue::Str(s.to_string()), JoinValue::Int(len)]; - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s); - assert_eq!( - dev, - JoinValue::Int(expected), - "unexpected result for input '{}'", - s - ); - } -} - -#[test] -fn normalized_pattern2_jsonparser_atoi_real_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_jsonparser_atoi_real_structured_for_normalized_dev(); - if nyash_rust::config::env::joinir_test_debug_enabled() { - eprintln!( - "[joinir/normalized-dev/test] structured jsonparser_atoi_real: {:#?}", - structured - ); - let normalized = normalize_pattern2_minimal(&structured); - eprintln!( - "[joinir/normalized-dev/test] normalized jsonparser_atoi_real: {:#?}", - normalized - ); - } - let entry = structured.entry.expect("structured entry required"); - let cases = [ - ("42", 42), - ("123abc", 123), - ("007", 7), - ("", 0), - ("abc", 0), - ("-42", -42), - ("+7", 7), - ("-0", 0), - ("-12x", -12), - ("+", 0), - ("-", 0), - ]; - - for (s, expected) in cases { - let input = [JoinValue::Str(s.to_string())]; - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s); - assert_eq!( - dev, - JoinValue::Int(expected), - "unexpected result for input '{}'", - s - ); - } -} - -#[test] -fn normalized_selfhost_token_scan_p2_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_selfhost_token_scan_p2_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - let cases = [0, 1, 3, 5]; - - for n in cases { - let input = [JoinValue::Int(n)]; - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch for n={}", n); - assert_eq!( - dev, - JoinValue::Int(n), - "unexpected result for selfhost_token_scan_p2 n={}", - n - ); - } -} - -#[test] -fn normalized_selfhost_token_scan_p2_accum_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_selfhost_token_scan_p2_accum_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - let cases = [0, 1, 3, 5]; - - for n in cases { - let input = [JoinValue::Int(n)]; - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch for selfhost_token_scan_p2_accum n={}", n); - } -} - -#[test] -fn normalized_pattern3_if_sum_minimal_runner_dev_switch_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_pattern3_if_sum_min_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - - // phase212_if_sum_min.hako 相当: sum=2 になることを期待 - let input: [JoinValue; 0] = []; - - let base = run_joinir_runner(&structured, entry, &input, false); - let dev = run_joinir_runner(&structured, entry, &input, true); - - assert_eq!(base, dev, "runner mismatch for P3 minimal if-sum"); - assert_eq!( - dev, - JoinValue::Int(2), - "unexpected result for P3 minimal if-sum (expected sum=2)", - ); -} - -#[test] -fn normalized_pattern3_if_sum_multi_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_pattern3_if_sum_multi_min_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - let input = [JoinValue::Int(0)]; - - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch for P3 if-sum multi"); - assert_eq!( - dev, - JoinValue::Int(2), - "unexpected result for P3 if-sum multi (expected sum=2)" - ); -} - -#[test] -fn normalized_pattern3_json_if_sum_min_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_pattern3_json_if_sum_min_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - let input = [JoinValue::Int(0)]; - - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch for P3 json if-sum"); - assert_eq!( - dev, - JoinValue::Int(10), - "unexpected result for P3 json if-sum (expected sum=10)" - ); -} - -#[test] -fn normalized_selfhost_if_sum_p3_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_selfhost_if_sum_p3_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - let cases = [0, 1, 3, 4, 5]; - - for n in cases { - let input = [JoinValue::Int(n)]; - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch for selfhost_if_sum_p3 n={}", n); - assert_eq!(dev, JoinValue::Int(expected_selfhost_if_sum_p3(n))); - } -} - -#[test] -fn normalized_selfhost_if_sum_p3_ext_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_selfhost_if_sum_p3_ext_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - let cases = [0, 1, 3, 4, 5]; - - for n in cases { - let input = [JoinValue::Int(n)]; - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch for selfhost_if_sum_p3_ext n={}", n); - assert_eq!(dev, JoinValue::Int(expected_selfhost_if_sum_p3_ext(n))); - } -} - -fn expected_selfhost_if_sum_p3(n: i64) -> i64 { - if n <= 1 { - return 0; - } - let sum = (n - 1) * n / 2; - let count = n - 1; - sum + count -} - -fn expected_selfhost_if_sum_p3_ext(n: i64) -> i64 { - if n <= 0 { - return 0; - } - // i=0: sum += 1 - // i=1..n-1: sum += i, count += 1 - let sum = 1 + (n - 1) * n / 2; - let count = n - 1; - sum + count -} - -/// Phase 53: selfhost args-parse P2 (practical variation with string carrier) -#[test] -fn normalized_selfhost_args_parse_p2_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_selfhost_args_parse_p2_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - // Test different argc values: 0, 1, 2, 3 - let cases = [0, 1, 2, 3]; - - for argc in cases { - let input = [JoinValue::Int(argc)]; - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch for selfhost_args_parse_p2 argc={}", argc); - } -} - -/// Phase 53: selfhost stmt-count P3 (practical variation with multi-branch if-else) -#[test] -fn normalized_selfhost_stmt_count_p3_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_selfhost_stmt_count_p3_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - // Test different statement counts: 0, 5, 10, 15 - let cases = [0, 5, 10, 15]; - - for n in cases { - let input = [JoinValue::Int(n)]; - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch for selfhost_stmt_count_p3 n={}", n); - } -} - -#[cfg(feature = "normalized_dev")] -#[test] -fn test_phase46_canonical_set_includes_p2_mid() { - use nyash_rust::mir::join_ir::normalized::shape_guard::{ - is_canonical_shape, NormalizedDevShape, - }; - - // Phase 46: Verify P2-Mid patterns are canonical - assert!(is_canonical_shape(&NormalizedDevShape::JsonparserAtoiReal)); - assert!(is_canonical_shape( - &NormalizedDevShape::JsonparserParseNumberReal - )); - assert!(is_canonical_shape( - &NormalizedDevShape::Pattern3IfSumMinimal - )); - assert!(is_canonical_shape( - &NormalizedDevShape::Pattern3IfSumMulti - )); - assert!(is_canonical_shape( - &NormalizedDevShape::Pattern3IfSumJson - )); - - // Verify P2-Core patterns still canonical - assert!(is_canonical_shape(&NormalizedDevShape::Pattern2Mini)); - assert!(is_canonical_shape(&NormalizedDevShape::JsonparserSkipWsMini)); - assert!(is_canonical_shape(&NormalizedDevShape::JsonparserSkipWsReal)); - assert!(is_canonical_shape(&NormalizedDevShape::JsonparserAtoiMini)); -} - -/// Phase 47-A: Test P3 minimal normalization -#[test] -fn test_phase47a_pattern3_if_sum_minimal_normalization() { - use nyash_rust::mir::join_ir::normalized::normalize_pattern3_if_sum_minimal; - - let module = build_pattern3_if_sum_min_structured_for_normalized_dev(); - - // Test that normalization succeeds (includes shape detection internally) - let result = normalize_pattern3_if_sum_minimal(&module); - assert!( - result.is_ok(), - "P3 normalization should succeed (shape detection + normalization): {:?}", - result.err() - ); - - let normalized = result.unwrap(); - assert_eq!( - normalized.functions.len(), - module.functions.len(), - "Normalized function count should match Structured" - ); - - // Verify normalized module has proper phase - assert_eq!( - normalized.phase, - nyash_rust::mir::join_ir::JoinIrPhase::Normalized, - "Normalized module should have Normalized phase" - ); -} - -/// Phase 47-A: Test P3 VM execution (basic smoke test) -#[test] -fn test_phase47a_pattern3_if_sum_minimal_runner() { - let module = build_pattern3_if_sum_min_structured_for_normalized_dev(); - - // Basic test: module should be runnable through JoinIR runner - // This test verifies the P3 fixture is valid and generates proper JoinIR - assert_eq!(module.functions.len(), 3, "P3 should have 3 functions"); - - let entry = module.entry.expect("P3 should have entry function"); - assert_eq!(entry.0, 0, "Entry should be function 0"); -} - -/// Phase 48-A: Test P4 minimal normalization -#[test] -fn test_phase48a_pattern4_continue_minimal_normalization() { - use nyash_rust::mir::join_ir::normalized::normalize_pattern4_continue_minimal; - - let module = build_pattern4_continue_min_structured_for_normalized_dev(); - - // Test that normalization succeeds (includes shape detection internally) - let result = normalize_pattern4_continue_minimal(&module); - assert!( - result.is_ok(), - "P4 normalization should succeed (shape detection + normalization): {:?}", - result.err() - ); - - let normalized = result.unwrap(); - assert_eq!( - normalized.functions.len(), - module.functions.len(), - "Normalized function count should match Structured" - ); - - // Verify normalized module has proper phase - assert_eq!( - normalized.phase, - nyash_rust::mir::join_ir::JoinIrPhase::Normalized, - "Normalized module should have Normalized phase" - ); -} - -/// Phase 48-A: Test P4 VM execution (basic smoke test) -#[test] -fn test_phase48a_pattern4_continue_minimal_runner() { - let module = build_pattern4_continue_min_structured_for_normalized_dev(); - - // Basic test: module should be runnable through JoinIR runner - // This test verifies the P4 fixture is valid and generates proper JoinIR - assert_eq!(module.functions.len(), 3, "P4 should have 3 functions"); - - let entry = module.entry.expect("P4 should have entry function"); - assert_eq!(entry.0, 0, "Entry should be function 0"); -} - -/// Phase 48-A: Test P4 minimal Runner dev switch matches Structured -#[test] -fn test_normalized_pattern4_continue_minimal_runner_dev_switch_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_pattern4_continue_min_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - - // pattern4_continue_min fixture: acc=4 (skipped i==2, so counted 0,1,3,4) - let input = [JoinValue::Int(5)]; // n = 5 - - let base = run_joinir_runner(&structured, entry, &input, false); - let dev = run_joinir_runner(&structured, entry, &input, true); - - assert_eq!(base, dev, "runner mismatch for P4 minimal continue"); - assert_eq!( - dev, - JoinValue::Int(4), - "unexpected result for P4 minimal continue (expected acc=4, skipped i==2)", - ); -} - -/// Phase 48-A: Test P4 minimal VM Bridge direct matches Structured -#[test] -fn test_normalized_pattern4_continue_minimal_vm_bridge_direct_matches_structured() { - let _ctx = normalized_dev_test_ctx(); - let structured = build_pattern4_continue_min_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - - // pattern4_continue_min fixture: acc=4 (skipped i==2) - let input = [JoinValue::Int(5)]; // n = 5 - - let base = run_joinir_vm_bridge(&structured, entry, &input, false); - let dev = run_joinir_vm_bridge(&structured, entry, &input, true); - - assert_eq!(base, dev, "vm bridge mismatch for P4 minimal continue"); - assert_eq!( - dev, - JoinValue::Int(4), - "unexpected result for P4 minimal continue (expected acc=4)", - ); -} - -/// Phase 48-C: P4 minimal should use canonical normalized route even without env -#[test] -fn test_normalized_pattern4_continue_minimal_canonical_matches_structured() { - let structured = build_pattern4_continue_min_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - - let input = [JoinValue::Int(5)]; - let structured_res = run_joinir_vm_bridge_structured_only(&structured, entry, &input); - let canonical = run_joinir_vm_bridge(&structured, entry, &input, false); - - assert_eq!( - structured_res, canonical, - "canonical P4 minimal result mismatch" - ); - assert_eq!(canonical, JoinValue::Int(4)); -} - -/// Phase 48-B: JsonParser _parse_array continue skip_ws (dev-only) VM Bridge comparison -#[test] -fn test_normalized_pattern4_jsonparser_parse_array_continue_skip_ws_vm_bridge_direct_matches_structured( -) { - let _ctx = normalized_dev_test_ctx(); - let structured = build_jsonparser_parse_array_continue_skip_ws_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - - // Fixture mirrors pattern4_continue_min: skip i == 2 - let cases = [3, 5, 7]; - - for n in cases { - let args = [JoinValue::Int(n)]; - let base = run_joinir_vm_bridge(&structured, entry, &args, false); - let dev = run_joinir_vm_bridge(&structured, entry, &args, true); - assert_eq!( - base, dev, - "vm bridge mismatch for array continue case n={}", - n - ); - } -} - -/// Phase 48-C: JsonParser _parse_array continue skip_ws canonical route should match Structured -#[test] -fn test_normalized_pattern4_jsonparser_parse_array_continue_skip_ws_canonical_matches_structured() -{ - let structured = build_jsonparser_parse_array_continue_skip_ws_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - - let cases = [3, 5, 7]; - for n in cases { - let args = [JoinValue::Int(n)]; - let structured_res = run_joinir_vm_bridge_structured_only(&structured, entry, &args); - let canonical = run_joinir_vm_bridge(&structured, entry, &args, false); - assert_eq!( - structured_res, canonical, - "canonical array continue mismatch n={}", - n - ); - } -} - -/// Phase 48-B: JsonParser _parse_object continue skip_ws (dev-only) VM Bridge comparison -#[test] -fn test_normalized_pattern4_jsonparser_parse_object_continue_skip_ws_vm_bridge_direct_matches_structured( -) { - let _ctx = normalized_dev_test_ctx(); - let structured = build_jsonparser_parse_object_continue_skip_ws_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - - // Fixture mirrors pattern4_continue_min: skip i == 2 - let cases = [4, 6, 8]; - - for n in cases { - let args = [JoinValue::Int(n)]; - let base = run_joinir_vm_bridge(&structured, entry, &args, false); - let dev = run_joinir_vm_bridge(&structured, entry, &args, true); - assert_eq!( - base, dev, - "vm bridge mismatch for object continue case n={}", - n - ); - } -} - -/// Phase 48-C: JsonParser _parse_object continue skip_ws canonical route should match Structured -#[test] -fn test_normalized_pattern4_jsonparser_parse_object_continue_skip_ws_canonical_matches_structured() -{ - let structured = build_jsonparser_parse_object_continue_skip_ws_structured_for_normalized_dev(); - let entry = structured.entry.expect("structured entry required"); - - let cases = [4, 6, 8]; - for n in cases { - let args = [JoinValue::Int(n)]; - let structured_res = run_joinir_vm_bridge_structured_only(&structured, entry, &args); - let canonical = run_joinir_vm_bridge(&structured, entry, &args, false); - assert_eq!( - structured_res, canonical, - "canonical object continue mismatch n={}", - n - ); - } -} - -/// Phase 54: False positive observation test - P2 structural axis discrimination -/// -/// This test validates that structural detection can discriminate between -/// canonical P2 and selfhost P2 shapes using structural features alone. -#[test] -fn test_phase54_structural_axis_discrimination_p2() { - use nyash_rust::mir::join_ir::normalized::shape_guard::{ - detect_shapes, is_canonical_shape, NormalizedDevShape, - }; - - // Canonical P2 shapes - let canonical_p2_shapes = vec![ - build_pattern2_minimal_structured(), - build_jsonparser_skip_ws_structured_for_normalized_dev(), - ]; - - // Selfhost P2 shapes (Phase 53) - let selfhost_p2_shapes = vec![ - build_selfhost_args_parse_p2_structured_for_normalized_dev(), - build_selfhost_token_scan_p2_structured_for_normalized_dev(), - ]; - - // Canonical P2 should be detected as canonical, NOT selfhost - for canonical in &canonical_p2_shapes { - let shapes = detect_shapes(canonical); - let has_canonical = shapes.iter().any(|s| is_canonical_shape(s)); - let has_selfhost_p2 = shapes.iter().any(|s| matches!( - s, - NormalizedDevShape::SelfhostArgsParseP2 - | NormalizedDevShape::SelfhostTokenScanP2 - | NormalizedDevShape::SelfhostTokenScanP2Accum - )); - - assert!(has_canonical, "canonical P2 should be detected as canonical: {:?}", shapes); - assert!(!has_selfhost_p2, "canonical P2 should NOT be detected as selfhost: {:?}", shapes); - } - - // Selfhost P2 should be detected as selfhost, NOT canonical - for selfhost in &selfhost_p2_shapes { - let shapes = detect_shapes(selfhost); - let has_canonical = shapes.iter().any(|s| is_canonical_shape(s)); - let has_selfhost_p2 = shapes.iter().any(|s| matches!( - s, - NormalizedDevShape::SelfhostArgsParseP2 - | NormalizedDevShape::SelfhostTokenScanP2 - | NormalizedDevShape::SelfhostTokenScanP2Accum - )); - - assert!(!has_canonical, "selfhost P2 should NOT be detected as canonical: {:?}", shapes); - assert!(has_selfhost_p2, "selfhost P2 should be detected as selfhost (with name guard): {:?}", shapes); - } -} - -/// Phase 54: False positive observation test - P3 structural axis discrimination -/// -/// This test validates that structural detection can discriminate between -/// canonical P3 and selfhost P3 shapes using structural features alone. -#[test] -fn test_phase54_structural_axis_discrimination_p3() { - use nyash_rust::mir::join_ir::normalized::shape_guard::{ - detect_shapes, is_canonical_shape, NormalizedDevShape, - }; - - // Canonical P3 shapes - let canonical_p3_shapes = vec![ - build_pattern3_if_sum_min_structured_for_normalized_dev(), - build_pattern3_if_sum_multi_min_structured_for_normalized_dev(), - ]; - - // Selfhost P3 shapes (Phase 53) - let selfhost_p3_shapes = vec![ - build_selfhost_stmt_count_p3_structured_for_normalized_dev(), - build_selfhost_if_sum_p3_structured_for_normalized_dev(), - ]; - - // Canonical P3 should be detected as canonical, NOT selfhost - for canonical in &canonical_p3_shapes { - let shapes = detect_shapes(canonical); - let has_canonical = shapes.iter().any(|s| is_canonical_shape(s)); - let has_selfhost_p3 = shapes.iter().any(|s| matches!( - s, - NormalizedDevShape::SelfhostStmtCountP3 - | NormalizedDevShape::SelfhostIfSumP3 - | NormalizedDevShape::SelfhostIfSumP3Ext - )); - - assert!(has_canonical, "canonical P3 should be detected as canonical: {:?}", shapes); - assert!(!has_selfhost_p3, "canonical P3 should NOT be detected as selfhost: {:?}", shapes); - } - - // Selfhost P3 should be detected as selfhost, NOT canonical - for selfhost in &selfhost_p3_shapes { - let shapes = detect_shapes(selfhost); - let has_canonical = shapes.iter().any(|s| is_canonical_shape(s)); - let has_selfhost_p3 = shapes.iter().any(|s| matches!( - s, - NormalizedDevShape::SelfhostStmtCountP3 - | NormalizedDevShape::SelfhostIfSumP3 - | NormalizedDevShape::SelfhostIfSumP3Ext - )); - - assert!(!has_canonical, "selfhost P3 should NOT be detected as canonical: {:?}", shapes); - assert!(has_selfhost_p3, "selfhost P3 should be detected as selfhost (with name guard): {:?}", shapes); - } -} - -/// Phase 60: Ownership relay threading helpers for P2 (analysis + contract) -/// -/// plan_to_p2_inputs remains Fail-Fast on relay_writes (legacy contract), -/// while plan_to_p2_inputs_with_relay accepts single-hop relay and promotes them -/// to carriers (dev-only). -#[test] -#[cfg(feature = "normalized_dev")] -fn test_phase60_ownership_p2_with_relay_conversion() { - use nyash_rust::mir::join_ir::ownership::{ - plan_to_p2_inputs, plan_to_p2_inputs_with_relay, OwnershipAnalyzer, - }; - use serde_json::json; - - // Create a simple P2 fixture JSON (loop with i and sum) - let json = json!({ - "functions": [{ - "name": "main", - "params": [], - "body": { - "kind": "Block", - "statements": [ - {"kind": "Local", "name": "sum", "init": {"kind": "Const", "value": 0}}, - {"kind": "Local", "name": "i", "init": {"kind": "Const", "value": 0}}, - { - "kind": "Loop", - "condition": { - "kind": "BinaryOp", - "op": "Lt", - "lhs": {"kind": "Var", "name": "i"}, - "rhs": {"kind": "Const", "value": 3} - }, - "body": { - "kind": "Block", - "statements": [ - { - "kind": "If", - "condition": { - "kind": "BinaryOp", - "op": "Ge", - "lhs": {"kind": "Var", "name": "i"}, - "rhs": {"kind": "Const", "value": 2} - }, - "then": {"kind": "Break"} - }, - { - "kind": "Assign", - "target": "sum", - "value": { - "kind": "BinaryOp", - "op": "Add", - "lhs": {"kind": "Var", "name": "sum"}, - "rhs": {"kind": "Var", "name": "i"} - } - }, - { - "kind": "Assign", - "target": "i", - "value": { - "kind": "BinaryOp", - "op": "Add", - "lhs": {"kind": "Var", "name": "i"}, - "rhs": {"kind": "Const", "value": 1} - } - } - ] - } - } - ] - } - }] - }); - - // Run ownership analyzer - let mut analyzer = OwnershipAnalyzer::new(); - let plans = analyzer.analyze_json(&json).expect("analysis should succeed"); - - // Find loop plan (the one that has relay writes to function-owned sum/i) - let loop_plan = plans - .iter() - .find(|p| !p.relay_writes.is_empty()) - .expect("should have a loop plan with relay writes"); - - eprintln!( - "[phase58/test] Loop plan: relay_writes={:?}", - loop_plan.relay_writes - ); - - // Legacy Fail-Fast: relay_writes should be rejected - let result = plan_to_p2_inputs(loop_plan, "i"); - assert!( - result.is_err(), - "Legacy contract: relay_writes should be rejected" - ); - let err = result.unwrap_err(); - assert!( - err.contains("relay_writes not yet supported"), - "Error should mention relay limitation, got: {}", - err - ); - - // Phase 60 dev-only: with_relay should accept and include relay vars as carriers - let inputs_with_relay = - plan_to_p2_inputs_with_relay(loop_plan, "i").expect("with_relay should accept"); - assert!( - inputs_with_relay.carriers.iter().any(|c| c.name == "sum"), - "relay carrier sum should be promoted" - ); - - eprintln!( - "[phase60/test] with_relay carriers={:?}", - inputs_with_relay - .carriers - .iter() - .map(|c| &c.name) - .collect::>() - ); - - // Also test the case where variables ARE owned by loop (future scenario) - // This would work once we support loop-local carriers - let loop_local_json = json!({ - "functions": [{ - "name": "main", - "params": [], - "body": { - "kind": "Loop", - "condition": {"kind": "Const", "value": true}, - "body": { - "kind": "Block", - "statements": [ - {"kind": "Local", "name": "i", "init": {"kind": "Const", "value": 0}}, - {"kind": "Local", "name": "sum", "init": {"kind": "Const", "value": 0}}, - { - "kind": "Assign", - "target": "sum", - "value": { - "kind": "BinaryOp", - "op": "Add", - "lhs": {"kind": "Var", "name": "sum"}, - "rhs": {"kind": "Var", "name": "i"} - } - }, - { - "kind": "Assign", - "target": "i", - "value": { - "kind": "BinaryOp", - "op": "Add", - "lhs": {"kind": "Var", "name": "i"}, - "rhs": {"kind": "Const", "value": 1} - } - }, - {"kind": "Break"} - ] - } - } - }] - }); - - let mut analyzer2 = OwnershipAnalyzer::new(); - let plans2 = analyzer2 - .analyze_json(&loop_local_json) - .expect("loop-local analysis should succeed"); - - // Find loop plan (variables owned by loop, not function) - let loop_plan2 = plans2 - .iter() - .find(|p| !p.owned_vars.is_empty()) - .expect("should have a loop plan with owned vars"); - - eprintln!( - "[phase58/test] Loop-local plan: owned_vars={:?}", - loop_plan2 - .owned_vars - .iter() - .map(|v| &v.name) - .collect::>() - ); - - // Convert to P2 inputs (should succeed - no relay) - let inputs = plan_to_p2_inputs(loop_plan2, "i").expect("should convert successfully"); - - eprintln!("[phase58/test] P2 inputs: {:?}", inputs); - - // Verify: i is skipped (loop var), sum becomes carrier - assert_eq!(inputs.carriers.len(), 1, "Should have 1 carrier (sum)"); - assert_eq!(inputs.carriers[0].name, "sum"); - - eprintln!("[phase60/test] Loop-local conversion verified"); -} - -/// Phase 60: P2 dev-only ownership relay route matches legacy Break lowering. -#[test] -#[cfg(feature = "normalized_dev")] -fn test_phase60_break_lowering_ownership_matches_legacy() { - use nyash_rust::mir::join_ir::frontend::ast_lowerer::lower_break_legacy_for_comparison; - use nyash_rust::mir::join_ir::frontend::ast_lowerer::AstToJoinIrLowerer; - - let _ctx = normalized_dev_test_ctx(); - - let program_json: serde_json::Value = serde_json::from_str(include_str!( - "../docs/private/roadmap2/phases/phase-34-joinir-frontend/fixtures/loop_frontend_break.program.json" - )) - .expect("fixture json"); - - let mut lowerer_new = AstToJoinIrLowerer::new(); - let structured_new = lowerer_new.lower_program_json(&program_json); - - let mut lowerer_old = AstToJoinIrLowerer::new(); - let structured_old = - lower_break_legacy_for_comparison(&mut lowerer_old, &program_json); - - let entry_new = structured_new.entry.expect("new entry"); - let entry_old = structured_old.entry.expect("old entry"); - let input = vec![JoinValue::Int(5)]; - - let out_old = run_joinir_vm_bridge(&structured_old, entry_old, &input, false); - let out_new = run_joinir_vm_bridge(&structured_new, entry_new, &input, false); - - assert_eq!( - out_old, out_new, - "ownership relay dev route must match legacy output" - ); -} - -/// Phase 59: P3 with outer-owned carriers (relay case) should fail-fast -#[test] -#[cfg(feature = "normalized_dev")] -fn test_phase59_ownership_p3_relay_failfast() { - use nyash_rust::mir::join_ir::ownership::{plan_to_p3_inputs, OwnershipAnalyzer}; - use serde_json::json; - - // P3 where sum/count are defined OUTSIDE the loop -> relay - let json = json!({ - "functions": [{ - "name": "main", - "params": [], - "body": { - "kind": "Block", - "statements": [ - {"kind": "Local", "name": "sum", "init": {"kind": "Const", "value": 0}}, - {"kind": "Local", "name": "count", "init": {"kind": "Const", "value": 0}}, - {"kind": "Local", "name": "i", "init": {"kind": "Const", "value": 0}}, - { - "kind": "Loop", - "condition": { - "kind": "BinaryOp", "op": "Lt", - "lhs": {"kind": "Var", "name": "i"}, - "rhs": {"kind": "Const", "value": 10} - }, - "body": { - "kind": "Block", - "statements": [ - { - "kind": "If", - "condition": { - "kind": "BinaryOp", "op": "Gt", - "lhs": {"kind": "Var", "name": "i"}, - "rhs": {"kind": "Const", "value": 0} - }, - "then": { - "kind": "Block", - "statements": [ - {"kind": "Assign", "target": "sum", "value": { - "kind": "BinaryOp", "op": "Add", - "lhs": {"kind": "Var", "name": "sum"}, - "rhs": {"kind": "Var", "name": "i"} - }}, - {"kind": "Assign", "target": "count", "value": { - "kind": "BinaryOp", "op": "Add", - "lhs": {"kind": "Var", "name": "count"}, - "rhs": {"kind": "Const", "value": 1} - }} - ] - } - }, - {"kind": "Assign", "target": "i", "value": { - "kind": "BinaryOp", "op": "Add", - "lhs": {"kind": "Var", "name": "i"}, - "rhs": {"kind": "Const", "value": 1} - }} - ] - } - } - ] - } - }] - }); - - let mut analyzer = OwnershipAnalyzer::new(); - let plans = analyzer.analyze_json(&json).expect("analysis should succeed"); - - // Find loop plan - let loop_plan = plans - .iter() - .find(|p| !p.relay_writes.is_empty()) - .expect("loop should have relay_writes for sum/count"); - - // Verify relay_writes contains sum and count - assert!(loop_plan.relay_writes.iter().any(|r| r.name == "sum")); - assert!(loop_plan.relay_writes.iter().any(|r| r.name == "count")); - - // plan_to_p3_inputs should fail - let result = plan_to_p3_inputs(loop_plan, "i"); - assert!(result.is_err(), "Should fail-fast on relay_writes"); - assert!( - result.unwrap_err().contains("relay_writes not yet supported for P3"), - "Error should mention P3 relay limitation" - ); - - eprintln!("[phase59/test] P3 relay fail-fast verified"); -} - -/// Phase 59: P3 with loop-local carriers should succeed -#[test] -#[cfg(feature = "normalized_dev")] -fn test_phase59_ownership_p3_loop_local_success() { - use nyash_rust::mir::join_ir::ownership::{plan_to_p3_inputs, OwnershipAnalyzer}; - use serde_json::json; - - // P3 where sum/count are defined INSIDE the loop -> no relay - let json = json!({ - "functions": [{ - "name": "main", - "params": [], - "body": { - "kind": "Loop", - "condition": {"kind": "Const", "value": true}, - "body": { - "kind": "Block", - "statements": [ - {"kind": "Local", "name": "i", "init": {"kind": "Const", "value": 0}}, - {"kind": "Local", "name": "sum", "init": {"kind": "Const", "value": 0}}, - {"kind": "Local", "name": "count", "init": {"kind": "Const", "value": 0}}, - { - "kind": "If", - "condition": { - "kind": "BinaryOp", "op": "Gt", - "lhs": {"kind": "Var", "name": "i"}, - "rhs": {"kind": "Const", "value": 0} - }, - "then": { - "kind": "Block", - "statements": [ - {"kind": "Assign", "target": "sum", "value": { - "kind": "BinaryOp", "op": "Add", - "lhs": {"kind": "Var", "name": "sum"}, - "rhs": {"kind": "Var", "name": "i"} - }}, - {"kind": "Assign", "target": "count", "value": { - "kind": "BinaryOp", "op": "Add", - "lhs": {"kind": "Var", "name": "count"}, - "rhs": {"kind": "Const", "value": 1} - }} - ] - } - }, - {"kind": "Break"} - ] - } - } - }] - }); - - let mut analyzer = OwnershipAnalyzer::new(); - let plans = analyzer.analyze_json(&json).expect("analysis should succeed"); - - // Find loop plan with owned vars - let loop_plan = plans - .iter() - .find(|p| p.owned_vars.iter().any(|v| v.name == "sum")) - .expect("loop should own sum"); - - // No relay - assert!( - loop_plan.relay_writes.is_empty(), - "No relay for loop-local vars" - ); - - // plan_to_p3_inputs should succeed - let inputs = plan_to_p3_inputs(loop_plan, "i").expect("Should succeed"); - - eprintln!("[phase59/test] P3 inputs: {:?}", inputs); - - // sum and count should be carriers - assert!(inputs.carriers.iter().any(|c| c.name == "sum")); - assert!(inputs.carriers.iter().any(|c| c.name == "count")); - assert_eq!( - inputs.carriers.len(), - 2, - "Should have 2 carriers (sum and count)" - ); - - eprintln!("[phase59/test] P3 loop-local conversion verified: sum and count correctly extracted as carriers"); -} - -/// Phase 60: Program(JSON v0) fixture (selfhost_if_sum_p3) should produce relay_writes and convert with single-hop relay. -#[test] -#[cfg(feature = "normalized_dev")] -fn test_phase60_ownership_p3_program_json_fixture_with_relay() { - use nyash_rust::mir::join_ir::ownership::{plan_to_p3_inputs_with_relay, OwnershipAnalyzer}; - - let program_json: serde_json::Value = serde_json::from_str(include_str!( - "../docs/private/roadmap2/phases/normalized_dev/fixtures/selfhost_if_sum_p3.program.json" - )) - .expect("fixture json"); - - let mut analyzer = OwnershipAnalyzer::new(); - let plans = analyzer - .analyze_json(&program_json) - .expect("Program(JSON v0) analysis should succeed"); - - let loop_plan = plans - .iter() - .find(|p| !p.relay_writes.is_empty()) - .expect("expected a loop plan with relay_writes"); - - // i/sum/count are defined outside the loop but updated in the loop body -> relay_writes - assert!(loop_plan.relay_writes.iter().any(|r| r.name == "i")); - assert!(loop_plan.relay_writes.iter().any(|r| r.name == "sum")); - assert!(loop_plan.relay_writes.iter().any(|r| r.name == "count")); - - let inputs = plan_to_p3_inputs_with_relay(loop_plan, "i").expect("with_relay should succeed"); - - let mut carriers: Vec<&str> = inputs.carriers.iter().map(|c| c.name.as_str()).collect(); - carriers.sort(); - assert_eq!(carriers, vec!["count", "sum"]); - - // n is read-only in loop condition -> capture + condition_capture - assert!(inputs.captures.iter().any(|n| n == "n")); - assert!(inputs.condition_captures.iter().any(|n| n == "n")); -} - -/// Phase 64: P3 production route with ownership analysis (dev-only integration test) -/// -/// This test verifies that `analyze_loop()` API works for simple P3 loops and that -/// multi-hop relay is correctly rejected with Fail-Fast error. -#[test] -#[cfg(feature = "normalized_dev")] -fn test_phase64_p3_ownership_prod_integration() { - use nyash_rust::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; - use nyash_rust::mir::join_ir::ownership::analyze_loop; - - // Helper: Create literal integer node - fn lit_i(i: i64) -> ASTNode { - ASTNode::Literal { - value: LiteralValue::Integer(i), - span: Span::unknown(), - } - } - - // Helper: Create variable node - fn var(name: &str) -> ASTNode { - ASTNode::Variable { - name: name.to_string(), - span: Span::unknown(), - } - } - - // Simple P3 loop: loop(i < 10) { local sum=0; local i=0; sum = sum + i; i = i + 1 } - let condition = ASTNode::BinaryOp { - operator: BinaryOperator::Less, - left: Box::new(var("i")), - right: Box::new(lit_i(10)), - span: Span::unknown(), - }; - - let body = vec![ - ASTNode::Local { - variables: vec!["sum".to_string()], - initial_values: vec![Some(Box::new(lit_i(0)))], - span: Span::unknown(), - }, - ASTNode::Local { - variables: vec!["i".to_string()], - initial_values: vec![Some(Box::new(lit_i(0)))], - span: Span::unknown(), - }, - ASTNode::Assignment { - target: Box::new(var("sum")), - value: Box::new(ASTNode::BinaryOp { - operator: BinaryOperator::Add, - left: Box::new(var("sum")), - right: Box::new(var("i")), - span: Span::unknown(), - }), - span: Span::unknown(), - }, - ASTNode::Assignment { - target: Box::new(var("i")), - value: Box::new(ASTNode::BinaryOp { - operator: BinaryOperator::Add, - left: Box::new(var("i")), - right: Box::new(lit_i(1)), - span: Span::unknown(), - }), - span: Span::unknown(), - }, - ]; - - // No parent-defined variables (both sum and i are loop-local) - let parent_defined = vec![]; - - // Analyze the loop - let plan = analyze_loop(&condition, &body, &parent_defined) - .expect("P3 analysis should succeed"); - - // Verify basic plan structure - assert!( - !plan.owned_vars.is_empty(), - "Should have owned vars (sum, i)" - ); - - // Find sum and i in owned_vars - let sum_var = plan - .owned_vars - .iter() - .find(|v| v.name == "sum") - .expect("sum should be owned"); - let i_var = plan - .owned_vars - .iter() - .find(|v| v.name == "i") - .expect("i should be owned"); - - // Both should be written - assert!(sum_var.is_written, "sum should be written"); - assert!(i_var.is_written, "i should be written"); - - // i is used in condition -> condition_only - assert!(i_var.is_condition_only, "i should be condition_only"); - - // sum is NOT used in condition - assert!( - !sum_var.is_condition_only, - "sum should NOT be condition_only" - ); - - // No relay writes (all variables are loop-local) - assert!( - plan.relay_writes.is_empty(), - "No relay writes for loop-local variables" - ); - - // Verify single-hop relay constraint: if relay_writes is non-empty, verify single-hop - for relay in &plan.relay_writes { - assert!( - relay.relay_path.len() <= 1, - "Multi-hop relay should be rejected (got relay_path.len = {})", - relay.relay_path.len() - ); - } - - eprintln!( - "[phase64/test] P3 ownership analysis succeeded: {} owned vars, {} relay writes", - plan.owned_vars.len(), - plan.relay_writes.len() - ); -} - -/// Phase 64: Multi-hop relay detection test -/// -/// Verifies that `analyze_loop()` correctly identifies multi-hop relay patterns. -/// The actual rejection (Fail-Fast) happens in `check_ownership_plan_consistency()`, -/// not in `analyze_loop()` itself. -#[test] -#[cfg(feature = "normalized_dev")] -fn test_phase64_p3_multihop_relay_detection() { - use nyash_rust::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; - use nyash_rust::mir::join_ir::ownership::AstOwnershipAnalyzer; - - // Helper functions - fn lit_i(i: i64) -> ASTNode { - ASTNode::Literal { - value: LiteralValue::Integer(i), - span: Span::unknown(), - } - } - - fn var(name: &str) -> ASTNode { - ASTNode::Variable { - name: name.to_string(), - span: Span::unknown(), - } - } - - // Function with nested loops: - // function test() { - // local sum = 0; - // local i = 0; - // loop(i < 5) { - // local j = 0; - // loop(j < 3) { - // sum = sum + 1; // Multi-hop relay: sum defined in function scope - // j = j + 1; - // } - // i = i + 1; - // } - // } - let inner_condition = ASTNode::BinaryOp { - operator: BinaryOperator::Less, - left: Box::new(var("j")), - right: Box::new(lit_i(3)), - span: Span::unknown(), - }; - - let inner_body = vec![ - ASTNode::Assignment { - target: Box::new(var("sum")), - value: Box::new(ASTNode::BinaryOp { - operator: BinaryOperator::Add, - left: Box::new(var("sum")), - right: Box::new(lit_i(1)), - span: Span::unknown(), - }), - span: Span::unknown(), - }, - ASTNode::Assignment { - target: Box::new(var("j")), - value: Box::new(ASTNode::BinaryOp { - operator: BinaryOperator::Add, - left: Box::new(var("j")), - right: Box::new(lit_i(1)), - span: Span::unknown(), - }), - span: Span::unknown(), - }, - ]; - - let outer_condition = ASTNode::BinaryOp { - operator: BinaryOperator::Less, - left: Box::new(var("i")), - right: Box::new(lit_i(5)), - span: Span::unknown(), - }; - - let outer_body = vec![ - ASTNode::Local { - variables: vec!["j".to_string()], - initial_values: vec![Some(Box::new(lit_i(0)))], - span: Span::unknown(), - }, - ASTNode::Loop { - condition: Box::new(inner_condition), - body: inner_body, - span: Span::unknown(), - }, - ASTNode::Assignment { - target: Box::new(var("i")), - value: Box::new(ASTNode::BinaryOp { - operator: BinaryOperator::Add, - left: Box::new(var("i")), - right: Box::new(lit_i(1)), - span: Span::unknown(), - }), - span: Span::unknown(), - }, - ]; - - let function_body = vec![ - ASTNode::Local { - variables: vec!["sum".to_string()], - initial_values: vec![Some(Box::new(lit_i(0)))], - span: Span::unknown(), - }, - ASTNode::Local { - variables: vec!["i".to_string()], - initial_values: vec![Some(Box::new(lit_i(0)))], - span: Span::unknown(), - }, - ASTNode::Loop { - condition: Box::new(outer_condition), - body: outer_body, - span: Span::unknown(), - }, - ]; - - let function = ASTNode::FunctionDeclaration { - name: "test".to_string(), - params: vec![], - body: function_body, - is_static: false, - is_override: false, - span: Span::unknown(), - }; - - // Analyze the entire function to detect nested loop relays - let mut analyzer = AstOwnershipAnalyzer::new(); - let plans = analyzer - .analyze_ast(&function) - .expect("Function analysis should succeed"); - - // Find the inner loop plan (should have multi-hop relay for 'sum') - let inner_loop_plan = plans - .iter() - .find(|p| { - // Inner loop should have relay write for 'sum' with relay_path.len() > 1 - p.relay_writes - .iter() - .any(|r| r.name == "sum" && r.relay_path.len() > 1) - }) - .expect("Expected inner loop plan with multi-hop relay for 'sum'"); - - let sum_relay = inner_loop_plan - .relay_writes - .iter() - .find(|r| r.name == "sum") - .expect("sum should be a relay write in inner loop"); - - // Verify multi-hop relay (relay_path should include both inner and outer loop scopes) - assert!( - sum_relay.relay_path.len() > 1, - "sum should have multi-hop relay (got relay_path.len = {})", - sum_relay.relay_path.len() - ); - - eprintln!( - "[phase64/test] Multi-hop relay detected: sum relay_path.len = {}", - sum_relay.relay_path.len() - ); - eprintln!("[phase64/test] This pattern would be rejected by check_ownership_plan_consistency()"); -} - -/// Phase 70-A: Verify that multihop relay runtime unsupported error has standardized tag. -/// -/// This test builds an OwnershipPlan with multihop relay and verifies that -/// `check_ownership_plan_consistency()` returns an error with the standard tag -/// `[ownership/relay:runtime_unsupported]`. -#[test] -#[cfg(feature = "normalized_dev")] -fn test_phase70a_multihop_relay_runtime_unsupported_tag() { - use nyash_rust::mir::join_ir::ownership::{ - CapturedVar, OwnershipPlan, RelayVar, ScopeId, ScopeOwnedVar, - }; - - // Build a plan with multihop relay (relay_path.len() == 2) - let mut plan = OwnershipPlan::new(ScopeId(2)); // Inner loop scope - plan.owned_vars.push(ScopeOwnedVar { - name: "j".to_string(), - is_written: true, - is_condition_only: false, - }); - plan.relay_writes.push(RelayVar { - name: "sum".to_string(), - owner_scope: ScopeId(0), // Function scope - relay_path: vec![ScopeId(2), ScopeId(1)], // 2 hops: inner → outer - }); - plan.captures.push(CapturedVar { - name: "i".to_string(), - owner_scope: ScopeId(0), - }); - - // Call the plan layer function (Phase 66 accepts multihop) - // The runtime check (check_ownership_plan_consistency) is private in pattern3_with_if_phi.rs, - // so we test the plan layer acceptance here. - use nyash_rust::mir::join_ir::ownership::plan_to_p3_inputs_with_relay; - - let result = plan_to_p3_inputs_with_relay(&plan, "j"); - - // Phase 66: plan_to_p3_inputs_with_relay NOW ACCEPTS multihop (relay_path.len() > 1) - // So this should PASS (not Err) - assert!( - result.is_ok(), - "Phase 66: plan_to_p3_inputs_with_relay should accept multihop relay" - ); - - // Verify the relay is in the output - let inputs = result.unwrap(); - let relay = inputs - .carriers - .iter() - .find(|c| c.name == "sum") - .expect("sum should be in carriers (via relay conversion)"); - eprintln!( - "[phase70a/test] Multihop relay accepted in plan layer: sum role={:?}", - relay.role - ); - - // The RUNTIME check (check_ownership_plan_consistency in pattern3_with_if_phi.rs) - // is what produces [ownership/relay:runtime_unsupported]. - // That function is private, so we document that the tag exists and - // will be hit when P3 lowering encounters this plan at runtime. - eprintln!("[phase70a/test] Runtime would fail with [ownership/relay:runtime_unsupported] tag"); -} - -/// Phase 70-B: Test that simple passthrough multihop relay is accepted. -/// -/// This test verifies the structural detection logic in OwnershipPlanValidator -/// correctly identifies supported multihop patterns (pure passthrough, no self-updates). -#[test] -#[cfg(feature = "normalized_dev")] -fn test_phase70b_multihop_relay_simple_passthrough_succeeds() { - use nyash_rust::mir::join_ir::ownership::{ - OwnershipPlan, OwnershipPlanValidator, RelayVar, ScopeId, ScopeOwnedVar, - }; - - // Build a plan for innermost loop (L3) with multihop relay - // L3 writes to 'counter' owned by L1, relayed through L2 - let mut plan_l3 = OwnershipPlan::new(ScopeId(3)); // Inner loop scope - plan_l3.owned_vars.push(ScopeOwnedVar { - name: "i".to_string(), // loop variable - is_written: true, - is_condition_only: true, - }); - plan_l3.relay_writes.push(RelayVar { - name: "counter".to_string(), - owner_scope: ScopeId(1), // L1 owns counter - relay_path: vec![ScopeId(3), ScopeId(2)], // 2 hops: L3 → L2 → L1 - }); - // No owned_vars for 'counter' - pure passthrough from L3's perspective - - // Phase 70-B: This should be accepted (passthrough pattern) - let result = OwnershipPlanValidator::validate_relay_support(&plan_l3); - assert!( - result.is_ok(), - "Phase 70-B: Simple passthrough multihop should be accepted, got: {:?}", - result - ); - - eprintln!("[phase70b/test] Simple passthrough multihop relay accepted (3-layer loop)"); -} - -/// Phase 70-B: Test that unsupported multihop patterns are still rejected. -/// -/// This test verifies that complex patterns (e.g., self-conflict where a scope -/// both owns and relays the same variable) are still rejected with the standard tag. -#[test] -#[cfg(feature = "normalized_dev")] -fn test_phase70b_multihop_relay_self_conflict_rejected() { - use nyash_rust::mir::join_ir::ownership::{ - OwnershipPlan, OwnershipPlanValidator, RelayVar, ScopeId, ScopeOwnedVar, - }; - - // Build a plan where L3 both owns and relays 'counter' (conflict) - // L3 → L2 → L1 (multihop with self-conflict at L3) - let mut plan_l3 = OwnershipPlan::new(ScopeId(3)); // Inner loop scope - plan_l3.owned_vars.push(ScopeOwnedVar { - name: "counter".to_string(), // L3 owns counter - is_written: true, - is_condition_only: false, - }); - plan_l3.relay_writes.push(RelayVar { - name: "counter".to_string(), // L3 also relays counter (conflict) - owner_scope: ScopeId(1), - relay_path: vec![ScopeId(3), ScopeId(2)], // L3 → L2 → L1 (multihop) - }); - - // Phase 70-B: This should be rejected (self-conflict) - let result = OwnershipPlanValidator::validate_relay_support(&plan_l3); - assert!( - result.is_err(), - "Phase 70-B: Self-conflict multihop should be rejected" - ); - - let err = result.unwrap_err(); - assert!( - err.contains("[ownership/relay:runtime_unsupported]"), - "Error should contain standard tag: {}", - err - ); - - eprintln!("[phase70b/test] Self-conflict multihop relay correctly rejected"); -} - -/// Phase 70-C: Merge Relay (multiple inner loops → same owner) -/// -/// This test verifies that OwnershipAnalyzer correctly detects the "merge relay" -/// pattern where multiple inner loops update the same owner variable. -/// -/// Structure: -/// ```text -/// loop L1 { -/// local total = 0 // owned by L1 -/// loop L2_A { -/// total++ // L2_A → L1 relay -/// } -/// loop L2_B { -/// total += 10 // L2_B → L1 relay -/// } -/// } -/// // L1 exit: merge both L2_A and L2_B's updates to 'total' -/// ``` -#[test] -#[cfg(feature = "normalized_dev")] -fn test_phase70c_merge_relay_multiple_inner_loops_detected() { - use nyash_rust::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; - use nyash_rust::mir::join_ir::ownership::AstOwnershipAnalyzer; - - // Build AST: loop L1 { local total=0; loop L2_A { total++ } loop L2_B { total+=10 } } - fn var(name: &str) -> ASTNode { - ASTNode::Variable { - name: name.to_string(), - span: Span::unknown(), - } - } - fn lit_i(i: i64) -> ASTNode { - ASTNode::Literal { - value: LiteralValue::Integer(i), - span: Span::unknown(), - } - } - fn lit_true() -> ASTNode { - ASTNode::Literal { - value: LiteralValue::Bool(true), - span: Span::unknown(), - } - } - - let ast = ASTNode::FunctionDeclaration { - name: "test_merge_relay".to_string(), - params: vec![], - body: vec![ - // L1: outer loop - ASTNode::Loop { - condition: Box::new(lit_true()), - body: vec![ - // local total = 0 (L1 owns) - ASTNode::Local { - variables: vec!["total".to_string()], - initial_values: vec![Some(Box::new(lit_i(0)))], - span: Span::unknown(), - }, - // L2_A: first inner loop (total++) - ASTNode::Loop { - condition: Box::new(lit_true()), - body: vec![ - ASTNode::Assignment { - target: Box::new(var("total")), - value: Box::new(ASTNode::BinaryOp { - operator: BinaryOperator::Add, - left: Box::new(var("total")), - right: Box::new(lit_i(1)), - span: Span::unknown(), - }), - span: Span::unknown(), - }, - ASTNode::Break { - span: Span::unknown(), - }, - ], - span: Span::unknown(), - }, - // L2_B: second inner loop (total += 10) - ASTNode::Loop { - condition: Box::new(lit_true()), - body: vec![ - ASTNode::Assignment { - target: Box::new(var("total")), - value: Box::new(ASTNode::BinaryOp { - operator: BinaryOperator::Add, - left: Box::new(var("total")), - right: Box::new(lit_i(10)), - span: Span::unknown(), - }), - span: Span::unknown(), - }, - ASTNode::Break { - span: Span::unknown(), - }, - ], - span: Span::unknown(), - }, - ASTNode::Break { - span: Span::unknown(), - }, - ], - span: Span::unknown(), - }, - ], - is_static: false, - is_override: false, - span: Span::unknown(), - }; - - let mut analyzer = AstOwnershipAnalyzer::new(); - let plans = analyzer.analyze_ast(&ast).expect("analysis should succeed"); - - // Find L1 (owner of 'total') - let l1_plan = plans - .iter() - .find(|p| p.owned_vars.iter().any(|v| v.name == "total")) - .expect("expected L1 plan with owned total"); - let l1_scope_id = l1_plan.scope_id; - - // Find L2_A and L2_B (both relay 'total' to L1) - let relay_plans: Vec<_> = plans - .iter() - .filter(|p| { - p.relay_writes.iter().any(|r| r.name == "total") - && p.scope_id != l1_scope_id - }) - .collect(); - - // Verify: two inner loops relay to the same owner - assert_eq!( - relay_plans.len(), - 2, - "Phase 70-C: Expected 2 inner loops relaying 'total' to L1, got {}", - relay_plans.len() - ); - - for (idx, plan) in relay_plans.iter().enumerate() { - let relay = plan - .relay_writes - .iter() - .find(|r| r.name == "total") - .expect("expected 'total' in relay_writes"); - - // Verify: both relay to the same owner (L1) - assert_eq!( - relay.owner_scope, l1_scope_id, - "Phase 70-C: relay {} should have owner_scope = L1", - idx - ); - - // Verify: single-hop relay (L2 → L1) - assert_eq!( - relay.relay_path.len(), - 1, - "Phase 70-C: relay {} should have single-hop path", - idx - ); - - // Verify: relay_path[0] is this scope (L2_A or L2_B) - assert_eq!( - relay.relay_path[0], plan.scope_id, - "Phase 70-C: relay {} relay_path[0] must be this scope", - idx - ); - } - - // Verify: L1 owned_vars contains 'total' and is_written=true - let total_var = l1_plan - .owned_vars - .iter() - .find(|v| v.name == "total") - .expect("expected 'total' in L1 owned_vars"); - assert!( - total_var.is_written, - "Phase 70-C: L1's 'total' should be is_written=true" - ); - - eprintln!("[phase70c/test] Merge relay pattern detected: 2 inner loops → same owner variable"); -} - -/// Phase 70-C: Merge Relay validation acceptance -/// -/// This test verifies that OwnershipPlanValidator ACCEPTS merge relay patterns -/// (multiple inner loops → same owner) because they are PERMITTED with owner merge. -#[test] -#[cfg(feature = "normalized_dev")] -fn test_phase70c_merge_relay_same_owner_accepted() { - use nyash_rust::mir::join_ir::ownership::{ - OwnershipPlan, OwnershipPlanValidator, RelayVar, ScopeId, - }; - - // Build two plans for L2_A and L2_B, both relaying to L1 - // L2_A plan: relay 'total' to L1 - let mut plan_l2a = OwnershipPlan::new(ScopeId(2)); - plan_l2a.relay_writes.push(RelayVar { - name: "total".to_string(), - owner_scope: ScopeId(1), // L1 owns total - relay_path: vec![ScopeId(2)], // Single hop: L2_A → L1 - }); - - // L2_B plan: relay 'total' to L1 - let mut plan_l2b = OwnershipPlan::new(ScopeId(3)); - plan_l2b.relay_writes.push(RelayVar { - name: "total".to_string(), - owner_scope: ScopeId(1), // L1 owns total (same owner) - relay_path: vec![ScopeId(3)], // Single hop: L2_B → L1 - }); - - // Phase 70-C: Both should be accepted (single-hop relay to same owner) - let result_a = OwnershipPlanValidator::validate_relay_support(&plan_l2a); - assert!( - result_a.is_ok(), - "Phase 70-C: L2_A relay to L1 should be accepted, got: {:?}", - result_a - ); - - let result_b = OwnershipPlanValidator::validate_relay_support(&plan_l2b); - assert!( - result_b.is_ok(), - "Phase 70-C: L2_B relay to L1 should be accepted, got: {:?}", - result_b - ); - - eprintln!("[phase70c/test] Merge relay validation accepted: multiple inner loops → same owner"); -} - -/// Phase 80-D (P3): Test Pattern3 BindingId lookup works -/// -/// Verifies that Pattern3 (if-sum) BindingId registration is operational. -/// For manual fallback detection, run with: NYASH_JOINIR_DEBUG=1 -/// Expected logs: [phase80/p3] Registered ... + [binding_pilot/hit] -/// No [binding_pilot/fallback] should appear. -#[test] -fn test_phase80_p3_bindingid_lookup_works() { - let module = build_pattern3_if_sum_min_structured_for_normalized_dev(); - - // Basic test: Pattern3 should compile and run with BindingId registration - assert_eq!(module.functions.len(), 3, "P3 should have 3 functions"); - - let entry = module.entry.expect("P3 should have entry function"); - assert_eq!(entry.0, 0, "Entry should be function 0"); - - // The fact that this compiles and runs means BindingId registration didn't break anything - // Manual verification: NYASH_JOINIR_DEBUG=1 cargo test test_phase80_p3_bindingid_lookup_works - // Should show [phase80/p3] logs and [binding_pilot/hit], NO [binding_pilot/fallback] -} - -/// Phase 80-D (P3): Test Pattern4 BindingId lookup works -/// -/// Verifies that Pattern4 (continue/Trim) BindingId registration is operational. -/// For manual fallback detection, run with: NYASH_JOINIR_DEBUG=1 -/// Expected logs: [phase80/p4] Registered ... + [binding_pilot/hit] -/// No [binding_pilot/fallback] should appear. -#[test] -fn test_phase80_p4_bindingid_lookup_works() { - let module = build_pattern4_continue_min_structured_for_normalized_dev(); - - // Basic test: Pattern4 should compile and run with BindingId registration - assert_eq!(module.functions.len(), 3, "P4 should have 3 functions"); - - let entry = module.entry.expect("P4 should have entry function"); - assert_eq!(entry.0, 0, "Entry should be function 0"); - - // The fact that this compiles and runs means BindingId registration didn't break anything - // Manual verification: NYASH_JOINIR_DEBUG=1 cargo test test_phase80_p4_bindingid_lookup_works - // Should show [phase80/p4] logs and [binding_pilot/hit], NO [binding_pilot/fallback] -} - -// Phase 79 の "BindingId lookup の E2E(subprocess)" は、Pattern2(DigitPos/Trim)の安定化と一緒に -// Phase 80-D 以降で復活させる(このファイルは Normalized JoinIR の SSOT テストに集中させる)。 - -// ========== Phase 81: Pattern2 ExitLine Contract Verification ========== - -/// Phase 81-B: DigitPos pattern ExitLine contract verification -/// -/// Tests that promoted `is_digit_pos` carrier (ConditionOnly) is correctly -/// excluded from Exit PHI while LoopState carriers (result, i) are included. -#[test] -fn test_phase81_digitpos_exitline_contract() { - // Use existing JsonParser _atoi fixture (DigitPos pattern with indexOf) - let module = build_jsonparser_atoi_structured_for_normalized_dev(); - - // Verify compilation succeeds (ExitLine reconnection works) - assert!( - !module.functions.is_empty(), - "DigitPos pattern should compile successfully" - ); - - // Verify module structure - assert_eq!( - module.functions.len(), - 3, - "DigitPos pattern should have 3 functions (k_entry, k_loop, k_body)" - ); - - // Verify entry function exists - let entry = module - .entry - .expect("DigitPos pattern should have entry function"); - - // Execute to verify correctness (DigitPos: "123" → 123) - let result = run_joinir_vm_bridge_structured_only( - &module, - entry, - &[ - JoinValue::Str("123".to_string()), - JoinValue::Int(3), - ], - ); - - assert_eq!( - result, - JoinValue::Int(123), - "DigitPos pattern should parse '123' correctly" - ); - - // Manual verification: Check that is_digit_pos is NOT in exit_bindings - // NYASH_JOINIR_DEBUG=1 cargo test test_phase81_digitpos_exitline_contract -- --nocapture 2>&1 | grep exit-line - // Should show: [joinir/exit-line] skip ConditionOnly carrier 'is_digit_pos' -} - -/// Phase 81-B: Trim pattern ExitLine contract verification -/// -/// Tests that promoted `is_ch_match` carrier (ConditionOnly) is correctly -/// excluded from Exit PHI while LoopState carriers (i) are included. -#[test] -fn test_phase81_trim_exitline_contract() { - // Use existing JsonParser skip_ws fixture (Trim pattern with whitespace check) - let module = build_jsonparser_skip_ws_structured_for_normalized_dev(); - - // Verify compilation succeeds (ExitLine reconnection works) - assert!( - !module.functions.is_empty(), - "Trim pattern should compile successfully" - ); - - // Verify module structure - assert!( - module.functions.len() >= 3, - "Trim pattern should have at least 3 functions" - ); - - // Verify entry function exists - let entry = module - .entry - .expect("Trim pattern should have entry function"); - - // Execute to verify correctness (skip_ws fixture) - // The skip_ws fixture takes a single int parameter (test size) - let result = run_joinir_vm_bridge_structured_only( - &module, - entry, - &[JoinValue::Int(5)], - ); - - // skip_ws fixture returns the input value (identity function for testing) - assert_eq!( - result, - JoinValue::Int(5), - "Trim pattern fixture should return input value" - ); - - // Manual verification: Check that is_ch_match is NOT in exit_bindings - // NYASH_JOINIR_DEBUG=1 cargo test test_phase81_trim_exitline_contract -- --nocapture 2>&1 | grep exit-line - // Should show: [joinir/exit-line] skip ConditionOnly carrier 'is_ch_match' -} +// moved into tests/normalized_joinir_min/ownership.rs diff --git a/tests/normalized_joinir_min/basic.rs b/tests/normalized_joinir_min/basic.rs new file mode 100644 index 00000000..5be4360b --- /dev/null +++ b/tests/normalized_joinir_min/basic.rs @@ -0,0 +1,444 @@ +use super::*; + +fn build_structured_pattern1() -> JoinModule { + let mut module = JoinModule::new(); + let mut loop_fn = JoinFunction::new( + JoinFuncId::new(1), + "loop_step".to_string(), + vec![ValueId(10)], + ); + + loop_fn.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: ValueId(11), + value: ConstValue::Integer(0), + })); + loop_fn.body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: ValueId(12), + op: BinOpKind::Add, + lhs: ValueId(10), + rhs: ValueId(11), + })); + loop_fn.body.push(JoinInst::Jump { + cont: JoinContId(2), + args: vec![ValueId(12)], + cond: None, // 単純経路: 無条件で k_exit に渡して終了 + }); + + let mut k_exit = JoinFunction::new(JoinFuncId::new(2), "k_exit".to_string(), vec![ValueId(12)]); + k_exit.body.push(JoinInst::Ret { + value: Some(ValueId(12)), + }); + + module.entry = Some(loop_fn.id); + module.add_function(loop_fn); + module.add_function(k_exit); + module +} + +#[test] +fn normalized_pattern1_minimal_smoke() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_structured_pattern1(); + let normalized = normalize_pattern1_minimal(&structured); + + assert_eq!(normalized.phase, JoinIrPhase::Normalized); + assert!(!normalized.env_layouts.is_empty()); + assert!(!normalized.functions.is_empty()); + + let restored = normalized + .to_structured() + .expect("should retain structured backup"); + assert!(restored.is_structured()); + assert_eq!(restored.functions.len(), structured.functions.len()); +} + +#[test] +fn normalized_pattern1_roundtrip_structured_equivalent() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_structured_pattern1(); + let normalized = normalize_pattern1_minimal(&structured); + let reconstructed = normalized_pattern1_to_structured(&normalized); + + assert!(reconstructed.is_structured()); + assert_eq!(reconstructed.functions.len(), structured.functions.len()); + + for (fid, func) in &structured.functions { + let recon = reconstructed + .functions + .get(fid) + .expect("function missing after reconstruction"); + assert_eq!(recon.params.len(), func.params.len()); + } +} + +#[test] +fn normalized_pattern1_exec_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_structured_pattern1(); + let normalized = normalize_pattern1_minimal(&structured); + let reconstructed = normalized_pattern1_to_structured(&normalized); + + let entry = structured.entry.unwrap_or(JoinFuncId::new(1)); + let input = [JoinValue::Int(0)]; + + let result_structured = run_joinir_vm_bridge(&structured, entry, &input, false); + let result_norm = run_joinir_vm_bridge(&reconstructed, entry, &input, false); + + assert_eq!(result_structured, result_norm); +} + +#[test] +fn normalized_pattern1_exec_matches_structured_roundtrip_backup() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_structured_pattern1(); + let normalized = normalize_pattern1_minimal(&structured); + let reconstructed = normalized_pattern1_to_structured(&normalized); + let restored_backup = normalized + .to_structured() + .expect("structured backup should be present"); + + let entry = structured.entry.unwrap_or(JoinFuncId::new(1)); + let input = [JoinValue::Int(0)]; + + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let recon = run_joinir_vm_bridge(&reconstructed, entry, &input, false); + let restored = run_joinir_vm_bridge(&restored_backup, entry, &input, false); + + assert_eq!(base, recon); + assert_eq!(base, restored); +} + +#[test] +fn normalized_pattern2_roundtrip_structure() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_pattern2_minimal_structured(); + let normalized = normalize_pattern2_minimal(&structured); + assert_eq!(normalized.phase, JoinIrPhase::Normalized); + + let reconstructed = normalized_pattern2_to_structured(&normalized); + assert!(reconstructed.is_structured()); + assert_eq!(reconstructed.functions.len(), structured.functions.len()); + + for name in ["main", "loop_step", "k_exit"] { + let original_has = structured.functions.values().any(|f| f.name == name); + let reconstructed_has = reconstructed.functions.values().any(|f| f.name == name); + assert!( + original_has && reconstructed_has, + "expected function '{}' to exist in both modules", + name + ); + } +} + +#[test] +fn normalized_pattern2_jsonparser_parse_number_real_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_jsonparser_parse_number_real_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + let cases = [ + ("42", 0, "42"), + ("123abc", 0, "123"), + ("9", 0, "9"), + ("abc", 0, ""), + ("xx7yy", 2, "7"), + ("007", 0, "007"), + ]; + + for (s, pos, expected) in cases { + let input = [JoinValue::Str(s.to_string()), JoinValue::Int(pos)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s); + assert_eq!( + dev, + JoinValue::Str(expected.to_string()), + "unexpected result for input '{}' (pos={}) (expected num_str)", + s, + pos + ); + } +} + +#[test] +fn normalized_pattern2_exec_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_pattern2_minimal_structured(); + let normalized = normalize_pattern2_minimal(&structured); + let reconstructed = normalized_pattern2_to_structured(&normalized); + + let entry = structured.entry.unwrap_or(JoinFuncId::new(0)); + let input = [JoinValue::Int(0)]; + + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let recon = run_joinir_vm_bridge(&reconstructed, entry, &input, false); + + assert_eq!(base, recon); +} + +#[test] +#[should_panic(expected = "normalize_pattern2_minimal")] +fn normalized_pattern2_rejects_non_pattern2_structured() { + let _ctx = normalized_dev_test_ctx(); + // Pattern1 Structured module should be rejected by Pattern2 normalizer. + let structured = build_structured_pattern1(); + let _ = normalize_pattern2_minimal(&structured); +} + +#[test] +fn normalized_pattern2_real_loop_roundtrip_structure() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_pattern2_break_fixture_structured(); + let normalized = normalize_pattern2_minimal(&structured); + let reconstructed = normalized_pattern2_to_structured(&normalized); + + assert!(reconstructed.is_structured()); + assert_eq!(structured.functions.len(), reconstructed.functions.len()); + assert_eq!(structured.entry, reconstructed.entry); + + let original_names: Vec<_> = structured + .functions + .values() + .map(|f| f.name.clone()) + .collect(); + for name in original_names { + let reconstructed_has = reconstructed.functions.values().any(|f| f.name == name); + assert!( + reconstructed_has, + "function '{}' missing after roundtrip", + name + ); + } +} + +#[test] +fn normalized_pattern2_real_loop_exec_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_pattern2_break_fixture_structured(); + let normalized = normalize_pattern2_minimal(&structured); + let reconstructed = normalized_pattern2_to_structured(&normalized); + + let entry = structured.entry.expect("structured entry required"); + let cases = [0, 1, 3, 5]; + + for n in cases { + let input = [JoinValue::Int(n)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let recon = run_joinir_vm_bridge(&reconstructed, entry, &input, false); + + assert_eq!(base, recon, "mismatch at n={}", n); + let expected_sum = n * (n.saturating_sub(1)) / 2; + assert_eq!( + base, + JoinValue::Int(expected_sum), + "unexpected loop result at n={}", + n + ); + } +} + +#[test] +fn normalized_pattern1_runner_dev_switch_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_structured_pattern1(); + let entry = structured.entry.expect("structured entry required"); + let input = [JoinValue::Int(7)]; + + let base = run_joinir_runner(&structured, entry, &input, false); + let dev = run_joinir_runner(&structured, entry, &input, true); + + assert_eq!(base, dev); + assert_eq!(base, JoinValue::Int(7)); +} + +#[test] +fn normalized_pattern2_runner_dev_switch_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_pattern2_break_fixture_structured(); + let entry = structured.entry.expect("structured entry required"); + let cases = [0, 1, 3, 5]; + + for n in cases { + let input = [JoinValue::Int(n)]; + let base = run_joinir_runner(&structured, entry, &input, false); + let dev = run_joinir_runner(&structured, entry, &input, true); + + assert_eq!(base, dev, "runner mismatch at n={}", n); + let expected_sum = n * (n.saturating_sub(1)) / 2; + assert_eq!( + dev, + JoinValue::Int(expected_sum), + "runner result mismatch at n={}", + n + ); + } +} + +#[test] +fn normalized_pattern2_jsonparser_runner_dev_switch_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_jsonparser_skip_ws_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + let cases = [0, 1, 2, 5]; + + for len in cases { + let input = [JoinValue::Int(len)]; + let base = run_joinir_runner(&structured, entry, &input, false); + let dev = run_joinir_runner(&structured, entry, &input, true); + + assert_eq!(base, dev, "runner mismatch at len={}", len); + assert_eq!(dev, JoinValue::Int(len), "unexpected result at len={}", len); + } +} + +#[test] +fn normalized_pattern2_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_pattern2_break_fixture_structured(); + let entry = structured.entry.expect("structured entry required"); + let cases = [0, 1, 3, 5]; + + for n in cases { + let input = [JoinValue::Int(n)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch at n={}", n); + let expected_sum = n * (n.saturating_sub(1)) / 2; + assert_eq!( + dev, + JoinValue::Int(expected_sum), + "vm bridge result mismatch at n={}", + n + ); + } +} + +#[test] +fn normalized_pattern1_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_structured_pattern1(); + let entry = structured.entry.expect("structured entry required"); + let cases = [0, 5, 7]; + + for n in cases { + let input = [JoinValue::Int(n)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch at n={}", n); + assert_eq!(dev, JoinValue::Int(n), "unexpected result at n={}", n); + } +} + +#[test] +fn normalized_pattern2_jsonparser_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_jsonparser_skip_ws_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + let cases = [0, 1, 2, 5]; + + for len in cases { + let input = [JoinValue::Int(len)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch at len={}", len); + assert_eq!(dev, JoinValue::Int(len), "unexpected result at len={}", len); + } +} + +#[test] +fn normalized_pattern2_jsonparser_skip_ws_real_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_jsonparser_skip_ws_real_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + let cases = [ + (" abc", 0, 3), + ("abc", 0, 0), + (" \t\nx", 0, 3), + (" \t\nx", 2, 3), + ]; + + for (s, pos, expected) in cases { + let input = [JoinValue::Str(s.to_string()), JoinValue::Int(pos)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s); + assert_eq!( + dev, + JoinValue::Int(expected), + "unexpected result for input '{}' (pos={})", + s, + pos + ); + } +} + +#[test] +fn normalized_pattern2_jsonparser_atoi_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_jsonparser_atoi_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + let cases = [("42", 2, 42), ("123abc", 6, 123), ("007", 3, 7), ("", 0, 0)]; + + for (s, len, expected) in cases { + let input = [JoinValue::Str(s.to_string()), JoinValue::Int(len)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s); + assert_eq!( + dev, + JoinValue::Int(expected), + "unexpected result for input '{}'", + s + ); + } +} + +#[test] +fn normalized_pattern2_jsonparser_atoi_real_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_jsonparser_atoi_real_structured_for_normalized_dev(); + if nyash_rust::config::env::joinir_test_debug_enabled() { + eprintln!( + "[joinir/normalized-dev/test] structured jsonparser_atoi_real: {:#?}", + structured + ); + let normalized = normalize_pattern2_minimal(&structured); + eprintln!( + "[joinir/normalized-dev/test] normalized jsonparser_atoi_real: {:#?}", + normalized + ); + } + let entry = structured.entry.expect("structured entry required"); + let cases = [ + ("42", 42), + ("123abc", 123), + ("007", 7), + ("", 0), + ("abc", 0), + ("-42", -42), + ("+7", 7), + ("-0", 0), + ("-12x", -12), + ("+", 0), + ("-", 0), + ]; + + for (s, expected) in cases { + let input = [JoinValue::Str(s.to_string())]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch for input '{}'", s); + assert_eq!( + dev, + JoinValue::Int(expected), + "unexpected result for input '{}'", + s + ); + } +} diff --git a/tests/normalized_joinir_min/ownership.rs b/tests/normalized_joinir_min/ownership.rs new file mode 100644 index 00000000..9e057bfe --- /dev/null +++ b/tests/normalized_joinir_min/ownership.rs @@ -0,0 +1,1231 @@ +use super::*; + +/// Phase 60: Ownership relay threading helpers for P2 (analysis + contract) +/// +/// plan_to_p2_inputs remains Fail-Fast on relay_writes (legacy contract), +/// while plan_to_p2_inputs_with_relay accepts single-hop relay and promotes them +/// to carriers (dev-only). +#[test] +#[cfg(feature = "normalized_dev")] +fn test_phase60_ownership_p2_with_relay_conversion() { + use nyash_rust::mir::join_ir::ownership::{ + plan_to_p2_inputs, plan_to_p2_inputs_with_relay, OwnershipAnalyzer, + }; + use serde_json::json; + + // Create a simple P2 fixture JSON (loop with i and sum) + let json = json!({ + "functions": [{ + "name": "main", + "params": [], + "body": { + "kind": "Block", + "statements": [ + {"kind": "Local", "name": "sum", "init": {"kind": "Const", "value": 0}}, + {"kind": "Local", "name": "i", "init": {"kind": "Const", "value": 0}}, + { + "kind": "Loop", + "condition": { + "kind": "BinaryOp", + "op": "Lt", + "lhs": {"kind": "Var", "name": "i"}, + "rhs": {"kind": "Const", "value": 3} + }, + "body": { + "kind": "Block", + "statements": [ + { + "kind": "If", + "condition": { + "kind": "BinaryOp", + "op": "Ge", + "lhs": {"kind": "Var", "name": "i"}, + "rhs": {"kind": "Const", "value": 2} + }, + "then": {"kind": "Break"} + }, + { + "kind": "Assign", + "target": "sum", + "value": { + "kind": "BinaryOp", + "op": "Add", + "lhs": {"kind": "Var", "name": "sum"}, + "rhs": {"kind": "Var", "name": "i"} + } + }, + { + "kind": "Assign", + "target": "i", + "value": { + "kind": "BinaryOp", + "op": "Add", + "lhs": {"kind": "Var", "name": "i"}, + "rhs": {"kind": "Const", "value": 1} + } + } + ] + } + } + ] + } + }] + }); + + // Run ownership analyzer + let mut analyzer = OwnershipAnalyzer::new(); + let plans = analyzer + .analyze_json(&json) + .expect("analysis should succeed"); + + // Find loop plan (the one that has relay writes to function-owned sum/i) + let loop_plan = plans + .iter() + .find(|p| !p.relay_writes.is_empty()) + .expect("should have a loop plan with relay writes"); + + eprintln!( + "[phase58/test] Loop plan: relay_writes={:?}", + loop_plan.relay_writes + ); + + // Legacy Fail-Fast: relay_writes should be rejected + let result = plan_to_p2_inputs(loop_plan, "i"); + assert!( + result.is_err(), + "Legacy contract: relay_writes should be rejected" + ); + let err = result.unwrap_err(); + assert!( + err.contains("relay_writes not yet supported"), + "Error should mention relay limitation, got: {}", + err + ); + + // Phase 60 dev-only: with_relay should accept and include relay vars as carriers + let inputs_with_relay = + plan_to_p2_inputs_with_relay(loop_plan, "i").expect("with_relay should accept"); + assert!( + inputs_with_relay.carriers.iter().any(|c| c.name == "sum"), + "relay carrier sum should be promoted" + ); + + eprintln!( + "[phase60/test] with_relay carriers={:?}", + inputs_with_relay + .carriers + .iter() + .map(|c| &c.name) + .collect::>() + ); + + // Also test the case where variables ARE owned by loop (future scenario) + // This would work once we support loop-local carriers + let loop_local_json = json!({ + "functions": [{ + "name": "main", + "params": [], + "body": { + "kind": "Loop", + "condition": {"kind": "Const", "value": true}, + "body": { + "kind": "Block", + "statements": [ + {"kind": "Local", "name": "i", "init": {"kind": "Const", "value": 0}}, + {"kind": "Local", "name": "sum", "init": {"kind": "Const", "value": 0}}, + { + "kind": "Assign", + "target": "sum", + "value": { + "kind": "BinaryOp", + "op": "Add", + "lhs": {"kind": "Var", "name": "sum"}, + "rhs": {"kind": "Var", "name": "i"} + } + }, + { + "kind": "Assign", + "target": "i", + "value": { + "kind": "BinaryOp", + "op": "Add", + "lhs": {"kind": "Var", "name": "i"}, + "rhs": {"kind": "Const", "value": 1} + } + }, + {"kind": "Break"} + ] + } + } + }] + }); + + let mut analyzer2 = OwnershipAnalyzer::new(); + let plans2 = analyzer2 + .analyze_json(&loop_local_json) + .expect("loop-local analysis should succeed"); + + // Find loop plan (variables owned by loop, not function) + let loop_plan2 = plans2 + .iter() + .find(|p| !p.owned_vars.is_empty()) + .expect("should have a loop plan with owned vars"); + + eprintln!( + "[phase58/test] Loop-local plan: owned_vars={:?}", + loop_plan2 + .owned_vars + .iter() + .map(|v| &v.name) + .collect::>() + ); + + // Convert to P2 inputs (should succeed - no relay) + let inputs = plan_to_p2_inputs(loop_plan2, "i").expect("should convert successfully"); + + eprintln!("[phase58/test] P2 inputs: {:?}", inputs); + + // Verify: i is skipped (loop var), sum becomes carrier + assert_eq!(inputs.carriers.len(), 1, "Should have 1 carrier (sum)"); + assert_eq!(inputs.carriers[0].name, "sum"); + + eprintln!("[phase60/test] Loop-local conversion verified"); +} + +/// Phase 60: P2 dev-only ownership relay route matches legacy Break lowering. +#[test] +#[cfg(feature = "normalized_dev")] +fn test_phase60_break_lowering_ownership_matches_legacy() { + use nyash_rust::mir::join_ir::frontend::ast_lowerer::lower_break_legacy_for_comparison; + use nyash_rust::mir::join_ir::frontend::ast_lowerer::AstToJoinIrLowerer; + + let _ctx = normalized_dev_test_ctx(); + + let program_json: serde_json::Value = serde_json::from_str(include_str!( + "../../docs/private/roadmap2/phases/phase-34-joinir-frontend/fixtures/loop_frontend_break.program.json" + )) + .expect("fixture json"); + + let mut lowerer_new = AstToJoinIrLowerer::new(); + let structured_new = lowerer_new.lower_program_json(&program_json); + + let mut lowerer_old = AstToJoinIrLowerer::new(); + let structured_old = lower_break_legacy_for_comparison(&mut lowerer_old, &program_json); + + let entry_new = structured_new.entry.expect("new entry"); + let entry_old = structured_old.entry.expect("old entry"); + let input = vec![JoinValue::Int(5)]; + + let out_old = run_joinir_vm_bridge(&structured_old, entry_old, &input, false); + let out_new = run_joinir_vm_bridge(&structured_new, entry_new, &input, false); + + assert_eq!( + out_old, out_new, + "ownership relay dev route must match legacy output" + ); +} + +/// Phase 59: P3 with outer-owned carriers (relay case) should fail-fast +#[test] +#[cfg(feature = "normalized_dev")] +fn test_phase59_ownership_p3_relay_failfast() { + use nyash_rust::mir::join_ir::ownership::{plan_to_p3_inputs, OwnershipAnalyzer}; + use serde_json::json; + + // P3 where sum/count are defined OUTSIDE the loop -> relay + let json = json!({ + "functions": [{ + "name": "main", + "params": [], + "body": { + "kind": "Block", + "statements": [ + {"kind": "Local", "name": "sum", "init": {"kind": "Const", "value": 0}}, + {"kind": "Local", "name": "count", "init": {"kind": "Const", "value": 0}}, + {"kind": "Local", "name": "i", "init": {"kind": "Const", "value": 0}}, + { + "kind": "Loop", + "condition": { + "kind": "BinaryOp", "op": "Lt", + "lhs": {"kind": "Var", "name": "i"}, + "rhs": {"kind": "Const", "value": 10} + }, + "body": { + "kind": "Block", + "statements": [ + { + "kind": "If", + "condition": { + "kind": "BinaryOp", "op": "Gt", + "lhs": {"kind": "Var", "name": "i"}, + "rhs": {"kind": "Const", "value": 0} + }, + "then": { + "kind": "Block", + "statements": [ + {"kind": "Assign", "target": "sum", "value": { + "kind": "BinaryOp", "op": "Add", + "lhs": {"kind": "Var", "name": "sum"}, + "rhs": {"kind": "Var", "name": "i"} + }}, + {"kind": "Assign", "target": "count", "value": { + "kind": "BinaryOp", "op": "Add", + "lhs": {"kind": "Var", "name": "count"}, + "rhs": {"kind": "Const", "value": 1} + }} + ] + } + }, + {"kind": "Assign", "target": "i", "value": { + "kind": "BinaryOp", "op": "Add", + "lhs": {"kind": "Var", "name": "i"}, + "rhs": {"kind": "Const", "value": 1} + }} + ] + } + } + ] + } + }] + }); + + let mut analyzer = OwnershipAnalyzer::new(); + let plans = analyzer + .analyze_json(&json) + .expect("analysis should succeed"); + + // Find loop plan + let loop_plan = plans + .iter() + .find(|p| !p.relay_writes.is_empty()) + .expect("loop should have relay_writes for sum/count"); + + // Verify relay_writes contains sum and count + assert!(loop_plan.relay_writes.iter().any(|r| r.name == "sum")); + assert!(loop_plan.relay_writes.iter().any(|r| r.name == "count")); + + // plan_to_p3_inputs should fail + let result = plan_to_p3_inputs(loop_plan, "i"); + assert!(result.is_err(), "Should fail-fast on relay_writes"); + assert!( + result + .unwrap_err() + .contains("relay_writes not yet supported for P3"), + "Error should mention P3 relay limitation" + ); + + eprintln!("[phase59/test] P3 relay fail-fast verified"); +} + +/// Phase 59: P3 with loop-local carriers should succeed +#[test] +#[cfg(feature = "normalized_dev")] +fn test_phase59_ownership_p3_loop_local_success() { + use nyash_rust::mir::join_ir::ownership::{plan_to_p3_inputs, OwnershipAnalyzer}; + use serde_json::json; + + // P3 where sum/count are defined INSIDE the loop -> no relay + let json = json!({ + "functions": [{ + "name": "main", + "params": [], + "body": { + "kind": "Loop", + "condition": {"kind": "Const", "value": true}, + "body": { + "kind": "Block", + "statements": [ + {"kind": "Local", "name": "i", "init": {"kind": "Const", "value": 0}}, + {"kind": "Local", "name": "sum", "init": {"kind": "Const", "value": 0}}, + {"kind": "Local", "name": "count", "init": {"kind": "Const", "value": 0}}, + { + "kind": "If", + "condition": { + "kind": "BinaryOp", "op": "Gt", + "lhs": {"kind": "Var", "name": "i"}, + "rhs": {"kind": "Const", "value": 0} + }, + "then": { + "kind": "Block", + "statements": [ + {"kind": "Assign", "target": "sum", "value": { + "kind": "BinaryOp", "op": "Add", + "lhs": {"kind": "Var", "name": "sum"}, + "rhs": {"kind": "Var", "name": "i"} + }}, + {"kind": "Assign", "target": "count", "value": { + "kind": "BinaryOp", "op": "Add", + "lhs": {"kind": "Var", "name": "count"}, + "rhs": {"kind": "Const", "value": 1} + }} + ] + } + }, + {"kind": "Break"} + ] + } + } + }] + }); + + let mut analyzer = OwnershipAnalyzer::new(); + let plans = analyzer + .analyze_json(&json) + .expect("analysis should succeed"); + + // Find loop plan with owned vars + let loop_plan = plans + .iter() + .find(|p| p.owned_vars.iter().any(|v| v.name == "sum")) + .expect("loop should own sum"); + + // No relay + assert!( + loop_plan.relay_writes.is_empty(), + "No relay for loop-local vars" + ); + + // plan_to_p3_inputs should succeed + let inputs = plan_to_p3_inputs(loop_plan, "i").expect("Should succeed"); + + eprintln!("[phase59/test] P3 inputs: {:?}", inputs); + + // sum and count should be carriers + assert!(inputs.carriers.iter().any(|c| c.name == "sum")); + assert!(inputs.carriers.iter().any(|c| c.name == "count")); + assert_eq!( + inputs.carriers.len(), + 2, + "Should have 2 carriers (sum and count)" + ); + + eprintln!("[phase59/test] P3 loop-local conversion verified: sum and count correctly extracted as carriers"); +} + +/// Phase 60: Program(JSON v0) fixture (selfhost_if_sum_p3) should produce relay_writes and convert with single-hop relay. +#[test] +#[cfg(feature = "normalized_dev")] +fn test_phase60_ownership_p3_program_json_fixture_with_relay() { + use nyash_rust::mir::join_ir::ownership::{plan_to_p3_inputs_with_relay, OwnershipAnalyzer}; + + let program_json: serde_json::Value = serde_json::from_str(include_str!( + "../../docs/private/roadmap2/phases/normalized_dev/fixtures/selfhost_if_sum_p3.program.json" + )) + .expect("fixture json"); + + let mut analyzer = OwnershipAnalyzer::new(); + let plans = analyzer + .analyze_json(&program_json) + .expect("Program(JSON v0) analysis should succeed"); + + let loop_plan = plans + .iter() + .find(|p| !p.relay_writes.is_empty()) + .expect("expected a loop plan with relay_writes"); + + // i/sum/count are defined outside the loop but updated in the loop body -> relay_writes + assert!(loop_plan.relay_writes.iter().any(|r| r.name == "i")); + assert!(loop_plan.relay_writes.iter().any(|r| r.name == "sum")); + assert!(loop_plan.relay_writes.iter().any(|r| r.name == "count")); + + let inputs = plan_to_p3_inputs_with_relay(loop_plan, "i").expect("with_relay should succeed"); + + let mut carriers: Vec<&str> = inputs.carriers.iter().map(|c| c.name.as_str()).collect(); + carriers.sort(); + assert_eq!(carriers, vec!["count", "sum"]); + + // n is read-only in loop condition -> capture + condition_capture + assert!(inputs.captures.iter().any(|n| n == "n")); + assert!(inputs.condition_captures.iter().any(|n| n == "n")); +} + +/// Phase 64: P3 production route with ownership analysis (dev-only integration test) +/// +/// This test verifies that `analyze_loop()` API works for simple P3 loops and that +/// multi-hop relay is correctly rejected with Fail-Fast error. +#[test] +#[cfg(feature = "normalized_dev")] +fn test_phase64_p3_ownership_prod_integration() { + use nyash_rust::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; + use nyash_rust::mir::join_ir::ownership::analyze_loop; + + // Helper: Create literal integer node + fn lit_i(i: i64) -> ASTNode { + ASTNode::Literal { + value: LiteralValue::Integer(i), + span: Span::unknown(), + } + } + + // Helper: Create variable node + fn var(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: Span::unknown(), + } + } + + // Simple P3 loop: loop(i < 10) { local sum=0; local i=0; sum = sum + i; i = i + 1 } + let condition = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(var("i")), + right: Box::new(lit_i(10)), + span: Span::unknown(), + }; + + let body = vec![ + ASTNode::Local { + variables: vec!["sum".to_string()], + initial_values: vec![Some(Box::new(lit_i(0)))], + span: Span::unknown(), + }, + ASTNode::Local { + variables: vec!["i".to_string()], + initial_values: vec![Some(Box::new(lit_i(0)))], + span: Span::unknown(), + }, + ASTNode::Assignment { + target: Box::new(var("sum")), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(var("sum")), + right: Box::new(var("i")), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ASTNode::Assignment { + target: Box::new(var("i")), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(var("i")), + right: Box::new(lit_i(1)), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ]; + + // No parent-defined variables (both sum and i are loop-local) + let parent_defined = vec![]; + + // Analyze the loop + let plan = + analyze_loop(&condition, &body, &parent_defined).expect("P3 analysis should succeed"); + + // Verify basic plan structure + assert!( + !plan.owned_vars.is_empty(), + "Should have owned vars (sum, i)" + ); + + // Find sum and i in owned_vars + let sum_var = plan + .owned_vars + .iter() + .find(|v| v.name == "sum") + .expect("sum should be owned"); + let i_var = plan + .owned_vars + .iter() + .find(|v| v.name == "i") + .expect("i should be owned"); + + // Both should be written + assert!(sum_var.is_written, "sum should be written"); + assert!(i_var.is_written, "i should be written"); + + // i is used in condition -> condition_only + assert!(i_var.is_condition_only, "i should be condition_only"); + + // sum is NOT used in condition + assert!( + !sum_var.is_condition_only, + "sum should NOT be condition_only" + ); + + // No relay writes (all variables are loop-local) + assert!( + plan.relay_writes.is_empty(), + "No relay writes for loop-local variables" + ); + + // Verify single-hop relay constraint: if relay_writes is non-empty, verify single-hop + for relay in &plan.relay_writes { + assert!( + relay.relay_path.len() <= 1, + "Multi-hop relay should be rejected (got relay_path.len = {})", + relay.relay_path.len() + ); + } + + eprintln!( + "[phase64/test] P3 ownership analysis succeeded: {} owned vars, {} relay writes", + plan.owned_vars.len(), + plan.relay_writes.len() + ); +} + +/// Phase 64: Multi-hop relay detection test +/// +/// Verifies that `analyze_loop()` correctly identifies multi-hop relay patterns. +/// The actual rejection (Fail-Fast) happens in `check_ownership_plan_consistency()`, +/// not in `analyze_loop()` itself. +#[test] +#[cfg(feature = "normalized_dev")] +fn test_phase64_p3_multihop_relay_detection() { + use nyash_rust::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; + use nyash_rust::mir::join_ir::ownership::AstOwnershipAnalyzer; + + // Helper functions + fn lit_i(i: i64) -> ASTNode { + ASTNode::Literal { + value: LiteralValue::Integer(i), + span: Span::unknown(), + } + } + + fn var(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: Span::unknown(), + } + } + + // Function with nested loops: + // function test() { + // local sum = 0; + // local i = 0; + // loop(i < 5) { + // local j = 0; + // loop(j < 3) { + // sum = sum + 1; // Multi-hop relay: sum defined in function scope + // j = j + 1; + // } + // i = i + 1; + // } + // } + let inner_condition = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(var("j")), + right: Box::new(lit_i(3)), + span: Span::unknown(), + }; + + let inner_body = vec![ + ASTNode::Assignment { + target: Box::new(var("sum")), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(var("sum")), + right: Box::new(lit_i(1)), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ASTNode::Assignment { + target: Box::new(var("j")), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(var("j")), + right: Box::new(lit_i(1)), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ]; + + let outer_condition = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(var("i")), + right: Box::new(lit_i(5)), + span: Span::unknown(), + }; + + let outer_body = vec![ + ASTNode::Local { + variables: vec!["j".to_string()], + initial_values: vec![Some(Box::new(lit_i(0)))], + span: Span::unknown(), + }, + ASTNode::Loop { + condition: Box::new(inner_condition), + body: inner_body, + span: Span::unknown(), + }, + ASTNode::Assignment { + target: Box::new(var("i")), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(var("i")), + right: Box::new(lit_i(1)), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ]; + + let function_body = vec![ + ASTNode::Local { + variables: vec!["sum".to_string()], + initial_values: vec![Some(Box::new(lit_i(0)))], + span: Span::unknown(), + }, + ASTNode::Local { + variables: vec!["i".to_string()], + initial_values: vec![Some(Box::new(lit_i(0)))], + span: Span::unknown(), + }, + ASTNode::Loop { + condition: Box::new(outer_condition), + body: outer_body, + span: Span::unknown(), + }, + ]; + + let function = ASTNode::FunctionDeclaration { + name: "test".to_string(), + params: vec![], + body: function_body, + is_static: false, + is_override: false, + span: Span::unknown(), + }; + + // Analyze the entire function to detect nested loop relays + let mut analyzer = AstOwnershipAnalyzer::new(); + let plans = analyzer + .analyze_ast(&function) + .expect("Function analysis should succeed"); + + // Find the inner loop plan (should have multi-hop relay for 'sum') + let inner_loop_plan = plans + .iter() + .find(|p| { + // Inner loop should have relay write for 'sum' with relay_path.len() > 1 + p.relay_writes + .iter() + .any(|r| r.name == "sum" && r.relay_path.len() > 1) + }) + .expect("Expected inner loop plan with multi-hop relay for 'sum'"); + + let sum_relay = inner_loop_plan + .relay_writes + .iter() + .find(|r| r.name == "sum") + .expect("sum should be a relay write in inner loop"); + + // Verify multi-hop relay (relay_path should include both inner and outer loop scopes) + assert!( + sum_relay.relay_path.len() > 1, + "sum should have multi-hop relay (got relay_path.len = {})", + sum_relay.relay_path.len() + ); + + eprintln!( + "[phase64/test] Multi-hop relay detected: sum relay_path.len = {}", + sum_relay.relay_path.len() + ); + eprintln!( + "[phase64/test] This pattern would be rejected by check_ownership_plan_consistency()" + ); +} + +/// Phase 70-A: Verify that multihop relay runtime unsupported error has standardized tag. +/// +/// This test builds an OwnershipPlan with multihop relay and verifies that +/// `check_ownership_plan_consistency()` returns an error with the standard tag +/// `[ownership/relay:runtime_unsupported]`. +#[test] +#[cfg(feature = "normalized_dev")] +fn test_phase70a_multihop_relay_runtime_unsupported_tag() { + use nyash_rust::mir::join_ir::ownership::{ + CapturedVar, OwnershipPlan, RelayVar, ScopeId, ScopeOwnedVar, + }; + + // Build a plan with multihop relay (relay_path.len() == 2) + let mut plan = OwnershipPlan::new(ScopeId(2)); // Inner loop scope + plan.owned_vars.push(ScopeOwnedVar { + name: "j".to_string(), + is_written: true, + is_condition_only: false, + }); + plan.relay_writes.push(RelayVar { + name: "sum".to_string(), + owner_scope: ScopeId(0), // Function scope + relay_path: vec![ScopeId(2), ScopeId(1)], // 2 hops: inner → outer + }); + plan.captures.push(CapturedVar { + name: "i".to_string(), + owner_scope: ScopeId(0), + }); + + // Call the plan layer function (Phase 66 accepts multihop) + // The runtime check (check_ownership_plan_consistency) is private in pattern3_with_if_phi.rs, + // so we test the plan layer acceptance here. + use nyash_rust::mir::join_ir::ownership::plan_to_p3_inputs_with_relay; + + let result = plan_to_p3_inputs_with_relay(&plan, "j"); + + // Phase 66: plan_to_p3_inputs_with_relay NOW ACCEPTS multihop (relay_path.len() > 1) + // So this should PASS (not Err) + assert!( + result.is_ok(), + "Phase 66: plan_to_p3_inputs_with_relay should accept multihop relay" + ); + + // Verify the relay is in the output + let inputs = result.unwrap(); + let relay = inputs + .carriers + .iter() + .find(|c| c.name == "sum") + .expect("sum should be in carriers (via relay conversion)"); + eprintln!( + "[phase70a/test] Multihop relay accepted in plan layer: sum role={:?}", + relay.role + ); + + // The RUNTIME check (check_ownership_plan_consistency in pattern3_with_if_phi.rs) + // is what produces [ownership/relay:runtime_unsupported]. + // That function is private, so we document that the tag exists and + // will be hit when P3 lowering encounters this plan at runtime. + eprintln!("[phase70a/test] Runtime would fail with [ownership/relay:runtime_unsupported] tag"); +} + +/// Phase 70-B: Test that simple passthrough multihop relay is accepted. +/// +/// This test verifies the structural detection logic in OwnershipPlanValidator +/// correctly identifies supported multihop patterns (pure passthrough, no self-updates). +#[test] +#[cfg(feature = "normalized_dev")] +fn test_phase70b_multihop_relay_simple_passthrough_succeeds() { + use nyash_rust::mir::join_ir::ownership::{ + OwnershipPlan, OwnershipPlanValidator, RelayVar, ScopeId, ScopeOwnedVar, + }; + + // Build a plan for innermost loop (L3) with multihop relay + // L3 writes to 'counter' owned by L1, relayed through L2 + let mut plan_l3 = OwnershipPlan::new(ScopeId(3)); // Inner loop scope + plan_l3.owned_vars.push(ScopeOwnedVar { + name: "i".to_string(), // loop variable + is_written: true, + is_condition_only: true, + }); + plan_l3.relay_writes.push(RelayVar { + name: "counter".to_string(), + owner_scope: ScopeId(1), // L1 owns counter + relay_path: vec![ScopeId(3), ScopeId(2)], // 2 hops: L3 → L2 → L1 + }); + // No owned_vars for 'counter' - pure passthrough from L3's perspective + + // Phase 70-B: This should be accepted (passthrough pattern) + let result = OwnershipPlanValidator::validate_relay_support(&plan_l3); + assert!( + result.is_ok(), + "Phase 70-B: Simple passthrough multihop should be accepted, got: {:?}", + result + ); + + eprintln!("[phase70b/test] Simple passthrough multihop relay accepted (3-layer loop)"); +} + +/// Phase 70-B: Test that unsupported multihop patterns are still rejected. +/// +/// This test verifies that complex patterns (e.g., self-conflict where a scope +/// both owns and relays the same variable) are still rejected with the standard tag. +#[test] +#[cfg(feature = "normalized_dev")] +fn test_phase70b_multihop_relay_self_conflict_rejected() { + use nyash_rust::mir::join_ir::ownership::{ + OwnershipPlan, OwnershipPlanValidator, RelayVar, ScopeId, ScopeOwnedVar, + }; + + // Build a plan where L3 both owns and relays 'counter' (conflict) + // L3 → L2 → L1 (multihop with self-conflict at L3) + let mut plan_l3 = OwnershipPlan::new(ScopeId(3)); // Inner loop scope + plan_l3.owned_vars.push(ScopeOwnedVar { + name: "counter".to_string(), // L3 owns counter + is_written: true, + is_condition_only: false, + }); + plan_l3.relay_writes.push(RelayVar { + name: "counter".to_string(), // L3 also relays counter (conflict) + owner_scope: ScopeId(1), + relay_path: vec![ScopeId(3), ScopeId(2)], // L3 → L2 → L1 (multihop) + }); + + // Phase 70-B: This should be rejected (self-conflict) + let result = OwnershipPlanValidator::validate_relay_support(&plan_l3); + assert!( + result.is_err(), + "Phase 70-B: Self-conflict multihop should be rejected" + ); + + let err = result.unwrap_err(); + assert!( + err.contains("[ownership/relay:runtime_unsupported]"), + "Error should contain standard tag: {}", + err + ); + + eprintln!("[phase70b/test] Self-conflict multihop relay correctly rejected"); +} + +/// Phase 70-C: Merge Relay (multiple inner loops → same owner) +/// +/// This test verifies that OwnershipAnalyzer correctly detects the "merge relay" +/// pattern where multiple inner loops update the same owner variable. +/// +/// Structure: +/// ```text +/// loop L1 { +/// local total = 0 // owned by L1 +/// loop L2_A { +/// total++ // L2_A → L1 relay +/// } +/// loop L2_B { +/// total += 10 // L2_B → L1 relay +/// } +/// } +/// // L1 exit: merge both L2_A and L2_B's updates to 'total' +/// ``` +#[test] +#[cfg(feature = "normalized_dev")] +fn test_phase70c_merge_relay_multiple_inner_loops_detected() { + use nyash_rust::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; + use nyash_rust::mir::join_ir::ownership::AstOwnershipAnalyzer; + + // Build AST: loop L1 { local total=0; loop L2_A { total++ } loop L2_B { total+=10 } } + fn var(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: Span::unknown(), + } + } + fn lit_i(i: i64) -> ASTNode { + ASTNode::Literal { + value: LiteralValue::Integer(i), + span: Span::unknown(), + } + } + fn lit_true() -> ASTNode { + ASTNode::Literal { + value: LiteralValue::Bool(true), + span: Span::unknown(), + } + } + + let ast = ASTNode::FunctionDeclaration { + name: "test_merge_relay".to_string(), + params: vec![], + body: vec![ + // L1: outer loop + ASTNode::Loop { + condition: Box::new(lit_true()), + body: vec![ + // local total = 0 (L1 owns) + ASTNode::Local { + variables: vec!["total".to_string()], + initial_values: vec![Some(Box::new(lit_i(0)))], + span: Span::unknown(), + }, + // L2_A: first inner loop (total++) + ASTNode::Loop { + condition: Box::new(lit_true()), + body: vec![ + ASTNode::Assignment { + target: Box::new(var("total")), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(var("total")), + right: Box::new(lit_i(1)), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ASTNode::Break { + span: Span::unknown(), + }, + ], + span: Span::unknown(), + }, + // L2_B: second inner loop (total += 10) + ASTNode::Loop { + condition: Box::new(lit_true()), + body: vec![ + ASTNode::Assignment { + target: Box::new(var("total")), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(var("total")), + right: Box::new(lit_i(10)), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ASTNode::Break { + span: Span::unknown(), + }, + ], + span: Span::unknown(), + }, + ASTNode::Break { + span: Span::unknown(), + }, + ], + span: Span::unknown(), + }, + ], + is_static: false, + is_override: false, + span: Span::unknown(), + }; + + let mut analyzer = AstOwnershipAnalyzer::new(); + let plans = analyzer.analyze_ast(&ast).expect("analysis should succeed"); + + // Find L1 (owner of 'total') + let l1_plan = plans + .iter() + .find(|p| p.owned_vars.iter().any(|v| v.name == "total")) + .expect("expected L1 plan with owned total"); + let l1_scope_id = l1_plan.scope_id; + + // Find L2_A and L2_B (both relay 'total' to L1) + let relay_plans: Vec<_> = plans + .iter() + .filter(|p| p.relay_writes.iter().any(|r| r.name == "total") && p.scope_id != l1_scope_id) + .collect(); + + // Verify: two inner loops relay to the same owner + assert_eq!( + relay_plans.len(), + 2, + "Phase 70-C: Expected 2 inner loops relaying 'total' to L1, got {}", + relay_plans.len() + ); + + for (idx, plan) in relay_plans.iter().enumerate() { + let relay = plan + .relay_writes + .iter() + .find(|r| r.name == "total") + .expect("expected 'total' in relay_writes"); + + // Verify: both relay to the same owner (L1) + assert_eq!( + relay.owner_scope, l1_scope_id, + "Phase 70-C: relay {} should have owner_scope = L1", + idx + ); + + // Verify: single-hop relay (L2 → L1) + assert_eq!( + relay.relay_path.len(), + 1, + "Phase 70-C: relay {} should have single-hop path", + idx + ); + + // Verify: relay_path[0] is this scope (L2_A or L2_B) + assert_eq!( + relay.relay_path[0], plan.scope_id, + "Phase 70-C: relay {} relay_path[0] must be this scope", + idx + ); + } + + // Verify: L1 owned_vars contains 'total' and is_written=true + let total_var = l1_plan + .owned_vars + .iter() + .find(|v| v.name == "total") + .expect("expected 'total' in L1 owned_vars"); + assert!( + total_var.is_written, + "Phase 70-C: L1's 'total' should be is_written=true" + ); + + eprintln!("[phase70c/test] Merge relay pattern detected: 2 inner loops → same owner variable"); +} + +/// Phase 70-C: Merge Relay validation acceptance +/// +/// This test verifies that OwnershipPlanValidator ACCEPTS merge relay patterns +/// (multiple inner loops → same owner) because they are PERMITTED with owner merge. +#[test] +#[cfg(feature = "normalized_dev")] +fn test_phase70c_merge_relay_same_owner_accepted() { + use nyash_rust::mir::join_ir::ownership::{ + OwnershipPlan, OwnershipPlanValidator, RelayVar, ScopeId, + }; + + // Build two plans for L2_A and L2_B, both relaying to L1 + // L2_A plan: relay 'total' to L1 + let mut plan_l2a = OwnershipPlan::new(ScopeId(2)); + plan_l2a.relay_writes.push(RelayVar { + name: "total".to_string(), + owner_scope: ScopeId(1), // L1 owns total + relay_path: vec![ScopeId(2)], // Single hop: L2_A → L1 + }); + + // L2_B plan: relay 'total' to L1 + let mut plan_l2b = OwnershipPlan::new(ScopeId(3)); + plan_l2b.relay_writes.push(RelayVar { + name: "total".to_string(), + owner_scope: ScopeId(1), // L1 owns total (same owner) + relay_path: vec![ScopeId(3)], // Single hop: L2_B → L1 + }); + + // Phase 70-C: Both should be accepted (single-hop relay to same owner) + let result_a = OwnershipPlanValidator::validate_relay_support(&plan_l2a); + assert!( + result_a.is_ok(), + "Phase 70-C: L2_A relay to L1 should be accepted, got: {:?}", + result_a + ); + + let result_b = OwnershipPlanValidator::validate_relay_support(&plan_l2b); + assert!( + result_b.is_ok(), + "Phase 70-C: L2_B relay to L1 should be accepted, got: {:?}", + result_b + ); + + eprintln!("[phase70c/test] Merge relay validation accepted: multiple inner loops → same owner"); +} + +/// Phase 80-D (P3): Test Pattern3 BindingId lookup works +/// +/// Verifies that Pattern3 (if-sum) BindingId registration is operational. +/// For manual fallback detection, run with: NYASH_JOINIR_DEBUG=1 +/// Expected logs: [phase80/p3] Registered ... + [binding_pilot/hit] +/// No [binding_pilot/fallback] should appear. +#[test] +fn test_phase80_p3_bindingid_lookup_works() { + let module = build_pattern3_if_sum_min_structured_for_normalized_dev(); + + // Basic test: Pattern3 should compile and run with BindingId registration + assert_eq!(module.functions.len(), 3, "P3 should have 3 functions"); + + let entry = module.entry.expect("P3 should have entry function"); + assert_eq!(entry.0, 0, "Entry should be function 0"); + + // The fact that this compiles and runs means BindingId registration didn't break anything + // Manual verification: NYASH_JOINIR_DEBUG=1 cargo test test_phase80_p3_bindingid_lookup_works + // Should show [phase80/p3] logs and [binding_pilot/hit], NO [binding_pilot/fallback] +} + +/// Phase 80-D (P3): Test Pattern4 BindingId lookup works +/// +/// Verifies that Pattern4 (continue/Trim) BindingId registration is operational. +/// For manual fallback detection, run with: NYASH_JOINIR_DEBUG=1 +/// Expected logs: [phase80/p4] Registered ... + [binding_pilot/hit] +/// No [binding_pilot/fallback] should appear. +#[test] +fn test_phase80_p4_bindingid_lookup_works() { + let module = build_pattern4_continue_min_structured_for_normalized_dev(); + + // Basic test: Pattern4 should compile and run with BindingId registration + assert_eq!(module.functions.len(), 3, "P4 should have 3 functions"); + + let entry = module.entry.expect("P4 should have entry function"); + assert_eq!(entry.0, 0, "Entry should be function 0"); + + // The fact that this compiles and runs means BindingId registration didn't break anything + // Manual verification: NYASH_JOINIR_DEBUG=1 cargo test test_phase80_p4_bindingid_lookup_works + // Should show [phase80/p4] logs and [binding_pilot/hit], NO [binding_pilot/fallback] +} + +// Phase 79 の "BindingId lookup の E2E(subprocess)" は、Pattern2(DigitPos/Trim)の安定化と一緒に +// Phase 80-D 以降で復活させる(このファイルは Normalized JoinIR の SSOT テストに集中させる)。 + +// ========== Phase 81: Pattern2 ExitLine Contract Verification ========== + +/// Phase 81-B: DigitPos pattern ExitLine contract verification +/// +/// Tests that promoted `is_digit_pos` carrier (ConditionOnly) is correctly +/// excluded from Exit PHI while LoopState carriers (result, i) are included. +#[test] +fn test_phase81_digitpos_exitline_contract() { + // Use existing JsonParser _atoi fixture (DigitPos pattern with indexOf) + let module = build_jsonparser_atoi_structured_for_normalized_dev(); + + // Verify compilation succeeds (ExitLine reconnection works) + assert!( + !module.functions.is_empty(), + "DigitPos pattern should compile successfully" + ); + + // Verify module structure + assert_eq!( + module.functions.len(), + 3, + "DigitPos pattern should have 3 functions (k_entry, k_loop, k_body)" + ); + + // Verify entry function exists + let entry = module + .entry + .expect("DigitPos pattern should have entry function"); + + // Execute to verify correctness (DigitPos: "123" → 123) + let result = run_joinir_vm_bridge_structured_only( + &module, + entry, + &[JoinValue::Str("123".to_string()), JoinValue::Int(3)], + ); + + assert_eq!( + result, + JoinValue::Int(123), + "DigitPos pattern should parse '123' correctly" + ); + + // Manual verification: Check that is_digit_pos is NOT in exit_bindings + // NYASH_JOINIR_DEBUG=1 cargo test test_phase81_digitpos_exitline_contract -- --nocapture 2>&1 | grep exit-line + // Should show: [joinir/exit-line] skip ConditionOnly carrier 'is_digit_pos' +} + +/// Phase 81-B: Trim pattern ExitLine contract verification +/// +/// Tests that promoted `is_ch_match` carrier (ConditionOnly) is correctly +/// excluded from Exit PHI while LoopState carriers (i) are included. +#[test] +fn test_phase81_trim_exitline_contract() { + // Use existing JsonParser skip_ws fixture (Trim pattern with whitespace check) + let module = build_jsonparser_skip_ws_structured_for_normalized_dev(); + + // Verify compilation succeeds (ExitLine reconnection works) + assert!( + !module.functions.is_empty(), + "Trim pattern should compile successfully" + ); + + // Verify module structure + assert!( + module.functions.len() >= 3, + "Trim pattern should have at least 3 functions" + ); + + // Verify entry function exists + let entry = module + .entry + .expect("Trim pattern should have entry function"); + + // Execute to verify correctness (skip_ws fixture) + // The skip_ws fixture takes a single int parameter (test size) + let result = run_joinir_vm_bridge_structured_only(&module, entry, &[JoinValue::Int(5)]); + + // skip_ws fixture returns the input value (identity function for testing) + assert_eq!( + result, + JoinValue::Int(5), + "Trim pattern fixture should return input value" + ); + + // Manual verification: Check that is_ch_match is NOT in exit_bindings + // NYASH_JOINIR_DEBUG=1 cargo test test_phase81_trim_exitline_contract -- --nocapture 2>&1 | grep exit-line + // Should show: [joinir/exit-line] skip ConditionOnly carrier 'is_ch_match' +} diff --git a/tests/normalized_joinir_min/selfhost.rs b/tests/normalized_joinir_min/selfhost.rs new file mode 100644 index 00000000..7e666a7b --- /dev/null +++ b/tests/normalized_joinir_min/selfhost.rs @@ -0,0 +1,149 @@ +use super::*; + +#[test] +fn normalized_selfhost_token_scan_p2_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_selfhost_token_scan_p2_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + let cases = [0, 1, 3, 5]; + + for n in cases { + let input = [JoinValue::Int(n)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch for n={}", n); + assert_eq!( + dev, + JoinValue::Int(n), + "unexpected result for selfhost_token_scan_p2 n={}", + n + ); + } +} + +#[test] +fn normalized_selfhost_token_scan_p2_accum_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_selfhost_token_scan_p2_accum_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + let cases = [0, 1, 3, 5]; + + for n in cases { + let input = [JoinValue::Int(n)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!( + base, dev, + "vm bridge mismatch for selfhost_token_scan_p2_accum n={}", + n + ); + } +} + +#[test] +fn normalized_selfhost_if_sum_p3_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_selfhost_if_sum_p3_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + let cases = [0, 1, 3, 4, 5]; + + for n in cases { + let input = [JoinValue::Int(n)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!( + base, dev, + "vm bridge mismatch for selfhost_if_sum_p3 n={}", + n + ); + assert_eq!(dev, JoinValue::Int(expected_selfhost_if_sum_p3(n))); + } +} + +#[test] +fn normalized_selfhost_if_sum_p3_ext_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_selfhost_if_sum_p3_ext_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + let cases = [0, 1, 3, 4, 5]; + + for n in cases { + let input = [JoinValue::Int(n)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!( + base, dev, + "vm bridge mismatch for selfhost_if_sum_p3_ext n={}", + n + ); + assert_eq!(dev, JoinValue::Int(expected_selfhost_if_sum_p3_ext(n))); + } +} + +fn expected_selfhost_if_sum_p3(n: i64) -> i64 { + if n <= 1 { + return 0; + } + let sum = (n - 1) * n / 2; + let count = n - 1; + sum + count +} + +fn expected_selfhost_if_sum_p3_ext(n: i64) -> i64 { + if n <= 0 { + return 0; + } + // i=0: sum += 1 + // i=1..n-1: sum += i, count += 1 + let sum = 1 + (n - 1) * n / 2; + let count = n - 1; + sum + count +} + +/// Phase 53: selfhost args-parse P2 (practical variation with string carrier) +#[test] +fn normalized_selfhost_args_parse_p2_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_selfhost_args_parse_p2_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + // Test different argc values: 0, 1, 2, 3 + let cases = [0, 1, 2, 3]; + + for argc in cases { + let input = [JoinValue::Int(argc)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!( + base, dev, + "vm bridge mismatch for selfhost_args_parse_p2 argc={}", + argc + ); + } +} + +/// Phase 53: selfhost stmt-count P3 (practical variation with multi-branch if-else) +#[test] +fn normalized_selfhost_stmt_count_p3_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_selfhost_stmt_count_p3_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + // Test different statement counts: 0, 5, 10, 15 + let cases = [0, 5, 10, 15]; + + for n in cases { + let input = [JoinValue::Int(n)]; + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!( + base, dev, + "vm bridge mismatch for selfhost_stmt_count_p3 n={}", + n + ); + } +} diff --git a/tests/normalized_joinir_min/shapes.rs b/tests/normalized_joinir_min/shapes.rs new file mode 100644 index 00000000..ea25fdca --- /dev/null +++ b/tests/normalized_joinir_min/shapes.rs @@ -0,0 +1,462 @@ +use super::*; + +#[test] +fn normalized_pattern3_if_sum_minimal_runner_dev_switch_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_pattern3_if_sum_min_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + + // phase212_if_sum_min.hako 相当: sum=2 になることを期待 + let input: [JoinValue; 0] = []; + + let base = run_joinir_runner(&structured, entry, &input, false); + let dev = run_joinir_runner(&structured, entry, &input, true); + + assert_eq!(base, dev, "runner mismatch for P3 minimal if-sum"); + assert_eq!( + dev, + JoinValue::Int(2), + "unexpected result for P3 minimal if-sum (expected sum=2)", + ); +} + +#[test] +fn normalized_pattern3_if_sum_multi_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_pattern3_if_sum_multi_min_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + let input = [JoinValue::Int(0)]; + + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch for P3 if-sum multi"); + assert_eq!( + dev, + JoinValue::Int(2), + "unexpected result for P3 if-sum multi (expected sum=2)" + ); +} + +#[test] +fn normalized_pattern3_json_if_sum_min_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_pattern3_json_if_sum_min_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + let input = [JoinValue::Int(0)]; + + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch for P3 json if-sum"); + assert_eq!( + dev, + JoinValue::Int(10), + "unexpected result for P3 json if-sum (expected sum=10)" + ); +} + +#[cfg(feature = "normalized_dev")] +#[test] +fn test_phase46_canonical_set_includes_p2_mid() { + use nyash_rust::mir::join_ir::normalized::shape_guard::{ + is_canonical_shape, NormalizedDevShape, + }; + + // Phase 46: Verify P2-Mid patterns are canonical + assert!(is_canonical_shape(&NormalizedDevShape::JsonparserAtoiReal)); + assert!(is_canonical_shape( + &NormalizedDevShape::JsonparserParseNumberReal + )); + assert!(is_canonical_shape( + &NormalizedDevShape::Pattern3IfSumMinimal + )); + assert!(is_canonical_shape(&NormalizedDevShape::Pattern3IfSumMulti)); + assert!(is_canonical_shape(&NormalizedDevShape::Pattern3IfSumJson)); + + // Verify P2-Core patterns still canonical + assert!(is_canonical_shape(&NormalizedDevShape::Pattern2Mini)); + assert!(is_canonical_shape( + &NormalizedDevShape::JsonparserSkipWsMini + )); + assert!(is_canonical_shape( + &NormalizedDevShape::JsonparserSkipWsReal + )); + assert!(is_canonical_shape(&NormalizedDevShape::JsonparserAtoiMini)); +} + +/// Phase 47-A: Test P3 minimal normalization +#[test] +fn test_phase47a_pattern3_if_sum_minimal_normalization() { + use nyash_rust::mir::join_ir::normalized::normalize_pattern3_if_sum_minimal; + + let module = build_pattern3_if_sum_min_structured_for_normalized_dev(); + + // Test that normalization succeeds (includes shape detection internally) + let result = normalize_pattern3_if_sum_minimal(&module); + assert!( + result.is_ok(), + "P3 normalization should succeed (shape detection + normalization): {:?}", + result.err() + ); + + let normalized = result.unwrap(); + assert_eq!( + normalized.functions.len(), + module.functions.len(), + "Normalized function count should match Structured" + ); + + // Verify normalized module has proper phase + assert_eq!( + normalized.phase, + nyash_rust::mir::join_ir::JoinIrPhase::Normalized, + "Normalized module should have Normalized phase" + ); +} + +/// Phase 47-A: Test P3 VM execution (basic smoke test) +#[test] +fn test_phase47a_pattern3_if_sum_minimal_runner() { + let module = build_pattern3_if_sum_min_structured_for_normalized_dev(); + + // Basic test: module should be runnable through JoinIR runner + // This test verifies the P3 fixture is valid and generates proper JoinIR + assert_eq!(module.functions.len(), 3, "P3 should have 3 functions"); + + let entry = module.entry.expect("P3 should have entry function"); + assert_eq!(entry.0, 0, "Entry should be function 0"); +} + +/// Phase 48-A: Test P4 minimal normalization +#[test] +fn test_phase48a_pattern4_continue_minimal_normalization() { + use nyash_rust::mir::join_ir::normalized::normalize_pattern4_continue_minimal; + + let module = build_pattern4_continue_min_structured_for_normalized_dev(); + + // Test that normalization succeeds (includes shape detection internally) + let result = normalize_pattern4_continue_minimal(&module); + assert!( + result.is_ok(), + "P4 normalization should succeed (shape detection + normalization): {:?}", + result.err() + ); + + let normalized = result.unwrap(); + assert_eq!( + normalized.functions.len(), + module.functions.len(), + "Normalized function count should match Structured" + ); + + // Verify normalized module has proper phase + assert_eq!( + normalized.phase, + nyash_rust::mir::join_ir::JoinIrPhase::Normalized, + "Normalized module should have Normalized phase" + ); +} + +/// Phase 48-A: Test P4 VM execution (basic smoke test) +#[test] +fn test_phase48a_pattern4_continue_minimal_runner() { + let module = build_pattern4_continue_min_structured_for_normalized_dev(); + + // Basic test: module should be runnable through JoinIR runner + // This test verifies the P4 fixture is valid and generates proper JoinIR + assert_eq!(module.functions.len(), 3, "P4 should have 3 functions"); + + let entry = module.entry.expect("P4 should have entry function"); + assert_eq!(entry.0, 0, "Entry should be function 0"); +} + +/// Phase 48-A: Test P4 minimal Runner dev switch matches Structured +#[test] +fn test_normalized_pattern4_continue_minimal_runner_dev_switch_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_pattern4_continue_min_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + + // pattern4_continue_min fixture: acc=4 (skipped i==2, so counted 0,1,3,4) + let input = [JoinValue::Int(5)]; // n = 5 + + let base = run_joinir_runner(&structured, entry, &input, false); + let dev = run_joinir_runner(&structured, entry, &input, true); + + assert_eq!(base, dev, "runner mismatch for P4 minimal continue"); + assert_eq!( + dev, + JoinValue::Int(4), + "unexpected result for P4 minimal continue (expected acc=4, skipped i==2)", + ); +} + +/// Phase 48-A: Test P4 minimal VM Bridge direct matches Structured +#[test] +fn test_normalized_pattern4_continue_minimal_vm_bridge_direct_matches_structured() { + let _ctx = normalized_dev_test_ctx(); + let structured = build_pattern4_continue_min_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + + // pattern4_continue_min fixture: acc=4 (skipped i==2) + let input = [JoinValue::Int(5)]; // n = 5 + + let base = run_joinir_vm_bridge(&structured, entry, &input, false); + let dev = run_joinir_vm_bridge(&structured, entry, &input, true); + + assert_eq!(base, dev, "vm bridge mismatch for P4 minimal continue"); + assert_eq!( + dev, + JoinValue::Int(4), + "unexpected result for P4 minimal continue (expected acc=4)", + ); +} + +/// Phase 48-C: P4 minimal should use canonical normalized route even without env +#[test] +fn test_normalized_pattern4_continue_minimal_canonical_matches_structured() { + let structured = build_pattern4_continue_min_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + + let input = [JoinValue::Int(5)]; + let structured_res = run_joinir_vm_bridge_structured_only(&structured, entry, &input); + let canonical = run_joinir_vm_bridge(&structured, entry, &input, false); + + assert_eq!( + structured_res, canonical, + "canonical P4 minimal result mismatch" + ); + assert_eq!(canonical, JoinValue::Int(4)); +} + +/// Phase 48-B: JsonParser _parse_array continue skip_ws (dev-only) VM Bridge comparison +#[test] +fn test_normalized_pattern4_jsonparser_parse_array_continue_skip_ws_vm_bridge_direct_matches_structured( +) { + let _ctx = normalized_dev_test_ctx(); + let structured = build_jsonparser_parse_array_continue_skip_ws_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + + // Fixture mirrors pattern4_continue_min: skip i == 2 + let cases = [3, 5, 7]; + + for n in cases { + let args = [JoinValue::Int(n)]; + let base = run_joinir_vm_bridge(&structured, entry, &args, false); + let dev = run_joinir_vm_bridge(&structured, entry, &args, true); + assert_eq!( + base, dev, + "vm bridge mismatch for array continue case n={}", + n + ); + } +} + +/// Phase 48-C: JsonParser _parse_array continue skip_ws canonical route should match Structured +#[test] +fn test_normalized_pattern4_jsonparser_parse_array_continue_skip_ws_canonical_matches_structured() { + let structured = build_jsonparser_parse_array_continue_skip_ws_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + + let cases = [3, 5, 7]; + for n in cases { + let args = [JoinValue::Int(n)]; + let structured_res = run_joinir_vm_bridge_structured_only(&structured, entry, &args); + let canonical = run_joinir_vm_bridge(&structured, entry, &args, false); + assert_eq!( + structured_res, canonical, + "canonical array continue mismatch n={}", + n + ); + } +} + +/// Phase 48-B: JsonParser _parse_object continue skip_ws (dev-only) VM Bridge comparison +#[test] +fn test_normalized_pattern4_jsonparser_parse_object_continue_skip_ws_vm_bridge_direct_matches_structured( +) { + let _ctx = normalized_dev_test_ctx(); + let structured = build_jsonparser_parse_object_continue_skip_ws_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + + // Fixture mirrors pattern4_continue_min: skip i == 2 + let cases = [4, 6, 8]; + + for n in cases { + let args = [JoinValue::Int(n)]; + let base = run_joinir_vm_bridge(&structured, entry, &args, false); + let dev = run_joinir_vm_bridge(&structured, entry, &args, true); + assert_eq!( + base, dev, + "vm bridge mismatch for object continue case n={}", + n + ); + } +} + +/// Phase 48-C: JsonParser _parse_object continue skip_ws canonical route should match Structured +#[test] +fn test_normalized_pattern4_jsonparser_parse_object_continue_skip_ws_canonical_matches_structured() +{ + let structured = build_jsonparser_parse_object_continue_skip_ws_structured_for_normalized_dev(); + let entry = structured.entry.expect("structured entry required"); + + let cases = [4, 6, 8]; + for n in cases { + let args = [JoinValue::Int(n)]; + let structured_res = run_joinir_vm_bridge_structured_only(&structured, entry, &args); + let canonical = run_joinir_vm_bridge(&structured, entry, &args, false); + assert_eq!( + structured_res, canonical, + "canonical object continue mismatch n={}", + n + ); + } +} + +/// Phase 54: False positive observation test - P2 structural axis discrimination +/// +/// This test validates that structural detection can discriminate between +/// canonical P2 and selfhost P2 shapes using structural features alone. +#[test] +fn test_phase54_structural_axis_discrimination_p2() { + use nyash_rust::mir::join_ir::normalized::shape_guard::{ + detect_shapes, is_canonical_shape, NormalizedDevShape, + }; + + // Canonical P2 shapes + let canonical_p2_shapes = vec![ + build_pattern2_minimal_structured(), + build_jsonparser_skip_ws_structured_for_normalized_dev(), + ]; + + // Selfhost P2 shapes (Phase 53) + let selfhost_p2_shapes = vec![ + build_selfhost_args_parse_p2_structured_for_normalized_dev(), + build_selfhost_token_scan_p2_structured_for_normalized_dev(), + ]; + + // Canonical P2 should be detected as canonical, NOT selfhost + for canonical in &canonical_p2_shapes { + let shapes = detect_shapes(canonical); + let has_canonical = shapes.iter().any(|s| is_canonical_shape(s)); + let has_selfhost_p2 = shapes.iter().any(|s| { + matches!( + s, + NormalizedDevShape::SelfhostArgsParseP2 + | NormalizedDevShape::SelfhostTokenScanP2 + | NormalizedDevShape::SelfhostTokenScanP2Accum + ) + }); + + assert!( + has_canonical, + "canonical P2 should be detected as canonical: {:?}", + shapes + ); + assert!( + !has_selfhost_p2, + "canonical P2 should NOT be detected as selfhost: {:?}", + shapes + ); + } + + // Selfhost P2 should be detected as selfhost, NOT canonical + for selfhost in &selfhost_p2_shapes { + let shapes = detect_shapes(selfhost); + let has_canonical = shapes.iter().any(|s| is_canonical_shape(s)); + let has_selfhost_p2 = shapes.iter().any(|s| { + matches!( + s, + NormalizedDevShape::SelfhostArgsParseP2 + | NormalizedDevShape::SelfhostTokenScanP2 + | NormalizedDevShape::SelfhostTokenScanP2Accum + ) + }); + + assert!( + !has_canonical, + "selfhost P2 should NOT be detected as canonical: {:?}", + shapes + ); + assert!( + has_selfhost_p2, + "selfhost P2 should be detected as selfhost (with name guard): {:?}", + shapes + ); + } +} + +/// Phase 54: False positive observation test - P3 structural axis discrimination +/// +/// This test validates that structural detection can discriminate between +/// canonical P3 and selfhost P3 shapes using structural features alone. +#[test] +fn test_phase54_structural_axis_discrimination_p3() { + use nyash_rust::mir::join_ir::normalized::shape_guard::{ + detect_shapes, is_canonical_shape, NormalizedDevShape, + }; + + // Canonical P3 shapes + let canonical_p3_shapes = vec![ + build_pattern3_if_sum_min_structured_for_normalized_dev(), + build_pattern3_if_sum_multi_min_structured_for_normalized_dev(), + ]; + + // Selfhost P3 shapes (Phase 53) + let selfhost_p3_shapes = vec![ + build_selfhost_stmt_count_p3_structured_for_normalized_dev(), + build_selfhost_if_sum_p3_structured_for_normalized_dev(), + ]; + + // Canonical P3 should be detected as canonical, NOT selfhost + for canonical in &canonical_p3_shapes { + let shapes = detect_shapes(canonical); + let has_canonical = shapes.iter().any(|s| is_canonical_shape(s)); + let has_selfhost_p3 = shapes.iter().any(|s| { + matches!( + s, + NormalizedDevShape::SelfhostStmtCountP3 + | NormalizedDevShape::SelfhostIfSumP3 + | NormalizedDevShape::SelfhostIfSumP3Ext + ) + }); + + assert!( + has_canonical, + "canonical P3 should be detected as canonical: {:?}", + shapes + ); + assert!( + !has_selfhost_p3, + "canonical P3 should NOT be detected as selfhost: {:?}", + shapes + ); + } + + // Selfhost P3 should be detected as selfhost, NOT canonical + for selfhost in &selfhost_p3_shapes { + let shapes = detect_shapes(selfhost); + let has_canonical = shapes.iter().any(|s| is_canonical_shape(s)); + let has_selfhost_p3 = shapes.iter().any(|s| { + matches!( + s, + NormalizedDevShape::SelfhostStmtCountP3 + | NormalizedDevShape::SelfhostIfSumP3 + | NormalizedDevShape::SelfhostIfSumP3Ext + ) + }); + + assert!( + !has_canonical, + "selfhost P3 should NOT be detected as canonical: {:?}", + shapes + ); + assert!( + has_selfhost_p3, + "selfhost P3 should be detected as selfhost (with name guard): {:?}", + shapes + ); + } +}