Iter 6: deps hardening, multi-diagnose, DESIGN audit

- ail deps now filters builtins, fn params, and let/match-pattern
  bindings. New value_names() in ailang-check::builtins is the single
  source of truth shared with the typechecker install path. walk_term
  threads a scope set; qualified `prefix.def` refs pass through.
- check_in_workspace returns Vec<CheckError>; check_workspace
  accumulates body diagnostics across defs and modules. Pass-1
  (top-level symbol table) and per-module type-def setup stay
  fail-fast — corrupt env would taint later diagnostics.
- DESIGN.md "What the MVP is NOT" was lying (ADTs, strings landed
  in Iter 2/3). Renamed to "What is not (yet) supported" and split
  into "not yet" + supported/smoke-tested. JOURNAL Iter 6 entry
  records the architecture self-check.

Tests: 47 green (was 44). +2 deps filter tests in e2e,
+1 multi-diagnose test in ailang-check workspace integration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 12:31:20 +02:00
parent efd209779c
commit 1a448309fa
7 changed files with 393 additions and 42 deletions
+85 -15
View File
@@ -735,11 +735,32 @@ fn workspace_error_to_diagnostic(
}
}
/// Collects the *external* references of a definition: everything its body
/// depends on that lives outside the def itself. Filtered out:
///
/// - **Built-ins** (`+`, `-`, `==`, `not`, …): operators provided by the
/// runtime, not the user. The list is owned by `ailang_check::builtins`.
/// - **Parameters** of the surrounding `fn`.
/// - **Local bindings** introduced by `let` and by pattern variables in
/// `match` arms.
///
/// Cross-module references (`<prefix>.<def>`) are always kept — a local
/// binding can never shadow a dotted name (the typechecker rejects defs
/// with a dot, see DESIGN.md "qualified cross-module references").
fn collect_refs(def: &ailang_core::Def) -> std::collections::BTreeSet<String> {
let mut out = std::collections::BTreeSet::new();
let builtins: std::collections::HashSet<&'static str> =
ailang_check::builtins::value_names().into_iter().collect();
match def {
ailang_core::Def::Fn(f) => walk_term(&f.body, &mut out),
ailang_core::Def::Const(c) => walk_term(&c.value, &mut out),
ailang_core::Def::Fn(f) => {
let mut scope: std::collections::HashSet<String> =
f.params.iter().cloned().collect();
walk_term(&f.body, &mut out, &builtins, &mut scope);
}
ailang_core::Def::Const(c) => {
let mut scope = std::collections::HashSet::new();
walk_term(&c.value, &mut out, &builtins, &mut scope);
}
ailang_core::Def::Type(td) => {
// A type def references the types of its fields.
for c in &td.ctors {
@@ -754,54 +775,103 @@ fn collect_refs(def: &ailang_core::Def) -> std::collections::BTreeSet<String> {
out
}
fn walk_term(t: &ailang_core::Term, out: &mut std::collections::BTreeSet<String>) {
fn walk_term(
t: &ailang_core::Term,
out: &mut std::collections::BTreeSet<String>,
builtins: &std::collections::HashSet<&'static str>,
scope: &mut std::collections::HashSet<String>,
) {
use ailang_core::Term;
match t {
Term::Lit { .. } => {}
Term::Var { name } => {
// Qualified (`prefix.def`) — always external; the typechecker
// forbids dots in def names, so no shadowing risk.
if name.contains('.') {
out.insert(name.clone());
return;
}
if scope.contains(name) || builtins.contains(name.as_str()) {
return;
}
out.insert(name.clone());
}
Term::App { callee, args } => {
walk_term(callee, out);
walk_term(callee, out, builtins, scope);
for a in args {
walk_term(a, out);
walk_term(a, out, builtins, scope);
}
}
Term::Let { value, body, .. } => {
walk_term(value, out);
walk_term(body, out);
Term::Let { name, value, body } => {
// `value` is in the outer scope; only `body` sees the binding.
walk_term(value, out, builtins, scope);
let inserted = scope.insert(name.clone());
walk_term(body, out, builtins, scope);
if inserted {
scope.remove(name);
}
}
Term::If { cond, then, else_ } => {
walk_term(cond, out);
walk_term(then, out);
walk_term(else_, out);
walk_term(cond, out, builtins, scope);
walk_term(then, out, builtins, scope);
walk_term(else_, out, builtins, scope);
}
Term::Do { op, args } => {
// Mark effect ops as `effect:io/print_int` so they can be
// separated from normal function calls.
out.insert(format!("effect:{op}"));
for a in args {
walk_term(a, out);
walk_term(a, out, builtins, scope);
}
}
Term::Ctor { type_name, ctor, args } => {
out.insert(format!("ctor:{type_name}/{ctor}"));
for a in args {
walk_term(a, out);
walk_term(a, out, builtins, scope);
}
}
Term::Match { scrutinee, arms } => {
walk_term(scrutinee, out);
walk_term(scrutinee, out, builtins, scope);
for arm in arms {
if let ailang_core::ast::Pattern::Ctor { ctor, .. } = &arm.pat {
out.insert(format!("ctor:{ctor}"));
}
walk_term(&arm.body, out);
let bound = bind_pattern(&arm.pat, scope);
walk_term(&arm.body, out, builtins, scope);
for n in bound {
scope.remove(&n);
}
}
}
}
}
/// Adds the variable bindings introduced by a pattern to `scope` and
/// returns the names that were freshly inserted (so the caller can roll
/// them back). MVP-restricted: nested ctor patterns only contain
/// `Var`/`Wild` — see DESIGN.md.
fn bind_pattern(
p: &ailang_core::ast::Pattern,
scope: &mut std::collections::HashSet<String>,
) -> Vec<String> {
use ailang_core::ast::Pattern;
let mut added = Vec::new();
match p {
Pattern::Wild | Pattern::Lit { .. } => {}
Pattern::Var { name } => {
if scope.insert(name.clone()) {
added.push(name.clone());
}
}
Pattern::Ctor { fields, .. } => {
for sub in fields {
added.extend(bind_pattern(sub, scope));
}
}
}
added
}
// --- ail diff -------------------------------------------------------------
/// Purely structural module diff. Top-level defs are identified by