logicaffeine_compile/analysis/
escape.rs1use std::collections::HashMap;
28use crate::ast::stmt::{Stmt, Expr, Block};
29use crate::intern::{Interner, Symbol};
30use crate::token::Span;
31
32#[derive(Debug, Clone)]
34pub struct EscapeError {
35 pub kind: EscapeErrorKind,
36 pub span: Span,
37}
38
39#[derive(Debug, Clone)]
40pub enum EscapeErrorKind {
41 ReturnEscape {
43 variable: String,
44 zone_name: String,
45 },
46 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
83pub struct EscapeChecker<'a> {
85 zone_depth: HashMap<Symbol, usize>,
87 current_depth: usize,
89 zone_stack: Vec<Symbol>,
91 interner: &'a Interner,
93}
94
95impl<'a> EscapeChecker<'a> {
96 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 pub fn check_program(&mut self, stmts: &[Stmt<'_>]) -> Result<(), EscapeError> {
108 self.check_block(stmts)
109 }
110
111 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 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 fn check_stmt(&mut self, stmt: &Stmt<'_>) -> Result<(), EscapeError> {
131 match stmt {
132 Stmt::Zone { name, body, .. } => {
133 self.current_depth += 1;
135 self.zone_stack.push(*name);
136
137 self.check_block(body)?;
139
140 self.zone_stack.pop();
142 self.current_depth -= 1;
143 }
144
145 Stmt::Let { var, .. } => {
146 self.zone_depth.insert(*var, self.current_depth);
148 }
149
150 Stmt::Return { value: Some(expr) } => {
151 self.check_no_escape(expr, 0)?;
153 }
154
155 Stmt::Set { target, value } => {
156 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 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 Stmt::Escape { .. } => {}
185
186 _ => {}
188 }
189 Ok(())
190 }
191
192 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 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 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 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 }
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 Expr::Literal(_) => {}
331 }
332 Ok(())
333 }
334
335 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 _ => self.check_no_escape(expr, max_depth)?,
364 }
365 Ok(())
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[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 assert_eq!(checker.current_depth, 0);
385 assert!(checker.zone_depth.is_empty());
386 }
387}