1use 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
13pub(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#[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, ¶ms, 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 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 state.apply_pending()?;
68 Ok(state.resolved)
69}
70
71struct MemberBody {
74 member: ClassMemberRef,
75 params: Vec<crate::TypedParam>,
76 body: StmtId,
77 this_ty: Type,
78}
79
80fn 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 mutable: bool,
175 source: BindingSource,
176}
177
178#[derive(Clone)]
179enum BindingSource {
180 Let(StmtId),
181 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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 => {} 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 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 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}