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 fn check_block(&mut self, stmts: &[Stmt<'_>]) -> Result<(), EscapeError> {
113 for stmt in stmts {
114 self.check_stmt(stmt)?;
115 }
116 Ok(())
117 }
118
119 fn check_stmt(&mut self, stmt: &Stmt<'_>) -> Result<(), EscapeError> {
121 match stmt {
122 Stmt::Zone { name, body, .. } => {
123 self.current_depth += 1;
125 self.zone_stack.push(*name);
126
127 self.check_block(body)?;
129
130 self.zone_stack.pop();
132 self.current_depth -= 1;
133 }
134
135 Stmt::Let { var, .. } => {
136 self.zone_depth.insert(*var, self.current_depth);
138 }
139
140 Stmt::Return { value: Some(expr) } => {
141 self.check_no_escape(expr, 0)?;
143 }
144
145 Stmt::Set { target, value } => {
146 let target_depth = self.zone_depth.get(target).copied().unwrap_or(0);
148 self.check_no_escape_with_target(value, target_depth, *target)?;
149 }
150
151 Stmt::If { then_block, else_block, .. } => {
153 self.check_block(then_block)?;
154 if let Some(else_b) = else_block {
155 self.check_block(else_b)?;
156 }
157 }
158
159 Stmt::While { body, .. } => {
160 self.check_block(body)?;
161 }
162
163 Stmt::Repeat { body, .. } => {
164 self.check_block(body)?;
165 }
166
167 Stmt::Inspect { arms, .. } => {
168 for arm in arms {
169 self.check_block(arm.body)?;
170 }
171 }
172
173 _ => {}
175 }
176 Ok(())
177 }
178
179 fn check_no_escape(&self, expr: &Expr<'_>, max_depth: usize) -> Result<(), EscapeError> {
181 match expr {
182 Expr::Identifier(sym) => {
183 if let Some(&depth) = self.zone_depth.get(sym) {
184 if depth > max_depth && depth > 0 {
185 let zone_name = self.zone_stack.get(depth - 1)
187 .map(|s| self.interner.resolve(*s).to_string())
188 .unwrap_or_else(|| "unknown".to_string());
189 let var_name = self.interner.resolve(*sym).to_string();
190 return Err(EscapeError {
191 kind: EscapeErrorKind::ReturnEscape {
192 variable: var_name,
193 zone_name,
194 },
195 span: Span::default(),
196 });
197 }
198 }
199 }
200
201 Expr::BinaryOp { left, right, .. } => {
203 self.check_no_escape(left, max_depth)?;
204 self.check_no_escape(right, max_depth)?;
205 }
206
207 Expr::Call { args, .. } => {
208 for arg in args {
209 self.check_no_escape(arg, max_depth)?;
210 }
211 }
212
213 Expr::FieldAccess { object, .. } => {
214 self.check_no_escape(object, max_depth)?;
215 }
216
217 Expr::Index { collection, index } => {
218 self.check_no_escape(collection, max_depth)?;
219 self.check_no_escape(index, max_depth)?;
220 }
221
222 Expr::Slice { collection, start, end } => {
223 self.check_no_escape(collection, max_depth)?;
224 self.check_no_escape(start, max_depth)?;
225 self.check_no_escape(end, max_depth)?;
226 }
227
228 Expr::Copy { expr } | Expr::Length { collection: expr } => {
229 self.check_no_escape(expr, max_depth)?;
230 }
231
232 Expr::List(items) | Expr::Tuple(items) => {
233 for item in items {
234 self.check_no_escape(item, max_depth)?;
235 }
236 }
237
238 Expr::Range { start, end } => {
239 self.check_no_escape(start, max_depth)?;
240 self.check_no_escape(end, max_depth)?;
241 }
242
243 Expr::New { init_fields, .. } => {
244 for (_, expr) in init_fields {
245 self.check_no_escape(expr, max_depth)?;
246 }
247 }
248
249 Expr::NewVariant { fields, .. } => {
250 for (_, expr) in fields {
251 self.check_no_escape(expr, max_depth)?;
252 }
253 }
254
255 Expr::ManifestOf { zone } => {
256 self.check_no_escape(zone, max_depth)?;
257 }
258
259 Expr::ChunkAt { index, zone } => {
260 self.check_no_escape(index, max_depth)?;
261 self.check_no_escape(zone, max_depth)?;
262 }
263
264 Expr::Contains { collection, value } => {
265 self.check_no_escape(collection, max_depth)?;
266 self.check_no_escape(value, max_depth)?;
267 }
268
269 Expr::Union { left, right } | Expr::Intersection { left, right } => {
270 self.check_no_escape(left, max_depth)?;
271 self.check_no_escape(right, max_depth)?;
272 }
273
274 Expr::Literal(_) => {}
276 }
277 Ok(())
278 }
279
280 fn check_no_escape_with_target(
282 &self,
283 expr: &Expr<'_>,
284 max_depth: usize,
285 target: Symbol,
286 ) -> Result<(), EscapeError> {
287 match expr {
288 Expr::Identifier(sym) => {
289 if let Some(&depth) = self.zone_depth.get(sym) {
290 if depth > max_depth && depth > 0 {
291 let zone_name = self.zone_stack.get(depth - 1)
292 .map(|s| self.interner.resolve(*s).to_string())
293 .unwrap_or_else(|| "unknown".to_string());
294 let var_name = self.interner.resolve(*sym).to_string();
295 let target_name = self.interner.resolve(target).to_string();
296 return Err(EscapeError {
297 kind: EscapeErrorKind::AssignmentEscape {
298 variable: var_name,
299 target: target_name,
300 zone_name,
301 },
302 span: Span::default(),
303 });
304 }
305 }
306 }
307 _ => self.check_no_escape(expr, max_depth)?,
309 }
310 Ok(())
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
322 fn test_escape_checker_basic() {
323 use crate::intern::Interner;
324
325 let mut interner = Interner::new();
326 let checker = EscapeChecker::new(&interner);
327
328 assert_eq!(checker.current_depth, 0);
330 assert!(checker.zone_depth.is_empty());
331 }
332}