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