1
//! `is_capture_free` soundness tests: a body inlined at a HOF call site must
2
//! reference no creation-site `Variable`. The walk must see through quasiquote
3
//! unquotes and nested lambdas (real capture → reject) while treating the
4
//! closure's own params as bound (shadowing an outer name → still inlinable).
5

            
6
use super::is_capture_free;
7
use crate::ast::{Expr, LambdaParams};
8
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
9

            
10
18
fn sym(s: &str) -> Expr {
11
18
    Expr::Symbol(s.to_string())
12
18
}
13

            
14
7
fn list(items: Vec<Expr>) -> Expr {
15
7
    Expr::List(items)
16
7
}
17

            
18
/// A symbol table where `x` is a creation-site `Variable` (a capturable name).
19
7
fn table_with_var_x() -> SymbolTable {
20
7
    let mut t = SymbolTable::new();
21
7
    t.define(Symbol::new("x", SymbolKind::Variable).with_value(Expr::Bool(true)));
22
7
    t
23
7
}
24

            
25
9
fn params(required: &[&str]) -> LambdaParams {
26
9
    LambdaParams::simple(required.iter().map(|s| (*s).to_string()).collect())
27
9
}
28

            
29
#[test]
30
1
fn free_variable_in_body_is_not_capture_free() {
31
1
    let body = list(vec![sym("+"), sym("acc"), sym("x")]);
32
1
    assert!(!is_capture_free(
33
1
        &table_with_var_x(),
34
1
        &params(&["acc"]),
35
1
        &body
36
1
    ));
37
1
}
38

            
39
#[test]
40
1
fn param_shadowing_outer_var_stays_capture_free() {
41
    // `x` is the closure's own param, not a capture, even though an outer `x`
42
    // exists — params shadow the creation scope.
43
1
    let body = list(vec![sym("+"), sym("x"), Expr::Bool(false)]);
44
1
    assert!(is_capture_free(&table_with_var_x(), &params(&["x"]), &body));
45
1
}
46

            
47
#[test]
48
1
fn quasiquote_unquoting_outer_var_is_not_capture_free() {
49
    // `(lambda (acc) `(foo ,x))` — the unquote evaluates the creation-site `x`.
50
1
    let body = Expr::Quasiquote(Box::new(list(vec![
51
1
        sym("foo"),
52
1
        Expr::Unquote(Box::new(sym("x"))),
53
1
    ])));
54
1
    assert!(!is_capture_free(
55
1
        &table_with_var_x(),
56
1
        &params(&["acc"]),
57
1
        &body
58
1
    ));
59
1
}
60

            
61
#[test]
62
1
fn quoted_outer_var_is_data_and_capture_free() {
63
    // `(lambda (acc) 'x)` — quoted `x` is data, never evaluated.
64
1
    let body = Expr::Quote(Box::new(sym("x")));
65
1
    assert!(is_capture_free(
66
1
        &table_with_var_x(),
67
1
        &params(&["acc"]),
68
1
        &body
69
    ));
70
1
}
71

            
72
#[test]
73
1
fn nested_lambda_capturing_outer_var_is_not_capture_free() {
74
    // `(lambda (acc) (map (lambda (z) (+ z x)) acc))` — the inner lambda body
75
    // captures the creation-site `x`; the walk must not stop at the binder.
76
1
    let inner = Expr::Lambda(
77
1
        params(&["z"]),
78
1
        Box::new(list(vec![sym("+"), sym("z"), sym("x")])),
79
1
    );
80
1
    let body = list(vec![sym("map"), inner, sym("acc")]);
81
1
    assert!(!is_capture_free(
82
1
        &table_with_var_x(),
83
1
        &params(&["acc"]),
84
1
        &body
85
1
    ));
86
1
}
87

            
88
#[test]
89
1
fn dotted_quasiquote_unquoting_outer_var_is_not_capture_free() {
90
    // `(lambda (acc) `(,x . nil))` — the unquoted `x` rides a dotted Cons; the
91
    // walk must descend into both Cons arms, not stop at the pair.
92
1
    let body = Expr::Quasiquote(Box::new(Expr::Cons(
93
1
        Box::new(Expr::Unquote(Box::new(sym("x")))),
94
1
        Box::new(Expr::Nil),
95
1
    )));
96
1
    assert!(!is_capture_free(
97
1
        &table_with_var_x(),
98
1
        &params(&["acc"]),
99
1
        &body
100
1
    ));
101
1
}
102

            
103
#[test]
104
1
fn macro_headed_form_is_not_capture_free() {
105
    // `(lambda (acc) (use-x))` where `use-x` is a macro: it can expand to a
106
    // creation-scope variable, so the body is conservatively non-inlinable.
107
1
    let mut t = SymbolTable::new();
108
1
    t.define(Symbol::new("use-x", SymbolKind::Macro));
109
1
    let body = list(vec![sym("use-x")]);
110
1
    assert!(!is_capture_free(&t, &params(&["acc"]), &body));
111
1
}
112

            
113
#[test]
114
1
fn lambda_with_optional_param_is_not_inline_candidate() {
115
    // Optional/key/aux defaults and `&rest` need their own capture analysis, so
116
    // such a lambda is never recorded for inlining (stays on `call_ref`).
117
1
    let mut p = params(&["acc"]);
118
1
    p.optional.push(("y".to_string(), Some(Expr::Bool(true))));
119
1
    let body = list(vec![sym("+"), sym("acc"), sym("y")]);
120
1
    assert!(!is_capture_free(&table_with_var_x(), &p, &body));
121
1
}