//! Iter it.2: pin tests for the structural-recursion guardedness //! pass + the it.2-only `tail:true` grandfather + the first real //! `Diverge` effect injection. //! //! Spec: `docs/specs/2026-05-15-iteration-discipline.md`. Mirrors the //! it.1 `loop_recur_pin.rs` harness verbatim (same inline //! `check_fixture` helper, no `mod common`). use ailang_check::check_workspace; use ailang_surface::load_workspace; use std::path::PathBuf; fn examples_dir() -> PathBuf { let manifest = env!("CARGO_MANIFEST_DIR"); PathBuf::from(manifest) .parent().expect("CARGO_MANIFEST_DIR has a parent (crates/ailang-check)") .parent().expect("CARGO_MANIFEST_DIR has a grandparent (crates/)") .join("examples") } /// Load the named fixture under `examples/`, run check_workspace, and /// return the diagnostic code list (one entry per Diagnostic). fn check_fixture(fixture_name: &str) -> Vec { let path = examples_dir().join(fixture_name); let ws = load_workspace(&path) .unwrap_or_else(|e| panic!("workspace `{fixture_name}` must load: {e:?}")); let diags = check_workspace(&ws); diags.iter().map(|d| d.code.clone()).collect() } #[test] fn structural_list_len_is_clean() { // Plain non-tail recursion on a Cons sub-component typechecks // clean: structurally guarded ⇒ pure + total, no diagnostics. assert!( check_fixture("struct_rec_list_len.ail").is_empty(), "expected zero diagnostics for structural list length" ); } #[test] fn foldl_accumulator_is_structural_and_clean() { // A foldl-shape accumulator walk (structural on the tail, // unconstrained accumulator) classifies as structural recursion // and stays clean + Diverge-free (spec D1). assert!( check_fixture("struct_rec_foldl_sum.ail").is_empty(), "expected zero diagnostics for foldl-shape accumulator walk" ); } #[test] fn non_structural_self_call_is_rejected() { // A self-call passing the un-decreased parameter at every // candidate position, not `tail`-marked, fires // `non-structural-recursion`. assert!( check_fixture("test_non_structural_recursion.ail.json") .contains(&"non-structural-recursion".to_string()), "expected non-structural-recursion diagnostic" ); } #[test] fn tree_forest_mutual_is_clean() { // Mutual tree/forest recursion over one ADT family (the // cross-reference edge unions {Tree, Forest}) classifies as a // mutual structural group: clean, Diverge-free. assert!( check_fixture("struct_rec_tree_forest.ail").is_empty(), "expected zero diagnostics for same-family mutual recursion" ); } #[test] fn mutual_cross_family_is_rejected() { // Mutual recursion whose members' structural params lie in two // unrelated ADT families is not a valid mutual structural group // (DD-3): the cross-call is an unguarded recursive call. assert!( check_fixture("test_mutual_cross_family.ail.json") .contains(&"non-structural-recursion".to_string()), "expected non-structural-recursion for cross-family mutual recursion" ); } #[test] fn loop_fn_declaring_diverge_is_clean() { // A loop-bearing fn that declares `!Diverge` in its effect row // reconciles clean (DD-4 / D2). assert!( check_fixture("loop_needs_diverge.ail").is_empty(), "expected zero diagnostics for loop fn declaring !Diverge" ); } #[test] fn loop_fn_missing_diverge_is_rejected() { // A loop-bearing fn missing `!Diverge` raises the existing // `undeclared-effect` (no new diagnostic variant; DD-4). assert!( check_fixture("test_loop_missing_diverge.ail.json") .contains(&"undeclared-effect".to_string()), "expected undeclared-effect for loop fn missing !Diverge" ); } #[test] fn structural_recursion_is_diverge_free() { // Structural recursion injects no effect: a structural list walk // with no `loop` and no declared `!Diverge` stays clean. assert!( check_fixture("struct_rec_list_len.ail").is_empty(), "structural recursion must be Diverge-free" ); } #[test] fn non_structural_recursion_code_is_registered() { // A CheckError::NonStructuralRecursion must map to the kebab code. // This step only pins the code() arm exists and returns the exact // string; fixture-level behaviour is pinned in Task 2+. use ailang_check::CheckError; let e = CheckError::NonStructuralRecursion { callee: "f".into(), arg: "n".into(), }; assert_eq!(e.code(), "non-structural-recursion"); }