Iter 18c.2: linearity check + suggested_rewrites

Adds a per-fn linearity check that runs on every fn whose
param_modes are all explicit (Borrow or Own, no Implicit), tracks
each binder's consume/borrow state through the AST, and emits
two diagnostics with form-A `suggested_rewrites`:

- `use-after-consume` — a binder is referenced after it has
  already been consumed.
- `consume-while-borrowed` — a binder is consumed while a borrow
  of it is still live (sibling arg slot, or an enclosing-fn
  Borrow param).

Fns with any Implicit param skip the check entirely; that's the
back-compat lane that keeps every existing fixture green. Pure
diagnostic addition: no IR change, no codegen change, no runtime
change.

Each diagnostic carries a non-empty `suggested_rewrites` whose
`replacement` is form-A AILang text (typically `(clone <name>)`).
Adds two new public entrypoints to ailang-surface — `parse_term`
and `term_to_form_a` — so the round-trip
`parse_term(term_to_form_a(t)) ≡ t` is the contract every
emitted replacement must satisfy. Tests assert the contract.

Linearity pass runs only on modules that typechecked clean (any
upstream typecheck error suppresses it for that module — running
on partly-defined IR would produce noise).

Tests: 3 unit tests in `linearity::tests`, 3 integration tests
in `ailang-check/tests/workspace.rs`, 1 serialisation test in
`diagnostic.rs`. `cargo test --workspace` green.
This commit is contained in:
2026-05-08 10:06:14 +02:00
parent 8d704cd9b5
commit 09fb5bb113
9 changed files with 987 additions and 4 deletions
+2 -2
View File
@@ -35,5 +35,5 @@ pub mod lex;
pub mod parse;
pub mod print;
pub use parse::{parse, ParseError};
pub use print::print;
pub use parse::{parse, parse_term, ParseError};
pub use print::{print, term_to_form_a};
+23
View File
@@ -130,6 +130,29 @@ pub fn parse(input: &str) -> Result<Module, ParseError> {
Ok(m)
}
/// Parse a single form-A term — the concrete-syntax counterpart of
/// [`Term`]. This is the dual of [`crate::print::term_to_form_a`] and is
/// used by callers that produce form-A snippets out-of-band, e.g. the
/// `suggested_rewrites` payload of `ail check --json` (Iter 18c.2). The
/// input must consume to EOF after the term — extra trailing tokens
/// produce [`ParseError::Unexpected`].
///
/// Round-trip: `parse_term(term_to_form_a(t)) ≡ t` for every term `t`
/// the surface can express.
pub fn parse_term(input: &str) -> Result<Term, ParseError> {
let toks = tokenize(input)?;
let mut p = Parser::new(&toks);
let t = p.parse_term()?;
if p.cur < p.toks.len() {
return Err(ParseError::Unexpected {
expected: "end of input".into(),
got: tok_label(&p.toks[p.cur].tok),
pos: p.toks[p.cur].span.start,
});
}
Ok(t)
}
fn tok_label(t: &Tok) -> String {
match t {
Tok::LParen => "`(`".into(),
+12
View File
@@ -31,6 +31,18 @@ pub fn print(module: &Module) -> String {
out
}
/// Print a single [`Term`] in form (A) — the dual of
/// [`crate::parse::parse_term`]. Used by callers that produce form-A
/// snippets out-of-band, e.g. the `suggested_rewrites` payload of
/// `ail check --json` (Iter 18c.2). The output is round-trip stable
/// (`parse_term(term_to_form_a(t)) ≡ t`) but does not include a trailing
/// newline; callers that want one append it themselves.
pub fn term_to_form_a(t: &Term) -> String {
let mut out = String::new();
write_term(&mut out, t, 0);
out
}
// ---- helpers --------------------------------------------------------------
fn indent(out: &mut String, level: usize) {