ca30606aec
Adds path-2 from the 16b.2 planning entry. Desugar now defers
LetRecs whose captures include Term::Let-bound names; a new
ailang-check::lift_letrecs pass runs after typecheck and uses the
elaborated env to resolve capture types. ailang-check learns a real
Term::LetRec typing rule in synth and verify_tail_positions.
- desugar: Term::LetRec arm gains a defer-arm; helpers promoted to
pub for reuse by the lift pass; find_non_callee_use moved before
classification.
- ailang-check::synth/verify_tail_positions: real LetRec rules
(effect-subset, locals install, recursive name in body+in_term).
- ailang-check::lift.rs (new, 720 LOC): post-typecheck lift with
post-order traversal, env-walk for capture-type resolution,
fast-path skip when no LetRec is present.
- ail::main.rs: build path now does load → check → desugar →
lift_letrecs → codegen.
- examples/local_rec_let_capture.{ailx,ail.json}: new fixture
capturing a let-bound `threshold` in a recursive helper.
- e2e + check unit tests: 106 → 110 (+4).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1013 lines
39 KiB
Rust
1013 lines
39 KiB
Rust
//! 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")
|
||
}
|
||
|
||
#[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<a>) -> List<b>` 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 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>) -> 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<Int>`. `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<a>`) 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<a, b> -> Pair<c, b>` and `Pair<a, b> -> Pair<a, c>`
|
||
/// 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<e, a>`); first combinator with three type vars
|
||
/// (`either : (e -> c) -> (a -> c) -> Either<e, a> -> 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<Either<e,a>> -> Pair<List<e>, List<a>>`).
|
||
///
|
||
/// 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<Int>, b=List<Int>)`)
|
||
/// 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, 0<n<length,
|
||
/// n>length) 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 (`<name>$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"]);
|
||
}
|
||
|
||
/// 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: `<root_dir>/<name>.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).
|
||
#[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"]);
|
||
}
|