Iter 18d.1: Term::ReuseAs schema + linearity check

Schema floor for explicit reuse hints. Adds Term::ReuseAs
{ source: Box<Term>, body: Box<Term> } with serde tag "reuse-as",
form-A (reuse-as <source> <body>). Schema choice (wrapper, not
modifier-on-Ctor) and rationale recorded in DESIGN.md.

Codegen is identity under all --alloc strategies — lower body,
drop source on the floor. Iter 18d.2 will lower this as the
in-place rewrite under --alloc=rc.

User-facing diagnostics:
- typecheck reuse-as-non-allocating-body: body must be a
  Term::Ctor or Term::Lam, the two AST shapes that allocate.
  Other shapes (literal, var, app, ...) emit this with a
  suggested_rewrite that drops the wrapper.
- linearity reuse-as-source-not-bare-var: source must be a
  Term::Var referring to an in-scope binder. Anything else
  (literal, nested expression) is rejected here. Suggested
  rewrite drops the wrapper.
- linearity use-after-consume at a reuse-as site: source must
  not have been consumed earlier in the body. Suggested rewrite
  drops the wrapper.
- Visiting reuse-as marks source consumed for the rest of the
  body, so a subsequent use of the same binder flags
  use-after-consume against it.

Uniqueness inference treats source as Consume — the consume
shows up in the side table for the codegen consumer.

Tests:
- 4 surface parse-tests (round-trip, no-args, one-arg, full
  parse_term/term_to_form_a round-trip).
- 1 typecheck unit test (non-allocating body).
- 2 linearity unit tests (non-var source, after-consume).
- 1 integration test (happy-path map_inc in all-explicit mode
  is linearity-clean).
- 1 E2E test running the canonical map_inc-via-reuse-as fixture
  under --alloc=gc, asserting stdout 9.
- New fixture examples/reuse_as_demo.{ailx,ail.json}.

Test deltas: E2E 55 -> 56, ailang-check unit 43 -> 46,
ailang-check workspace 8 -> 9, surface 14 -> 18. cargo test
--workspace green. No existing fixture's canonical JSON changed.
This commit is contained in:
2026-05-08 10:55:31 +02:00
parent 8f40e8c31f
commit 74379b29e5
15 changed files with 785 additions and 1 deletions
+23
View File
@@ -184,6 +184,12 @@ fn walk(t: &Term, out: &mut NonEscapeSet) {
// sub-terms looking for Let-allocation candidates.
walk(value, out);
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: identity at IR level (codegen lowers `body`,
// ignores `source`). Walk both children for completeness.
walk(source, out);
walk(body, out);
}
Term::Lit { .. } | Term::Var { .. } => {}
}
}
@@ -349,6 +355,18 @@ fn escapes(t: &Term, tainted: &BTreeSet<String>, in_tail: bool) -> bool {
// through with the same `in_tail` context.
escapes(value, tainted, in_tail)
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: identity at IR level. The whole expression's
// value is `body`, so its escape verdict is the body's
// (threaded through `in_tail`). The `source` is evaluated
// but its value is discarded; it cannot escape via the
// tail position, but a tainted name reachable inside it
// could still escape — walk it in non-tail mode.
if escapes(source, tainted, false) {
return true;
}
escapes(body, tainted, in_tail)
}
}
}
@@ -448,6 +466,11 @@ fn collect_free_vars(t: &Term, bound: &mut BTreeSet<String>, out: &mut BTreeSet<
// Iter 18c.1: clone is identity for free-var collection.
collect_free_vars(value, bound, out);
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: free vars are the union of source and body.
collect_free_vars(source, bound, out);
collect_free_vars(body, bound, out);
}
}
}
+27
View File
@@ -1512,6 +1512,18 @@ impl<'a> Emitter<'a> {
}
Ok((val_ssa, val_ty))
}
Term::ReuseAs { source: _, body } => {
// Iter 18d.1: identity. Iter 18d.2 will lower this as
// in-place rewrite under --alloc=rc. For now we simply
// lower `body` and return its `(ssa, ty)`. The `source`
// is dropped on the floor — in shipping fixtures it is
// always a `Term::Var` (no IR side effects), so the
// identity lowering is observably equivalent to lowering
// `body` alone. The linearity check (18c.2) and
// typecheck (18d.1) reject ill-formed shapes before we
// get here.
self.lower_term(body)
}
}
}
@@ -2774,6 +2786,11 @@ impl<'a> Emitter<'a> {
// free vars of `(clone X)` are exactly the free vars of `X`.
Self::collect_captures(value, bound, captures, captures_set, builtins, top_level);
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: free vars are the union of source and body.
Self::collect_captures(source, bound, captures, captures_set, builtins, top_level);
Self::collect_captures(body, bound, captures, captures_set, builtins, top_level);
}
}
}
@@ -3284,6 +3301,11 @@ impl<'a> Emitter<'a> {
// Iter 18c.1: clone is identity — same type as inner.
self.synth_with_extras(value, extras)
}
Term::ReuseAs { body, .. } => {
// Iter 18d.1: identity — the result type is the body's
// type. The source is dropped at codegen.
self.synth_with_extras(body, extras)
}
}
}
}
@@ -3677,6 +3699,11 @@ fn apply_subst_to_term(t: &Term, subst: &BTreeMap<String, Type>) -> Term {
// Iter 18c.1: structural recursion through the wrapper.
value: Box::new(apply_subst_to_term(value, subst)),
},
Term::ReuseAs { source, body } => Term::ReuseAs {
// Iter 18d.1: structural recursion through both children.
source: Box::new(apply_subst_to_term(source, subst)),
body: Box::new(apply_subst_to_term(body, subst)),
},
}
}