Skip to main content

seqc/
capture_analysis.rs

1//! Capture Analysis for Closures
2//!
3//! This module handles the analysis of closure captures - determining which values
4//! from the creation site need to be captured in a closure's environment.
5//!
6//! The key insight is that closures bridge two stack effects:
7//! - **Body effect**: what the quotation body actually needs to execute
8//! - **Call effect**: what the call site will provide when the closure is invoked
9//!
10//! The difference between these determines what must be captured at creation time.
11//!
12//! ## Example
13//!
14//! ```text
15//! : add-to ( Int -- [Int -- Int] )
16//!   [ add ] ;
17//! ```
18//!
19//! Here:
20//! - Body needs: `(Int Int -- Int)` (add requires two integers)
21//! - Call provides: `(Int -- Int)` (caller provides one integer)
22//! - Captures: `[Int]` (one integer captured from creation site)
23
24use crate::types::{Effect, StackType, Type};
25
26/// Calculate capture types for a closure
27///
28/// Given:
29/// - `body_effect`: what the quotation body needs (e.g., `Int Int -- Int`)
30/// - `call_effect`: what the call site will provide (e.g., `Int -- Int`)
31///
32/// Returns:
33/// - `captures`: types to capture from creation stack (e.g., `[Int]`)
34///
35/// # Capture Ordering
36///
37/// Captures are returned bottom-to-top (deepest value first), matching the
38/// runtime's env layout and the order the closure body pushes them:
39///
40/// ```text
41/// Stack at creation: ( ...rest bottom top )
42/// push_closure pops top-down, then reverses:
43///   env[0] = bottom (caller's deepest capture)
44///   env[N-1] = top   (caller's shallowest capture)
45/// Closure function pushes env[0], env[1], ..., env[N-1] in order, so
46/// the body stack looks like ( ...rest bottom top ) — identical to the
47/// caller's visual order.
48/// ```
49///
50/// Single captures are unaffected (a one-element vector reversed is itself).
51///
52/// # Errors
53///
54/// Returns an error if the call site provides more values than the body needs.
55pub fn calculate_captures(body_effect: &Effect, call_effect: &Effect) -> Result<Vec<Type>, String> {
56    // Extract concrete types from stack types (bottom to top)
57    let body_inputs = extract_concrete_types(&body_effect.inputs);
58    let call_inputs = extract_concrete_types(&call_effect.inputs);
59
60    // Validate: call site shouldn't provide MORE than body needs
61    if call_inputs.len() > body_inputs.len() {
62        return Err(format!(
63            "Closure signature error: call site provides {} values but body only needs {}",
64            call_inputs.len(),
65            body_inputs.len()
66        ));
67    }
68
69    // Calculate how many to capture (from bottom of stack)
70    let capture_count = body_inputs.len() - call_inputs.len();
71
72    // Verify the topmost body inputs (the non-captured ones) align with
73    // what the call site provides. If they don't match, the body is
74    // incompatible with the combinator regardless of captures.
75    let body_provided = &body_inputs[capture_count..];
76    for (i, (body_type, call_type)) in body_provided.iter().zip(call_inputs.iter()).enumerate() {
77        if body_type != call_type {
78            // Type variables (like Acc, T from row polymorphism) won't match
79            // concrete types here — that's expected, because the body's types
80            // are inferred from a seeded row-variable stack. Skip the check
81            // for type variables; they'll be verified by downstream unification.
82            let is_var = matches!(body_type, Type::Var(_)) || matches!(call_type, Type::Var(_));
83            if !is_var {
84                return Err(format!(
85                    "Closure capture error: body input at position {} (from top) is {}, \
86                     but combinator provides {}. The non-captured inputs must match.",
87                    i, body_type, call_type
88                ));
89            }
90        }
91    }
92
93    // Captures are the first N types (bottom of stack)
94    // Example: body needs [Int, String] (bottom to top), call provides [String]
95    // Captures: [Int] (the bottom type)
96    Ok(body_inputs[0..capture_count].to_vec())
97}
98
99/// Extract concrete types from a stack type (bottom to top order)
100///
101/// This function traverses a `StackType` and returns a vector of concrete types
102/// in bottom-to-top order (deepest stack element first).
103///
104/// # Example
105///
106/// ```text
107/// Input: Cons { rest: Cons { rest: Empty, top: Int }, top: String }
108/// Output: [Int, String]  (bottom to top)
109/// ```
110///
111/// # Row Variables
112///
113/// Row variables (like `..a`) are skipped - this function only extracts
114/// concrete types. This is appropriate for capture analysis where we need
115/// to know the actual types being captured.
116///
117/// # Performance
118///
119/// Uses recursion to build the vector in the correct order without needing
120/// to clone the entire stack structure or reverse the result.
121pub(crate) fn extract_concrete_types(stack: &StackType) -> Vec<Type> {
122    // Use recursion to build the vector in bottom-to-top order
123    fn collect(stack: &StackType, result: &mut Vec<Type>) {
124        match stack {
125            StackType::Cons { rest, top } => {
126                // First recurse to collect types below, then add this type
127                collect(rest, result);
128                result.push(top.clone());
129            }
130            StackType::Empty | StackType::RowVar(_) => {
131                // Base case: nothing more to collect
132            }
133        }
134    }
135
136    let mut types = Vec::new();
137    collect(stack, &mut types);
138    types
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::types::{Effect, StackType, Type};
145
146    fn make_stack(types: &[Type]) -> StackType {
147        let mut stack = StackType::Empty;
148        for t in types {
149            stack = StackType::Cons {
150                rest: Box::new(stack),
151                top: t.clone(),
152            };
153        }
154        stack
155    }
156
157    fn make_effect(inputs: &[Type], outputs: &[Type]) -> Effect {
158        Effect {
159            inputs: make_stack(inputs),
160            outputs: make_stack(outputs),
161            effects: Vec::new(),
162        }
163    }
164
165    #[test]
166    fn test_extract_empty_stack() {
167        let types = extract_concrete_types(&StackType::Empty);
168        assert!(types.is_empty());
169    }
170
171    #[test]
172    fn test_extract_single_type() {
173        let stack = make_stack(&[Type::Int]);
174        let types = extract_concrete_types(&stack);
175        assert_eq!(types, vec![Type::Int]);
176    }
177
178    #[test]
179    fn test_extract_multiple_types() {
180        let stack = make_stack(&[Type::Int, Type::String, Type::Bool]);
181        let types = extract_concrete_types(&stack);
182        assert_eq!(types, vec![Type::Int, Type::String, Type::Bool]);
183    }
184
185    #[test]
186    fn test_calculate_no_captures() {
187        // Body needs (Int -- Int), call provides (Int -- Int)
188        let body = make_effect(&[Type::Int], &[Type::Int]);
189        let call = make_effect(&[Type::Int], &[Type::Int]);
190
191        let captures = calculate_captures(&body, &call).unwrap();
192        assert!(captures.is_empty());
193    }
194
195    #[test]
196    fn test_calculate_one_capture() {
197        // Body needs (Int Int -- Int), call provides (Int -- Int)
198        // Should capture one Int
199        let body = make_effect(&[Type::Int, Type::Int], &[Type::Int]);
200        let call = make_effect(&[Type::Int], &[Type::Int]);
201
202        let captures = calculate_captures(&body, &call).unwrap();
203        assert_eq!(captures, vec![Type::Int]);
204    }
205
206    #[test]
207    fn test_calculate_multiple_captures() {
208        // Body needs (Int String Bool -- Bool), call provides (Bool -- Bool)
209        // Should capture [Int, String] (bottom to top)
210        let body = make_effect(&[Type::Int, Type::String, Type::Bool], &[Type::Bool]);
211        let call = make_effect(&[Type::Bool], &[Type::Bool]);
212
213        let captures = calculate_captures(&body, &call).unwrap();
214        assert_eq!(captures, vec![Type::Int, Type::String]);
215    }
216
217    #[test]
218    fn test_calculate_all_captured() {
219        // Body needs (Int String -- Int), call provides ( -- Int)
220        // Should capture [Int, String]
221        let body = make_effect(&[Type::Int, Type::String], &[Type::Int]);
222        let call = make_effect(&[], &[Type::Int]);
223
224        let captures = calculate_captures(&body, &call).unwrap();
225        assert_eq!(captures, vec![Type::Int, Type::String]);
226    }
227
228    #[test]
229    fn test_calculate_error_too_many_call_inputs() {
230        // Body needs (Int -- Int), call provides (Int Int -- Int)
231        // Error: call provides more than body needs
232        let body = make_effect(&[Type::Int], &[Type::Int]);
233        let call = make_effect(&[Type::Int, Type::Int], &[Type::Int]);
234
235        let result = calculate_captures(&body, &call);
236        assert!(result.is_err());
237        assert!(
238            result
239                .unwrap_err()
240                .contains("provides 2 values but body only needs 1")
241        );
242    }
243}