Skip to main content

submilli_engine/typechecker/
capture.rs

1//! Capture analysis pass — marks `let` bindings, parameters, and their
2//! references as boxed when they're captured by inner closures, and
3//! populates each `Closure.captured` with the env-layout codegen needs.
4
5use std::collections::{BTreeMap, BTreeSet};
6
7use crate::compiler_error::{CompilerFailure, CompilerStage};
8use crate::{
9    CapturedVar, ClosureBody, ExprId, Ident, StmtId, Type, TypedAst, TypedChainPart, TypedExprKind,
10    TypedStmtKind, tree_height,
11};
12
13/// Capture-pass name for `this`. `this` is a keyword, so it can never collide
14/// with a user binding, and codegen recognises the same key when it rebinds the
15/// receiver inside a closure body.
16pub(crate) const THIS_BINDING: &str = "this";
17
18pub fn capture(mut ta: TypedAst) -> Result<TypedAst, CompilerFailure> {
19    tree_height::check_typed(&ta, CompilerStage::Infer)?;
20    resolve_locals(&mut ta)?;
21    Ok(ta)
22}
23
24/// Declaration identities for storage-flow analysis after desugaring. The
25/// declaration span distinguishes same-named bindings in nested scopes.
26#[derive(Default)]
27pub(crate) struct ResolvedLocals {
28    pub reads: BTreeMap<ExprId, Ident>,
29    pub writes: BTreeMap<StmtId, Ident>,
30}
31
32pub(crate) fn resolve_locals(
33    ta: &mut TypedAst,
34) -> Result<ResolvedLocals, crate::compiler_error::CompilerFailure> {
35    let mut state = State {
36        ta,
37        frames: Vec::new(),
38        pending: Vec::new(),
39        resolved: ResolvedLocals::default(),
40    };
41    let function_bodies: Vec<(usize, Vec<crate::TypedParam>, StmtId)> = state
42        .ta
43        .functions
44        .iter()
45        .enumerate()
46        .map(|(idx, f)| (idx, f.params.clone(), f.body))
47        .collect();
48    for (idx, params, body) in function_bodies {
49        state.walk_function(idx, &params, body)?;
50    }
51    state.frames.push(Frame::default());
52    let stmt_ids: Vec<StmtId> = state.ta.top_level_statements.clone();
53    for sid in stmt_ids {
54        state.walk_stmt(sid)?;
55    }
56    state.pop_frame()?;
57    // Class member bodies are function scopes too: a closure inside a method
58    // captures that method's params and locals, and without this pass its
59    // capture list stays empty and codegen has no local to read.
60    for m in class_member_bodies(state.ta) {
61        state.walk_class_member(m.member, &m.params, m.body, m.this_ty)?;
62    }
63    for (this_ty, expr_id) in field_initializers_with_receiver(state.ta) {
64        state.walk_initializer(expr_id, this_ty)?;
65    }
66    // Deferred so refs before a capturing closure still see the final `boxed` flag.
67    state.apply_pending()?;
68    Ok(state.resolved)
69}
70
71/// A class member body, the parameters in scope for it, and the instance type
72/// of `this` inside it.
73struct MemberBody {
74    member: ClassMemberRef,
75    params: Vec<crate::TypedParam>,
76    body: StmtId,
77    this_ty: Type,
78}
79
80/// Field-initializer expressions paired with the `this` in scope for them.
81/// They run inside the constructor, so `this` is available exactly as it is in
82/// a member body. Distinct from `TypedAst::class_field_initializers`, which
83/// yields the bare expressions for passes that need no receiver.
84fn field_initializers_with_receiver(ta: &TypedAst) -> Vec<(Type, ExprId)> {
85    let mut out = Vec::new();
86    for ty_decl in &ta.types {
87        let crate::TypedTypeDecl::Class(c) = ty_decl else {
88            continue;
89        };
90        let this_ty = Type::class_ref(
91            crate::Package(ta.package_name.clone()),
92            c.name.name.clone(),
93            c.mangled_name.clone(),
94            Vec::new(),
95        );
96        for f in &c.fields {
97            if let Some(init) = f.initializer {
98                out.push((this_ty.clone(), init));
99            }
100        }
101    }
102    out
103}
104
105fn class_member_bodies(ta: &TypedAst) -> Vec<MemberBody> {
106    let mut out = Vec::new();
107    for (decl, ty_decl) in ta.types.iter().enumerate() {
108        let crate::TypedTypeDecl::Class(c) = ty_decl else {
109            continue;
110        };
111        let this_ty = Type::class_ref(
112            crate::Package(ta.package_name.clone()),
113            c.name.name.clone(),
114            c.mangled_name.clone(),
115            Vec::new(),
116        );
117        if let Some(ctor) = &c.constructor {
118            out.push(MemberBody {
119                member: ClassMemberRef::Constructor { decl },
120                params: ctor.params.clone(),
121                body: ctor.body,
122                this_ty: this_ty.clone(),
123            });
124        }
125        for (index, m) in c.methods.iter().enumerate() {
126            out.push(MemberBody {
127                member: ClassMemberRef::Method { decl, index },
128                params: m.params.clone(),
129                body: m.body,
130                this_ty: this_ty.clone(),
131            });
132        }
133        for (index, a) in c.accessors.iter().enumerate() {
134            let params = match a {
135                crate::TypedClassAccessor::Getter { .. } => Vec::new(),
136                crate::TypedClassAccessor::Setter { param, .. } => vec![param.clone()],
137            };
138            out.push(MemberBody {
139                member: ClassMemberRef::Accessor { decl, index },
140                params,
141                body: a.body(),
142                this_ty: this_ty.clone(),
143            });
144        }
145    }
146    out
147}
148
149struct State<'a> {
150    ta: &'a mut TypedAst,
151    frames: Vec<Frame>,
152    pending: Vec<Pending>,
153    resolved: ResolvedLocals,
154}
155
156enum Pending {
157    LocalRef(ExprId, BindingSource),
158    AssignLocal(StmtId, BindingSource),
159    PostfixLocal(ExprId, BindingSource),
160}
161
162#[derive(Default)]
163struct Frame {
164    locals: BTreeMap<String, LocalBinding>,
165    captures: Vec<CapturedVar>,
166    captured_names: BTreeSet<String>,
167}
168
169#[derive(Clone)]
170struct LocalBinding {
171    name_ident: Ident,
172    ty: Type,
173    /// Captured `let`/params get boxed; captured `const`s are copied.
174    mutable: bool,
175    source: BindingSource,
176}
177
178#[derive(Clone)]
179enum BindingSource {
180    Let(StmtId),
181    /// The binding of `catches[index]` in the `try` statement `try_stmt`.
182    Catch {
183        try_stmt: StmtId,
184        index: usize,
185    },
186    ParameterBinding(StmtId),
187    Const,
188    Param {
189        owner: ParamOwner,
190        index: usize,
191    },
192}
193
194#[derive(Clone)]
195enum ParamOwner {
196    Function(usize),
197    Closure(ExprId),
198    ClassMember(ClassMemberRef),
199}
200
201/// Locates a class member's parameter list in `TypedAst::types`, so a captured
202/// parameter can be marked boxed the same way a function's is.
203#[derive(Clone, Copy)]
204enum ClassMemberRef {
205    Constructor { decl: usize },
206    Method { decl: usize, index: usize },
207    Accessor { decl: usize, index: usize },
208}
209
210impl ClassMemberRef {
211    fn decl(&self) -> usize {
212        let (Self::Constructor { decl } | Self::Method { decl, .. } | Self::Accessor { decl, .. }) =
213            self;
214        *decl
215    }
216
217    /// The member's parameter at `index`, wherever it lives. A setter has
218    /// exactly one parameter and no `Vec` to index, so it answers for index 0.
219    fn param_mut<'a>(
220        &self,
221        ta: &'a mut TypedAst,
222        index: usize,
223    ) -> Option<&'a mut crate::TypedParam> {
224        let crate::TypedTypeDecl::Class(c) = ta.types.get_mut(self.decl())? else {
225            return None;
226        };
227        match self {
228            Self::Constructor { .. } => c.constructor.as_mut()?.params.get_mut(index),
229            Self::Method { index: member, .. } => c.methods.get_mut(*member)?.params.get_mut(index),
230            Self::Accessor { index: member, .. } => match c.accessors.get_mut(*member)? {
231                crate::TypedClassAccessor::Setter { param, .. } if index == 0 => Some(param),
232                _ => None,
233            },
234        }
235    }
236
237    fn param<'a>(&self, ta: &'a TypedAst, index: usize) -> Option<&'a crate::TypedParam> {
238        let crate::TypedTypeDecl::Class(c) = ta.types.get(self.decl())? else {
239            return None;
240        };
241        match self {
242            Self::Constructor { .. } => c.constructor.as_ref()?.params.get(index),
243            Self::Method { index: member, .. } => c.methods.get(*member)?.params.get(index),
244            Self::Accessor { index: member, .. } => match c.accessors.get(*member)? {
245                crate::TypedClassAccessor::Setter { param, .. } if index == 0 => Some(param),
246                _ => None,
247            },
248        }
249    }
250}
251
252impl State<'_> {
253    fn walk_function(
254        &mut self,
255        function_idx: usize,
256        params: &[crate::TypedParam],
257        body: StmtId,
258    ) -> Result<(), crate::compiler_error::CompilerFailure> {
259        self.walk_body(ParamOwner::Function(function_idx), params, body, None)?;
260        Ok(())
261    }
262
263    fn walk_class_member(
264        &mut self,
265        member: ClassMemberRef,
266        params: &[crate::TypedParam],
267        body: StmtId,
268        this_ty: Type,
269    ) -> Result<(), crate::compiler_error::CompilerFailure> {
270        self.walk_body(ParamOwner::ClassMember(member), params, body, Some(this_ty))?;
271        Ok(())
272    }
273
274    /// A field initializer is an expression root rather than a body, but it
275    /// runs with the same `this` in scope.
276    fn walk_initializer(
277        &mut self,
278        expr_id: ExprId,
279        this_ty: Type,
280    ) -> Result<(), crate::compiler_error::CompilerFailure> {
281        self.frames.push(Frame::default());
282        self.bind(LocalBinding {
283            name_ident: Ident {
284                name: THIS_BINDING.to_string(),
285                span: self
286                    .ta
287                    .try_expr(expr_id)
288                    .map_err(crate::typechecker::arena_failure)?
289                    .span,
290            },
291            ty: this_ty,
292            mutable: false,
293            source: BindingSource::Const,
294        })?;
295        self.walk_expr(expr_id)?;
296        self.pop_frame()?;
297        Ok(())
298    }
299
300    fn walk_body(
301        &mut self,
302        owner: ParamOwner,
303        params: &[crate::TypedParam],
304        body: StmtId,
305        this_ty: Option<Type>,
306    ) -> Result<(), crate::compiler_error::CompilerFailure> {
307        self.frames.push(Frame::default());
308        // `this` is an ordinary immutable binding for capture purposes, so a
309        // closure in a member body captures it by value like any `const`.
310        if let Some(ty) = this_ty {
311            self.bind(LocalBinding {
312                name_ident: Ident {
313                    name: THIS_BINDING.to_string(),
314                    span: self
315                        .ta
316                        .try_stmt(body)
317                        .map_err(crate::typechecker::arena_failure)?
318                        .span,
319                },
320                ty,
321                mutable: false,
322                source: BindingSource::Const,
323            })?;
324        }
325        for (i, p) in params.iter().enumerate() {
326            self.bind(LocalBinding {
327                name_ident: p.name.clone(),
328                ty: p.ty.clone(),
329                mutable: true,
330                source: BindingSource::Param {
331                    owner: owner.clone(),
332                    index: i,
333                },
334            })?;
335        }
336        self.predeclare_parameter_bindings(body)?;
337        self.walk_stmt(body)?;
338        self.pop_frame()?;
339        Ok(())
340    }
341
342    /// Default closures can capture a later destructured parameter before
343    /// its initializer runs. Keep its declaration identity and shared cell.
344    fn predeclare_parameter_bindings(&mut self, body: StmtId) -> Result<(), CompilerFailure> {
345        let Some(&count) = self.ta.parameter_default_prologues.get(&body) else {
346            return Ok(());
347        };
348        let TypedStmtKind::Block(statements) = &self
349            .ta
350            .try_stmt(body)
351            .map_err(crate::typechecker::arena_failure)?
352            .kind
353        else {
354            return Err(crate::typechecker::invariant_failure(
355                "parameter prologue body is not a block",
356            ));
357        };
358        let statements = statements
359            .get(..count)
360            .ok_or_else(|| {
361                crate::typechecker::invariant_failure("parameter prologue exceeds body length")
362            })?
363            .to_vec();
364        for id in statements {
365            if !self.ta.parameter_initializations.contains(&id) {
366                continue;
367            }
368            let kind = &self
369                .ta
370                .try_stmt(id)
371                .map_err(crate::typechecker::arena_failure)?
372                .kind;
373            let (TypedStmtKind::Let { name, ty, .. } | TypedStmtKind::Const { name, ty, .. }) =
374                kind
375            else {
376                continue;
377            };
378            self.bind(LocalBinding {
379                name_ident: name.clone(),
380                ty: ty.clone(),
381                mutable: true,
382                source: BindingSource::ParameterBinding(id),
383            })?;
384        }
385        Ok(())
386    }
387
388    fn walk_stmt(&mut self, id: StmtId) -> Result<(), crate::compiler_error::CompilerFailure> {
389        let kind = self
390            .ta
391            .try_stmt(id)
392            .map_err(crate::typechecker::arena_failure)?
393            .kind
394            .clone();
395        let _: () = match kind {
396            TypedStmtKind::Block(stmts) => {
397                // Blocks change lexical lookup without introducing a closure
398                // boundary. Deferred references retain their binding source.
399                let locals = self.frames.last().map(|frame| frame.locals.clone());
400                for s in stmts {
401                    self.walk_stmt(s)?;
402                }
403                if let (Some(frame), Some(locals)) = (self.frames.last_mut(), locals) {
404                    frame.locals = locals;
405                }
406            }
407            TypedStmtKind::Let {
408                name, ty, value, ..
409            } => {
410                self.walk_expr(value)?;
411                if self.ta.parameter_initializations.contains(&id) {
412                    return Ok(());
413                }
414                self.bind(LocalBinding {
415                    name_ident: name,
416                    ty,
417                    mutable: true,
418                    source: BindingSource::Let(id),
419                })?;
420            }
421            TypedStmtKind::Const {
422                name, ty, value, ..
423            } => {
424                self.walk_expr(value)?;
425                if self.ta.parameter_initializations.contains(&id) {
426                    return Ok(());
427                }
428                self.bind(LocalBinding {
429                    name_ident: name,
430                    ty,
431                    mutable: false,
432                    source: BindingSource::Const,
433                })?;
434            }
435            TypedStmtKind::If {
436                condition,
437                then_block,
438                else_block,
439            } => {
440                self.walk_expr(condition)?;
441                self.walk_stmt(then_block)?;
442                if let Some(eb) = else_block {
443                    self.walk_stmt(eb)?;
444                }
445            }
446            TypedStmtKind::While { condition, body } => {
447                self.walk_expr(condition)?;
448                self.walk_stmt(body)?;
449            }
450            TypedStmtKind::For {
451                init,
452                condition,
453                update,
454                body,
455            } => {
456                // Init binding's scope is the for-body.
457                self.frames.push(Frame::default());
458                if let Some(i) = init {
459                    self.walk_stmt(i)?;
460                }
461                if let Some(c) = condition {
462                    self.walk_expr(c)?;
463                }
464                if let Some(u) = update {
465                    self.walk_stmt(u)?;
466                }
467                self.walk_stmt(body)?;
468                self.pop_frame()?;
469            }
470            TypedStmtKind::ForOf {
471                name,
472                element_ty,
473                iter,
474                body,
475                binding_kind,
476                kind: _,
477            } => {
478                self.walk_expr(iter)?;
479                self.frames.push(Frame::default());
480                self.bind(LocalBinding {
481                    name_ident: name,
482                    ty: element_ty,
483                    mutable: matches!(binding_kind, crate::BindingKind::Let),
484                    source: BindingSource::Const,
485                })?;
486                self.walk_stmt(body)?;
487                self.pop_frame()?;
488            }
489            TypedStmtKind::DoWhile { body, condition } => {
490                self.walk_stmt(body)?;
491                self.walk_expr(condition)?;
492            }
493            TypedStmtKind::Switch {
494                discriminant,
495                cases,
496                default,
497                ..
498            } => {
499                self.walk_expr(discriminant)?;
500                for comparison in cases
501                    .iter()
502                    .flat_map(crate::TypedSwitchCase::label_comparisons)
503                {
504                    self.walk_expr(comparison)?;
505                }
506                for case in cases {
507                    self.walk_stmt(case.body)?;
508                }
509                if let Some(d) = default {
510                    self.walk_stmt(d)?;
511                }
512            }
513            TypedStmtKind::Break | TypedStmtKind::Continue => {}
514            TypedStmtKind::ReboxLocal { ident, .. } => {
515                let binding = self.require_source(&ident.name)?;
516                self.resolved.writes.insert(id, binding.name_ident.clone());
517            }
518            TypedStmtKind::Return(value) => {
519                if let Some(v) = value {
520                    self.walk_expr(v)?;
521                }
522            }
523            TypedStmtKind::Expr(e) => self.walk_expr(e)?,
524            TypedStmtKind::AssignLocal { ident, value, .. } => {
525                self.walk_expr(value)?;
526                let binding = self.require_source(&ident.name)?;
527                self.resolved.writes.insert(id, binding.name_ident.clone());
528                self.mark_cross_frame_capture(&ident.name, &binding)?;
529                self.pending.push(Pending::AssignLocal(id, binding.source));
530            }
531            TypedStmtKind::AssignGlobal { value, .. } => self.walk_expr(value)?,
532            TypedStmtKind::AssignField {
533                receiver, value, ..
534            } => {
535                self.walk_expr(receiver)?;
536                self.walk_expr(value)?;
537            }
538            TypedStmtKind::AssignIndex {
539                receiver,
540                index,
541                value,
542                ..
543            } => {
544                self.walk_expr(receiver)?;
545                self.walk_expr(index)?;
546                self.walk_expr(value)?;
547            }
548            TypedStmtKind::NarrowRegion { source, body, .. } => {
549                // Shadow binding is synthetic — no capture implications.
550                self.walk_expr(source)?;
551                self.walk_stmt(body)?;
552            }
553            TypedStmtKind::Throw { value } => self.walk_expr(value)?,
554            TypedStmtKind::Try {
555                body,
556                catches,
557                finally,
558            } => {
559                // the try body, each catch body, and finally body
560                // are independent lexical scopes. A catch binding
561                // `e` lives only inside its own catch body (mirrors
562                // ForOf's loop variable). Closures capturing `e` flip
563                // its `boxed` flag the same way ForOf handles its
564                // loop var.
565                self.walk_stmt(body)?;
566                for (index, clause) in catches.iter().enumerate() {
567                    self.frames.push(Frame::default());
568                    self.bind(LocalBinding {
569                        name_ident: clause.binding.clone(),
570                        ty: clause.ty.clone(),
571                        mutable: true,
572                        source: BindingSource::Catch {
573                            try_stmt: id,
574                            index,
575                        },
576                    })?;
577                    self.walk_stmt(clause.body)?;
578                    self.pop_frame()?;
579                }
580                if let Some(f) = finally {
581                    self.walk_stmt(f)?;
582                }
583            }
584        };
585        Ok(())
586    }
587
588    fn walk_expr(&mut self, id: ExprId) -> Result<(), crate::compiler_error::CompilerFailure> {
589        let expr = self
590            .ta
591            .try_expr(id)
592            .map_err(crate::typechecker::arena_failure)?;
593        let span = expr.span;
594        let expr_ty = expr.ty.clone();
595        let kind = expr.kind.clone();
596        let _: () = match kind {
597            TypedExprKind::LocalRef { ident, .. } => {
598                let b = self
599                    .require_source(&ident.name)
600                    .map_err(|failure| failure.with_span(span))?;
601                self.resolved.reads.insert(id, b.name_ident.clone());
602                self.mark_cross_frame_capture(&ident.name, &b)?;
603                self.pending.push(Pending::LocalRef(id, b.source));
604            }
605            // `this` inside a closure is a capture of the enclosing member's
606            // receiver; in the member body itself this resolves same-frame and
607            // does nothing.
608            TypedExprKind::This => {
609                let b = self.require_source(THIS_BINDING)?;
610                self.mark_cross_frame_capture(THIS_BINDING, &b)?;
611            }
612            TypedExprKind::LocalNarrowRef { path, .. } => {
613                if path.chain.is_empty()
614                    && let super::infer::narrowing::BindingId::Local { name, .. } = &path.root
615                {
616                    let binding = self
617                        .require_source(name)
618                        .map_err(|failure| failure.with_span(span))?;
619                    // Codegen reads a local narrowed to `never` as it is (a
620                    // `never[]` an alias filled holds real values), so a
621                    // closure needs it.
622                    if matches!(expr_ty, Type::Never) {
623                        self.mark_cross_frame_capture(name, &binding)?;
624                    }
625                    self.resolved.reads.insert(id, binding.name_ident);
626                }
627            }
628            TypedExprKind::Closure { params, body, .. } => {
629                self.frames.push(Frame::default());
630                if self.ta.closure_this.contains_key(&id) {
631                    self.bind(LocalBinding {
632                        name_ident: Ident {
633                            name: THIS_BINDING.to_string(),
634                            span: self
635                                .ta
636                                .try_expr(id)
637                                .map_err(crate::typechecker::arena_failure)?
638                                .span,
639                        },
640                        ty: Type::Unknown,
641                        mutable: false,
642                        source: BindingSource::Const,
643                    })?;
644                }
645                if let Some(name) = self.ta.closure_names.get(&id).cloned() {
646                    self.bind(LocalBinding {
647                        name_ident: name,
648                        ty: self
649                            .ta
650                            .try_expr(id)
651                            .map_err(crate::typechecker::arena_failure)?
652                            .ty
653                            .clone(),
654                        mutable: false,
655                        source: BindingSource::Const,
656                    })?;
657                }
658                for (i, p) in params.iter().enumerate() {
659                    self.bind(LocalBinding {
660                        name_ident: p.name.clone(),
661                        ty: p.ty.clone(),
662                        mutable: true,
663                        source: BindingSource::Param {
664                            owner: ParamOwner::Closure(id),
665                            index: i,
666                        },
667                    })?;
668                }
669                match body {
670                    ClosureBody::Expr(e) => self.walk_expr(e)?,
671                    ClosureBody::Block(b) => {
672                        self.predeclare_parameter_bindings(b)?;
673                        self.walk_stmt(b)?;
674                    }
675                }
676                let frame = self.frames.pop().ok_or_else(|| {
677                    crate::typechecker::invariant_failure("missing closure capture frame")
678                })?;
679                let TypedExprKind::Closure { captured, .. } = &mut self
680                    .ta
681                    .try_expr_mut(id)
682                    .map_err(crate::typechecker::arena_failure)?
683                    .kind
684                else {
685                    return Err(crate::typechecker::invariant_failure(
686                        "capture closure kind changed",
687                    ));
688                };
689                *captured = frame.captures;
690            }
691            TypedExprKind::Binary { lhs, rhs, .. } => {
692                self.walk_expr(lhs)?;
693                self.walk_expr(rhs)?;
694            }
695            TypedExprKind::EffectThen { effect, result } => {
696                self.walk_expr(effect)?;
697                self.walk_expr(result)?;
698            }
699            TypedExprKind::Sequence { stmts, result } => {
700                for stmt in stmts {
701                    self.walk_stmt(stmt)?;
702                }
703                self.walk_expr(result)?;
704            }
705            TypedExprKind::Unary { operand, .. } => self.walk_expr(operand)?,
706            TypedExprKind::TypeofTag { value, .. } | TypedExprKind::InstanceOf { value, .. } => {
707                self.walk_expr(value)?;
708            }
709            TypedExprKind::Call { args, .. } | TypedExprKind::McpCall { args, .. } => {
710                for a in args {
711                    self.walk_expr(a)?;
712                }
713            }
714            // `super.m()` dispatches on the receiver without naming `this`, so
715            // it captures it just like an explicit `this` would.
716            TypedExprKind::SuperCtorCall { args, .. }
717            | TypedExprKind::SuperMethodCall { args, .. } => {
718                for a in args {
719                    self.walk_expr(a)?;
720                }
721                {
722                    let b = self.require_source(THIS_BINDING)?;
723                    self.mark_cross_frame_capture(THIS_BINDING, &b)?;
724                }
725            }
726            TypedExprKind::CallClosure { callee, args } => {
727                self.walk_expr(callee)?;
728                for a in args {
729                    self.walk_expr(a)?;
730                }
731            }
732            TypedExprKind::GenericCall { args, .. } => {
733                for a in args {
734                    self.walk_expr(a.expr)?;
735                }
736            }
737            TypedExprKind::MethodCall { receiver, args, .. } => {
738                self.walk_expr(receiver)?;
739                for a in args {
740                    self.walk_expr(a)?;
741                }
742            }
743            TypedExprKind::GenericMethodCall { receiver, args, .. } => {
744                self.walk_expr(receiver)?;
745                for a in args {
746                    self.walk_expr(a.expr)?;
747                }
748            }
749            TypedExprKind::IntrinsicCall { args, .. } => {
750                for a in args {
751                    self.walk_expr(a)?;
752                }
753            }
754            TypedExprKind::ObjectLiteral { members, .. } => {
755                for member in members {
756                    for expression in member.expressions() {
757                        self.walk_expr(expression)?;
758                    }
759                }
760            }
761            TypedExprKind::ArrayLiteral { elements, .. } => {
762                for e in elements {
763                    self.walk_expr(e.expr_id())?;
764                }
765            }
766            TypedExprKind::TupleLiteral { elements, .. } => {
767                for e in elements {
768                    self.walk_expr(e)?;
769                }
770            }
771            TypedExprKind::FieldAccess { receiver, .. }
772            | TypedExprKind::InterfacePropertyAccess { receiver, .. } => {
773                self.walk_expr(receiver)?;
774            }
775            TypedExprKind::IndexAccess { receiver, index } => {
776                self.walk_expr(receiver)?;
777                self.walk_expr(index)?;
778            }
779            TypedExprKind::Narrowed { source, inner, .. } => {
780                self.walk_expr(source)?;
781                self.walk_expr(inner)?;
782            }
783            TypedExprKind::Ternary { cond, then_, else_ } => {
784                self.walk_expr(cond)?;
785                self.walk_expr(then_)?;
786                self.walk_expr(else_)?;
787            }
788            TypedExprKind::NullishCoalesce { lhs, rhs } => {
789                self.walk_expr(lhs)?;
790                self.walk_expr(rhs)?;
791            }
792            TypedExprKind::OptionalChain { base, parts } => {
793                self.walk_expr(base)?;
794                for part in parts {
795                    match part {
796                        TypedChainPart::Index { idx, .. } => self.walk_expr(idx)?,
797                        TypedChainPart::Call { args, .. }
798                        | TypedChainPart::MethodCall { args, .. } => {
799                            for a in args {
800                                self.walk_expr(a)?;
801                            }
802                        }
803                        TypedChainPart::Field { .. }
804                        | TypedChainPart::InterfaceProperty { .. }
805                        | TypedChainPart::NonNull { .. } => {}
806                    }
807                }
808            }
809            TypedExprKind::PostfixUnary { target, .. } => match target {
810                crate::PostfixTarget::Local { ident, .. } => {
811                    let b = self.require_source(&ident.name)?;
812                    self.resolved.reads.insert(id, b.name_ident.clone());
813                    self.mark_cross_frame_capture(&ident.name, &b)?;
814                    self.pending.push(Pending::PostfixLocal(id, b.source));
815                }
816                crate::PostfixTarget::Global { .. } => {}
817                crate::PostfixTarget::Field { receiver, .. } => self.walk_expr(receiver)?,
818                crate::PostfixTarget::Index {
819                    receiver, index, ..
820                } => {
821                    self.walk_expr(receiver)?;
822                    self.walk_expr(index)?;
823                }
824            },
825            TypedExprKind::NonNullAssert { value } | TypedExprKind::Cast { value, .. } => {
826                self.walk_expr(value)?;
827            }
828            TypedExprKind::Number(_)
829            | TypedExprKind::BigInt(_)
830            | TypedExprKind::String(_)
831            | TypedExprKind::Boolean(_)
832            | TypedExprKind::Null
833            | TypedExprKind::Undefined
834            | TypedExprKind::Regex { .. }
835            | TypedExprKind::GlobalRef { .. }
836            | TypedExprKind::FunctionRef { .. }
837            | TypedExprKind::NumberEnumMember { .. }
838            | TypedExprKind::StringEnumMember { .. } => {}
839        };
840        Ok(())
841    }
842
843    fn bind(
844        &mut self,
845        binding: LocalBinding,
846    ) -> Result<(), crate::compiler_error::CompilerFailure> {
847        let frame = self
848            .frames
849            .last_mut()
850            .ok_or_else(|| crate::typechecker::invariant_failure("missing capture scope"))?;
851        frame
852            .locals
853            .insert(binding.name_ident.name.clone(), binding);
854        Ok(())
855    }
856
857    fn pop_frame(&mut self) -> Result<Frame, crate::compiler_error::CompilerFailure> {
858        self.frames
859            .pop()
860            .ok_or_else(|| crate::typechecker::invariant_failure("capture scope push/pop mismatch"))
861    }
862
863    /// Returns binding regardless of depth; callers handle cross-frame via [`mark_cross_frame_capture`].
864    fn resolve_source(&self, name: &str) -> Option<LocalBinding> {
865        self.frames
866            .iter()
867            .rev()
868            .find_map(|frame| frame.locals.get(name).cloned())
869    }
870
871    fn require_source(
872        &self,
873        name: &str,
874    ) -> Result<LocalBinding, crate::compiler_error::CompilerFailure> {
875        self.resolve_source(name).ok_or_else(|| {
876            crate::typechecker::invariant_failure(format!("missing capture binding `{name}`"))
877        })
878    }
879
880    /// Forwards capture through intermediate frames and boxes the source binding.
881    /// No-op for same-frame — those writes are deferred to pass 2 (`apply_pending`).
882    fn mark_cross_frame_capture(
883        &mut self,
884        name: &str,
885        binding: &LocalBinding,
886    ) -> Result<(), crate::compiler_error::CompilerFailure> {
887        let defining_idx = self
888            .frames
889            .iter()
890            .rposition(|f| f.locals.contains_key(name))
891            .ok_or_else(|| {
892                crate::typechecker::invariant_failure("missing resolved capture binding")
893            })?;
894        let last_idx = self.frames.len() - 1;
895        if defining_idx == last_idx {
896            return Ok(());
897        }
898        for f in self.frames.iter_mut().skip(defining_idx + 1) {
899            if f.captured_names.insert(name.to_string()) {
900                f.captures.push(CapturedVar {
901                    name: binding.name_ident.clone(),
902                    ty: binding.ty.clone(),
903                    boxed: binding.mutable,
904                });
905            }
906        }
907        let _: () = if binding.mutable {
908            let source = binding.source.clone();
909            self.mark_source_boxed(&source)?;
910        };
911        Ok(())
912    }
913
914    fn mark_source_boxed(
915        &mut self,
916        source: &BindingSource,
917    ) -> Result<(), crate::compiler_error::CompilerFailure> {
918        let _: () = match source {
919            BindingSource::Let(sid) => {
920                let TypedStmtKind::Let { boxed, .. } = &mut self
921                    .ta
922                    .try_stmt_mut(*sid)
923                    .map_err(crate::typechecker::arena_failure)?
924                    .kind
925                else {
926                    return Err(crate::typechecker::invariant_failure(
927                        "capture source is not a let binding",
928                    ));
929                };
930                *boxed = true;
931            }
932            BindingSource::Catch { try_stmt, index } => {
933                self.catch_clause_mut(*try_stmt, *index)?.boxed = true;
934            }
935            BindingSource::ParameterBinding(sid) => self.box_parameter_binding(*sid)?,
936            BindingSource::Const => {} // ordinary const captures are copied
937            BindingSource::Param { owner, index } => match owner {
938                ParamOwner::Function(fidx) => {
939                    let param = self
940                        .ta
941                        .functions
942                        .get_mut(*fidx)
943                        .and_then(|function| function.params.get_mut(*index))
944                        .ok_or_else(|| {
945                            crate::typechecker::invariant_failure(
946                                "missing function capture parameter",
947                            )
948                        })?;
949                    param.boxed = true;
950                }
951                ParamOwner::Closure(eid) => {
952                    let TypedExprKind::Closure { params, .. } = &mut self
953                        .ta
954                        .try_expr_mut(*eid)
955                        .map_err(crate::typechecker::arena_failure)?
956                        .kind
957                    else {
958                        return Err(crate::typechecker::invariant_failure(
959                            "capture parameter owner is not a closure",
960                        ));
961                    };
962                    let param = params.get_mut(*index).ok_or_else(|| {
963                        crate::typechecker::invariant_failure("missing closure capture parameter")
964                    })?;
965                    param.boxed = true;
966                }
967                ParamOwner::ClassMember(member) => {
968                    let param = member.param_mut(self.ta, *index).ok_or_else(|| {
969                        crate::typechecker::invariant_failure(
970                            "missing class member capture parameter",
971                        )
972                    })?;
973                    param.boxed = true;
974                }
975            },
976        };
977        Ok(())
978    }
979
980    fn catch_clause(
981        &self,
982        try_stmt: StmtId,
983        index: usize,
984    ) -> Result<&crate::TypedCatchClause, crate::compiler_error::CompilerFailure> {
985        let TypedStmtKind::Try { catches, .. } = &self
986            .ta
987            .try_stmt(try_stmt)
988            .map_err(crate::typechecker::arena_failure)?
989            .kind
990        else {
991            return Err(crate::typechecker::invariant_failure(
992                "capture source is not a try statement",
993            ));
994        };
995        catches
996            .get(index)
997            .ok_or_else(|| crate::typechecker::invariant_failure("missing catch capture binding"))
998    }
999
1000    fn catch_clause_mut(
1001        &mut self,
1002        try_stmt: StmtId,
1003        index: usize,
1004    ) -> Result<&mut crate::TypedCatchClause, crate::compiler_error::CompilerFailure> {
1005        let TypedStmtKind::Try { catches, .. } = &mut self
1006            .ta
1007            .try_stmt_mut(try_stmt)
1008            .map_err(crate::typechecker::arena_failure)?
1009            .kind
1010        else {
1011            return Err(crate::typechecker::invariant_failure(
1012                "capture source is not a try statement",
1013            ));
1014        };
1015        catches
1016            .get_mut(index)
1017            .ok_or_else(|| crate::typechecker::invariant_failure("missing catch capture binding"))
1018    }
1019
1020    fn box_parameter_binding(&mut self, id: StmtId) -> Result<(), CompilerFailure> {
1021        let statement = self
1022            .ta
1023            .try_stmt_mut(id)
1024            .map_err(crate::typechecker::arena_failure)?;
1025        match &mut statement.kind {
1026            TypedStmtKind::Let { boxed, .. } => *boxed = true,
1027            TypedStmtKind::Const {
1028                name,
1029                ty,
1030                value,
1031                doc,
1032            } => {
1033                statement.kind = TypedStmtKind::Let {
1034                    name: name.clone(),
1035                    ty: ty.clone(),
1036                    value: *value,
1037                    doc: doc.clone(),
1038                    boxed: true,
1039                };
1040            }
1041            _ => {
1042                return Err(crate::typechecker::invariant_failure(
1043                    "parameter capture source is not a binding",
1044                ));
1045            }
1046        }
1047        Ok(())
1048    }
1049
1050    /// Deferred so same-frame refs before a capturing closure still see the final `boxed` flag.
1051    fn apply_pending(&mut self) -> Result<(), crate::compiler_error::CompilerFailure> {
1052        let pending = std::mem::take(&mut self.pending);
1053        for p in pending {
1054            match p {
1055                Pending::LocalRef(eid, source) => {
1056                    let boxed = self.source_is_boxed(&source)?;
1057                    let TypedExprKind::LocalRef { boxed: slot, .. } = &mut self
1058                        .ta
1059                        .try_expr_mut(eid)
1060                        .map_err(crate::typechecker::arena_failure)?
1061                        .kind
1062                    else {
1063                        return Err(crate::typechecker::invariant_failure(
1064                            "pending capture target kind changed",
1065                        ));
1066                    };
1067                    *slot = boxed;
1068                }
1069                Pending::AssignLocal(sid, source) => {
1070                    let boxed = self.source_is_boxed(&source)?;
1071                    let TypedStmtKind::AssignLocal { boxed: slot, .. } = &mut self
1072                        .ta
1073                        .try_stmt_mut(sid)
1074                        .map_err(crate::typechecker::arena_failure)?
1075                        .kind
1076                    else {
1077                        return Err(crate::typechecker::invariant_failure(
1078                            "pending capture target kind changed",
1079                        ));
1080                    };
1081                    *slot = boxed;
1082                }
1083                Pending::PostfixLocal(eid, source) => {
1084                    let boxed = self.source_is_boxed(&source)?;
1085                    let TypedExprKind::PostfixUnary {
1086                        target: crate::PostfixTarget::Local { boxed: slot, .. },
1087                        ..
1088                    } = &mut self
1089                        .ta
1090                        .try_expr_mut(eid)
1091                        .map_err(crate::typechecker::arena_failure)?
1092                        .kind
1093                    else {
1094                        return Err(crate::typechecker::invariant_failure(
1095                            "pending capture target kind changed",
1096                        ));
1097                    };
1098                    *slot = boxed;
1099                }
1100            }
1101        }
1102        Ok(())
1103    }
1104
1105    fn source_is_boxed(
1106        &self,
1107        source: &BindingSource,
1108    ) -> Result<bool, crate::compiler_error::CompilerFailure> {
1109        Ok(match source {
1110            BindingSource::Let(sid) => match &self
1111                .ta
1112                .try_stmt(*sid)
1113                .map_err(crate::typechecker::arena_failure)?
1114                .kind
1115            {
1116                TypedStmtKind::Let { boxed, .. } => *boxed,
1117                _ => {
1118                    return Err(crate::typechecker::invariant_failure(
1119                        "capture source is not a let binding",
1120                    ));
1121                }
1122            },
1123            BindingSource::Catch { try_stmt, index } => self.catch_clause(*try_stmt, *index)?.boxed,
1124            BindingSource::ParameterBinding(sid) => match &self
1125                .ta
1126                .try_stmt(*sid)
1127                .map_err(crate::typechecker::arena_failure)?
1128                .kind
1129            {
1130                TypedStmtKind::Let { boxed, .. } => *boxed,
1131                TypedStmtKind::Const { .. } => false,
1132                _ => {
1133                    return Err(crate::typechecker::invariant_failure(
1134                        "parameter capture source is not a binding",
1135                    ));
1136                }
1137            },
1138            BindingSource::Const => false,
1139            BindingSource::Param { owner, index } => match owner {
1140                ParamOwner::Function(fidx) => {
1141                    self.ta
1142                        .functions
1143                        .get(*fidx)
1144                        .and_then(|f| f.params.get(*index))
1145                        .ok_or_else(|| {
1146                            crate::typechecker::invariant_failure(
1147                                "missing function capture parameter",
1148                            )
1149                        })?
1150                        .boxed
1151                }
1152                ParamOwner::Closure(eid) => match &self
1153                    .ta
1154                    .try_expr(*eid)
1155                    .map_err(crate::typechecker::arena_failure)?
1156                    .kind
1157                {
1158                    TypedExprKind::Closure { params, .. } => {
1159                        params
1160                            .get(*index)
1161                            .ok_or_else(|| {
1162                                crate::typechecker::invariant_failure(
1163                                    "missing closure capture parameter",
1164                                )
1165                            })?
1166                            .boxed
1167                    }
1168                    _ => {
1169                        return Err(crate::typechecker::invariant_failure(
1170                            "capture parameter owner is not a closure",
1171                        ));
1172                    }
1173                },
1174                ParamOwner::ClassMember(member) => {
1175                    member
1176                        .param(self.ta, *index)
1177                        .ok_or_else(|| {
1178                            crate::typechecker::invariant_failure(
1179                                "missing class member capture parameter",
1180                            )
1181                        })?
1182                        .boxed
1183                }
1184            },
1185        })
1186    }
1187}
1188
1189#[cfg(test)]
1190mod tests {
1191    use super::capture;
1192    use crate::{
1193        Asi, CapturedVar, ClosureBody, ExprId, StmtId, Token, TokenKind, Type, TypedAst,
1194        TypedChainPart, TypedExprKind, TypedStmtKind, infer, parse,
1195    };
1196
1197    #[test]
1198    fn missing_capture_parameter_is_a_fatal_failure() {
1199        let mut ta = typecheck("function main(): number { return 1; }");
1200        let mut state = super::State {
1201            ta: &mut ta,
1202            frames: Vec::new(),
1203            pending: Vec::new(),
1204            resolved: super::ResolvedLocals::default(),
1205        };
1206        let source = super::BindingSource::Param {
1207            owner: super::ParamOwner::Function(0),
1208            index: usize::MAX,
1209        };
1210        assert!(matches!(
1211            state.source_is_boxed(&source),
1212            Err(crate::compiler_error::CompilerFailure::Internal { .. })
1213        ));
1214        assert!(matches!(
1215            state.mark_source_boxed(&source),
1216            Err(crate::compiler_error::CompilerFailure::Internal { .. })
1217        ));
1218    }
1219
1220    #[test]
1221    fn missing_local_binding_stops_capture_and_a_fresh_pass_succeeds() {
1222        let source = "function main(value: number): number { return value; }";
1223        let mut ta = typecheck(source);
1224        let mut changed = false;
1225        for id in ta.expr_ids().unwrap() {
1226            if let TypedExprKind::LocalRef { ident, .. } = &mut ta.try_expr_mut(id).unwrap().kind {
1227                ident.name = "missing".into();
1228                changed = true;
1229                break;
1230            }
1231        }
1232        assert!(changed);
1233        assert!(matches!(
1234            capture(ta),
1235            Err(crate::compiler_error::CompilerFailure::Internal { .. })
1236        ));
1237        assert!(capture(typecheck(source)).is_ok());
1238    }
1239
1240    #[test]
1241    fn body_exit_narrowings_do_not_leak_into_later_declarations() {
1242        for declaration in [
1243            "function first(x: { ok: boolean }): void { if (!x.ok) throw new Error(\"bad\"); }",
1244            "class C { first(x: { ok: boolean }): void { if (!x.ok) throw new Error(\"bad\"); } second(): number { return 1; } }",
1245            "class C { static first(x: { ok: boolean }): void { if (!x.ok) throw new Error(\"bad\"); } static second(): number { return 1; } }",
1246            "class C { constructor(x: { ok: boolean }) { if (!x.ok) throw new Error(\"bad\"); } second(): number { return 1; } }",
1247            "class C { set value(x: { ok: boolean }) { if (!x.ok) throw new Error(\"bad\"); } second(): number { return 1; } }",
1248        ] {
1249            let source = format!("{declaration} function main(): number {{ return 1; }}");
1250            assert!(capture(typecheck(&source)).is_ok(), "{source}");
1251        }
1252    }
1253
1254    fn typecheck(source: &str) -> TypedAst {
1255        let mut asi = Asi::new(source, crate::FileId(0));
1256        let mut tokens: Vec<Token> = Vec::new();
1257        loop {
1258            let tok = asi.next_token();
1259            let is_eof = matches!(tok.kind, TokenKind::Eof);
1260            tokens.push(tok);
1261            if is_eof {
1262                break;
1263            }
1264        }
1265        let lex_diags = asi.into_diagnostics();
1266        assert!(
1267            lex_diags.is_empty(),
1268            "unexpected lexer diags: {lex_diags:?}"
1269        );
1270        let (ast, parse_diags) = parse(source, tokens, crate::FileId(0));
1271        assert!(
1272            parse_diags.is_empty(),
1273            "unexpected parser diags: {parse_diags:?}"
1274        );
1275        let (prelude_defs, host_defs, _) =
1276            crate::runtime::prelude::cached_runtime_package_declarations();
1277        let mut packages = Vec::with_capacity(prelude_defs.len() + host_defs.len());
1278        packages.extend(prelude_defs.iter());
1279        packages.extend(host_defs.iter());
1280        let (ta, infer_diags) = infer(source, "main", &ast, &packages);
1281        assert!(
1282            infer_diags.is_empty(),
1283            "unexpected infer diags: {infer_diags:?}"
1284        );
1285        ta
1286    }
1287
1288    fn run(source: &str) -> TypedAst {
1289        let mut ta = typecheck(source);
1290        ta = capture(ta).unwrap();
1291        ta
1292    }
1293
1294    #[derive(Debug, PartialEq)]
1295    struct Flag {
1296        kind: &'static str,
1297        name: String,
1298        boxed: bool,
1299    }
1300
1301    fn collect_flags(ta: &TypedAst) -> Vec<Flag> {
1302        let mut out = Vec::new();
1303        for f in &ta.functions {
1304            for p in &f.params {
1305                out.push(Flag {
1306                    kind: "Param",
1307                    name: p.name.name.clone(),
1308                    boxed: p.boxed,
1309                });
1310            }
1311            walk_stmt(ta, f.body, &mut out);
1312        }
1313        for &id in &ta.top_level_statements {
1314            walk_stmt(ta, id, &mut out);
1315        }
1316        out
1317    }
1318
1319    fn walk_stmt(ta: &TypedAst, id: StmtId, out: &mut Vec<Flag>) {
1320        match &ta.try_stmt(id).unwrap().kind {
1321            TypedStmtKind::Let {
1322                name, value, boxed, ..
1323            } => {
1324                out.push(Flag {
1325                    kind: "Let",
1326                    name: name.name.clone(),
1327                    boxed: *boxed,
1328                });
1329                walk_expr(ta, *value, out);
1330            }
1331            TypedStmtKind::Const { value, .. } => walk_expr(ta, *value, out),
1332            TypedStmtKind::If {
1333                condition,
1334                then_block,
1335                else_block,
1336            } => {
1337                walk_expr(ta, *condition, out);
1338                walk_stmt(ta, *then_block, out);
1339                if let Some(eb) = else_block {
1340                    walk_stmt(ta, *eb, out);
1341                }
1342            }
1343            TypedStmtKind::While { condition, body } => {
1344                walk_expr(ta, *condition, out);
1345                walk_stmt(ta, *body, out);
1346            }
1347            TypedStmtKind::For {
1348                init,
1349                condition,
1350                update,
1351                body,
1352            } => {
1353                if let Some(i) = init {
1354                    walk_stmt(ta, *i, out);
1355                }
1356                if let Some(c) = condition {
1357                    walk_expr(ta, *c, out);
1358                }
1359                if let Some(u) = update {
1360                    walk_stmt(ta, *u, out);
1361                }
1362                walk_stmt(ta, *body, out);
1363            }
1364            TypedStmtKind::ForOf { iter, body, .. } => {
1365                walk_expr(ta, *iter, out);
1366                walk_stmt(ta, *body, out);
1367            }
1368            TypedStmtKind::DoWhile { body, condition } => {
1369                walk_stmt(ta, *body, out);
1370                walk_expr(ta, *condition, out);
1371            }
1372            TypedStmtKind::Switch {
1373                discriminant,
1374                cases,
1375                default,
1376                ..
1377            } => {
1378                walk_expr(ta, *discriminant, out);
1379                for comparison in cases
1380                    .iter()
1381                    .flat_map(crate::TypedSwitchCase::label_comparisons)
1382                {
1383                    walk_expr(ta, comparison, out);
1384                }
1385                for case in cases {
1386                    walk_stmt(ta, case.body, out);
1387                }
1388                if let Some(d) = default {
1389                    walk_stmt(ta, *d, out);
1390                }
1391            }
1392            TypedStmtKind::Break | TypedStmtKind::Continue | TypedStmtKind::ReboxLocal { .. } => {}
1393            TypedStmtKind::Return(value) => {
1394                if let Some(v) = value {
1395                    walk_expr(ta, *v, out);
1396                }
1397            }
1398            TypedStmtKind::Expr(e) => walk_expr(ta, *e, out),
1399            TypedStmtKind::Block(stmts) => {
1400                for &s in stmts {
1401                    walk_stmt(ta, s, out);
1402                }
1403            }
1404            TypedStmtKind::AssignLocal {
1405                ident,
1406                value,
1407                boxed,
1408                ..
1409            } => {
1410                out.push(Flag {
1411                    kind: "AssignLocal",
1412                    name: ident.name.clone(),
1413                    boxed: *boxed,
1414                });
1415                walk_expr(ta, *value, out);
1416            }
1417            TypedStmtKind::AssignGlobal { value, .. } => walk_expr(ta, *value, out),
1418            TypedStmtKind::AssignField {
1419                receiver, value, ..
1420            } => {
1421                walk_expr(ta, *receiver, out);
1422                walk_expr(ta, *value, out);
1423            }
1424            TypedStmtKind::AssignIndex {
1425                receiver,
1426                index,
1427                value,
1428                ..
1429            } => {
1430                walk_expr(ta, *receiver, out);
1431                walk_expr(ta, *index, out);
1432                walk_expr(ta, *value, out);
1433            }
1434            TypedStmtKind::NarrowRegion { source, body, .. } => {
1435                walk_expr(ta, *source, out);
1436                walk_stmt(ta, *body, out);
1437            }
1438            TypedStmtKind::Throw { value } => walk_expr(ta, *value, out),
1439            TypedStmtKind::Try {
1440                body,
1441                catches,
1442                finally,
1443            } => {
1444                walk_stmt(ta, *body, out);
1445                for c in catches {
1446                    out.push(Flag {
1447                        kind: "Catch",
1448                        name: c.binding.name.clone(),
1449                        boxed: c.boxed,
1450                    });
1451                    walk_stmt(ta, c.body, out);
1452                }
1453                if let Some(f) = finally {
1454                    walk_stmt(ta, *f, out);
1455                }
1456            }
1457        }
1458    }
1459
1460    fn walk_expr(ta: &TypedAst, id: ExprId, out: &mut Vec<Flag>) {
1461        match &ta.try_expr(id).unwrap().kind {
1462            TypedExprKind::LocalRef { ident, boxed } => out.push(Flag {
1463                kind: "LocalRef",
1464                name: ident.name.clone(),
1465                boxed: *boxed,
1466            }),
1467            TypedExprKind::LocalNarrowRef { binding, .. } => out.push(Flag {
1468                kind: "LocalNarrowRef",
1469                name: binding.name.clone(),
1470                boxed: false,
1471            }),
1472            TypedExprKind::GlobalRef { .. } | TypedExprKind::FunctionRef { .. } => {}
1473            TypedExprKind::Number(_)
1474            | TypedExprKind::BigInt(_)
1475            | TypedExprKind::String(_)
1476            | TypedExprKind::Boolean(_)
1477            | TypedExprKind::Null
1478            | TypedExprKind::Undefined
1479            | TypedExprKind::This
1480            | TypedExprKind::Regex { .. }
1481            | TypedExprKind::NumberEnumMember { .. }
1482            | TypedExprKind::StringEnumMember { .. } => {}
1483            TypedExprKind::Binary { lhs, rhs, .. } => {
1484                walk_expr(ta, *lhs, out);
1485                walk_expr(ta, *rhs, out);
1486            }
1487            TypedExprKind::EffectThen { effect, result } => {
1488                walk_expr(ta, *effect, out);
1489                walk_expr(ta, *result, out);
1490            }
1491            TypedExprKind::Sequence { stmts, result } => {
1492                for &stmt in stmts {
1493                    walk_stmt(ta, stmt, out);
1494                }
1495                walk_expr(ta, *result, out);
1496            }
1497            TypedExprKind::Unary { operand, .. } => walk_expr(ta, *operand, out),
1498            TypedExprKind::TypeofTag { value, .. } | TypedExprKind::InstanceOf { value, .. } => {
1499                walk_expr(ta, *value, out);
1500            }
1501            TypedExprKind::Call { args, .. }
1502            | TypedExprKind::McpCall { args, .. }
1503            | TypedExprKind::SuperCtorCall { args, .. }
1504            | TypedExprKind::SuperMethodCall { args, .. } => {
1505                for &a in args {
1506                    walk_expr(ta, a, out);
1507                }
1508            }
1509            TypedExprKind::CallClosure { callee, args } => {
1510                walk_expr(ta, *callee, out);
1511                for &a in args {
1512                    walk_expr(ta, a, out);
1513                }
1514            }
1515            TypedExprKind::GenericCall { args, .. } => {
1516                for a in args {
1517                    walk_expr(ta, a.expr, out);
1518                }
1519            }
1520            TypedExprKind::MethodCall { receiver, args, .. } => {
1521                walk_expr(ta, *receiver, out);
1522                for &a in args {
1523                    walk_expr(ta, a, out);
1524                }
1525            }
1526            TypedExprKind::GenericMethodCall { receiver, args, .. } => {
1527                walk_expr(ta, *receiver, out);
1528                for a in args {
1529                    walk_expr(ta, a.expr, out);
1530                }
1531            }
1532            TypedExprKind::IntrinsicCall { args, .. } => {
1533                for &a in args {
1534                    walk_expr(ta, a, out);
1535                }
1536            }
1537            TypedExprKind::ObjectLiteral { members, .. } => {
1538                for member in members {
1539                    for expression in member.expressions() {
1540                        walk_expr(ta, expression, out);
1541                    }
1542                }
1543            }
1544            TypedExprKind::ArrayLiteral { elements, .. } => {
1545                for e in elements {
1546                    walk_expr(ta, e.expr_id(), out);
1547                }
1548            }
1549            TypedExprKind::TupleLiteral { elements, .. } => {
1550                for &e in elements {
1551                    walk_expr(ta, e, out);
1552                }
1553            }
1554            TypedExprKind::FieldAccess { receiver, .. }
1555            | TypedExprKind::InterfacePropertyAccess { receiver, .. } => {
1556                walk_expr(ta, *receiver, out);
1557            }
1558            TypedExprKind::IndexAccess { receiver, index } => {
1559                walk_expr(ta, *receiver, out);
1560                walk_expr(ta, *index, out);
1561            }
1562            TypedExprKind::Closure { params, body, .. } => {
1563                for p in params {
1564                    out.push(Flag {
1565                        kind: "Param",
1566                        name: p.name.name.clone(),
1567                        boxed: p.boxed,
1568                    });
1569                }
1570                match *body {
1571                    ClosureBody::Expr(e) => walk_expr(ta, e, out),
1572                    ClosureBody::Block(b) => walk_stmt(ta, b, out),
1573                }
1574            }
1575            TypedExprKind::Narrowed { source, inner, .. } => {
1576                walk_expr(ta, *source, out);
1577                walk_expr(ta, *inner, out);
1578            }
1579            TypedExprKind::Ternary { cond, then_, else_ } => {
1580                walk_expr(ta, *cond, out);
1581                walk_expr(ta, *then_, out);
1582                walk_expr(ta, *else_, out);
1583            }
1584            TypedExprKind::NullishCoalesce { lhs, rhs } => {
1585                walk_expr(ta, *lhs, out);
1586                walk_expr(ta, *rhs, out);
1587            }
1588            TypedExprKind::OptionalChain { base, parts } => {
1589                walk_expr(ta, *base, out);
1590                for part in parts {
1591                    match part {
1592                        TypedChainPart::Index { idx, .. } => walk_expr(ta, *idx, out),
1593                        TypedChainPart::Call { args, .. }
1594                        | TypedChainPart::MethodCall { args, .. } => {
1595                            for a in args {
1596                                walk_expr(ta, *a, out);
1597                            }
1598                        }
1599                        TypedChainPart::Field { .. }
1600                        | TypedChainPart::InterfaceProperty { .. }
1601                        | TypedChainPart::NonNull { .. } => {}
1602                    }
1603                }
1604            }
1605            TypedExprKind::PostfixUnary { target, .. } => match target {
1606                crate::PostfixTarget::Local { ident, boxed, .. } => out.push(Flag {
1607                    kind: "PostfixLocal",
1608                    name: ident.name.clone(),
1609                    boxed: *boxed,
1610                }),
1611                crate::PostfixTarget::Global { .. } => {}
1612                crate::PostfixTarget::Field { receiver, .. } => walk_expr(ta, *receiver, out),
1613                crate::PostfixTarget::Index {
1614                    receiver, index, ..
1615                } => {
1616                    walk_expr(ta, *receiver, out);
1617                    walk_expr(ta, *index, out);
1618                }
1619            },
1620            TypedExprKind::NonNullAssert { value } | TypedExprKind::Cast { value, .. } => {
1621                walk_expr(ta, *value, out);
1622            }
1623        }
1624    }
1625
1626    fn first_closure(ta: &TypedAst) -> (Vec<CapturedVar>, ExprId) {
1627        for f in &ta.functions {
1628            if let Some(found) = scan_stmt(ta, f.body) {
1629                return found;
1630            }
1631        }
1632        for &id in &ta.top_level_statements {
1633            if let Some(found) = scan_stmt(ta, id) {
1634                return found;
1635            }
1636        }
1637        panic!("no Closure found in subtree");
1638    }
1639
1640    fn scan_stmt(ta: &TypedAst, id: StmtId) -> Option<(Vec<CapturedVar>, ExprId)> {
1641        match &ta.try_stmt(id).unwrap().kind {
1642            TypedStmtKind::Let { value, .. }
1643            | TypedStmtKind::Const { value, .. }
1644            | TypedStmtKind::AssignLocal { value, .. }
1645            | TypedStmtKind::AssignGlobal { value, .. } => scan_expr(ta, *value),
1646            TypedStmtKind::If {
1647                condition,
1648                then_block,
1649                else_block,
1650            } => scan_expr(ta, *condition)
1651                .or_else(|| scan_stmt(ta, *then_block))
1652                .or_else(|| else_block.and_then(|eb| scan_stmt(ta, eb))),
1653            TypedStmtKind::While { condition, body } => {
1654                scan_expr(ta, *condition).or_else(|| scan_stmt(ta, *body))
1655            }
1656            TypedStmtKind::For {
1657                init,
1658                condition,
1659                update,
1660                body,
1661            } => init
1662                .and_then(|i| scan_stmt(ta, i))
1663                .or_else(|| condition.and_then(|c| scan_expr(ta, c)))
1664                .or_else(|| update.and_then(|u| scan_stmt(ta, u)))
1665                .or_else(|| scan_stmt(ta, *body)),
1666            TypedStmtKind::ForOf { iter, body, .. } => {
1667                scan_expr(ta, *iter).or_else(|| scan_stmt(ta, *body))
1668            }
1669            TypedStmtKind::DoWhile { body, condition } => {
1670                scan_stmt(ta, *body).or_else(|| scan_expr(ta, *condition))
1671            }
1672            TypedStmtKind::Switch {
1673                discriminant,
1674                cases,
1675                default,
1676                ..
1677            } => scan_expr(ta, *discriminant)
1678                .or_else(|| cases.iter().find_map(|c| scan_stmt(ta, c.body)))
1679                .or_else(|| default.and_then(|d| scan_stmt(ta, d))),
1680            TypedStmtKind::Break | TypedStmtKind::Continue | TypedStmtKind::ReboxLocal { .. } => {
1681                None
1682            }
1683            TypedStmtKind::Return(value) => value.and_then(|v| scan_expr(ta, v)),
1684            TypedStmtKind::Expr(e) => scan_expr(ta, *e),
1685            TypedStmtKind::Block(stmts) => stmts.iter().find_map(|&s| scan_stmt(ta, s)),
1686            TypedStmtKind::AssignField {
1687                receiver, value, ..
1688            } => scan_expr(ta, *receiver).or_else(|| scan_expr(ta, *value)),
1689            TypedStmtKind::AssignIndex {
1690                receiver,
1691                index,
1692                value,
1693                ..
1694            } => scan_expr(ta, *receiver)
1695                .or_else(|| scan_expr(ta, *index))
1696                .or_else(|| scan_expr(ta, *value)),
1697            TypedStmtKind::NarrowRegion { source, body, .. } => {
1698                scan_expr(ta, *source).or_else(|| scan_stmt(ta, *body))
1699            }
1700            TypedStmtKind::Throw { value } => scan_expr(ta, *value),
1701            TypedStmtKind::Try {
1702                body,
1703                catches,
1704                finally,
1705            } => scan_stmt(ta, *body)
1706                .or_else(|| catches.iter().find_map(|c| scan_stmt(ta, c.body)))
1707                .or_else(|| finally.and_then(|f| scan_stmt(ta, f))),
1708        }
1709    }
1710
1711    fn scan_expr(ta: &TypedAst, id: ExprId) -> Option<(Vec<CapturedVar>, ExprId)> {
1712        if let TypedExprKind::Closure { captured, .. } = &ta.try_expr(id).unwrap().kind {
1713            return Some((captured.clone(), id));
1714        }
1715        match &ta.try_expr(id).unwrap().kind {
1716            TypedExprKind::Binary { lhs, rhs, .. } => {
1717                scan_expr(ta, *lhs).or_else(|| scan_expr(ta, *rhs))
1718            }
1719            TypedExprKind::Unary { operand, .. } => scan_expr(ta, *operand),
1720            TypedExprKind::TypeofTag { value, .. } | TypedExprKind::InstanceOf { value, .. } => {
1721                scan_expr(ta, *value)
1722            }
1723            TypedExprKind::Call { args, .. } | TypedExprKind::McpCall { args, .. } => {
1724                args.iter().find_map(|&a| scan_expr(ta, a))
1725            }
1726            TypedExprKind::CallClosure { callee, args } => {
1727                scan_expr(ta, *callee).or_else(|| args.iter().find_map(|&a| scan_expr(ta, a)))
1728            }
1729            TypedExprKind::GenericCall { args, .. } => {
1730                args.iter().find_map(|a| scan_expr(ta, a.expr))
1731            }
1732            TypedExprKind::MethodCall { receiver, args, .. } => {
1733                scan_expr(ta, *receiver).or_else(|| args.iter().find_map(|&a| scan_expr(ta, a)))
1734            }
1735            TypedExprKind::GenericMethodCall { receiver, args, .. } => {
1736                scan_expr(ta, *receiver).or_else(|| args.iter().find_map(|a| scan_expr(ta, a.expr)))
1737            }
1738            TypedExprKind::IntrinsicCall { args, .. } => {
1739                args.iter().find_map(|&a| scan_expr(ta, a))
1740            }
1741            TypedExprKind::ObjectLiteral { members, .. } => members
1742                .iter()
1743                .flat_map(|member| member.expressions())
1744                .find_map(|expression| scan_expr(ta, expression)),
1745            TypedExprKind::ArrayLiteral { elements, .. } => {
1746                elements.iter().find_map(|e| scan_expr(ta, e.expr_id()))
1747            }
1748            TypedExprKind::FieldAccess { receiver, .. }
1749            | TypedExprKind::InterfacePropertyAccess { receiver, .. } => scan_expr(ta, *receiver),
1750            TypedExprKind::IndexAccess { receiver, index } => {
1751                scan_expr(ta, *receiver).or_else(|| scan_expr(ta, *index))
1752            }
1753            _ => None,
1754        }
1755    }
1756
1757    #[test]
1758    fn mvp_program_has_no_boxed_flags() {
1759        let ta = run(
1760            "function f(n: number): number { let x: number = n + 1; return x; } let r: number = f(2);",
1761        );
1762        let flags = collect_flags(&ta);
1763        assert!(flags.iter().all(|f| !f.boxed), "{flags:?}");
1764    }
1765
1766    #[test]
1767    fn single_let_captured_is_boxed() {
1768        let ta = run("function host(): void { let x: number = 0; const g = () => x; }");
1769        let flags = collect_flags(&ta);
1770        let x_let = flags
1771            .iter()
1772            .find(|f| f.kind == "Let" && f.name == "x")
1773            .expect("Let x");
1774        assert!(x_let.boxed, "expected outer let x to be boxed");
1775        let x_ref = flags
1776            .iter()
1777            .find(|f| f.kind == "LocalRef" && f.name == "x")
1778            .expect("LocalRef x");
1779        assert!(
1780            x_ref.boxed,
1781            "expected LocalRef(x) inside closure to be boxed"
1782        );
1783
1784        let (captured, _) = first_closure(&ta);
1785        assert_eq!(captured.len(), 1);
1786        assert_eq!(captured[0].name.name, "x");
1787        assert_eq!(captured[0].ty, Type::Number);
1788        assert!(captured[0].boxed);
1789    }
1790
1791    #[test]
1792    fn single_const_captured_is_unboxed() {
1793        let ta = run("function host(): void { const x: number = 0; const g = () => x; }");
1794        let flags = collect_flags(&ta);
1795        let x_ref = flags
1796            .iter()
1797            .find(|f| f.kind == "LocalRef" && f.name == "x")
1798            .expect("LocalRef x");
1799        assert!(
1800            !x_ref.boxed,
1801            "expected LocalRef(x) referencing const to be unboxed"
1802        );
1803
1804        let (captured, _) = first_closure(&ta);
1805        assert_eq!(captured.len(), 1);
1806        assert_eq!(captured[0].name.name, "x");
1807        assert!(!captured[0].boxed, "const captures are copied, not boxed");
1808    }
1809
1810    #[test]
1811    fn param_captured_is_boxed() {
1812        let ta = run("function host(x: number): void { const g = () => x; }");
1813        let flags = collect_flags(&ta);
1814        let x_param = flags
1815            .iter()
1816            .find(|f| f.kind == "Param" && f.name == "x")
1817            .expect("Param x");
1818        assert!(x_param.boxed, "captured param must be boxed");
1819    }
1820
1821    #[test]
1822    fn write_to_captured_let_is_boxed() {
1823        let ta =
1824            run("function host(): void { let x: number = 0; const g = () => { x = x + 1; }; }");
1825        let flags = collect_flags(&ta);
1826        let x_let = flags
1827            .iter()
1828            .find(|f| f.kind == "Let" && f.name == "x")
1829            .unwrap();
1830        assert!(x_let.boxed, "let x must be boxed");
1831        let assign = flags
1832            .iter()
1833            .find(|f| f.kind == "AssignLocal" && f.name == "x")
1834            .expect("AssignLocal x");
1835        assert!(assign.boxed, "AssignLocal(x) must be boxed");
1836        let refs: Vec<&Flag> = flags
1837            .iter()
1838            .filter(|f| f.kind == "LocalRef" && f.name == "x")
1839            .collect();
1840        assert!(!refs.is_empty(), "expected at least one LocalRef x");
1841        for r in &refs {
1842            assert!(r.boxed, "LocalRef(x) inside closure must be boxed");
1843        }
1844    }
1845
1846    #[test]
1847    fn same_frame_reassign_of_captured_let_is_boxed() {
1848        let ta = run("function host(): void { let x: number = 0; const g = () => x; x = x + 1; }");
1849        let flags = collect_flags(&ta);
1850        let x_let = flags
1851            .iter()
1852            .find(|f| f.kind == "Let" && f.name == "x")
1853            .expect("Let x");
1854        assert!(
1855            x_let.boxed,
1856            "let x must be boxed because closure captures it"
1857        );
1858        let assign = flags
1859            .iter()
1860            .find(|f| f.kind == "AssignLocal" && f.name == "x")
1861            .expect("AssignLocal x");
1862        assert!(
1863            assign.boxed,
1864            "same-frame AssignLocal(x) must be boxed (SUB-148)",
1865        );
1866        // Every LocalRef(x) — inside the closure body AND inside the
1867        // outer-frame `x + 1` value — must be boxed.
1868        let refs: Vec<&Flag> = flags
1869            .iter()
1870            .filter(|f| f.kind == "LocalRef" && f.name == "x")
1871            .collect();
1872        assert!(
1873            refs.len() >= 2,
1874            "expected refs in closure + same-frame value, got {refs:?}"
1875        );
1876        for r in &refs {
1877            assert!(r.boxed, "every LocalRef(x) must be boxed: {r:?}");
1878        }
1879    }
1880
1881    #[test]
1882    fn same_frame_reassign_appearing_before_closure_is_boxed() {
1883        let ta = run("function host(): void { let x: number = 0; x = x + 1; const g = () => x; }");
1884        let flags = collect_flags(&ta);
1885        let x_let = flags
1886            .iter()
1887            .find(|f| f.kind == "Let" && f.name == "x")
1888            .expect("Let x");
1889        assert!(x_let.boxed);
1890        let assign = flags
1891            .iter()
1892            .find(|f| f.kind == "AssignLocal" && f.name == "x")
1893            .expect("AssignLocal x");
1894        assert!(
1895            assign.boxed,
1896            "AssignLocal before inner closure must still be boxed (SUB-148 ordering)",
1897        );
1898        for r in flags
1899            .iter()
1900            .filter(|f| f.kind == "LocalRef" && f.name == "x")
1901        {
1902            assert!(
1903                r.boxed,
1904                "LocalRef(x) must be boxed regardless of walk order: {r:?}"
1905            );
1906        }
1907    }
1908
1909    #[test]
1910    fn same_frame_param_reassign_with_inner_capture_is_boxed() {
1911        let ta = run(
1912            "function f(p: number): void { p = p + 1; const inner = () => p; } \
1913             function main(): void {}",
1914        );
1915        let flags = collect_flags(&ta);
1916        let p_param = flags
1917            .iter()
1918            .find(|f| f.kind == "Param" && f.name == "p")
1919            .expect("Param p");
1920        assert!(p_param.boxed, "captured param must be boxed");
1921        let assign = flags
1922            .iter()
1923            .find(|f| f.kind == "AssignLocal" && f.name == "p")
1924            .expect("AssignLocal p");
1925        assert!(
1926            assign.boxed,
1927            "same-frame AssignLocal(p) for a captured param must be boxed (SUB-148)",
1928        );
1929        for r in flags
1930            .iter()
1931            .filter(|f| f.kind == "LocalRef" && f.name == "p")
1932        {
1933            assert!(r.boxed, "every LocalRef(p) must be boxed: {r:?}");
1934        }
1935    }
1936
1937    #[test]
1938    fn transitive_capture_forwards_through_intermediate_closure() {
1939        let ta = run("function host(): void { let x: number = 0; \
1940              const mid = () => { const inner = () => x; }; }");
1941
1942        let flags = collect_flags(&ta);
1943        let x_let = flags
1944            .iter()
1945            .find(|f| f.kind == "Let" && f.name == "x")
1946            .unwrap();
1947        assert!(x_let.boxed);
1948
1949        let (mid_captured, mid_id) = first_closure(&ta);
1950        assert_eq!(mid_captured.len(), 1, "mid forwards `x`");
1951        assert_eq!(mid_captured[0].name.name, "x");
1952        assert!(mid_captured[0].boxed);
1953
1954        let (inner_captured, _) = match &ta.try_expr(mid_id).unwrap().kind {
1955            TypedExprKind::Closure {
1956                body: ClosureBody::Block(b),
1957                ..
1958            } => scan_stmt(&ta, *b).expect("inner closure"),
1959            _ => panic!("mid is not a block-body closure"),
1960        };
1961        assert_eq!(inner_captured.len(), 1, "inner captures `x`");
1962        assert_eq!(inner_captured[0].name.name, "x");
1963        assert!(inner_captured[0].boxed);
1964    }
1965
1966    #[test]
1967    fn shadowed_binding_does_not_capture_outer() {
1968        let ta = run("function host(): void { let x: number = 0; \
1969              const g = () => { let x: number = 1; x = x + 1; }; }");
1970        let flags = collect_flags(&ta);
1971        let x_lets: Vec<&Flag> = flags
1972            .iter()
1973            .filter(|f| f.kind == "Let" && f.name == "x")
1974            .collect();
1975        assert_eq!(x_lets.len(), 2, "two `let x` declarations");
1976        for l in x_lets {
1977            assert!(!l.boxed, "neither `let x` should be boxed");
1978        }
1979        let (captured, _) = first_closure(&ta);
1980        assert!(captured.is_empty(), "shadowed name doesn't capture");
1981    }
1982
1983    #[test]
1984    fn multiple_captures_recorded_in_order() {
1985        let ta = run("function host(p: number): void { \
1986               let a: number = 1; \
1987               const b: number = 2; \
1988               const g = () => a + b + p; \
1989             }");
1990        let (captured, _) = first_closure(&ta);
1991        assert_eq!(captured.len(), 3, "{captured:?}");
1992
1993        let names: Vec<&str> = captured.iter().map(|c| c.name.name.as_str()).collect();
1994        assert_eq!(names, vec!["a", "b", "p"]);
1995
1996        let a = &captured[0];
1997        let b = &captured[1];
1998        let p = &captured[2];
1999        assert!(a.boxed, "let captured → boxed");
2000        assert!(!b.boxed, "const captured → unboxed");
2001        assert!(p.boxed, "param captured → boxed");
2002    }
2003
2004    #[test]
2005    fn capture_is_idempotent() {
2006        let mut ta = typecheck("function host(): void { let x: number = 0; const g = () => x; }");
2007        ta = capture(ta).unwrap();
2008        let after_one = format!("{ta:?}");
2009        ta = capture(ta).unwrap();
2010        let after_two = format!("{ta:?}");
2011        assert_eq!(
2012            after_one, after_two,
2013            "second run should not change anything"
2014        );
2015    }
2016
2017    #[test]
2018    fn capture_runs_on_empty_program() {
2019        let mut ta = typecheck("");
2020        ta = capture(ta).unwrap();
2021        assert!(ta.globals.is_empty());
2022        assert!(ta.functions.is_empty());
2023        assert!(ta.top_level_statements.is_empty());
2024    }
2025}