Skip to main content

logicaffeine_compile/analysis/
escape.rs

1//! Escape analysis for zone safety.
2//!
3//! Implements the "Hotel California" containment rule: values can enter
4//! zones but cannot escape. This pass checks for obvious violations before
5//! code generation, providing Socratic error messages.
6//!
7//! # The Zone Rule
8//!
9//! Variables defined inside a zone are deallocated when the zone ends.
10//! Returning or assigning them to outer variables would create dangling
11//! references. This pass catches these common patterns with friendly errors.
12//!
13//! # Example
14//!
15//! ```text
16//! Zone "temp":
17//!     Let x be 5.
18//!     Return x.        ← Error: x cannot escape zone "temp"
19//! ```
20//!
21//! # Limitations
22//!
23//! More complex escape patterns (e.g., through closures or indirect references)
24//! are caught by Rust's borrow checker at compile time. This pass catches the
25//! common cases with better LOGOS-specific error messages.
26
27use std::collections::HashMap;
28use crate::ast::stmt::{Stmt, Expr, Block};
29use crate::intern::{Interner, Symbol};
30use crate::token::Span;
31
32/// Error type for escape violations
33#[derive(Debug, Clone)]
34pub struct EscapeError {
35    pub kind: EscapeErrorKind,
36    pub span: Span,
37}
38
39#[derive(Debug, Clone)]
40pub enum EscapeErrorKind {
41    /// Variable cannot escape zone via return
42    ReturnEscape {
43        variable: String,
44        zone_name: String,
45    },
46    /// Variable cannot escape zone via assignment to outer variable
47    AssignmentEscape {
48        variable: String,
49        target: String,
50        zone_name: String,
51    },
52}
53
54impl std::fmt::Display for EscapeError {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        match &self.kind {
57            EscapeErrorKind::ReturnEscape { variable, zone_name } => {
58                write!(
59                    f,
60                    "Reference '{}' cannot escape zone '{}'.\n\n\
61                    Variables allocated inside a zone are deallocated when the zone ends.\n\
62                    Returning them would create a dangling reference.\n\n\
63                    Tip: Copy the data if you need it outside the zone.",
64                    variable, zone_name
65                )
66            }
67            EscapeErrorKind::AssignmentEscape { variable, target, zone_name } => {
68                write!(
69                    f,
70                    "Reference '{}' cannot escape zone '{}' via assignment to '{}'.\n\n\
71                    Variables allocated inside a zone are deallocated when the zone ends.\n\
72                    Assigning them to outer scope variables would create a dangling reference.\n\n\
73                    Tip: Copy the data if you need it outside the zone.",
74                    variable, zone_name, target
75                )
76            }
77        }
78    }
79}
80
81impl std::error::Error for EscapeError {}
82
83/// Tracks the "zone depth" of variables for escape analysis
84pub struct EscapeChecker<'a> {
85    /// Maps variable symbols to their zone depth (0 = global/outside all zones)
86    zone_depth: HashMap<Symbol, usize>,
87    /// Current zone depth (increases when entering zones)
88    current_depth: usize,
89    /// Stack of zone names for error messages
90    zone_stack: Vec<Symbol>,
91    /// String interner for resolving symbols
92    interner: &'a Interner,
93}
94
95impl<'a> EscapeChecker<'a> {
96    /// Create a new escape checker
97    pub fn new(interner: &'a Interner) -> Self {
98        Self {
99            zone_depth: HashMap::new(),
100            current_depth: 0,
101            zone_stack: Vec::new(),
102            interner,
103        }
104    }
105
106    /// Check a program (list of statements) for escape violations
107    pub fn check_program(&mut self, stmts: &[Stmt<'_>]) -> Result<(), EscapeError> {
108        self.check_block(stmts)
109    }
110
111    /// Collect every escape finding with its top-level statement index
112    /// (mapping onto `Parser::stmt_spans()`), continuing past failures.
113    pub fn check_program_collect(&mut self, stmts: &[Stmt<'_>]) -> Vec<(usize, EscapeError)> {
114        stmts
115            .iter()
116            .enumerate()
117            .filter_map(|(index, stmt)| self.check_stmt(stmt).err().map(|e| (index, e)))
118            .collect()
119    }
120
121    /// Check a block of statements
122    fn check_block(&mut self, stmts: &[Stmt<'_>]) -> Result<(), EscapeError> {
123        for stmt in stmts {
124            self.check_stmt(stmt)?;
125        }
126        Ok(())
127    }
128
129    /// Check a single statement for escape violations
130    fn check_stmt(&mut self, stmt: &Stmt<'_>) -> Result<(), EscapeError> {
131        match stmt {
132            Stmt::Zone { name, body, .. } => {
133                // Enter zone: increase depth
134                self.current_depth += 1;
135                self.zone_stack.push(*name);
136
137                // Check body statements
138                self.check_block(body)?;
139
140                // Exit zone: decrease depth
141                self.zone_stack.pop();
142                self.current_depth -= 1;
143            }
144
145            Stmt::Let { var, .. } => {
146                // Register variable at current depth
147                self.zone_depth.insert(*var, self.current_depth);
148            }
149
150            Stmt::Return { value: Some(expr) } => {
151                // Return escapes all zones (target depth = 0)
152                self.check_no_escape(expr, 0)?;
153            }
154
155            Stmt::Set { target, value } => {
156                // Assignment: check if value escapes to target's depth
157                let target_depth = self.zone_depth.get(target).copied().unwrap_or(0);
158                self.check_no_escape_with_target(value, target_depth, *target)?;
159            }
160
161            // Recurse into nested blocks
162            Stmt::If { then_block, else_block, .. } => {
163                self.check_block(then_block)?;
164                if let Some(else_b) = else_block {
165                    self.check_block(else_b)?;
166                }
167            }
168
169            Stmt::While { body, .. } => {
170                self.check_block(body)?;
171            }
172
173            Stmt::Repeat { body, .. } => {
174                self.check_block(body)?;
175            }
176
177            Stmt::Inspect { arms, .. } => {
178                for arm in arms {
179                    self.check_block(arm.body)?;
180                }
181            }
182
183            // Escape blocks are opaque — the Rust compiler handles zone safety for raw code
184            Stmt::Escape { .. } => {}
185
186            // Other statements don't introduce escape risks
187            _ => {}
188        }
189        Ok(())
190    }
191
192    /// Check that an expression doesn't escape to a shallower depth
193    fn check_no_escape(&self, expr: &Expr<'_>, max_depth: usize) -> Result<(), EscapeError> {
194        match expr {
195            Expr::Identifier(sym) => {
196                if let Some(&depth) = self.zone_depth.get(sym) {
197                    if depth > max_depth && depth > 0 {
198                        // This variable was defined in a deeper zone
199                        let zone_name = self.zone_stack.get(depth - 1)
200                            .map(|s| self.interner.resolve(*s).to_string())
201                            .unwrap_or_else(|| "unknown".to_string());
202                        let var_name = self.interner.resolve(*sym).to_string();
203                        return Err(EscapeError {
204                            kind: EscapeErrorKind::ReturnEscape {
205                                variable: var_name,
206                                zone_name,
207                            },
208                            span: Span::default(),
209                        });
210                    }
211                }
212            }
213
214            // Recurse into compound expressions
215            Expr::BinaryOp { left, right, .. } => {
216                self.check_no_escape(left, max_depth)?;
217                self.check_no_escape(right, max_depth)?;
218            }
219
220            Expr::Call { args, .. } => {
221                for arg in args {
222                    self.check_no_escape(arg, max_depth)?;
223                }
224            }
225
226            Expr::FieldAccess { object, .. } => {
227                self.check_no_escape(object, max_depth)?;
228            }
229
230            Expr::Index { collection, index } => {
231                self.check_no_escape(collection, max_depth)?;
232                self.check_no_escape(index, max_depth)?;
233            }
234
235            Expr::Slice { collection, start, end } => {
236                self.check_no_escape(collection, max_depth)?;
237                self.check_no_escape(start, max_depth)?;
238                self.check_no_escape(end, max_depth)?;
239            }
240
241            Expr::Copy { expr } | Expr::Give { value: expr } | Expr::Length { collection: expr }
242            | Expr::Not { operand: expr } => {
243                self.check_no_escape(expr, max_depth)?;
244            }
245
246            Expr::List(items) | Expr::Tuple(items) => {
247                for item in items {
248                    self.check_no_escape(item, max_depth)?;
249                }
250            }
251
252            Expr::Range { start, end } => {
253                self.check_no_escape(start, max_depth)?;
254                self.check_no_escape(end, max_depth)?;
255            }
256
257            Expr::New { init_fields, .. } => {
258                for (_, expr) in init_fields {
259                    self.check_no_escape(expr, max_depth)?;
260                }
261            }
262
263            Expr::NewVariant { fields, .. } => {
264                for (_, expr) in fields {
265                    self.check_no_escape(expr, max_depth)?;
266                }
267            }
268
269            Expr::ManifestOf { zone } => {
270                self.check_no_escape(zone, max_depth)?;
271            }
272
273            Expr::ChunkAt { index, zone } => {
274                self.check_no_escape(index, max_depth)?;
275                self.check_no_escape(zone, max_depth)?;
276            }
277
278            Expr::Contains { collection, value } => {
279                self.check_no_escape(collection, max_depth)?;
280                self.check_no_escape(value, max_depth)?;
281            }
282
283            Expr::Union { left, right } | Expr::Intersection { left, right } => {
284                self.check_no_escape(left, max_depth)?;
285                self.check_no_escape(right, max_depth)?;
286            }
287
288            Expr::WithCapacity { value, capacity } => {
289                self.check_no_escape(value, max_depth)?;
290                self.check_no_escape(capacity, max_depth)?;
291            }
292
293            Expr::OptionSome { value } => {
294                self.check_no_escape(value, max_depth)?;
295            }
296            Expr::OptionNone => {}
297
298            // Escape expressions are opaque — the Rust compiler handles zone safety for raw code
299            Expr::Escape { .. } => {}
300
301            Expr::Closure { body, .. } => {
302                match body {
303                    crate::ast::stmt::ClosureBody::Expression(expr) => {
304                        self.check_no_escape(expr, max_depth)?;
305                    }
306                    crate::ast::stmt::ClosureBody::Block(_) => {
307                        // Block closures don't escape zone values through their definition.
308                        // The closure body is a separate scope; zone safety for the body
309                        // is checked when the closure is called, not when it's defined.
310                    }
311                }
312            }
313
314            Expr::CallExpr { callee, args } => {
315                self.check_no_escape(callee, max_depth)?;
316                for arg in args {
317                    self.check_no_escape(arg, max_depth)?;
318                }
319            }
320
321            Expr::InterpolatedString(parts) => {
322                for part in parts {
323                    if let crate::ast::stmt::StringPart::Expr { value, .. } = part {
324                        self.check_no_escape(value, max_depth)?;
325                    }
326                }
327            }
328
329            // Literals are always safe
330            Expr::Literal(_) => {}
331        }
332        Ok(())
333    }
334
335    /// Check that an expression doesn't escape via assignment
336    fn check_no_escape_with_target(
337        &self,
338        expr: &Expr<'_>,
339        max_depth: usize,
340        target: Symbol,
341    ) -> Result<(), EscapeError> {
342        match expr {
343            Expr::Identifier(sym) => {
344                if let Some(&depth) = self.zone_depth.get(sym) {
345                    if depth > max_depth && depth > 0 {
346                        let zone_name = self.zone_stack.get(depth - 1)
347                            .map(|s| self.interner.resolve(*s).to_string())
348                            .unwrap_or_else(|| "unknown".to_string());
349                        let var_name = self.interner.resolve(*sym).to_string();
350                        let target_name = self.interner.resolve(target).to_string();
351                        return Err(EscapeError {
352                            kind: EscapeErrorKind::AssignmentEscape {
353                                variable: var_name,
354                                target: target_name,
355                                zone_name,
356                            },
357                            span: Span::default(),
358                        });
359                    }
360                }
361            }
362            // For compound expressions, use the simpler check (return style error)
363            _ => self.check_no_escape(expr, max_depth)?,
364        }
365        Ok(())
366    }
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    // Note: Full integration tests are in tests/phase85_zones.rs
374    // These unit tests verify the basic mechanics of the escape checker
375
376    #[test]
377    fn test_escape_checker_basic() {
378        use crate::intern::Interner;
379
380        let mut interner = Interner::new();
381        let checker = EscapeChecker::new(&interner);
382
383        // Just verify creation works
384        assert_eq!(checker.current_depth, 0);
385        assert!(checker.zone_depth.is_empty());
386    }
387}