//! End-to-end test: load example module, compile to binary, run it. //! //! This test guards the most important property of the entire pipeline: //! AST → typecheck → LLVM IR → clang → binary → correct stdout. use std::path::Path; use std::process::Command; fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } fn build_and_run(example: &str) -> String { // Workspace root is two levels above the crate manifest. let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let src = workspace.join("examples").join(example); let tmp = std::env::temp_dir().join(format!( "ailang_e2e_{}_{}", example.replace('.', "_"), std::process::id() )); std::fs::create_dir_all(&tmp).unwrap(); let out = tmp.join("bin"); let status = Command::new(ail_bin()) .args(["build", src.to_str().unwrap(), "-o"]) .arg(&out) .status() .expect("ail build failed to run"); assert!(status.success(), "ail build failed for {example}"); let output = Command::new(&out).output().expect("execute binary"); assert!( output.status.success(), "binary {} exited non-zero", out.display() ); String::from_utf8(output.stdout).expect("stdout utf8") } /// Iter 18b: build with an explicit `--alloc=` and run. /// Mirrors [`build_and_run`] but threads the allocator selector through /// to `ail build`. Used by the alloc-equivalence tests that assert the /// `gc` and `rc` paths produce byte-identical stdout. fn build_and_run_with_alloc(example: &str, alloc: &str) -> String { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let src = workspace.join("examples").join(example); let tmp = std::env::temp_dir().join(format!( "ailang_e2e_alloc_{}_{}_{}", alloc, example.replace('.', "_"), std::process::id() )); std::fs::create_dir_all(&tmp).unwrap(); let out = tmp.join("bin"); let status = Command::new(ail_bin()) .args([ "build", src.to_str().unwrap(), &format!("--alloc={alloc}"), "-o", ]) .arg(&out) .status() .expect("ail build failed to run"); assert!( status.success(), "ail build --alloc={alloc} failed for {example}" ); let output = Command::new(&out).output().expect("execute binary"); assert!( output.status.success(), "binary {} (--alloc={alloc}) exited non-zero", out.display() ); String::from_utf8(output.stdout).expect("stdout utf8") } #[test] fn sum_1_to_10_is_55() { let stdout = build_and_run("sum.ail.json"); assert_eq!(stdout.trim(), "55"); } /// Guards block tracking in codegen: max3 has nested `if`s, and wrong /// phi block tracking would produce a wrong result here. #[test] fn max3_picks_largest() { let stdout = build_and_run("max3.ail.json"); assert_eq!(stdout.trim(), "17"); } #[test] fn hello_world_str_lit() { let stdout = build_and_run("hello.ail.json"); assert_eq!(stdout.trim(), "Hello, AILang."); } /// Guards ADT codegen + match: recursive list, sum_list via match on Cons/Nil. #[test] fn list_sum_via_match() { let stdout = build_and_run("list.ail.json"); assert_eq!(stdout.trim(), "42"); } /// Iter 7: first-class function references — `apply(inc, 41)` must /// produce 42, exercising fn-typed parameters and indirect call. #[test] fn higher_order_apply_inc() { let stdout = build_and_run("hof.ail.json"); assert_eq!(stdout.trim(), "42"); } /// Iter 8b: lambdas with capture. `let n = 3 in apply(\x. x + n, 39)` /// must print 42 — the lambda captures `n` from the enclosing `let`, /// the env struct is malloc'd, the closure pair is passed to `apply`, /// and apply's indirect call unpacks both halves. #[test] fn closure_captures_let_n() { let stdout = build_and_run("closure.ail.json"); assert_eq!(stdout.trim(), "42"); } /// Iter 9 dogfood: a non-trivial program that combines ADTs, recursion, /// closure-without-capture, fn-typed parameters, IO effects, and `let`- /// sequencing of effectful sub-expressions in a pattern-match arm. /// `map (\x. x * 2)` over `[1, 2, 3]` then print each element. #[test] fn list_map_doubles_then_prints() { let stdout = build_and_run("list_map.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["2", "4", "6"]); } /// Iter 14a: end-to-end exercise of parameterised ADTs through a /// polymorphic higher-order fn. `data List a` plus /// `map : forall a b. ((a) -> b, List) -> List` recursive, /// instantiated at `(Int, Int)`. Guards: forall with two type vars + /// recursive call inside its own body + ctor construction with /// substituted field types + match-arm bindings flowing into an /// effectful tail call. #[test] fn list_map_poly_inc_then_prints() { let stdout = build_and_run("list_map_poly.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["2", "3", "4"]); } /// Iter 14f: stress the Boehm conservative GC integration end-to-end. /// `build 50` performs 50 `Cons` allocations via `GC_malloc`; `sum_list` /// walks the resulting `List Int` and prints the sum (1275 = 50*51/2). /// If GC is wired wrongly — missing `-lgc`, missing /// `declare ptr @GC_malloc(i64)`, libgc misbehaving on this build host — /// the test fails at link or run time. The match-on-Bool against /// `(== n 0)` shape is used because `(pat-lit 0)` against an `Int` /// scrutinee with a wildcard fallback is not currently in the grammar. #[test] fn gc_handles_recursive_list_construction() { let stdout = build_and_run("gc_stress.ail.json"); assert_eq!( stdout.trim(), "1275", "expected sum [50,49,..,1] = 1275; \ GC_malloc / -lgc integration may be broken" ); } /// Iter 17a: per-fn arena via stack `alloca` for non-escaping /// allocations. The fixture's `peek` and `count` fns each build a /// `Box(_)` whose payload is dropped via a wildcard pattern; the /// allocation is fully consumed locally and never flows to a fn /// arg, ctor field, or the fn return. Escape analysis must flag /// these allocations as non-escaping; codegen must lower them with /// `alloca` instead of `@GC_malloc`. Two assertions: /// (1) stdout matches the documented expected outputs; /// (2) the emitted IR contains at least one `alloca i8, i64 16` /// for the Box and zero `@GC_malloc` calls inside `peek`/`count`. #[test] fn iter17a_local_box_alloca() { let stdout = build_and_run("escape_local_demo.ail.json"); assert_eq!( stdout.lines().collect::>(), vec!["42", "42", "5", "0"], "escape_local_demo stdout drift" ); // IR check: peek and count should each contain `alloca` and no // `@GC_malloc`. Emit, then split into per-fn bodies and inspect. let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let src = workspace .join("examples") .join("escape_local_demo.ail.json"); let tmp = std::env::temp_dir().join(format!( "ailang_iter17a_alloca_{}", std::process::id() )); std::fs::create_dir_all(&tmp).unwrap(); let out_ll = tmp.join("escape_local_demo.ll"); let status = Command::new(ail_bin()) .args(["emit-ir", src.to_str().unwrap(), "-o"]) .arg(&out_ll) .status() .expect("ail emit-ir failed to run"); assert!(status.success(), "ail emit-ir failed"); let ir = std::fs::read_to_string(&out_ll).expect("read emitted IR"); for fn_name in ["peek", "count"] { let header = format!("@ail_escape_local_demo_{fn_name}("); let start = ir .find(&header) .unwrap_or_else(|| panic!("fn {fn_name} not found in IR")); let after_header = &ir[start..]; let end = after_header .find("\n}\n") .expect("fn body terminator not found"); let body = &after_header[..end]; assert!( body.contains("alloca i8, i64 16"), "fn {fn_name} should contain `alloca i8, i64 16`; body:\n{body}" ); assert!( !body.contains("@GC_malloc"), "fn {fn_name} should NOT contain `@GC_malloc` (allocation \ must be alloca'd); body:\n{body}" ); } } /// Iter 14e: `tail: true` annotation on `print_list`'s recursive /// call must reach LLVM as a `musttail call`. Asserted by emitting IR /// for list_map_poly and grepping for the exact instruction. This is /// the only direct evidence that the type-system marker actually /// influences codegen — the e2e test above only checks observed /// stdout, which `musttail` does not change. #[test] fn iter14e_print_list_recursion_emits_musttail() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let src = workspace.join("examples").join("list_map_poly.ail.json"); let tmp = std::env::temp_dir().join(format!( "ailang_iter14e_musttail_{}", std::process::id() )); std::fs::create_dir_all(&tmp).unwrap(); let out_ll = tmp.join("list_map_poly.ll"); let status = Command::new(ail_bin()) .args(["emit-ir", src.to_str().unwrap(), "-o"]) .arg(&out_ll) .status() .expect("ail emit-ir failed to run"); assert!(status.success(), "ail emit-ir failed"); let ir = std::fs::read_to_string(&out_ll).expect("read emitted IR"); // Find the musttail call to print_list inside the print_list body. // The exact line shape: // %vN = musttail call i8 @ail_list_map_poly_print_list(ptr %vM) let has_musttail = ir.lines().any(|l| { l.contains("musttail call") && l.contains("@ail_list_map_poly_print_list(") }); assert!( has_musttail, "expected `musttail call ... @ail_list_map_poly_print_list(...)` \ in emitted IR; not found.\nIR:\n{ir}" ); // Defence-in-depth: the musttail call must be immediately followed // by a `ret i8 %v...`. We assert the next non-empty line is a ret. let mut lines = ir.lines(); while let Some(line) = lines.next() { if line.contains("musttail call") && line.contains("@ail_list_map_poly_print_list(") { let next = lines.next().unwrap_or("").trim(); assert!( next.starts_with("ret i8 "), "musttail call must be immediately followed by `ret i8 ...`; \ got: `{next}`" ); return; } } panic!("musttail call line not found (sanity check)"); } /// Iter 11 dogfood: insertion sort over an 11-element IntList. /// Exercises `<=`, `if`, mutual-leaf recursion (`insert` and `sort`), /// nested ctor construction, and the Iter 10 seq operator inside /// print_list. Validates that the language handles deeper ADT /// recursion + branching without surprises. #[test] fn insertion_sort_orders_list() { let stdout = build_and_run("sort.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!( lines, vec!["1", "1", "2", "3", "3", "4", "5", "5", "5", "6", "9"], ); } /// Iter 12b: polymorphism end-to-end. `id : forall a. (a) -> a` /// gets called with `42` (Int) and `true` (Bool); each instantiation /// triggers a separate specialised LLVM fn. Output: 42, then "true". #[test] fn polymorphic_id_at_int_and_bool() { let stdout = build_and_run("poly_id.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["42", "true"]); } /// Iter 12b: polymorphism with a function-typed parameter. /// `apply : forall a b. ((a) -> b, a) -> b` invoked as /// `apply(succ, 41) == 42`. Exercises the closure-pair ABI inside a /// monomorphised body (a fn-typed param survives substitution). #[test] fn polymorphic_apply_with_fn_param() { let stdout = build_and_run("poly_apply.ail.json"); assert_eq!(stdout.trim(), "42"); } /// Iter 13b: round-trip a primitive through a parameterised ADT and a /// polymorphic-fn boundary. `type Box[a] = MkBox(a)` plus /// `unbox : forall a. (Box) -> a` plus `print_int(unbox(MkBox(42)))`. /// Guards: ctor lower with substituted LLVM field types, /// match-arm field bindings carrying the substituted AILang type /// down into a polymorphic fn body. #[test] fn parameterised_box_round_trip() { let stdout = build_and_run("box.ail.json"); assert_eq!(stdout.trim(), "42"); } /// Iter 13b: pattern-match on `Maybe`. `or_else(Some(7), 99) == 7` /// then `or_else(None, 99) == 99`. Guards: match-arm field /// substitution at a parameterised ctor (the `Some(x)` arm must bind /// `x : Int`, not `x : a`). #[test] fn parameterised_maybe_match() { let stdout = build_and_run("maybe_int.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["7", "99"]); } /// Iter 15a: cross-module reference to a parameterised ADT, including /// its ctors and a polymorphic combinator instantiated at `(Int, Int)`. /// `std_maybe_demo` imports `std_maybe`, qualifies the type-name slot /// of every `term-ctor` (`std_maybe.Maybe`), and exercises /// `from_maybe`, `is_some`, `is_none`, and `map_maybe` once each. /// Property protected: the qualified-only convention (Decision 6's /// architectural pin extended to types and ctors per the brief) /// flows end-to-end through check, codegen, and runtime. #[test] fn cross_module_maybe_demo() { let stdout = build_and_run("std_maybe_demo.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["7", "99", "true", "true", "42"]); } /// Iter 15b: drives the polymorphic `std_list` combinators end-to-end. /// Exercises a recursive cross-module ADT (`std_list.List`) consumed /// from a separate module that also imports `std_maybe`. Guards the /// `qualify_local_types` propagation in both `Term::Ctor` synth and /// `Term::Match` field binding — without it, recursive ctor fields /// (`Cons a (List a)`) stay unqualified at the cross-module use site /// and fail to unify against the qualified scrutinee args. #[test] fn std_list_demo() { let stdout = build_and_run("std_list_demo.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!( lines, vec![ "5", "false", "true", "1", "4", "10", "5", "2", "2", "15", "15" ] ); } /// Iter 15c: empirical stress test for `std_list`'s folds at N=1000. /// /// Property protected: `fold_left`'s tail-call marker actually /// survives monomorphisation and lowers to `musttail call` in the /// monomorphised `fold_left__I_I` body, so a 1000-element fold runs /// in constant stack and does not segfault. `fold_right` is /// constructor-blocked (the recursive call sits as the second arg /// to `f`, not in tail position) and runs at a 1000-deep stack — /// well within the default 8MB Linux stack budget. `build` itself /// is recursive-but-unmarked (Cons-blocked) and likewise relies on /// the stack budget at this scale. Both folds over the same /// commutative-associative `+` produce the same sum (500500 = /// 1000 * 1001 / 2). /// /// If `fold_left` crashed here the tail marker would have been /// lost in monomorphisation; if `fold_right` or `build` crashed, /// stack frames would be far larger than estimated. Neither /// happens at N=1000. #[test] fn std_list_stress_1000_element_folds() { let stdout = build_and_run("std_list_stress.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!( lines, vec!["500500", "500500"], "expected sum 1..1000 = 500500 from both fold_left and \ fold_right; mismatch indicates a fold bug or stack \ overflow corrupting output" ); } /// Iter 15e: `ail render | ail parse` round-trips canonical bytes. /// Property protected: the CLI's `render` is the exact inverse of /// `parse`, not the older human-pretty form. Regression of this /// would mean piping `render` into `parse` no longer yields a /// re-loadable module — a contract the help text now claims. /// /// Picks `std_either.ail.json` (richest fixture: 2-type-var data, /// 3-type-var fn, recursive ctors irrelevant) but the round-trip /// test in `ailang-surface` covers all 25 fixtures structurally. /// This test specifically exercises the CLI shell pipeline. #[test] fn render_parse_round_trip_canonical() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let src = workspace.join("examples").join("std_either.ail.json"); let original = std::fs::read(&src).expect("read fixture"); let render = Command::new(ail_bin()) .args(["render", src.to_str().unwrap()]) .output() .expect("run ail render"); assert!(render.status.success(), "ail render failed"); let tmp = std::env::temp_dir().join(format!("ailang_render_e2e_{}.ailx", std::process::id())); std::fs::write(&tmp, &render.stdout).expect("write rendered ailx"); let parsed = Command::new(ail_bin()) .args(["parse", tmp.to_str().unwrap()]) .output() .expect("run ail parse"); assert!(parsed.status.success(), "ail parse failed on rendered output"); let mut round = parsed.stdout; while round.last() == Some(&b'\n') { round.pop(); } let mut orig = original; while orig.last() == Some(&b'\n') { orig.pop(); } assert_eq!(orig, round, "render | parse must produce canonical bytes"); } /// Iter 16a: nested constructor patterns. Property protected: /// `ailang_core::desugar` flattens `Cons a (Cons b _)` to a chain of /// single-level matches before the checker/codegen sees it. Without /// the desugar pass the checker would emit /// `nested-ctor-pattern-not-allowed`; without the pre-codegen call, /// the inner binders would be silently dropped by the codegen. #[test] fn nested_ctor_pattern_first_two_sum() { let stdout = build_and_run("nested_pat.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["30"]); } /// Iter 15f: `std_pair` end-to-end. Polymorphic product type with /// two type vars and a single constructor; six combinators including /// the 3-type-var `map_first` / `map_second` reshapers. Property /// protected: `Pair -> Pair` and `Pair -> Pair` /// monomorphise correctly when each is exercised at distinct /// `(a, b, c)` triples — the codegen must emit the right field types /// for the substituted MkPair output. #[test] fn std_pair_demo() { let stdout = build_and_run("std_pair_demo.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["7", "9", "9", "7", "8", "18"]); } /// Iter 15d: `std_either` end-to-end. First stdlib ADT with two type /// parameters (`Either`); first combinator with three type vars /// (`either : (e -> c) -> (a -> c) -> Either -> c`). /// /// Property protected: monomorphisation correctly distinguishes per- /// call-site instantiations, including (a) two `from_right` instances /// differing only in the unused `e` (Int vs $u) and (b) the /// 3-type-var `either__I_I_I` instantiation. If wildcard substitution /// regressed, one of those would either fail to emit or collide with /// another instantiation. #[test] fn std_either_demo() { let stdout = build_and_run("std_either_demo.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!( lines, vec!["42", "99", "true", "true", "42", "6", "101"] ); } /// Iter 15g: `std_either_list` end-to-end. First stdlib fn set that /// imports three other stdlib modules (`std_list`, `std_either`, /// `std_pair`) and returns a compound polymorphic ADT tree /// (`partition_eithers : List> -> Pair, List>`). /// /// Property protected: monomorphisation across `List × Either × Pair` /// in a single call chain — `std_either_list.partition_eithers` is /// instantiated at `(e=Int, a=Int)` and the result feeds /// `std_pair.fst` / `std_pair.snd` (each at `(a=List, b=List)`) /// into `std_list.length` (at `a=Int`). If cross-module ctor resolution /// or substitution-through-nested-Con regressed, one of the /// instantiations would either fail to emit or produce a wrong length. /// The four expected outputs (2, 3, 2, 3) also pin the order /// preservation of `lefts` and `rights` against the input /// `[Left 1, Right 10, Left 2, Right 20, Right 30]`. #[test] fn std_either_list_demo() { let stdout = build_and_run("std_either_list_demo.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["2", "3", "2", "3"]); } /// Iter 15h: `std_list.take` and `std_list.drop` end-to-end. Both are /// index-driven recursive ADT-pattern fns — the first `std_list` /// combinators that pair `if (<= n 0)` Int-arithmetic guards with a /// recursive Cons/Nil match. Property protected: the `if` base-case /// guard wraps a `match` whose recursive arm bodies use `(- n 1)` and /// re-enter through `app` — a shape no other `std_list` fn has so /// far. Six checks pin all three boundary regimes (n=0, 0length) on each combinator. Expected stdout: 0, 3, 5, 5, 3, 0. #[test] fn std_list_more_demo() { let stdout = build_and_run("std_list_more_demo.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["0", "3", "5", "5", "3", "0"]); } /// Iter 16b.1: local recursive `let`. Property protected: a /// `(let-rec ...)` term in a fn body whose recursive body has no /// captures from the enclosing scope is lifted by the 16a desugar /// pass to a synthetic top-level fn (`$lr_N`), and the /// resulting module type-checks and runs identically to a /// hand-written top-level recursive fn. The fixture exercises /// `fact` at n=1 (base case), n=3, n=5 — outputs 1, 6, 120. /// If the desugar lift regressed (e.g. failed to substitute call /// sites in the in-term, or generated a colliding lifted name), /// the build would fail at typecheck or the binary would print /// wrong values. #[test] fn local_rec_factorial_demo() { let stdout = build_and_run("local_rec_demo.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["1", "6", "120"]); } /// Iter 16b.2: LetRec capture of a fn-param (path-1 safe subset). /// Property protected: the desugar pass lifts a LetRec whose body /// captures one or more enclosing-scope names (here: `n` from /// `sum_below`'s params) into a synthetic top-level fn whose /// signature has the capture types appended, and rewrites every /// call site of the LetRec name in body and in_term to pass the /// captures positionally as extra args. Without 16b.2, the /// 16b.1-era panic ("would capture") would fire and the build /// would fail. Three call sites: sum_below(1)=0, sum_below(5)=10, /// sum_below(10)=45. #[test] fn local_rec_capture_demo() { let stdout = build_and_run("local_rec_capture.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["0", "10", "45"]); } /// Iter 16b.3: LetRec capture of a `Term::Let`-bound name. Property /// protected: the desugar pass leaves a LetRec whose only outside- /// scope captures are Let-bound (type unknown until typecheck) in /// place; the post-typecheck `lift_letrecs` pass in `ailang-check` /// resolves capture types from the typechecker's env, lifts to a /// synthetic top-level fn (`loop$lr_0(i, n, threshold)`), and /// rewrites every call site. Without 16b.3, the 16b.2-era panic /// ("16b.3 — let-binding captures") would fire. The threshold here /// is `(app + 5 5)` so the lift exercises the type-synthesis path /// (not just a literal). count_below(0)=0, count_below(5)=5, /// count_below(15)=9 (i in 1..9 are below threshold 10). #[test] fn local_rec_let_capture_demo() { let stdout = build_and_run("local_rec_let_capture.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["0", "5", "9"]); } /// Iter 16b.4: LetRec capture of a Match-arm pattern binding. /// Property protected: the desugar pass leaves a LetRec whose /// captures are `Pattern::Ctor`-Var-bound (here `threshold` and /// `n`, both fields of `MkPair` in `Pair Int Int`) in place; the /// post-typecheck `lift_letrecs` pass walks the enclosing /// `Term::Match` arm, calls `type_check_pattern_for_lift` to /// substitute the matched ADT's type args (`[Int, Int]`) into /// `MkPair`'s declared field types, and lifts to a synthetic /// top-level fn `loop$lr_0(i: Int, threshold: Int, n: Int) -> /// Int`, rewriting every call site. Without 16b.4, the 16b.3-era /// panic ("16b.4 — match-arm captures") would fire. /// count_below(MkPair 10 0)=0, count_below(MkPair 10 5)=5, /// count_below(MkPair 10 15)=9. #[test] fn local_rec_match_capture_demo() { let stdout = build_and_run("local_rec_match_capture.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["0", "5", "9"]); } /// Iter 16b.5: LetRec name as a value in the in-clause (no capture). /// Property protected: the desugar pass detects a non-callee use of /// the LetRec name in `in_term` and wraps the rewritten in-term in /// `(let factorial (lam ...) ...)` whose lam eta-expands the lifted /// fn. The LetRec name then resolves to a closure-pair value usable /// anywhere a `Fn(Int) -> Int` is expected (here: passed to a HOF /// `apply5`). Without 16b.5, the desugar would panic with the /// "name-as-value not supported" message and the build would fail. /// Expected stdout: 120 (= apply5 factorial = factorial 5). #[test] fn local_rec_as_value_demo() { let stdout = build_and_run("local_rec_as_value.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["120"]); } /// Iter 16b.5: LetRec name as a value in the in-clause WITH capture. /// Property protected: the eta-Lam wrap composes correctly with the /// 16b.2 capture-augmentation. The lifted fn has signature /// `factorial_plus$lr_N(n: Int, base: Int) -> Int`; the eta-Lam /// wrap binds `factorial_plus` to a `(lam (n) (app /// factorial_plus$lr_N n base))` whose free var `base` becomes a /// standard 8b closure-env capture. This is the dynamic-env path: /// the lifted fn has extra params, the eta-Lam captures them. /// Expected stdout: 1320 (= 5*4*3*2*(1+10)), 12120 (= 5*4*3*2*(1+100)). #[test] fn local_rec_as_value_capture_demo() { let stdout = build_and_run("local_rec_as_value_capture.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["1320", "12120"]); } /// Iter 16b.6: LetRec inside a polymorphic enclosing fn. /// Property protected: when the enclosing fn is `Forall(a). Fn(...)`, /// the desugar/lift pipeline now (a) enters fn-params as `KnownType` /// (not `LetBound`) so a LetRec inside CAN capture them, and (b) /// builds the synthetic lifted fn's signature as /// `Forall(a). Fn(t1..tk, T1..Tm) -> tr`, mirroring the enclosing /// fn's type vars. Capture types `T1..Tm` may mention `a`; the /// outer Forall binds them. Codegen's mono pipeline (Iter 12b/14a) /// then specialises `loop$lr_0` at every call with the same type /// args as the enclosing `apply_n_times`. /// /// `main` drives `apply_n_times` at TWO distinct instantiations /// (`Int` with `succ`, `Bool` with `flip`) so the mono path is /// exercised twice, producing both `loop$lr_0__I` and /// `loop$lr_0__B` in the IR. Without 16b.6 the desugar pass would /// have rejected the LetRec capture as a `LetBound`-style failure /// (the 16b.2-era `Type::Forall` fallback marked all fn-params as /// `LetBound`). /// /// Expected stdout: 5 (succ applied 5 times to 0), false (flip /// applied 4 times to false — even count of flips ⇒ false). #[test] fn poly_rec_capture_demo() { let stdout = build_and_run("poly_rec_capture.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["5", "false"]); } /// Iter 16b.7: nested LetRec where the inner one captures the OUTER /// LetRec's PARAMS (not its name). /// /// Property protected: when an inner LetRec lifts inside an outer /// LetRec's body, the outer's params are `ScopeEntry::KnownType` in /// the inner's outer-scope (set up by the desugar pass at the same /// site that marks the outer's NAME as `EnclosingLetRec`). The inner /// lift therefore takes the 16b.2 fast path with the outer's params /// appended to the inner's signature; once the outer is then lifted, /// the call sites of `inner$lr_M(args, outer_param)` continue to /// resolve `outer_param` as outer's still-in-scope param (later /// renamed by the outer's own lift to its lifted-fn param of the /// same name). Inner capturing the OUTER's NAME remains rejected — /// see the desugar/lift `EnclosingLetRec` panic with the /// closure-of-self message. /// /// `nested_sum(n)` = sum of 1..=n by counting i ones for each i in /// 1..=n. Expected stdout: 1, 6, 15 for n in {1, 3, 5}. #[test] fn nested_let_rec_demo() { let stdout = build_and_run("nested_let_rec.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["1", "6", "15"]); } /// Guards `ail diff`: a modified body changes the hash of `sum`, while /// `main` stays unchanged. Expects exit code 1, `changed` contains exactly /// `sum`, `unchanged` contains `main`, `added`/`removed` empty. #[test] fn diff_detects_changed_def() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let src_a = workspace.join("examples").join("sum.ail.json"); // Variant: load sum.ail.json, mutate the `then` branch (1 instead of 0) // in the `sum` definition. `main` stays bit-identical. let raw = std::fs::read(&src_a).expect("read sum.ail.json"); let mut module: serde_json::Value = serde_json::from_slice(&raw).expect("parse sum.ail.json"); { let defs = module .get_mut("defs") .and_then(|d| d.as_array_mut()) .expect("defs array"); for def in defs.iter_mut() { if def.get("name").and_then(|n| n.as_str()) == Some("sum") { // Replace the then branch literal 0 → literal 1. let new_then = serde_json::json!({ "t": "lit", "lit": { "kind": "int", "value": 1 } }); def["body"]["then"] = new_then; } } } let tmp = std::env::temp_dir().join(format!( "ailang_diff_changed_{}", std::process::id() )); std::fs::create_dir_all(&tmp).unwrap(); let path_b = tmp.join("sum_v2.ail.json"); std::fs::write(&path_b, serde_json::to_vec_pretty(&module).unwrap()) .expect("write sum_v2.ail.json"); let output = Command::new(ail_bin()) .args([ "diff", src_a.to_str().unwrap(), path_b.to_str().unwrap(), "--json", ]) .output() .expect("ail diff failed to run"); let code = output.status.code().expect("process terminated by signal"); assert_eq!( code, 1, "expected exit code 1 for differing modules; stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("stdout must be valid JSON"); let added = v["added"].as_array().expect("added array"); let removed = v["removed"].as_array().expect("removed array"); let changed = v["changed"].as_array().expect("changed array"); let unchanged = v["unchanged"].as_array().expect("unchanged array"); assert!(added.is_empty(), "expected added empty: {added:?}"); assert!(removed.is_empty(), "expected removed empty: {removed:?}"); assert_eq!(changed.len(), 1, "expected one changed entry: {changed:?}"); assert_eq!( changed[0].get("name").and_then(|n| n.as_str()), Some("sum") ); assert_ne!( changed[0].get("hash_a").and_then(|n| n.as_str()), changed[0].get("hash_b").and_then(|n| n.as_str()), "hash_a and hash_b must differ for a changed def" ); assert!( unchanged.iter().any(|e| e.get("name").and_then(|n| n.as_str()) == Some("main")), "expected `main` in unchanged: {unchanged:?}" ); } /// Diff of a module with itself: exit 0, all lists except `unchanged` empty. #[test] fn diff_no_changes_exit_zero() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let src = workspace.join("examples").join("sum.ail.json"); let output = Command::new(ail_bin()) .args([ "diff", src.to_str().unwrap(), src.to_str().unwrap(), "--json", ]) .output() .expect("ail diff failed to run"); let code = output.status.code().expect("process terminated by signal"); assert_eq!( code, 0, "expected exit code 0 for identical modules; stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("stdout must be valid JSON"); assert!(v["added"].as_array().unwrap().is_empty()); assert!(v["removed"].as_array().unwrap().is_empty()); assert!(v["changed"].as_array().unwrap().is_empty()); assert!( !v["unchanged"].as_array().unwrap().is_empty(), "self-diff should report unchanged defs" ); } /// Guards the workspace loader (Iter 5a): the entry module `ws_main` /// imports `ws_lib`; both must be found by the loader, loaded, and listed /// in the JSON output. Cross-module typecheck/codegen is explicitly not /// part of this test — it only verifies the loader pipeline. #[test] fn workspace_lists_imported_modules() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let entry = workspace.join("examples").join("ws_main.ail.json"); let output = Command::new(ail_bin()) .args(["workspace", entry.to_str().unwrap(), "--json"]) .output() .expect("ail workspace failed to run"); assert!( output.status.success(), "ail workspace exited non-zero; stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("stdout must be valid JSON"); assert_eq!( v.get("entry").and_then(|n| n.as_str()), Some("ws_main"), "entry must be ws_main: {stdout}" ); let modules = v["modules"].as_array().expect("modules must be array"); assert_eq!(modules.len(), 2, "expected 2 modules: {stdout}"); let names: Vec<&str> = modules .iter() .filter_map(|m| m.get("name").and_then(|n| n.as_str())) .collect(); assert!(names.contains(&"ws_main"), "ws_main missing: {names:?}"); assert!(names.contains(&"ws_lib"), "ws_lib missing: {names:?}"); } /// Guards Iter 5b: `ail check examples/ws_main.ail.json --json` must /// resolve the cross-module call `ws_lib.add`. Expected: exit 0, /// stdout exactly `[]` (empty diagnostic array). #[test] fn check_workspace_resolves_import() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let entry = workspace.join("examples").join("ws_main.ail.json"); let output = Command::new(ail_bin()) .args(["check", entry.to_str().unwrap(), "--json"]) .output() .expect("ail check --json failed to run"); let code = output.status.code().expect("process terminated by signal"); assert_eq!( code, 0, "expected exit 0; stderr: {}; stdout: {}", String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stdout), ); let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); assert_eq!(stdout.trim(), "[]", "expected empty diagnostic array"); } /// Guards Iter 5c (cross-module codegen): `ail build` over /// `examples/ws_main.ail.json` must lower the workspace incl. `ws_lib`; /// `@ail_ws_main_main` must call `@ail_ws_lib_add(2,3)` and print `5`. #[test] fn workspace_build_runs_imported_fn() { let stdout = build_and_run("ws_main.ail.json"); assert_eq!(stdout.trim(), "5", "ws_lib.add(2,3) should print 5"); } /// Guards Iter 5d: `ail manifest --workspace --json` lists defs from all /// modules of the workspace — visible via a `module` field per entry. #[test] fn manifest_workspace_lists_all_defs() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let entry = workspace.join("examples").join("ws_main.ail.json"); let output = Command::new(ail_bin()) .args([ "manifest", entry.to_str().unwrap(), "--workspace", "--json", ]) .output() .expect("ail manifest --workspace failed to run"); assert!( output.status.success(), "ail manifest --workspace exited non-zero; stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("stdout must be valid JSON"); let symbols = v["symbols"].as_array().expect("symbols must be array"); let modules: Vec<&str> = symbols .iter() .filter_map(|s| s.get("module").and_then(|m| m.as_str())) .collect(); assert!( modules.contains(&"ws_main"), "expected at least one symbol with module=ws_main; got {modules:?}" ); assert!( modules.contains(&"ws_lib"), "expected at least one symbol with module=ws_lib; got {modules:?}" ); } /// Guards Iter 5d: `ail describe --workspace ws_lib.add` resolves the /// qualified dot notation to the actually imported module. #[test] fn describe_workspace_resolves_qualified_name() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let entry = workspace.join("examples").join("ws_main.ail.json"); let output = Command::new(ail_bin()) .args([ "describe", entry.to_str().unwrap(), "ws_lib.add", "--workspace", "--json", ]) .output() .expect("ail describe --workspace failed to run"); assert!( output.status.success(), "ail describe --workspace exited non-zero; stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("stdout must be valid JSON"); assert_eq!( v.get("module").and_then(|m| m.as_str()), Some("ws_lib"), "module field must be ws_lib; got {stdout}" ); assert_eq!( v.get("name").and_then(|n| n.as_str()), Some("add"), "def name must be add; got {stdout}" ); } /// Guards Iter 6: `ail deps` no longer surfaces built-ins, params, or /// let-bindings as edges. The single-module `sum` def in `sum.ail.json` /// uses `+`, `-`, `==`, the param `n`, and the recursive call `sum`. /// Only the recursive call must remain. #[test] fn deps_filters_builtins_params_locals() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let src = workspace.join("examples").join("sum.ail.json"); let output = Command::new(ail_bin()) .args(["deps", src.to_str().unwrap(), "--of", "sum", "--json"]) .output() .expect("ail deps failed to run"); assert!( output.status.success(), "ail deps exited non-zero; stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON"); let arr = v.as_array().expect("array of {name, refs}"); let entry = arr .iter() .find(|e| e["name"].as_str() == Some("sum")) .expect("sum entry present"); let refs: Vec<&str> = entry["refs"] .as_array() .expect("refs array") .iter() .filter_map(|x| x.as_str()) .collect(); assert_eq!( refs, vec!["sum"], "expected only the recursive call `sum`; got {refs:?}" ); } /// Guards Iter 6 in workspace mode: edges into built-ins (`ws_lib.+`) /// and into the params (`ws_lib.a`, `ws_lib.b`) of `ws_lib.add` must be /// gone after the deps refactor. #[test] fn deps_workspace_filters_builtins_and_params() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let entry = workspace.join("examples").join("ws_main.ail.json"); let output = Command::new(ail_bin()) .args(["deps", entry.to_str().unwrap(), "--workspace", "--json"]) .output() .expect("ail deps --workspace failed to run"); assert!(output.status.success()); let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("valid JSON"); let edges = v["edges"].as_array().expect("edges array"); for e in edges { let to_def = e.get("to_def").and_then(|x| x.as_str()).unwrap_or(""); assert_ne!(to_def, "+", "built-in `+` must not appear: {e}"); assert_ne!(to_def, "a", "param `a` must not appear: {e}"); assert_ne!(to_def, "b", "param `b` must not appear: {e}"); } } /// Guards Iter 5d: `ail deps --workspace` contains a cross-module edge /// `ws_main.main -> ws_lib.add`, because `ws_main` calls `ws_lib.add(2,3)`. #[test] fn deps_workspace_includes_cross_module() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let entry = workspace.join("examples").join("ws_main.ail.json"); let output = Command::new(ail_bin()) .args(["deps", entry.to_str().unwrap(), "--workspace", "--json"]) .output() .expect("ail deps --workspace failed to run"); assert!( output.status.success(), "ail deps --workspace exited non-zero; stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("stdout must be valid JSON"); let edges = v["edges"].as_array().expect("edges must be array"); let has_cross_edge = edges.iter().any(|e| { e.get("from_module").and_then(|s| s.as_str()) == Some("ws_main") && e.get("from_def").and_then(|s| s.as_str()) == Some("main") && e.get("to_module").and_then(|s| s.as_str()) == Some("ws_lib") && e.get("to_def").and_then(|s| s.as_str()) == Some("add") }); assert!( has_cross_edge, "expected cross-module edge ws_main.main -> ws_lib.add; got {stdout}" ); } /// Guards Iter 5d: `ail diff --workspace` detects an additional module /// in the B workspace as an `added_modules` entry and exits with code 1. #[test] fn diff_workspace_added_module() { use std::fs; fn write_module(dir: &Path, name: &str, body: serde_json::Value) { let p = dir.join(format!("{name}.ail.json")); fs::write(&p, serde_json::to_vec_pretty(&body).unwrap()).unwrap(); } let dir_a = std::env::temp_dir().join(format!( "ailang_diff_ws_a_{}", std::process::id() )); let dir_b = std::env::temp_dir().join(format!( "ailang_diff_ws_b_{}", std::process::id() )); let _ = fs::remove_dir_all(&dir_a); let _ = fs::remove_dir_all(&dir_b); fs::create_dir_all(&dir_a).unwrap(); fs::create_dir_all(&dir_b).unwrap(); // Both workspaces have an empty module `root`. B additionally // imports `extra`. Loader convention: `/.ail.json`, // module name must match the file name. let empty_root = serde_json::json!({ "schema": "ailang/v0", "name": "root", "imports": [], "defs": [], }); let root_with_import = serde_json::json!({ "schema": "ailang/v0", "name": "root", "imports": [{ "module": "extra" }], "defs": [], }); let extra = serde_json::json!({ "schema": "ailang/v0", "name": "extra", "imports": [], "defs": [], }); write_module(&dir_a, "root", empty_root); write_module(&dir_b, "root", root_with_import); write_module(&dir_b, "extra", extra); let entry_a = dir_a.join("root.ail.json"); let entry_b = dir_b.join("root.ail.json"); let output = Command::new(ail_bin()) .args([ "diff", entry_a.to_str().unwrap(), entry_b.to_str().unwrap(), "--workspace", "--json", ]) .output() .expect("ail diff --workspace failed to run"); let code = output.status.code().expect("process terminated by signal"); assert_eq!( code, 1, "expected exit 1 when workspaces differ; stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("stdout must be valid JSON"); let added = v["added_modules"] .as_array() .expect("added_modules must be array"); assert!( added.iter().any(|e| e.get("name").and_then(|n| n.as_str()) == Some("extra")), "expected `extra` in added_modules; got {stdout}" ); let removed = v["removed_modules"] .as_array() .expect("removed_modules must be array"); assert!( removed.is_empty(), "expected removed_modules empty; got {stdout}" ); } /// Guards the `--json` diagnostic format for tooling consumers. /// `broken_unbound.ail.json` references a non-existent variable; /// exit code 1 is expected, plus at least one diagnostic with /// `severity == "error"` and `code == "unbound-var"`. #[test] fn check_json_unbound_var() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let src = workspace.join("examples").join("broken_unbound.ail.json"); let output = Command::new(ail_bin()) .args(["check", src.to_str().unwrap(), "--json"]) .output() .expect("ail check --json failed to run"); let code = output.status.code().expect("process terminated by signal"); assert_eq!(code, 1, "expected exit code 1, stderr: {}", String::from_utf8_lossy(&output.stderr)); let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); let diags: serde_json::Value = serde_json::from_str(stdout.trim()).expect("stdout must be valid JSON"); let arr = diags.as_array().expect("diagnostics must be a JSON array"); assert!( arr.iter().any(|d| { d.get("severity").and_then(|v| v.as_str()) == Some("error") && d.get("code").and_then(|v| v.as_str()) == Some("unbound-var") }), "expected at least one error diagnostic with code unbound-var; got: {stdout}" ); } /// Iter 16c: literal patterns at top level and inside Ctor /// sub-patterns. Property protected: the 16a desugar pass rewrites /// every `Pattern::Lit` to a `Term::If` against the scrutinee/field /// before either typecheck or codegen sees it. Without 16c, the /// codegen would emit a Match arm with a Lit pattern and produce /// either a wrong result or an `unreachable!` panic depending on /// the path. The fixture exercises both sites: `classify` (top- /// level lit arms) and `categorize_first` (nested lit-in-Ctor). /// /// Iter 16d update: the trailing `(case _ 0)` arm of /// `categorize_first` was removed when the chain machinery's /// terminator switched from a `Unit` literal to the polymorphic /// `__unreachable__` builtin. Output is unchanged — this test /// guards both 16c (lit-pattern desugar) and 16d (no-workaround /// chain default). #[test] fn lit_pat_demo() { let stdout = build_and_run("lit_pat.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["100", "200", "999", "-1", "0", "7"]); } /// Iter 16d: `__unreachable__` as an explicit user-callable /// primitive. Property protected: a polymorphic `forall a. a` value /// reference (a) typechecks against any expected type at the use /// site (here `Int`), and (b) codegen lowers it to LLVM /// `unreachable`, with the surrounding `if` correctly forwarding /// the live branch's value past the terminated branch. The fixture /// only ever hits the live branch, so the binary must succeed and /// print the expected outputs. #[test] fn unreachable_demo() { let stdout = build_and_run("unreachable_demo.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["4", "5"]); } /// Iter 18a: `(borrow T)` and `(own T)` mode annotations on /// fn-type parameters. Properties protected: /// (1) the parser accepts `(borrow ...)` and `(own ...)` only as /// wrappers in fn-type param/ret slots and round-trips them /// into [`ailang_core::ParamMode`] on `Type::Fn`; /// (2) the canonical-JSON serialisation emits `param_modes` only /// when at least one mode is non-Implicit (verified by reading /// the file and confirming the substring is present), so /// pre-18a fixtures continue to hash bit-identically; /// (3) the typechecker treats modes as transparent — it accepts a /// fn whose parameter is `(borrow (con List))` even when the /// same value is later passed to a fn whose parameter is /// `(own (con List))`, because Iter 18a does not enforce /// mode compatibility (deferred to 18c); /// (4) codegen ignores modes entirely: list_length and sum_list /// compile to the same LLVM IR they would have without the /// wrappers, and the binary prints `3` then `6`. #[test] fn borrow_own_demo_modes_are_metadata_only() { // Sanity-check (2): the on-disk JSON contains `param_modes` // tokens for both fns. If the schema regressed and the field // were dropped, this would fail before the binary even runs. let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let json_path = workspace.join("examples").join("borrow_own_demo.ail.json"); let json = std::fs::read_to_string(&json_path).expect("read borrow_own_demo.ail.json"); assert!( json.contains("\"param_modes\":[\"borrow\"]"), "expected `param_modes:[\"borrow\"]` in canonical JSON; the schema for `(borrow T)` regressed" ); assert!( json.contains("\"param_modes\":[\"own\"]"), "expected `param_modes:[\"own\"]` in canonical JSON; the schema for `(own T)` regressed" ); let stdout = build_and_run("borrow_own_demo.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!(lines, vec!["3", "6"]); } /// Iter 16e: polymorphic `==`. Properties protected: /// (1) `==` typechecks at Int / Bool / Str / Unit (the fixture /// uses every supported case directly via `(app == ...)`); /// (2) codegen dispatches each arg-type to the right LLVM shape /// — `icmp eq i64`, `icmp eq i1`, `@strcmp + icmp eq i32 0`, /// constant `i1 true` — and the binary's stdout matches the /// boolean truth-table for those operators; /// (3) 16c's `build_eq` desugar of `(pat-lit "hi")` over a `Str` /// scrutinee now goes through, since `==` no longer rejects /// non-Int args at typecheck. #[test] fn eq_demo() { let stdout = build_and_run("eq_demo.ail.json"); let lines: Vec<&str> = stdout.lines().collect(); assert_eq!( lines, vec![ "true", "false", // == on Int "false", "true", // == on Bool "true", "false", // == on Str "true", // == on Unit "1", "2", "0", // classify_str: Str lit-pattern desugar ] ); } /// Iter 18b: --alloc=rc routes allocation through ailang_rc_alloc /// (8-byte refcount header, libc malloc backing). With no inc/dec /// emission yet (18c work), programs leak under this mode but must /// still produce correct stdout — that's the validation 18b ships. #[test] fn alloc_rc_produces_same_stdout_as_gc() { // Pick a fixture with non-trivial allocation: list-building + match. let example = "list.ail.json"; let stdout_gc = build_and_run_with_alloc(example, "gc"); let stdout_rc = build_and_run_with_alloc(example, "rc"); assert_eq!(stdout_gc, stdout_rc, "alloc=rc must match alloc=gc"); assert_eq!(stdout_rc.trim(), "42"); } /// Iter 18c.1: `Term::Clone` is a pure schema addition. In 18c.1 the /// wrapper is identity for both typechecker (same type as inner) and /// codegen (same SSA reg, no extra IR) — programs that contain /// `(clone X)` produce the same stdout they would have produced /// without the wrapper. Iter 18c.3 will replace the codegen /// passthrough with `call void @ailang_rc_inc` under `--alloc=rc`. #[test] fn clone_demo_is_identity_in_18c1() { let stdout = build_and_run("clone_demo.ail.json"); assert_eq!(stdout.trim(), "42"); } /// Iter 18b: extends `alloc_rc_produces_same_stdout_as_gc` to a larger /// fixture (`std_list_demo`) so more allocation sites — folds, maps, /// cross-module ctors — are exercised under `--alloc=rc`. Same /// invariant: stdout must be byte-identical to the `gc` build. #[test] fn alloc_rc_matches_gc_on_std_list_demo() { let example = "std_list_demo.ail.json"; let stdout_gc = build_and_run_with_alloc(example, "gc"); let stdout_rc = build_and_run_with_alloc(example, "rc"); assert_eq!( stdout_gc, stdout_rc, "alloc=rc must match alloc=gc on std_list_demo" ); }