74379b29e5
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.
723 lines
26 KiB
Rust
723 lines
26 KiB
Rust
//! Per-fn escape analysis for ADT and closure allocations (Iter 17a).
|
|
//!
|
|
//! Identifies `Term::Ctor` and `Term::Lam` allocations whose value
|
|
//! does not escape the function in which they are allocated. Such
|
|
//! allocations can be lowered to LLVM `alloca` instead of `@GC_malloc`,
|
|
//! producing stack-allocated boxes that are auto-freed at fn return —
|
|
//! semantically equivalent to the all-GC version but bypassing the
|
|
//! collector entirely for fully-local data.
|
|
//!
|
|
//! The analysis is *purely an optimisation*. A pessimistic answer
|
|
//! (claiming an allocation escapes when it does not) only loses
|
|
//! efficiency, never correctness.
|
|
//!
|
|
//! ## Rule (conservative, name-based taint propagation)
|
|
//!
|
|
//! For each allocation site that is the value of a `Term::Let`:
|
|
//!
|
|
//! `let X = ALLOC in B`
|
|
//!
|
|
//! we ask whether the body `B` makes the value of `X` (or any value
|
|
//! derived from it via projection) reachable past the fn return.
|
|
//!
|
|
//! Taint propagation rule:
|
|
//! - The bound name `X` is **tainted** in `B`.
|
|
//! - A `Term::Match` whose scrutinee is a `Var` referring to a tainted
|
|
//! name propagates taint to every pattern-bound name in every arm.
|
|
//! (Pattern bindings hold field projections of the scrutinee, which
|
|
//! live inside the same allocation.)
|
|
//! - A `Term::Let { name = Y, value = Var(t), ... }` where `t` is
|
|
//! tainted makes `Y` tainted too in the let's body.
|
|
//!
|
|
//! Escape rule (any tainted name appearing in any of these positions
|
|
//! marks the allocation as escaping):
|
|
//! - **Tail position** of `B` (the value of `B` is the value of the
|
|
//! enclosing region; the let-body's value flows up).
|
|
//! - **`Term::App` arg position** (arg flows into a callee, which may
|
|
//! keep the value).
|
|
//! - **`Term::Do` arg position** (effect ops are runtime calls).
|
|
//! - **`Term::Ctor` field position** (storing into another box; the
|
|
//! parent box may itself escape).
|
|
//! - **`Term::Lam` capture** (free var of an enclosed lambda — the
|
|
//! lambda's env, possibly heap-allocated, holds the value past the
|
|
//! current fn frame).
|
|
//! - **`Term::App` callee position** when the callee is **not** the
|
|
//! simple Var-tainted call (we only special-case calling a
|
|
//! tainted-Var-bound closure as non-escaping; any other indirect
|
|
//! callee derived from the tainted value is escape).
|
|
//!
|
|
//! Allowed (non-escaping) positions:
|
|
//! - **Scrutinee of `Term::Match`** (scrutinee is read-then-projected;
|
|
//! the box itself doesn't escape unless an arm's body escapes a
|
|
//! pattern binding, handled by the taint propagation above).
|
|
//! - **Callee of `Term::App` when the callee is a bare `Var` to the
|
|
//! tainted name** (the closure pair is loaded and called locally).
|
|
//!
|
|
//! ## Lambda-specific note
|
|
//!
|
|
//! Closures often escape (passed as args to higher-order fns). The
|
|
//! same rule applies. A `let f = \x. body in B` allocation is
|
|
//! non-escaping iff `f` appears in `B` exclusively in the callee
|
|
//! position of `Term::App` (so the closure pair is loaded and
|
|
//! invoked locally).
|
|
//!
|
|
//! The Lam's *body* is itself a fresh fn frame from the analyzer's
|
|
//! perspective — the Lam body's captures already cross a fn
|
|
//! boundary, so the analyzer does not recurse into it. (The Lam's
|
|
//! body would be analyzed in its own top-level pass over the lifted
|
|
//! thunk, but in this codebase lambdas are not lifted to top-level
|
|
//! before codegen — they are emitted as deferred thunks during the
|
|
//! same fn-emission pass. We handle this by running the analysis on
|
|
//! every fn body and on every Lam body recursively.)
|
|
//!
|
|
//! ## What this analysis is NOT
|
|
//!
|
|
//! - Not a region inference system. Per-fn only.
|
|
//! - Not field-sensitive (we taint pattern bindings wholesale).
|
|
//! - Not flow-sensitive within an arm (we just check if any tainted
|
|
//! name appears in any escape position).
|
|
//!
|
|
//! Precision can be improved later; correctness is the priority for
|
|
//! this iter.
|
|
|
|
use ailang_core::ast::{Pattern, Term};
|
|
use std::collections::BTreeSet;
|
|
|
|
/// Set of allocation sites (identified by raw pointer address cast to
|
|
/// `usize`) that the escape analysis has proven do not escape their
|
|
/// allocating fn.
|
|
///
|
|
/// Pointer identity is stable for the duration of codegen because the
|
|
/// `Module` AST is borrowed immutably throughout.
|
|
pub type NonEscapeSet = BTreeSet<usize>;
|
|
|
|
/// Run the analysis over a fn body. Returns the set of allocation
|
|
/// sites (pointers to `Term::Ctor` / `Term::Lam` nodes) that may
|
|
/// be safely lowered with `alloca` instead of `@GC_malloc`.
|
|
pub fn analyze_fn_body(body: &Term) -> NonEscapeSet {
|
|
let mut out = NonEscapeSet::new();
|
|
walk(body, &mut out);
|
|
out
|
|
}
|
|
|
|
/// Walk the term, looking for `Let { name, value: Ctor | Lam, body }`
|
|
/// shapes. For each such shape, run the escape check on `body` with
|
|
/// the bound name as the initial tainted seed. If the check returns
|
|
/// false (no escape), record the value's pointer in `out`.
|
|
///
|
|
/// Then recurse into all sub-terms so that nested let-allocations and
|
|
/// lambdas are analyzed too.
|
|
fn walk(t: &Term, out: &mut NonEscapeSet) {
|
|
match t {
|
|
Term::Let { name, value, body } => {
|
|
// Candidate shape: let X = Ctor|Lam in B.
|
|
let alloc_kind = match value.as_ref() {
|
|
Term::Ctor { .. } => Some("ctor"),
|
|
Term::Lam { .. } => Some("lam"),
|
|
_ => None,
|
|
};
|
|
if alloc_kind.is_some() {
|
|
let mut tainted = BTreeSet::new();
|
|
tainted.insert(name.clone());
|
|
// The let's body is in tail position iff the let is —
|
|
// but we don't know the outer context here. Treat the
|
|
// body as if it were tail-of-the-fn: safer (more
|
|
// conservative). If body's tail value contains the
|
|
// tainted name, it escapes the let, which transitively
|
|
// escapes the fn.
|
|
if !escapes(body, &tainted, true) {
|
|
let ptr = (value.as_ref() as *const Term) as usize;
|
|
out.insert(ptr);
|
|
}
|
|
}
|
|
// Recurse into value and body for any nested allocations.
|
|
walk(value, out);
|
|
walk(body, out);
|
|
}
|
|
Term::App { callee, args, .. } => {
|
|
walk(callee, out);
|
|
for a in args {
|
|
walk(a, out);
|
|
}
|
|
}
|
|
Term::Do { args, .. } => {
|
|
for a in args {
|
|
walk(a, out);
|
|
}
|
|
}
|
|
Term::Ctor { args, .. } => {
|
|
for a in args {
|
|
walk(a, out);
|
|
}
|
|
}
|
|
Term::Match { scrutinee, arms } => {
|
|
walk(scrutinee, out);
|
|
for arm in arms {
|
|
walk(&arm.body, out);
|
|
}
|
|
}
|
|
Term::If { cond, then, else_ } => {
|
|
walk(cond, out);
|
|
walk(then, out);
|
|
walk(else_, out);
|
|
}
|
|
Term::Seq { lhs, rhs } => {
|
|
walk(lhs, out);
|
|
walk(rhs, out);
|
|
}
|
|
Term::Lam { body, .. } => {
|
|
// Recurse into the lambda body — it is itself a fn frame
|
|
// for our analysis purposes (each lifted thunk runs the
|
|
// same analysis when emit_fn is called for it; for
|
|
// un-lifted closures we still want to flag inner local
|
|
// allocations).
|
|
walk(body, out);
|
|
}
|
|
Term::LetRec { body, in_term, .. } => {
|
|
// Iter 16b.x: LetRec is normally eliminated before codegen.
|
|
// Still walk for completeness.
|
|
walk(body, out);
|
|
walk(in_term, out);
|
|
}
|
|
Term::Clone { value } => {
|
|
// Iter 18c.1: clone is identity at IR level — only walk
|
|
// 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 { .. } => {}
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if any tainted name escapes from the term `t`.
|
|
///
|
|
/// `in_tail` indicates whether `t`'s value flows up to the fn return
|
|
/// (or to the let-body whose value flows up, transitively). At the
|
|
/// initial call from `walk`, we set `in_tail = true` for the let's
|
|
/// body because the body's value is the let's value, which we cannot
|
|
/// see beyond — better to assume escape.
|
|
fn escapes(t: &Term, tainted: &BTreeSet<String>, in_tail: bool) -> bool {
|
|
match t {
|
|
Term::Lit { .. } => false,
|
|
Term::Var { name } => {
|
|
// A bare Var reference whose value flows to tail = escape.
|
|
in_tail && tainted.contains(name)
|
|
}
|
|
Term::App { callee, args, .. } => {
|
|
// Callee position. Special case: a direct call to a
|
|
// tainted Var (calling the bound closure) is not escape;
|
|
// the closure pair is loaded and invoked locally.
|
|
match callee.as_ref() {
|
|
Term::Var { name: cname } if tainted.contains(cname) => {
|
|
// Calling locally is fine; don't flag the callee as escape.
|
|
}
|
|
_ => {
|
|
if escapes(callee, tainted, false) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
// Args: any tainted Var as arg = escape. Sub-expressions
|
|
// recurse with in_tail=false.
|
|
for a in args {
|
|
if let Term::Var { name } = a {
|
|
if tainted.contains(name) {
|
|
return true;
|
|
}
|
|
}
|
|
if escapes(a, tainted, false) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
Term::Do { args, .. } => {
|
|
for a in args {
|
|
if let Term::Var { name } = a {
|
|
if tainted.contains(name) {
|
|
return true;
|
|
}
|
|
}
|
|
if escapes(a, tainted, false) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
Term::Ctor { args, .. } => {
|
|
for a in args {
|
|
if let Term::Var { name } = a {
|
|
if tainted.contains(name) {
|
|
return true;
|
|
}
|
|
}
|
|
if escapes(a, tainted, false) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
Term::Let { name, value, body } => {
|
|
if escapes(value, tainted, false) {
|
|
return true;
|
|
}
|
|
// If the let's value is a tainted Var, the new binding
|
|
// inherits taint. (Same for `Var`-only sub-expr; for any
|
|
// other shape we'd need a stronger analysis to detect
|
|
// taint flowing through computation. Conservative cut:
|
|
// only direct Var aliasing propagates.)
|
|
let mut local = tainted.clone();
|
|
if let Term::Var { name: vn } = value.as_ref() {
|
|
if tainted.contains(vn) {
|
|
local.insert(name.clone());
|
|
}
|
|
}
|
|
// Body: tail position propagates from the enclosing let.
|
|
escapes(body, &local, in_tail)
|
|
}
|
|
Term::Match { scrutinee, arms } => {
|
|
if escapes(scrutinee, tainted, false) {
|
|
return true;
|
|
}
|
|
// If the scrutinee is a tainted Var, propagate taint to
|
|
// pattern bindings (each arm independently).
|
|
let scrutinee_tainted = match scrutinee.as_ref() {
|
|
Term::Var { name } => tainted.contains(name),
|
|
_ => false,
|
|
};
|
|
for arm in arms {
|
|
let mut arm_tainted = tainted.clone();
|
|
if scrutinee_tainted {
|
|
let mut binds = Vec::new();
|
|
pattern_bound_names(&arm.pat, &mut binds);
|
|
for b in binds {
|
|
arm_tainted.insert(b);
|
|
}
|
|
}
|
|
if escapes(&arm.body, &arm_tainted, in_tail) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
Term::If { cond, then, else_ } => {
|
|
if escapes(cond, tainted, false) {
|
|
return true;
|
|
}
|
|
if escapes(then, tainted, in_tail) {
|
|
return true;
|
|
}
|
|
if escapes(else_, tainted, in_tail) {
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
Term::Seq { lhs, rhs } => {
|
|
if escapes(lhs, tainted, false) {
|
|
return true;
|
|
}
|
|
escapes(rhs, tainted, in_tail)
|
|
}
|
|
Term::Lam { params, body, .. } => {
|
|
// A lambda captures free vars. If any free var is tainted,
|
|
// the closure escapes the value via its env. (Even if the
|
|
// closure pair itself doesn't escape, capturing creates a
|
|
// store of the tainted value into a heap env, which the
|
|
// collector keeps. For correctness we just need: tainted
|
|
// value's lifetime <= current fn frame.)
|
|
//
|
|
// The lambda's own body is analyzed independently when its
|
|
// turn comes (via `walk`); here we only check captures.
|
|
let mut frees = BTreeSet::new();
|
|
let mut bound_in_lam: BTreeSet<String> = params.iter().cloned().collect();
|
|
collect_free_vars(body, &mut bound_in_lam, &mut frees);
|
|
for f in &frees {
|
|
if tainted.contains(f) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
Term::LetRec { .. } => {
|
|
// LetRec is normally eliminated by the lift-pass before
|
|
// codegen. If one slips through, be conservative and
|
|
// declare escape.
|
|
true
|
|
}
|
|
Term::Clone { value } => {
|
|
// Iter 18c.1: clone is identity. The wrapper's escape
|
|
// verdict is exactly that of its inner term, threaded
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Collect pattern-bound names (variables only; wildcards and
|
|
/// literals bind nothing).
|
|
fn pattern_bound_names(p: &Pattern, out: &mut Vec<String>) {
|
|
match p {
|
|
Pattern::Wild | Pattern::Lit { .. } => {}
|
|
Pattern::Var { name } => out.push(name.clone()),
|
|
Pattern::Ctor { fields, .. } => {
|
|
for f in fields {
|
|
pattern_bound_names(f, out);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Collect free variables of a term relative to a `bound` set. Used
|
|
/// only to check whether a Lam captures any tainted name. Mirrors
|
|
/// `Emitter::collect_captures` in lib.rs but without the builtin /
|
|
/// top-level filter (those don't matter for taint).
|
|
fn collect_free_vars(t: &Term, bound: &mut BTreeSet<String>, out: &mut BTreeSet<String>) {
|
|
match t {
|
|
Term::Lit { .. } => {}
|
|
Term::Var { name } => {
|
|
if !bound.contains(name) {
|
|
out.insert(name.clone());
|
|
}
|
|
}
|
|
Term::App { callee, args, .. } => {
|
|
collect_free_vars(callee, bound, out);
|
|
for a in args {
|
|
collect_free_vars(a, bound, out);
|
|
}
|
|
}
|
|
Term::Do { args, .. } => {
|
|
for a in args {
|
|
collect_free_vars(a, bound, out);
|
|
}
|
|
}
|
|
Term::Ctor { args, .. } => {
|
|
for a in args {
|
|
collect_free_vars(a, bound, out);
|
|
}
|
|
}
|
|
Term::Let { name, value, body } => {
|
|
collect_free_vars(value, bound, out);
|
|
let inserted = bound.insert(name.clone());
|
|
collect_free_vars(body, bound, out);
|
|
if inserted {
|
|
bound.remove(name);
|
|
}
|
|
}
|
|
Term::If { cond, then, else_ } => {
|
|
collect_free_vars(cond, bound, out);
|
|
collect_free_vars(then, bound, out);
|
|
collect_free_vars(else_, bound, out);
|
|
}
|
|
Term::Match { scrutinee, arms } => {
|
|
collect_free_vars(scrutinee, bound, out);
|
|
for arm in arms {
|
|
let mut binds = Vec::new();
|
|
pattern_bound_names(&arm.pat, &mut binds);
|
|
let mut newly = Vec::new();
|
|
for n in &binds {
|
|
if bound.insert(n.clone()) {
|
|
newly.push(n.clone());
|
|
}
|
|
}
|
|
collect_free_vars(&arm.body, bound, out);
|
|
for n in newly {
|
|
bound.remove(&n);
|
|
}
|
|
}
|
|
}
|
|
Term::Lam { params, body, .. } => {
|
|
let mut newly = Vec::new();
|
|
for p in params {
|
|
if bound.insert(p.clone()) {
|
|
newly.push(p.clone());
|
|
}
|
|
}
|
|
collect_free_vars(body, bound, out);
|
|
for n in newly {
|
|
bound.remove(&n);
|
|
}
|
|
}
|
|
Term::Seq { lhs, rhs } => {
|
|
collect_free_vars(lhs, bound, out);
|
|
collect_free_vars(rhs, bound, out);
|
|
}
|
|
Term::LetRec { body, in_term, .. } => {
|
|
collect_free_vars(body, bound, out);
|
|
collect_free_vars(in_term, bound, out);
|
|
}
|
|
Term::Clone { value } => {
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use ailang_core::ast::{Arm, Literal, Type};
|
|
|
|
fn lit_int(v: i64) -> Term {
|
|
Term::Lit {
|
|
lit: Literal::Int { value: v },
|
|
}
|
|
}
|
|
fn var(s: &str) -> Term {
|
|
Term::Var { name: s.into() }
|
|
}
|
|
fn ctor(ty: &str, c: &str, args: Vec<Term>) -> Term {
|
|
Term::Ctor {
|
|
type_name: ty.into(),
|
|
ctor: c.into(),
|
|
args,
|
|
}
|
|
}
|
|
|
|
/// Match-only consumption: `let xs = Cons(1, Nil) in match xs of _ -> 0`.
|
|
/// xs only used as match scrutinee; arm body returns a literal.
|
|
/// Expected: alloca-eligible.
|
|
#[test]
|
|
fn local_ctor_match_only() {
|
|
let alloc = ctor(
|
|
"L",
|
|
"Cons",
|
|
vec![lit_int(1), ctor("L", "Nil", vec![])],
|
|
);
|
|
let alloc_ptr = (&alloc as *const Term) as usize;
|
|
let body = Term::Let {
|
|
name: "xs".into(),
|
|
value: Box::new(alloc.clone()),
|
|
body: Box::new(Term::Match {
|
|
scrutinee: Box::new(var("xs")),
|
|
arms: vec![Arm {
|
|
pat: Pattern::Wild,
|
|
body: lit_int(0),
|
|
}],
|
|
}),
|
|
};
|
|
let neset = analyze_fn_body(&body);
|
|
// The allocation site we care about is the `value` field of
|
|
// the Let. Since `body` was constructed inline (the Let's
|
|
// value is a Box copy of `alloc`), use the boxed copy's addr.
|
|
// Walk the AST to find it.
|
|
let let_value_ptr = match &body {
|
|
Term::Let { value, .. } => (value.as_ref() as *const Term) as usize,
|
|
_ => unreachable!(),
|
|
};
|
|
assert!(
|
|
neset.contains(&let_value_ptr),
|
|
"expected let-bound ctor to be non-escaping; neset={:?}",
|
|
neset
|
|
);
|
|
// Avoid unused warning in case ptr identity changed.
|
|
let _ = alloc_ptr;
|
|
}
|
|
|
|
/// Returned value: `let xs = Cons(1, Nil) in xs`. xs in tail
|
|
/// position. Expected: NOT alloca-eligible.
|
|
#[test]
|
|
fn returned_ctor_escapes() {
|
|
let alloc = ctor(
|
|
"L",
|
|
"Cons",
|
|
vec![lit_int(1), ctor("L", "Nil", vec![])],
|
|
);
|
|
let body = Term::Let {
|
|
name: "xs".into(),
|
|
value: Box::new(alloc),
|
|
body: Box::new(var("xs")),
|
|
};
|
|
let neset = analyze_fn_body(&body);
|
|
let let_value_ptr = match &body {
|
|
Term::Let { value, .. } => (value.as_ref() as *const Term) as usize,
|
|
_ => unreachable!(),
|
|
};
|
|
assert!(
|
|
!neset.contains(&let_value_ptr),
|
|
"let-bound ctor that flows to tail must escape"
|
|
);
|
|
}
|
|
|
|
/// Passed to a fn: `let xs = Cons(1, Nil) in length(xs)`. xs in
|
|
/// arg position. Expected: NOT alloca-eligible.
|
|
#[test]
|
|
fn ctor_passed_as_arg_escapes() {
|
|
let alloc = ctor("L", "Nil", vec![]);
|
|
let body = Term::Let {
|
|
name: "xs".into(),
|
|
value: Box::new(alloc),
|
|
body: Box::new(Term::App {
|
|
callee: Box::new(var("length")),
|
|
args: vec![var("xs")],
|
|
tail: false,
|
|
}),
|
|
};
|
|
let neset = analyze_fn_body(&body);
|
|
let let_value_ptr = match &body {
|
|
Term::Let { value, .. } => (value.as_ref() as *const Term) as usize,
|
|
_ => unreachable!(),
|
|
};
|
|
assert!(
|
|
!neset.contains(&let_value_ptr),
|
|
"let-bound ctor passed as arg must escape"
|
|
);
|
|
}
|
|
|
|
/// Stored into another ctor: `let h = Mk(1) in Cons(h, Nil)`.
|
|
/// h is a field of the outer Cons. Expected: NOT alloca-eligible.
|
|
#[test]
|
|
fn ctor_stored_in_ctor_field_escapes() {
|
|
let h_alloc = ctor("X", "Mk", vec![lit_int(1)]);
|
|
let body = Term::Let {
|
|
name: "h".into(),
|
|
value: Box::new(h_alloc),
|
|
body: Box::new(ctor(
|
|
"L",
|
|
"Cons",
|
|
vec![var("h"), ctor("L", "Nil", vec![])],
|
|
)),
|
|
};
|
|
let neset = analyze_fn_body(&body);
|
|
let let_value_ptr = match &body {
|
|
Term::Let { value, .. } => (value.as_ref() as *const Term) as usize,
|
|
_ => unreachable!(),
|
|
};
|
|
assert!(
|
|
!neset.contains(&let_value_ptr),
|
|
"let-bound ctor stored as ctor field must escape"
|
|
);
|
|
}
|
|
|
|
/// Match arm returns a pattern-bound name: `let xs = Cons(1, Nil)
|
|
/// in match xs of Cons h _ -> h | Nil -> 0`. h is tainted via the
|
|
/// scrutinee, returned in arm tail. Expected: NOT alloca-eligible.
|
|
#[test]
|
|
fn pattern_binding_returned_escapes() {
|
|
let alloc = ctor(
|
|
"L",
|
|
"Cons",
|
|
vec![lit_int(1), ctor("L", "Nil", vec![])],
|
|
);
|
|
let body = Term::Let {
|
|
name: "xs".into(),
|
|
value: Box::new(alloc),
|
|
body: Box::new(Term::Match {
|
|
scrutinee: Box::new(var("xs")),
|
|
arms: vec![
|
|
Arm {
|
|
pat: Pattern::Ctor {
|
|
ctor: "Cons".into(),
|
|
fields: vec![
|
|
Pattern::Var { name: "h".into() },
|
|
Pattern::Wild,
|
|
],
|
|
},
|
|
body: var("h"),
|
|
},
|
|
Arm {
|
|
pat: Pattern::Ctor {
|
|
ctor: "Nil".into(),
|
|
fields: vec![],
|
|
},
|
|
body: lit_int(0),
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
let neset = analyze_fn_body(&body);
|
|
let let_value_ptr = match &body {
|
|
Term::Let { value, .. } => (value.as_ref() as *const Term) as usize,
|
|
_ => unreachable!(),
|
|
};
|
|
assert!(
|
|
!neset.contains(&let_value_ptr),
|
|
"scrutinee whose pattern binding flows to arm tail must escape"
|
|
);
|
|
}
|
|
|
|
/// Lambda call-only: `let f = \x. x in f(42)`. f only used as
|
|
/// callee. Expected: alloca-eligible.
|
|
#[test]
|
|
fn local_lam_call_only() {
|
|
let lam = Term::Lam {
|
|
params: vec!["x".into()],
|
|
param_tys: vec![Type::int()],
|
|
ret_ty: Box::new(Type::int()),
|
|
effects: vec![],
|
|
body: Box::new(var("x")),
|
|
};
|
|
let body = Term::Let {
|
|
name: "f".into(),
|
|
value: Box::new(lam),
|
|
body: Box::new(Term::App {
|
|
callee: Box::new(var("f")),
|
|
args: vec![lit_int(42)],
|
|
tail: false,
|
|
}),
|
|
};
|
|
let neset = analyze_fn_body(&body);
|
|
let let_value_ptr = match &body {
|
|
Term::Let { value, .. } => (value.as_ref() as *const Term) as usize,
|
|
_ => unreachable!(),
|
|
};
|
|
assert!(
|
|
neset.contains(&let_value_ptr),
|
|
"let-bound lambda used only as callee must be non-escaping; got {:?}",
|
|
neset
|
|
);
|
|
}
|
|
|
|
/// Lambda passed to HOF: `let f = \x. x in map(f, xs)`. f passed
|
|
/// as arg. Expected: NOT alloca-eligible.
|
|
#[test]
|
|
fn lam_passed_as_arg_escapes() {
|
|
let lam = Term::Lam {
|
|
params: vec!["x".into()],
|
|
param_tys: vec![Type::int()],
|
|
ret_ty: Box::new(Type::int()),
|
|
effects: vec![],
|
|
body: Box::new(var("x")),
|
|
};
|
|
let body = Term::Let {
|
|
name: "f".into(),
|
|
value: Box::new(lam),
|
|
body: Box::new(Term::App {
|
|
callee: Box::new(var("map")),
|
|
args: vec![var("f"), var("xs")],
|
|
tail: false,
|
|
}),
|
|
};
|
|
let neset = analyze_fn_body(&body);
|
|
let let_value_ptr = match &body {
|
|
Term::Let { value, .. } => (value.as_ref() as *const Term) as usize,
|
|
_ => unreachable!(),
|
|
};
|
|
assert!(
|
|
!neset.contains(&let_value_ptr),
|
|
"let-bound lambda passed as arg must escape"
|
|
);
|
|
}
|
|
}
|