Lines
100 %
Functions
Branches
//! `is_capture_free` soundness tests: a body inlined at a HOF call site must
//! reference no creation-site `Variable`. The walk must see through quasiquote
//! unquotes and nested lambdas (real capture → reject) while treating the
//! closure's own params as bound (shadowing an outer name → still inlinable).
use super::is_capture_free;
use crate::ast::{Expr, LambdaParams};
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
fn sym(s: &str) -> Expr {
Expr::Symbol(s.to_string())
}
fn list(items: Vec<Expr>) -> Expr {
Expr::List(items)
/// A symbol table where `x` is a creation-site `Variable` (a capturable name).
fn table_with_var_x() -> SymbolTable {
let mut t = SymbolTable::new();
t.define(Symbol::new("x", SymbolKind::Variable).with_value(Expr::Bool(true)));
t
fn params(required: &[&str]) -> LambdaParams {
LambdaParams::simple(required.iter().map(|s| (*s).to_string()).collect())
#[test]
fn free_variable_in_body_is_not_capture_free() {
let body = list(vec![sym("+"), sym("acc"), sym("x")]);
assert!(!is_capture_free(
&table_with_var_x(),
¶ms(&["acc"]),
&body
));
fn param_shadowing_outer_var_stays_capture_free() {
// `x` is the closure's own param, not a capture, even though an outer `x`
// exists — params shadow the creation scope.
let body = list(vec![sym("+"), sym("x"), Expr::Bool(false)]);
assert!(is_capture_free(&table_with_var_x(), ¶ms(&["x"]), &body));
fn quasiquote_unquoting_outer_var_is_not_capture_free() {
// `(lambda (acc) `(foo ,x))` — the unquote evaluates the creation-site `x`.
let body = Expr::Quasiquote(Box::new(list(vec![
sym("foo"),
Expr::Unquote(Box::new(sym("x"))),
])));
fn quoted_outer_var_is_data_and_capture_free() {
// `(lambda (acc) 'x)` — quoted `x` is data, never evaluated.
let body = Expr::Quote(Box::new(sym("x")));
assert!(is_capture_free(
fn nested_lambda_capturing_outer_var_is_not_capture_free() {
// `(lambda (acc) (map (lambda (z) (+ z x)) acc))` — the inner lambda body
// captures the creation-site `x`; the walk must not stop at the binder.
let inner = Expr::Lambda(
params(&["z"]),
Box::new(list(vec![sym("+"), sym("z"), sym("x")])),
);
let body = list(vec![sym("map"), inner, sym("acc")]);
fn dotted_quasiquote_unquoting_outer_var_is_not_capture_free() {
// `(lambda (acc) `(,x . nil))` — the unquoted `x` rides a dotted Cons; the
// walk must descend into both Cons arms, not stop at the pair.
let body = Expr::Quasiquote(Box::new(Expr::Cons(
Box::new(Expr::Unquote(Box::new(sym("x")))),
Box::new(Expr::Nil),
)));
fn macro_headed_form_is_not_capture_free() {
// `(lambda (acc) (use-x))` where `use-x` is a macro: it can expand to a
// creation-scope variable, so the body is conservatively non-inlinable.
t.define(Symbol::new("use-x", SymbolKind::Macro));
let body = list(vec![sym("use-x")]);
assert!(!is_capture_free(&t, ¶ms(&["acc"]), &body));
fn lambda_with_optional_param_is_not_inline_candidate() {
// Optional/key/aux defaults and `&rest` need their own capture analysis, so
// such a lambda is never recorded for inlining (stays on `call_ref`).
let mut p = params(&["acc"]);
p.optional.push(("y".to_string(), Some(Expr::Bool(true))));
let body = list(vec![sym("+"), sym("acc"), sym("y")]);
assert!(!is_capture_free(&table_with_var_x(), &p, &body));