96db54d15d
Iteration-discipline milestone, 1 of 3. Adds named loop + recur as strictly-additive first-class AST nodes: parse/print/prose/serde/ round-trip/schema lockstep, typecheck (binder typing + recur arity/type unification via loop_stack threaded as mut.2's mut_scope_stack; recur-tail-position via verify_loop_body), codegen (loop-header + one phi per binder; recur back-edge br with a NEW parallel block_terminated setter; lambda-boundary loop_frames save/restore). Four Recur* CheckError variants. Strictly additive: zero deletions touch tail-app/tail-do or the seven existing block_terminated sites — this is what makes the destructive it.3 safe. recur synth = fresh metavar (resolves the plan's flagged Type::unit() risk). loop_counter->55, loop_in_lambda->49, four negatives fire, tail-app byte-identical, cargo test --workspace green. Specfda9b78, plan7381a42.
61 lines
2.1 KiB
Rust
61 lines
2.1 KiB
Rust
//! Iter it.1 (Task 5): pin tests that the four `recur` negative
|
|
//! fixtures produce their exact diagnostic codes, and that the
|
|
//! positive `loop_smoke` fixture typechecks clean.
|
|
//!
|
|
//! Spec: `docs/specs/2026-05-15-iteration-discipline.md`. The four
|
|
//! negatives live as canonical `.ail.json` (diagnostic code is the
|
|
//! load-bearing assertion, not the surface form), mirroring the
|
|
//! `mut_typecheck_pin` carve-out precedent.
|
|
|
|
use ailang_check::check_workspace;
|
|
use ailang_surface::load_workspace;
|
|
use std::path::PathBuf;
|
|
|
|
fn examples_dir() -> PathBuf {
|
|
let manifest = env!("CARGO_MANIFEST_DIR");
|
|
PathBuf::from(manifest)
|
|
.parent().expect("CARGO_MANIFEST_DIR has a parent (crates/ailang-check)")
|
|
.parent().expect("CARGO_MANIFEST_DIR has a grandparent (crates/)")
|
|
.join("examples")
|
|
}
|
|
|
|
/// Load the named fixture under `examples/`, run check_workspace, and
|
|
/// return the diagnostic code list (one entry per Diagnostic).
|
|
fn check_fixture(fixture_name: &str) -> Vec<String> {
|
|
let path = examples_dir().join(fixture_name);
|
|
let ws = load_workspace(&path)
|
|
.unwrap_or_else(|e| panic!("workspace `{fixture_name}` must load: {e:?}"));
|
|
let diags = check_workspace(&ws);
|
|
diags.iter().map(|d| d.code.clone()).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn loop_smoke_typechecks_clean() {
|
|
let codes = check_fixture("loop_smoke.ail");
|
|
assert!(codes.is_empty(), "expected zero diagnostics, got {codes:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn recur_outside_loop_is_rejected() {
|
|
let codes = check_fixture("test_recur_outside_loop.ail.json");
|
|
assert_eq!(codes, vec!["recur-outside-loop".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn recur_arity_mismatch_is_rejected() {
|
|
let codes = check_fixture("test_recur_arity_mismatch.ail.json");
|
|
assert_eq!(codes, vec!["recur-arity-mismatch".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn recur_type_mismatch_is_rejected() {
|
|
let codes = check_fixture("test_recur_type_mismatch.ail.json");
|
|
assert_eq!(codes, vec!["recur-type-mismatch".to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn recur_not_in_tail_position_is_rejected() {
|
|
let codes = check_fixture("test_recur_not_in_tail_position.ail.json");
|
|
assert_eq!(codes, vec!["recur-not-in-tail-position".to_string()]);
|
|
}
|