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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user