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:
@@ -41,6 +41,11 @@
|
||||
//! - `module-hash-mismatch` — workspace loader (Iter 5b, in the CLI path)
|
||||
//! - `tail-call-not-in-tail-position` (Iter 14e, see Decision 8)
|
||||
//! - `ambiguous-ctor` — `ctx`: `{"ctor": "<n>", "candidates": ["m1.T", "m2.T"]}` (Iter 15a)
|
||||
//! - `use-after-consume` — `ctx`: `{"binder": "<n>"}` (Iter 18c.2);
|
||||
//! carries non-empty [`Diagnostic::suggested_rewrites`] showing how to
|
||||
//! spell the fix in form-A AILang.
|
||||
//! - `consume-while-borrowed` — `ctx`: `{"binder": "<n>"}` (Iter 18c.2);
|
||||
//! ditto on `suggested_rewrites`.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -83,6 +88,31 @@ pub struct Diagnostic {
|
||||
pub def: Option<String>,
|
||||
/// Free structured context. Empty = `{}`.
|
||||
pub ctx: serde_json::Value,
|
||||
/// Iter 18c.2: machine-applicable fix suggestions, each a snippet of
|
||||
/// form-A AILang the author can paste back at the offending site.
|
||||
/// Empty = no rewrite suggested. The list is always emitted (so JSON
|
||||
/// consumers see a stable shape; they can branch on `.is_empty()`).
|
||||
/// Currently populated by the linearity check (`use-after-consume` /
|
||||
/// `consume-while-borrowed` codes); other diagnostics emit `[]`.
|
||||
pub suggested_rewrites: Vec<SuggestedRewrite>,
|
||||
}
|
||||
|
||||
/// One machine-applicable rewrite suggestion attached to a [`Diagnostic`].
|
||||
///
|
||||
/// Iter 18c.2: emitted by the linearity check to point the author (or an
|
||||
/// LLM consumer of `ail check --json`) at the spelled fix. `replacement`
|
||||
/// is form-A AILang text — parseable by `ailang_surface::parse_term`.
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct SuggestedRewrite {
|
||||
/// One-line free-form description of what the rewrite does (e.g.
|
||||
/// `"wrap the offending use in (clone X)"`). Not parsed by tooling.
|
||||
pub description: String,
|
||||
/// Form-A AILang snippet to substitute at the offending site. For
|
||||
/// the linearity-check diagnostics this is typically `(clone <n>)`
|
||||
/// or a small enclosing rewrite. The string MUST parse via the
|
||||
/// surface `parse_term` entrypoint; [`crate::linearity`] tests
|
||||
/// guard the round-trip.
|
||||
pub replacement: String,
|
||||
}
|
||||
|
||||
impl Diagnostic {
|
||||
@@ -97,9 +127,25 @@ impl Diagnostic {
|
||||
message: message.into(),
|
||||
def: None,
|
||||
ctx: serde_json::Value::Object(serde_json::Map::new()),
|
||||
suggested_rewrites: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 18c.2: append a [`SuggestedRewrite`]. Builder-style. Used by
|
||||
/// the linearity check to attach the form-A fix it computed. Other
|
||||
/// diagnostics leave the field empty.
|
||||
pub fn with_suggested_rewrite(
|
||||
mut self,
|
||||
description: impl Into<String>,
|
||||
replacement: impl Into<String>,
|
||||
) -> Self {
|
||||
self.suggested_rewrites.push(SuggestedRewrite {
|
||||
description: description.into(),
|
||||
replacement: replacement.into(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the affected top-level def name. Builder-style: returns
|
||||
/// `self`. Used by [`crate::CheckError::to_diagnostic`] when the
|
||||
/// error was wrapped in [`crate::CheckError::Def`].
|
||||
@@ -128,11 +174,13 @@ mod tests {
|
||||
.with_def("main");
|
||||
let s = serde_json::to_string(&d).unwrap();
|
||||
// All fields must be present, severity lowercase, ctx is an
|
||||
// empty object (not null, not omitted).
|
||||
// empty object (not null, not omitted), suggested_rewrites is
|
||||
// always-emitted as `[]` for non-linearity diagnostics.
|
||||
assert!(s.contains("\"severity\":\"error\""), "{s}");
|
||||
assert!(s.contains("\"code\":\"unbound-var\""), "{s}");
|
||||
assert!(s.contains("\"def\":\"main\""), "{s}");
|
||||
assert!(s.contains("\"ctx\":{}"), "{s}");
|
||||
assert!(s.contains("\"suggested_rewrites\":[]"), "{s}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -143,4 +191,17 @@ mod tests {
|
||||
assert!(s.contains("\"expected\":\"Int\""), "{s}");
|
||||
assert!(s.contains("\"actual\":\"Bool\""), "{s}");
|
||||
}
|
||||
|
||||
/// Iter 18c.2: a diagnostic carrying a `SuggestedRewrite` serialises
|
||||
/// the rewrite under `suggested_rewrites` with the description and
|
||||
/// the form-A replacement.
|
||||
#[test]
|
||||
fn suggested_rewrite_serializes() {
|
||||
let d = Diagnostic::error("use-after-consume", "use of `xs` after consume")
|
||||
.with_suggested_rewrite("wrap earlier use in (clone)", "(clone xs)");
|
||||
let s = serde_json::to_string(&d).unwrap();
|
||||
assert!(s.contains("\"suggested_rewrites\":["), "{s}");
|
||||
assert!(s.contains("\"description\":\"wrap earlier use in (clone)\""), "{s}");
|
||||
assert!(s.contains("\"replacement\":\"(clone xs)\""), "{s}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user