iter loop-recur.2: typecheck semantics — binder typing, recur checks, verify_loop_body

Second of three iterations of the standalone loop/recur milestone
(plan 5ac57fe). Replaces the iter-1 synth CheckError::Internal stub
for Term::Loop/Term::Recur with real binder typing + positional
recur arity/type checking via a new loop_stack: &mut Vec<Vec<Type>>
frame threaded as mut.2's mut_scope_stack, a new private
verify_loop_body tail-position pass (sibling of the byte-frozen
verify_tail_positions — 0 deletions there), and the four Recur*
diagnostics firing point-exactly on four negative fixtures. The
iter-1 loop_sum_to.ail fixture now also typechecks clean; an
infinite loop typechecks (no termination claim). NO codegen (the
iter-1 lower_term stub stays — iter 3); NO Diverge/guardedness
(spec boundary).

Three Boss design calls implemented verbatim and journalled:
RecurTypeMismatch is an Assign-style structural pre-check (not a
unify-propagate); loop_stack is positional Vec<Type> (binder names
via ordinary locals); diagnostic-code precedence is by pass
ordering (no explicit logic).

Two DONE_WITH_CONCERNS, both journalled: a plan-ordering defect
(Task 2's compile gate forced the check_fn threading the plan
deferred to Task 4 — resolved byte-identically, only resequenced)
and the recurring mut.2-class recon-undercount of cross-module
synth callers (resolved via the plan's compile-sweep oracle).

Boss systemic fix folded in: planner SKILL.md Step-5 gains item 7
(compile-gate vs. deferred-caller ordering) so the plan-ordering
defect class is scrubbed at plan time. cargo test --workspace
608 -> 616 / 0 red (Boss-reran independently).
This commit is contained in:
2026-05-17 23:33:43 +02:00
parent 5ac57fe8de
commit 1566ce0b29
16 changed files with 771 additions and 39 deletions
+36
View File
@@ -251,6 +251,42 @@ fn check_human_mode_renders_mut_diagnostic_code_exactly_once() {
}
}
/// loop-recur iter 2: the four `Recur*` Display bodies must be
/// bracket-`[code]`-free (F2 convention) — the human-mode
/// formatter supplies `[<code>]` exactly once. Same observable
/// property as the mut sibling, over the four recur negatives.
#[test]
fn check_human_mode_renders_recur_diagnostic_code_exactly_once() {
let cases = [
("test_recur_outside_loop.ail.json", "recur-outside-loop"),
("test_recur_arity_mismatch.ail.json", "recur-arity-mismatch"),
("test_recur_type_mismatch.ail.json", "recur-type-mismatch"),
(
"test_recur_not_in_tail_position.ail.json",
"recur-not-in-tail-position",
),
];
for (fixture_name, code) in cases {
let fixture = examples_dir().join(fixture_name);
let output = Command::new(ail_bin())
.args(["check", fixture.to_str().unwrap()])
.output()
.expect("ail binary must launch");
assert!(
!output.status.success(),
"ail check (human mode) must fail on {fixture_name}"
);
let stderr = String::from_utf8(output.stderr).expect("stderr is utf-8");
let needle = format!("[{code}]");
let occurrences = stderr.matches(&needle).count();
assert_eq!(
occurrences, 1,
"expected `[{code}]` exactly once in human stderr, found {occurrences}; \
full stderr:\n{stderr}"
);
}
}
/// Property: `ail check --json` on a `.ail` (Form A) source file with
/// a syntax error returns a structured `surface-parse-error`
/// diagnostic (non-empty diagnostics array, exit code != 0), rather