#![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_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, build_jsonparser_skip_ws_real_structured_for_normalized_dev, build_jsonparser_skip_ws_structured_for_normalized_dev, build_pattern2_break_fixture_structured, build_pattern2_minimal_structured, build_pattern3_if_sum_min_structured_for_normalized_dev, build_pattern3_if_sum_multi_min_structured_for_normalized_dev, build_pattern3_json_if_sum_min_structured_for_normalized_dev, build_pattern4_continue_min_structured_for_normalized_dev, build_selfhost_args_parse_p2_structured_for_normalized_dev, build_selfhost_if_sum_p3_ext_structured_for_normalized_dev, build_selfhost_if_sum_p3_structured_for_normalized_dev, build_selfhost_stmt_count_p3_structured_for_normalized_dev, 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::ValueId; use std::collections::BTreeMap; fn normalized_dev_test_ctx() -> NormalizedTestContext<'static> { let ctx = test_ctx(); assert!( normalized_dev_enabled(), "Phase 40: normalized_dev must be enabled for normalized_* tests (feature + NYASH_JOINIR_NORMALIZED_DEV_RUN=1)" ); ctx } fn assert_normalized_dev_ready() { assert!( normalized_dev_enabled(), "Phase 40: normalized_dev must be enabled for normalized_* tests (feature + NYASH_JOINIR_NORMALIZED_DEV_RUN=1)" ); } fn run_joinir_runner( module: &JoinModule, entry: JoinFuncId, args: &[JoinValue], normalized: bool, ) -> JoinValue { let _guard = NormalizedDevEnvGuard::new(normalized); if normalized { assert_normalized_dev_ready(); } let mut vm = MirInterpreter::new(); run_joinir_function(&mut vm, module, entry, args).expect("JoinIR runner should succeed") } fn run_joinir_vm_bridge( module: &JoinModule, entry: JoinFuncId, args: &[JoinValue], normalized: bool, ) -> JoinValue { let _guard = NormalizedDevEnvGuard::new(normalized); if normalized { assert_normalized_dev_ready(); } run_joinir_via_vm(module, entry, args).expect("JoinIR→MIR execution should succeed") } fn run_joinir_vm_bridge_structured_only( module: &JoinModule, entry: JoinFuncId, args: &[JoinValue], ) -> JoinValue { let mir = convert_join_module_to_mir_with_meta(module, &BTreeMap::new()).expect("structured bridge"); let mut vm = MirInterpreter::new(); let entry_name = format!("join_func_{}", entry.0); let vm_args: Vec = args.iter().cloned().map(|v| v.into_vm_value()).collect(); let result = vm .execute_function_with_args(&mir, &entry_name, &vm_args) .expect("VM execution should succeed"); 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)], ); 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()); // 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' }