Skip to main content

submilli_engine/
lower_patterns.rs

1//! Pre-infer pass eliminating destructuring patterns from [`Ast`].
2//! Runs between [`crate::parse`] and [`crate::infer`]; downstream sees only plain
3//! `Let`/`Const`/`ObjectRest`/`ForOf` and `ParamDecl` with `pattern: None`.
4//! Synthesised array-pattern `IndexAccess` nodes are tagged in [`Ast::pattern_origins`]
5//! so the typechecker can rephrase index errors as destructure-specific diagnostics.
6
7use std::collections::BTreeSet;
8
9use crate::compiler_error::{CompilerFailure, CompilerStage};
10use crate::tree_height;
11
12use crate::{
13    Ast, Binding, BindingKind, ClassMember, Expr, ExprId, ExprKind, Ident, ParamDecl,
14    PatternOrigin, Span, Stmt, StmtId, StmtKind,
15};
16
17pub fn lower(mut ast: Ast) -> Result<Ast, CompilerFailure> {
18    // Callers may supply an AST that did not come from `parse_checked`. Pattern
19    // lowering reports its failures in the inference stage it feeds.
20    tree_height::check_syntax(&ast).map_err(|failure| failure.with_stage(CompilerStage::Infer))?;
21    let mut ctx = LowerCtx { next_tmp: 0 };
22    ctx.lower_arrows(&mut ast)?;
23    ctx.lower_function_params(&mut ast)?;
24    ctx.lower_class_member_params(&mut ast)?;
25    ctx.lower_for_of_patterns(&mut ast)?;
26    ctx.lower_for_pattern_inits(&mut ast)?;
27    ctx.lower_pattern_stmts(&mut ast)?;
28    Ok(ast)
29}
30
31/// Starts every `fresh` name, so none can collide with a source identifier.
32const FRESH_MARKER: &str = "#pattern_";
33
34/// The `fresh` prefix of a lowered destructured parameter.
35const PATTERN_PARAM: &str = "p";
36
37/// Whether `name` stands in for a destructured parameter, which has no source
38/// name: either the fresh `#pattern_p_N` from lowering, or the parser's empty
39/// placeholder on an interface method signature, which has no body to lower.
40pub(crate) fn is_pattern_param(name: &str) -> bool {
41    name.is_empty()
42        || name
43            .strip_prefix(FRESH_MARKER)
44            .and_then(|rest| rest.strip_prefix(PATTERN_PARAM))
45            .is_some_and(|rest| rest.starts_with('_'))
46}
47
48/// The `fresh` prefix of the value a destructuring declaration unpacks.
49const PATTERN_VALUE: &str = "dst";
50
51/// Whether `name` is the fresh `#pattern_dst_N` that holds the value a
52/// destructuring declaration unpacks.
53pub(crate) fn is_pattern_value(name: &str) -> bool {
54    name.strip_prefix(FRESH_MARKER)
55        .and_then(|rest| rest.strip_prefix(PATTERN_VALUE))
56        .is_some_and(|rest| rest.starts_with('_'))
57}
58
59struct LowerCtx {
60    next_tmp: u32,
61}
62
63/// What a pattern's bindings inherit from the declaration that holds it.
64#[derive(Clone, Copy)]
65struct PatternRoot {
66    is_const: bool,
67    /// Whether the declaration annotates the pattern's type, which its
68    /// defaults must then fit.
69    annotated: bool,
70}
71
72impl LowerCtx {
73    fn fresh(&mut self, prefix: &str, span: Span) -> Result<Ident, CompilerFailure> {
74        let n = self.next_tmp;
75        self.next_tmp = self
76            .next_tmp
77            .checked_add(1)
78            .ok_or_else(|| CompilerFailure::Limit {
79                stage: CompilerStage::Infer,
80                span: None,
81                message: "pattern temporary capacity exceeded".into(),
82                help: vec![],
83            })?;
84        Ok(Ident {
85            // Distinct from source identifiers and the later desugar pass.
86            name: format!("{FRESH_MARKER}{prefix}_{n}"),
87            span,
88        })
89    }
90
91    fn lower_arrows(&mut self, ast: &mut Ast) -> Result<(), CompilerFailure> {
92        for id in ast
93            .expr_ids()
94            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
95        {
96            let has_pattern = match &ast
97                .try_expr(id)
98                .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
99                .kind
100            {
101                ExprKind::Arrow { params, .. } => params.iter().any(|p| p.pattern.is_some()),
102                _ => false,
103            };
104            if has_pattern {
105                self.lower_arrow(ast, id)?;
106            }
107        }
108
109        Ok(())
110    }
111
112    fn lower_arrow(&mut self, ast: &mut Ast, id: ExprId) -> Result<(), CompilerFailure> {
113        // Clone the kind to release the read borrow before we start
114        // mutating other arena slots.
115        let kind = ast
116            .try_expr(id)
117            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
118            .kind
119            .clone();
120        let ExprKind::Arrow {
121            mut params,
122            return_type,
123            type_predicate,
124            body,
125        } = kind
126        else {
127            return Err(lowering_failure(
128                "unexpected node kind during pattern lowering",
129            ));
130        };
131
132        self.materialise_pattern_params(ast, &mut params)?;
133
134        ast.try_expr_mut(id)
135            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
136            .kind = ExprKind::Arrow {
137            params,
138            return_type,
139            type_predicate,
140            body,
141        };
142
143        Ok(())
144    }
145
146    fn lower_function_params(&mut self, ast: &mut Ast) -> Result<(), CompilerFailure> {
147        for id in ast
148            .stmt_ids()
149            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
150        {
151            let has_pattern = match &ast
152                .try_stmt(id)
153                .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
154                .kind
155            {
156                StmtKind::Function { params, .. } => params.iter().any(|p| p.pattern.is_some()),
157                _ => false,
158            };
159            if has_pattern {
160                self.lower_function(ast, id)?;
161            }
162        }
163
164        Ok(())
165    }
166
167    fn lower_function(&mut self, ast: &mut Ast, id: StmtId) -> Result<(), CompilerFailure> {
168        let stmt_span = ast
169            .try_stmt(id)
170            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
171            .span;
172        let kind = std::mem::replace(
173            &mut ast
174                .try_stmt_mut(id)
175                .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
176                .kind,
177            StmtKind::Block(Vec::new()), // sentinel, restored before return
178        );
179        let StmtKind::Function {
180            name,
181            generics,
182            mut params,
183            return_type,
184            type_predicate,
185            body,
186            doc,
187        } = kind
188        else {
189            return Err(lowering_failure(
190                "unexpected node kind during pattern lowering",
191            ));
192        };
193
194        self.materialise_pattern_params(ast, &mut params)?;
195
196        ast.try_stmt_mut(id)
197            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
198            .kind = StmtKind::Function {
199            name,
200            generics,
201            params,
202            return_type,
203            type_predicate,
204            body,
205            doc,
206        };
207        // Span is unchanged, but reassert for the linter.
208        let _ = stmt_span;
209
210        Ok(())
211    }
212
213    fn lower_class_member_params(&mut self, ast: &mut Ast) -> Result<(), CompilerFailure> {
214        for id in ast
215            .stmt_ids()
216            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
217        {
218            let StmtKind::ClassDecl { members, .. } = &mut ast
219                .try_stmt_mut(id)
220                .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
221                .kind
222            else {
223                continue;
224            };
225            let mut taken = std::mem::take(members);
226            let lowered = self.lower_member_params(ast, &mut taken);
227            // Restore the members before reporting a failure, so the class
228            // keeps its shape either way.
229            let StmtKind::ClassDecl { members, .. } = &mut ast
230                .try_stmt_mut(id)
231                .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
232                .kind
233            else {
234                return Err(lowering_failure(
235                    "unexpected node kind during pattern lowering",
236                ));
237            };
238            *members = taken;
239            lowered?;
240        }
241
242        Ok(())
243    }
244
245    fn lower_member_params(
246        &mut self,
247        ast: &mut Ast,
248        members: &mut [ClassMember],
249    ) -> Result<(), CompilerFailure> {
250        for member in members {
251            let params = match member {
252                ClassMember::Method { params, .. } | ClassMember::Constructor { params, .. } => {
253                    params.as_mut_slice()
254                }
255                ClassMember::Accessor {
256                    param: Some(param), ..
257                } => std::slice::from_mut(param.as_mut()),
258                ClassMember::Accessor { param: None, .. } | ClassMember::Field { .. } => continue,
259            };
260            self.materialise_pattern_params(ast, params)?;
261        }
262
263        Ok(())
264    }
265
266    fn lower_for_of_patterns(&mut self, ast: &mut Ast) -> Result<(), CompilerFailure> {
267        for id in ast
268            .stmt_ids()
269            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
270        {
271            if !matches!(
272                ast.try_stmt(id)
273                    .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
274                    .kind,
275                StmtKind::ForOfPattern { .. }
276            ) {
277                continue;
278            }
279            self.expand_for_of_pattern(ast, id)?;
280        }
281
282        Ok(())
283    }
284
285    fn expand_for_of_pattern(&mut self, ast: &mut Ast, id: StmtId) -> Result<(), CompilerFailure> {
286        let stmt_span = ast
287            .try_stmt(id)
288            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
289            .span;
290        let kind = std::mem::replace(
291            &mut ast
292                .try_stmt_mut(id)
293                .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
294                .kind,
295            StmtKind::Block(Vec::new()),
296        );
297        let StmtKind::ForOfPattern {
298            binding_kind,
299            binding,
300            ty,
301            iter,
302            body,
303        } = kind
304        else {
305            return Err(lowering_failure(
306                "unexpected node kind during pattern lowering",
307            ));
308        };
309
310        let pattern_span = binding.span();
311        let dst = self.fresh(PATTERN_VALUE, pattern_span)?;
312        let is_const = matches!(binding_kind, BindingKind::Const);
313        let root = PatternRoot {
314            is_const,
315            annotated: ty.is_some(),
316        };
317        let mut decompose = self.emit_decompose(
318            ast,
319            binding,
320            dst.clone(),
321            root,
322            /*doc=*/ None,
323            /*literal=*/ None,
324        )?;
325        let mut source_bindings = Vec::new();
326        for &id in &decompose {
327            match &ast
328                .try_stmt(id)
329                .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
330                .kind
331            {
332                StmtKind::Let { name, .. }
333                | StmtKind::Const { name, .. }
334                | StmtKind::ObjectRest { name, .. } => source_bindings.push(name.clone()),
335                _ => {}
336            }
337        }
338        ast.for_of_pattern_bindings.insert(id, source_bindings);
339        // The loop head and the user's body are distinct lexical scopes.
340        decompose.push(body);
341        let body = ast
342            .try_push_stmt(Stmt {
343                kind: StmtKind::Block(decompose),
344                span: ast
345                    .try_stmt(body)
346                    .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
347                    .span,
348            })
349            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
350
351        ast.try_stmt_mut(id)
352            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
353            .kind = StmtKind::ForOf {
354            binding_kind,
355            name: dst,
356            ty,
357            iter,
358            body,
359        };
360        let _ = stmt_span;
361
362        Ok(())
363    }
364
365    fn lower_for_pattern_inits(&mut self, ast: &mut Ast) -> Result<(), CompilerFailure> {
366        for id in ast
367            .stmt_ids()
368            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
369        {
370            let StmtKind::For {
371                init: Some(init), ..
372            } = ast
373                .try_stmt(id)
374                .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
375                .kind
376            else {
377                continue;
378            };
379            let is_let = match ast
380                .try_stmt(init)
381                .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
382                .kind
383            {
384                StmtKind::LetPattern { .. } => true,
385                StmtKind::ConstPattern { .. } => false,
386                _ => continue,
387            };
388            self.hoist_for_pattern_init(ast, id, init, is_let)?;
389        }
390
391        Ok(())
392    }
393
394    /// `for (let [i, j] = a; c; u) body` becomes
395    /// `{ const dst = a; let i = dst[0]; let j = dst[1]; for (; c; u) body }`.
396    /// The bindings keep the loop's scope but lose the per-iteration copies a
397    /// `for` head's `let` gets, which only a closure capturing them observes;
398    /// inference rejects that case using [`Ast::for_pattern_init_bindings`].
399    fn hoist_for_pattern_init(
400        &mut self,
401        ast: &mut Ast,
402        id: StmtId,
403        init: StmtId,
404        is_let: bool,
405    ) -> Result<(), CompilerFailure> {
406        let mut hoisted = self.expand_pattern_stmt(ast, init)?;
407        let loop_stmt = ast
408            .try_stmt_mut(id)
409            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
410        let span = loop_stmt.span;
411        let kind = std::mem::replace(&mut loop_stmt.kind, StmtKind::Block(Vec::new()));
412        let StmtKind::For {
413            condition,
414            update,
415            body,
416            ..
417        } = kind
418        else {
419            return Err(lowering_failure(
420                "unexpected node kind during pattern lowering",
421            ));
422        };
423        let lowered_loop = ast
424            .try_push_stmt(Stmt {
425                kind: StmtKind::For {
426                    init: None,
427                    condition,
428                    update,
429                    body,
430                },
431                span,
432            })
433            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
434        if is_let {
435            let bindings = let_names(ast, &hoisted)?;
436            if !bindings.is_empty() {
437                ast.for_pattern_init_bindings.insert(lowered_loop, bindings);
438            }
439        }
440        hoisted.push(lowered_loop);
441        ast.try_stmt_mut(id)
442            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
443            .kind = StmtKind::Block(hoisted);
444
445        Ok(())
446    }
447
448    fn lower_pattern_stmts(&mut self, ast: &mut Ast) -> Result<(), CompilerFailure> {
449        let top = std::mem::take(&mut ast.top_level);
450        ast.top_level = self.expand_list(ast, top)?;
451
452        for id in ast
453            .stmt_ids()
454            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
455        {
456            let needs = matches!(
457                ast.try_stmt(id)
458                    .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
459                    .kind,
460                StmtKind::Block(_)
461            );
462            if !needs {
463                continue;
464            }
465            let StmtKind::Block(taken) = std::mem::replace(
466                &mut ast
467                    .try_stmt_mut(id)
468                    .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
469                    .kind,
470                StmtKind::Block(Vec::new()),
471            ) else {
472                return Err(lowering_failure(
473                    "unexpected node kind during pattern lowering",
474                ));
475            };
476            let expanded = self.expand_list(ast, taken)?;
477            ast.try_stmt_mut(id)
478                .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
479                .kind = StmtKind::Block(expanded);
480        }
481
482        Ok(())
483    }
484
485    fn expand_list(
486        &mut self,
487        ast: &mut Ast,
488        ids: Vec<StmtId>,
489    ) -> Result<Vec<StmtId>, CompilerFailure> {
490        let mut out = Vec::with_capacity(ids.len());
491        for id in ids {
492            let is_pattern = matches!(
493                ast.try_stmt(id)
494                    .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
495                    .kind,
496                StmtKind::LetPattern { .. } | StmtKind::ConstPattern { .. }
497            );
498            if !is_pattern {
499                out.push(id);
500                continue;
501            }
502            let expanded = self.expand_pattern_stmt(ast, id)?;
503            out.extend(expanded);
504        }
505        Ok(out)
506    }
507
508    fn expand_pattern_stmt(
509        &mut self,
510        ast: &mut Ast,
511        id: StmtId,
512    ) -> Result<Vec<StmtId>, CompilerFailure> {
513        let span = ast
514            .try_stmt(id)
515            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
516            .span;
517        let kind = std::mem::replace(
518            &mut ast
519                .try_stmt_mut(id)
520                .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
521                .kind,
522            StmtKind::Block(Vec::new()), // sentinel; the slot is unreachable post-lowering
523        );
524        let (is_const, binding, ty, value, doc) = match kind {
525            StmtKind::LetPattern {
526                binding,
527                ty,
528                value,
529                doc,
530            } => (false, binding, ty, value, doc),
531            StmtKind::ConstPattern {
532                binding,
533                ty,
534                value,
535                doc,
536            } => (true, binding, ty, value, doc),
537            _ => {
538                return Err(lowering_failure(
539                    "unexpected node kind during pattern lowering",
540                ));
541            }
542        };
543
544        let literal = match ty {
545            None => mark_literal_source(ast, &binding, value)?,
546            Some(_) => None,
547        };
548        let pattern_span = binding.span();
549        let dst = self.fresh(PATTERN_VALUE, pattern_span)?;
550        let annotated = ty.is_some();
551
552        // temp is always `const` even for `let` patterns — it's never re-assigned
553        let dst_stmt = ast
554            .try_push_stmt(Stmt {
555                kind: StmtKind::Const {
556                    name: dst.clone(),
557                    ty,
558                    value,
559                    doc: None,
560                },
561                span,
562            })
563            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
564
565        let mut out = vec![dst_stmt];
566        let root = PatternRoot {
567            is_const,
568            annotated,
569        };
570        out.extend(self.emit_decompose(ast, binding, dst, root, doc, literal.as_ref())?);
571        Ok(out)
572    }
573
574    fn materialise_pattern_params(
575        &mut self,
576        ast: &mut Ast,
577        params: &mut [ParamDecl],
578    ) -> Result<(), CompilerFailure> {
579        for param in params.iter_mut() {
580            let Some(pattern) = param.pattern.take() else {
581                continue;
582            };
583            let fresh = self.fresh(PATTERN_PARAM, pattern.span())?;
584            param.name = fresh.clone();
585            let root = PatternRoot {
586                is_const: false,
587                annotated: param.ty.is_some(),
588            };
589            // An unannotated parameter's default is typed as a declaration's
590            // initializer is. The argument, not the default, is what the
591            // pattern takes apart: a caller or a contextual type may supply
592            // more elements or properties than the default has.
593            let literal = match (&param.ty, param.default) {
594                (None, Some(default)) => mark_literal_source(ast, &pattern, default)?,
595                _ => None,
596            };
597            if let (Some(LiteralSource::Tuple(len)), Some(default)) = (literal, param.default)
598                && let Some(pad_defaults) = defaults_past_end(&pattern, len)
599            {
600                ast.tuple_pattern_pad_defaults.insert(default, pad_defaults);
601            }
602            let statements =
603                self.emit_decompose(ast, pattern, fresh, root, /*doc=*/ None, None)?;
604            if ast
605                .parameter_bindings
606                .insert(param.name.span, statements)
607                .is_some()
608            {
609                return Err(lowering_failure(
610                    "duplicate parameter binding span during pattern lowering",
611                ));
612            }
613        }
614
615        Ok(())
616    }
617
618    /// `doc` attaches to the first emitted stmt only. `literal` describes the
619    /// literal `source` holds, if the pattern types one.
620    fn emit_decompose(
621        &mut self,
622        ast: &mut Ast,
623        binding: Binding,
624        source: Ident,
625        root: PatternRoot,
626        mut doc: Option<crate::DocComment>,
627        literal: Option<&LiteralSource>,
628    ) -> Result<Vec<StmtId>, CompilerFailure> {
629        let is_const = root.is_const;
630        let mut out = Vec::new();
631        match binding {
632            Binding::Object {
633                fields,
634                rest,
635                span: _,
636            } => {
637                let bound_names: Vec<Ident> = fields.iter().map(|f| f.source.clone()).collect();
638                for field in fields {
639                    // A defaulted property the literal lacks reads `undefined`,
640                    // as tsc reads it, unless the literal inherits it.
641                    let absent = matches!(
642                        literal,
643                        Some(LiteralSource::Object(names)) if !names.contains(&field.source.name)
644                    ) && !OBJECT_PROTOTYPE_MEMBERS
645                        .contains(&field.source.name.as_str());
646                    let access = if absent && field.default.is_some() {
647                        push_void_zero(ast, field.span)?
648                    } else {
649                        let recv =
650                            push_expr(ast, ExprKind::Identifier(source.clone()), field.span)?;
651                        push_expr(
652                            ast,
653                            ExprKind::FieldAccess {
654                                receiver: recv,
655                                name: field.source.clone(),
656                            },
657                            field.span,
658                        )?
659                    };
660                    let value = self.apply_binding_default(
661                        ast,
662                        access,
663                        field.default,
664                        root.annotated,
665                        field.span,
666                        &mut out,
667                    )?;
668                    let decl = ast
669                        .try_push_stmt(Stmt {
670                            kind: make_decl(is_const, field.local, value, doc.take()),
671                            span: field.span,
672                        })
673                        .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
674                    out.push(decl);
675                }
676                if let Some(rest_ident) = rest {
677                    let src_expr = ast
678                        .try_push_expr(Expr {
679                            kind: ExprKind::Identifier(source.clone()),
680                            span: rest_ident.span,
681                        })
682                        .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
683                    let decl = ast
684                        .try_push_stmt(Stmt {
685                            kind: StmtKind::ObjectRest {
686                                is_const,
687                                name: rest_ident.clone(),
688                                source: src_expr,
689                                exclude: bound_names,
690                                ty: None,
691                                doc: doc.take(),
692                            },
693                            span: rest_ident.span,
694                        })
695                        .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
696                    out.push(decl);
697                }
698            }
699            Binding::Array {
700                elems,
701                defaults,
702                rest,
703                span: pattern_span,
704            } => {
705                if elems.len() != defaults.len() {
706                    return Err(lowering_failure(
707                        "array pattern defaults do not match element positions",
708                    ));
709                }
710                let elems_len = elems.len();
711                for (i, (slot, default)) in elems.into_iter().zip(defaults).enumerate() {
712                    let Some(local) = slot else {
713                        if default.is_some() {
714                            return Err(lowering_failure("array pattern hole has a default value"));
715                        }
716                        continue;
717                    };
718                    // A tuple literal holds nothing past its end, so a defaulted
719                    // slot there reads `undefined`, as tsc reads it, and the
720                    // literal keeps its own length.
721                    let past_tuple_end =
722                        matches!(literal, Some(LiteralSource::Tuple(len)) if i >= *len);
723                    let access = if default.is_some() && past_tuple_end {
724                        push_void_zero(ast, local.span)?
725                    } else {
726                        let origin = PatternOrigin {
727                            pattern_span,
728                            slot_arity: elems_len,
729                            defaulted: default.is_some(),
730                        };
731                        self.array_slot_read(ast, &source, i, origin, local.span)?
732                    };
733                    let value = self.apply_binding_default(
734                        ast,
735                        access,
736                        default,
737                        root.annotated,
738                        local.span,
739                        &mut out,
740                    )?;
741                    let decl = ast
742                        .try_push_stmt(Stmt {
743                            kind: make_decl(is_const, local.clone(), value, doc.take()),
744                            span: local.span,
745                        })
746                        .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
747                    out.push(decl);
748                }
749                if let Some(rest_ident) = rest {
750                    let rest_value = match literal {
751                        Some(&LiteralSource::Tuple(len)) if len > elems_len => {
752                            remaining_elements(ast, &source, elems_len..len, rest_ident.span)?
753                        }
754                        _ => array_rest(ast, &source, elems_len, rest_ident.span)?,
755                    };
756                    let decl = ast
757                        .try_push_stmt(Stmt {
758                            kind: make_decl(is_const, rest_ident.clone(), rest_value, doc.take()),
759                            span: rest_ident.span,
760                        })
761                        .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
762                    out.push(decl);
763                }
764            }
765        }
766        Ok(out)
767    }
768
769    /// `source[index]`, tagged with `origin` so the typechecker words index
770    /// errors for the pattern. A defaulted slot's read is guarded by length.
771    fn array_slot_read(
772        &mut self,
773        ast: &mut Ast,
774        source: &Ident,
775        index: usize,
776        origin: PatternOrigin,
777        span: Span,
778    ) -> Result<ExprId, CompilerFailure> {
779        let receiver = push_expr(ast, ExprKind::Identifier(source.clone()), span)?;
780        let index = push_expr(ast, ExprKind::Number(index as f64), span)?;
781        let access = push_expr(ast, ExprKind::IndexAccess { receiver, index }, span)?;
782        let defaulted = origin.defaulted;
783        ast.pattern_origins.insert(access, origin);
784        if !defaulted {
785            return Ok(access);
786        }
787        self.guard_defaulted_array_read(ast, receiver, index, access, span)
788    }
789
790    /// A pattern default treats an absent element as undefined, while ordinary
791    /// index expressions retain their bounds checks.
792    fn guard_defaulted_array_read(
793        &mut self,
794        ast: &mut Ast,
795        receiver: ExprId,
796        index: ExprId,
797        access: ExprId,
798        span: Span,
799    ) -> Result<ExprId, CompilerFailure> {
800        let length = push_expr(
801            ast,
802            ExprKind::FieldAccess {
803                receiver,
804                name: Ident {
805                    name: "length".to_string(),
806                    span,
807                },
808            },
809            span,
810        )?;
811        let in_bounds = push_expr(
812            ast,
813            ExprKind::Binary {
814                op: crate::BinOp::Lt,
815                lhs: index,
816                rhs: length,
817            },
818            span,
819        )?;
820        let undefined = push_void_zero(ast, span)?;
821        push_expr(
822            ast,
823            ExprKind::Ternary {
824                cond: in_bounds,
825                then_: access,
826                else_: undefined,
827            },
828            span,
829        )
830    }
831
832    /// Save the property/index read once, then evaluate its default only for undefined.
833    /// `void 0` is immune to a source binding named `undefined`.
834    fn apply_binding_default(
835        &mut self,
836        ast: &mut Ast,
837        access: ExprId,
838        default: Option<ExprId>,
839        annotated: bool,
840        span: Span,
841        out: &mut Vec<StmtId>,
842    ) -> Result<ExprId, CompilerFailure> {
843        let Some(default) = default else {
844            return Ok(access);
845        };
846        let saved = self.fresh("value", span)?;
847        let declaration = ast
848            .try_push_stmt(Stmt {
849                kind: make_decl(true, saved.clone(), access, None),
850                span,
851            })
852            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?;
853        out.push(declaration);
854        let saved_value = push_expr(ast, ExprKind::Identifier(saved.clone()), span)?;
855        let undefined = push_void_zero(ast, span)?;
856        let condition = push_expr(
857            ast,
858            ExprKind::Binary {
859                op: crate::BinOp::Eq,
860                lhs: saved_value,
861                rhs: undefined,
862            },
863            span,
864        )?;
865        let selected = push_expr(
866            ast,
867            ExprKind::Ternary {
868                cond: condition,
869                then_: default,
870                else_: saved_value,
871            },
872            span,
873        )?;
874        ast.binding_defaults.insert(
875            selected,
876            crate::ast::BindingDefault {
877                saved,
878                default,
879                annotated,
880            },
881        );
882        Ok(selected)
883    }
884}
885
886/// A literal a pattern takes apart, which TypeScript types from the pattern.
887enum LiteralSource {
888    /// An array literal of this many elements, typed as a tuple.
889    Tuple(usize),
890    /// An object literal with these property names.
891    Object(BTreeSet<String>),
892}
893
894/// Describe the literal `value` that `binding` takes apart, and record how
895/// inference types it: an array literal as a tuple, and an object literal
896/// with each defaulted property the pattern reads optional, as tsc types it.
897fn mark_literal_source(
898    ast: &mut Ast,
899    binding: &Binding,
900    value: ExprId,
901) -> Result<Option<LiteralSource>, CompilerFailure> {
902    if let Some(len) = tuple_source_len(ast, binding, value)? {
903        ast.tuple_pattern_sources.insert(value);
904        return Ok(Some(LiteralSource::Tuple(len)));
905    }
906    if let Binding::Object { fields, .. } = binding
907        && let Some(names) = object_literal_names(ast, value)?
908    {
909        let defaulted: BTreeSet<String> = fields
910            .iter()
911            .filter(|field| field.default.is_some() && names.contains(&field.source.name))
912            .map(|field| field.source.name.clone())
913            .collect();
914        if !defaulted.is_empty() {
915            ast.object_pattern_sources.insert(value, defaulted);
916        }
917        return Ok(Some(LiteralSource::Object(names)));
918    }
919    mark_operand_tuple_sources(ast, binding, value)?;
920    Ok(None)
921}
922
923/// The properties every object literal inherits from `Object.prototype`.
924pub(crate) const OBJECT_PROTOTYPE_MEMBERS: &[&str] = &[
925    "constructor",
926    "hasOwnProperty",
927    "isPrototypeOf",
928    "propertyIsEnumerable",
929    "toLocaleString",
930    "toString",
931    "valueOf",
932    "__proto__",
933    "__defineGetter__",
934    "__defineSetter__",
935    "__lookupGetter__",
936    "__lookupSetter__",
937];
938
939/// The defaults of an array pattern's slots from `len` on, when each is a
940/// named slot with a default.
941fn defaults_past_end(binding: &Binding, len: usize) -> Option<Vec<ExprId>> {
942    let Binding::Array {
943        elems, defaults, ..
944    } = binding
945    else {
946        return None;
947    };
948    let past: Vec<ExprId> = elems
949        .iter()
950        .zip(defaults)
951        .skip(len)
952        .map(|(slot, default)| slot.is_some().then_some(*default).flatten())
953        .collect::<Option<_>>()?;
954    (!past.is_empty()).then_some(past)
955}
956
957/// An array literal the pattern may take apart as one operand of `value`, a
958/// fallback (`x ?? []`, `x || []`) or a conditional's branch, is a tuple as in
959/// tsc, where the pattern is the context of every such operand.
960fn mark_operand_tuple_sources(
961    ast: &mut Ast,
962    binding: &Binding,
963    value: ExprId,
964) -> Result<(), CompilerFailure> {
965    let operands = match &ast
966        .try_expr(value)
967        .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
968        .kind
969    {
970        ExprKind::Paren(inner) => vec![*inner],
971        ExprKind::Binary {
972            op: crate::BinOp::NullishCoalesce | crate::BinOp::Or,
973            rhs,
974            ..
975        } => vec![*rhs],
976        ExprKind::Ternary { then_, else_, .. } => vec![*then_, *else_],
977        _ => return Ok(()),
978    };
979    for operand in operands {
980        if tuple_source_len(ast, binding, operand)?.is_some() {
981            ast.tuple_pattern_sources.insert(operand);
982        } else {
983            mark_operand_tuple_sources(ast, binding, operand)?;
984        }
985    }
986    Ok(())
987}
988
989/// The property names of `value` when it is an object literal whose every
990/// property is named.
991fn object_literal_names(
992    ast: &Ast,
993    value: ExprId,
994) -> Result<Option<BTreeSet<String>>, CompilerFailure> {
995    let ExprKind::ObjectLiteral { members } = &ast
996        .try_expr(value)
997        .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
998        .kind
999    else {
1000        return Ok(None);
1001    };
1002    let mut names = BTreeSet::new();
1003    for member in members {
1004        let crate::ObjectLiteralMember::Field(field) = member else {
1005            return Ok(None);
1006        };
1007        names.insert(field.name.name.clone());
1008    }
1009    Ok(Some(names))
1010}
1011
1012/// The length of `value` when it is an array literal without spreads that
1013/// `binding`, an array pattern, takes apart slot by slot, so TypeScript types
1014/// it as a tuple. A pattern of only a rest element takes the literal whole, as
1015/// an array.
1016fn tuple_source_len(
1017    ast: &Ast,
1018    binding: &Binding,
1019    value: ExprId,
1020) -> Result<Option<usize>, CompilerFailure> {
1021    let Binding::Array { elems, .. } = binding else {
1022        return Ok(None);
1023    };
1024    if elems.is_empty() {
1025        return Ok(None);
1026    }
1027    let ExprKind::ArrayLiteral { elements } = &ast
1028        .try_expr(value)
1029        .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
1030        .kind
1031    else {
1032        return Ok(None);
1033    };
1034    let has_spread = elements
1035        .iter()
1036        .any(|element| matches!(element, crate::ArrayLiteralElement::Spread { .. }));
1037    Ok((!has_spread).then_some(elements.len()))
1038}
1039
1040/// `source.slice(from, source.length)`: the elements an array pattern's rest
1041/// element collects.
1042fn array_rest(
1043    ast: &mut Ast,
1044    source: &Ident,
1045    from: usize,
1046    span: Span,
1047) -> Result<ExprId, CompilerFailure> {
1048    let from_lit = push_expr(ast, ExprKind::Number(from as f64), span)?;
1049    let slice_receiver = push_expr(ast, ExprKind::Identifier(source.clone()), span)?;
1050    let slice_callee = push_expr(
1051        ast,
1052        ExprKind::FieldAccess {
1053            receiver: slice_receiver,
1054            name: Ident {
1055                name: "slice".to_string(),
1056                span,
1057            },
1058        },
1059        span,
1060    )?;
1061    let length_receiver = push_expr(ast, ExprKind::Identifier(source.clone()), span)?;
1062    let length_access = push_expr(
1063        ast,
1064        ExprKind::FieldAccess {
1065            receiver: length_receiver,
1066            name: Ident {
1067                name: "length".to_string(),
1068                span,
1069            },
1070        },
1071        span,
1072    )?;
1073    let slice_call = push_expr(
1074        ast,
1075        ExprKind::Call {
1076            callee: slice_callee,
1077            type_args: None,
1078            args: vec![from_lit, length_access],
1079        },
1080        span,
1081    )?;
1082    Ok(slice_call)
1083}
1084
1085/// `[source[from], …, source[to - 1]].slice(0, to - from)`: the rest of a
1086/// tuple-typed literal. The remaining elements form a tuple of their own, and
1087/// slicing it yields an array of their types: one that can grow, where
1088/// TypeScript types the rest as a tuple.
1089fn remaining_elements(
1090    ast: &mut Ast,
1091    source: &Ident,
1092    indices: std::ops::Range<usize>,
1093    span: Span,
1094) -> Result<ExprId, CompilerFailure> {
1095    let count = indices.len();
1096    let mut elements = Vec::with_capacity(count);
1097    for i in indices {
1098        let receiver = push_expr(ast, ExprKind::Identifier(source.clone()), span)?;
1099        let index = push_expr(ast, ExprKind::Number(i as f64), span)?;
1100        let access = push_expr(ast, ExprKind::IndexAccess { receiver, index }, span)?;
1101        elements.push(crate::ArrayLiteralElement::Value(access));
1102    }
1103    let remaining = push_expr(ast, ExprKind::ArrayLiteral { elements }, span)?;
1104    ast.tuple_pattern_sources.insert(remaining);
1105    let slice = push_expr(
1106        ast,
1107        ExprKind::FieldAccess {
1108            receiver: remaining,
1109            name: Ident {
1110                name: "slice".to_string(),
1111                span,
1112            },
1113        },
1114        span,
1115    )?;
1116    let from = push_expr(ast, ExprKind::Number(0.0), span)?;
1117    let to = push_expr(ast, ExprKind::Number(count as f64), span)?;
1118    push_expr(
1119        ast,
1120        ExprKind::Call {
1121            callee: slice,
1122            type_args: None,
1123            args: vec![from, to],
1124        },
1125        span,
1126    )
1127}
1128
1129fn push_expr(ast: &mut Ast, kind: ExprKind, span: Span) -> Result<ExprId, CompilerFailure> {
1130    ast.try_push_expr(Expr { kind, span })
1131        .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))
1132}
1133
1134/// `void 0`, which is `undefined` even where a source binding shadows the name.
1135fn push_void_zero(ast: &mut Ast, span: Span) -> Result<ExprId, CompilerFailure> {
1136    let zero = push_expr(ast, ExprKind::Number(0.0), span)?;
1137    push_expr(ast, ExprKind::Void { operand: zero }, span)
1138}
1139
1140fn make_decl(
1141    is_const: bool,
1142    name: Ident,
1143    value: ExprId,
1144    doc: Option<crate::DocComment>,
1145) -> StmtKind {
1146    if is_const {
1147        StmtKind::Const {
1148            name,
1149            ty: None,
1150            value,
1151            doc,
1152        }
1153    } else {
1154        StmtKind::Let {
1155            name,
1156            ty: None,
1157            value,
1158            doc,
1159        }
1160    }
1161}
1162
1163/// The names the `let` statements among `stmts` declare.
1164fn let_names(ast: &Ast, stmts: &[StmtId]) -> Result<Vec<Ident>, CompilerFailure> {
1165    let mut names = Vec::new();
1166    for &stmt in stmts {
1167        if let StmtKind::Let { name, .. } = &ast
1168            .try_stmt(stmt)
1169            .map_err(|error| error.into_compiler_failure(CompilerStage::Infer))?
1170            .kind
1171        {
1172            names.push(name.clone());
1173        }
1174    }
1175    Ok(names)
1176}
1177
1178fn lowering_failure(message: &str) -> CompilerFailure {
1179    CompilerFailure::Internal {
1180        stage: CompilerStage::Infer,
1181        span: None,
1182        message: message.into(),
1183    }
1184}
1185
1186#[cfg(test)]
1187mod tests {
1188    use super::lower;
1189    use crate::{Asi, Binding, Stmt, StmtKind, Token, TokenKind, parse};
1190
1191    fn pipeline(source: &str) -> crate::Ast {
1192        let mut asi = Asi::new(source, crate::FileId(0));
1193        let mut tokens: Vec<Token> = Vec::new();
1194        loop {
1195            let t = asi.next_token();
1196            let is_eof = matches!(t.kind, TokenKind::Eof);
1197            tokens.push(t);
1198            if is_eof {
1199                break;
1200            }
1201        }
1202        let _ = asi.into_diagnostics();
1203        let (mut ast, diags) = parse(source, tokens, crate::FileId(0));
1204        assert!(diags.is_empty(), "unexpected parse diags: {diags:?}");
1205        ast = lower(ast).unwrap();
1206        ast
1207    }
1208
1209    fn no_patterns_left(ast: &crate::Ast) {
1210        for i in 0..ast.stmts_len() {
1211            let s = ast.try_stmt(crate::StmtId(i as u32)).unwrap();
1212            assert!(
1213                !matches!(
1214                    s.kind,
1215                    StmtKind::LetPattern { .. }
1216                        | StmtKind::ConstPattern { .. }
1217                        | StmtKind::ForOfPattern { .. }
1218                ),
1219                "found unlowered pattern at stmt #{i}: {:?}",
1220                s.kind,
1221            );
1222            if let StmtKind::Function { params, .. } = &s.kind {
1223                for (pi, p) in params.iter().enumerate() {
1224                    assert!(
1225                        p.pattern.is_none(),
1226                        "function param #{pi} still has a pattern after lowering",
1227                    );
1228                }
1229            }
1230        }
1231        for i in 0..ast.exprs_len() {
1232            let e = ast.try_expr(crate::ExprId(i as u32)).unwrap();
1233            if let crate::ExprKind::Arrow { params, .. } = &e.kind {
1234                for (pi, p) in params.iter().enumerate() {
1235                    assert!(
1236                        p.pattern.is_none(),
1237                        "arrow param #{pi} still has a pattern after lowering",
1238                    );
1239                }
1240            }
1241        }
1242    }
1243
1244    #[test]
1245    fn lowers_simple_object_destructure() {
1246        let ast = pipeline("const { a, b } = obj;");
1247        no_patterns_left(&ast);
1248        // top_level: __dst, a-decl, b-decl
1249        assert_eq!(ast.top_level.len(), 3);
1250        let names: Vec<String> = ast
1251            .top_level
1252            .iter()
1253            .map(|id| match &ast.try_stmt(*id).unwrap().kind {
1254                StmtKind::Const { name, .. } => name.name.clone(),
1255                other => panic!("expected Const, got {other:?}"),
1256            })
1257            .collect();
1258        assert!(names[0].starts_with("#pattern_dst_"));
1259        assert_eq!(names[1], "a");
1260        assert_eq!(names[2], "b");
1261    }
1262
1263    #[test]
1264    fn lowers_renamed_object_destructure() {
1265        let ast = pipeline("const { a: x, b: y } = obj;");
1266        no_patterns_left(&ast);
1267        let names: Vec<String> = ast
1268            .top_level
1269            .iter()
1270            .map(|id| match &ast.try_stmt(*id).unwrap().kind {
1271                StmtKind::Const { name, .. } => name.name.clone(),
1272                other => panic!("expected Const, got {other:?}"),
1273            })
1274            .collect();
1275        assert_eq!(names[1], "x");
1276        assert_eq!(names[2], "y");
1277    }
1278
1279    #[test]
1280    fn lowers_array_destructure() {
1281        let ast = pipeline("const [x, y] = arr;");
1282        no_patterns_left(&ast);
1283        assert_eq!(ast.top_level.len(), 3);
1284        if let StmtKind::Const { value, .. } = &ast.try_stmt(ast.top_level[1]).unwrap().kind {
1285            assert!(matches!(
1286                ast.try_expr(*value).unwrap().kind,
1287                crate::ExprKind::IndexAccess { .. }
1288            ));
1289        }
1290    }
1291
1292    #[test]
1293    fn lowers_array_with_hole_and_rest() {
1294        let ast = pipeline("const [, x, ...rest] = arr;");
1295        no_patterns_left(&ast);
1296        // __dst, x (= arr[1]), rest (= arr.slice(2))
1297        assert_eq!(ast.top_level.len(), 3);
1298        if let StmtKind::Const { name, value, .. } = &ast.try_stmt(ast.top_level[1]).unwrap().kind {
1299            assert_eq!(name.name, "x");
1300            if let crate::ExprKind::IndexAccess { index, .. } = &ast.try_expr(*value).unwrap().kind
1301            {
1302                assert_eq!(
1303                    ast.try_expr(*index).unwrap().kind,
1304                    crate::ExprKind::Number(1.0)
1305                );
1306            } else {
1307                panic!("expected IndexAccess");
1308            }
1309        }
1310        if let StmtKind::Const { name, value, .. } = &ast.try_stmt(ast.top_level[2]).unwrap().kind {
1311            assert_eq!(name.name, "rest");
1312            match &ast.try_expr(*value).unwrap().kind {
1313                crate::ExprKind::Call { callee, args, .. } => {
1314                    assert_eq!(args.len(), 2, "slice takes (start, end)");
1315                    assert_eq!(
1316                        ast.try_expr(args[0]).unwrap().kind,
1317                        crate::ExprKind::Number(2.0)
1318                    );
1319                    match &ast.try_expr(*callee).unwrap().kind {
1320                        crate::ExprKind::FieldAccess { name, .. } => {
1321                            assert_eq!(name.name, "slice");
1322                        }
1323                        other => panic!("expected slice FieldAccess, got {other:?}"),
1324                    }
1325                }
1326                other => panic!("expected Call, got {other:?}"),
1327            }
1328        }
1329    }
1330
1331    #[test]
1332    fn lowers_object_rest_to_object_rest() {
1333        let ast = pipeline("const { a, ...rest } = obj;");
1334        no_patterns_left(&ast);
1335        assert_eq!(ast.top_level.len(), 3);
1336        match &ast.try_stmt(ast.top_level[2]).unwrap().kind {
1337            StmtKind::ObjectRest {
1338                is_const,
1339                name,
1340                exclude,
1341                ..
1342            } => {
1343                assert!(*is_const);
1344                assert_eq!(name.name, "rest");
1345                assert_eq!(exclude.len(), 1);
1346                assert_eq!(exclude[0].name, "a");
1347            }
1348            other => panic!("expected ObjectRest, got {other:?}"),
1349        }
1350    }
1351
1352    #[test]
1353    fn lowers_function_param_pattern() {
1354        let src = "function f({ a, b }: T): number { return a + b; }";
1355        let ast = pipeline(src);
1356        no_patterns_left(&ast);
1357        let Stmt {
1358            kind: StmtKind::Function { body, params, .. },
1359            ..
1360        } = ast.try_stmt(ast.top_level[0]).unwrap().clone()
1361        else {
1362            panic!("expected Function");
1363        };
1364        assert_eq!(params.len(), 1);
1365        assert!(params[0].pattern.is_none());
1366        assert!(params[0].name.name.starts_with("#pattern_p_"));
1367        let body_stmts = match &ast.try_stmt(body).unwrap().kind {
1368            StmtKind::Block(s) => s.clone(),
1369            _ => panic!("body is not a Block"),
1370        };
1371        assert_eq!(body_stmts.len(), 1);
1372        let bindings = ast.parameter_bindings.get(&params[0].name.span).unwrap();
1373        assert_eq!(bindings.len(), 2);
1374        if let StmtKind::Let { name, .. } = &ast.try_stmt(bindings[0]).unwrap().kind {
1375            assert_eq!(name.name, "a");
1376        } else {
1377            panic!("expected Let a");
1378        }
1379        if let StmtKind::Let { name, .. } = &ast.try_stmt(bindings[1]).unwrap().kind {
1380            assert_eq!(name.name, "b");
1381        } else {
1382            panic!("expected Let b");
1383        }
1384        assert!(matches!(
1385            ast.try_stmt(body_stmts[0]).unwrap().kind,
1386            StmtKind::Return(_)
1387        ));
1388    }
1389
1390    #[test]
1391    fn parameter_object_rest_has_a_mutable_authored_binding() {
1392        let ast = pipeline("function f({ head, ...tail }: T): number { return tail.value; }");
1393        let StmtKind::Function { params, .. } = &ast.try_stmt(ast.top_level[0]).unwrap().kind
1394        else {
1395            panic!("expected function");
1396        };
1397        let bindings = ast.parameter_bindings.get(&params[0].name.span).unwrap();
1398        assert_eq!(bindings.len(), 2);
1399        let StmtKind::ObjectRest { is_const, name, .. } = &ast.try_stmt(bindings[1]).unwrap().kind
1400        else {
1401            panic!("expected object rest binding");
1402        };
1403        assert!(!*is_const);
1404        assert_eq!(name.name, "tail");
1405    }
1406
1407    #[test]
1408    fn lowers_arrow_expr_body_with_pattern() {
1409        let src = "const f = ({a, b}: T): number => a + b;";
1410        let ast = pipeline(src);
1411        no_patterns_left(&ast);
1412        let mut found = false;
1413        for i in 0..ast.exprs_len() {
1414            let e = ast.try_expr(crate::ExprId(i as u32)).unwrap();
1415            if let crate::ExprKind::Arrow { params, body, .. } = &e.kind {
1416                assert!(matches!(body, crate::ArrowBody::Expr(_)));
1417                assert_eq!(
1418                    ast.parameter_bindings
1419                        .get(&params[0].name.span)
1420                        .unwrap()
1421                        .len(),
1422                    2
1423                );
1424                found = true;
1425            }
1426        }
1427        assert!(found, "no Arrow found in expr arena");
1428    }
1429
1430    #[test]
1431    fn lowers_for_of_array_pattern() {
1432        // `for (const [a, b] of pairs) { … }` becomes
1433        // `for (const #pattern_dst_N of pairs) { const a = #pattern_dst_N[0];
1434        // const b = #pattern_dst_N[1]; { ... } }` after lowering.
1435        let src = "function main(): void { for (const [a, b] of pairs) { use(a, b); } }";
1436        let ast = pipeline(src);
1437        no_patterns_left(&ast);
1438
1439        let Stmt {
1440            kind: StmtKind::Function { body, .. },
1441            ..
1442        } = ast.try_stmt(ast.top_level[0]).unwrap().clone()
1443        else {
1444            panic!("expected Function");
1445        };
1446        let body_stmts = match &ast.try_stmt(body).unwrap().kind {
1447            StmtKind::Block(s) => s.clone(),
1448            _ => panic!("function body is not a Block"),
1449        };
1450        assert_eq!(body_stmts.len(), 1, "main body has just the for-of");
1451
1452        let (loop_name, loop_body) = match &ast.try_stmt(body_stmts[0]).unwrap().kind {
1453            StmtKind::ForOf { name, body, .. } => (name.name.clone(), *body),
1454            other => panic!("expected ForOf, got {other:?}"),
1455        };
1456        assert!(
1457            loop_name.starts_with("#pattern_dst_"),
1458            "loop var was rewritten to a synth temp; got {loop_name}",
1459        );
1460
1461        let loop_body_stmts = match &ast.try_stmt(loop_body).unwrap().kind {
1462            StmtKind::Block(s) => s.clone(),
1463            _ => panic!("for-of body is not a Block"),
1464        };
1465        assert_eq!(loop_body_stmts.len(), 3);
1466        if let StmtKind::Const { name, value, .. } = &ast.try_stmt(loop_body_stmts[0]).unwrap().kind
1467        {
1468            assert_eq!(name.name, "a");
1469            assert!(matches!(
1470                ast.try_expr(*value).unwrap().kind,
1471                crate::ExprKind::IndexAccess { .. }
1472            ));
1473            // Origin-tagged so the typechecker can rephrase index errors.
1474            assert!(ast.pattern_origins.contains_key(value));
1475        } else {
1476            panic!("expected Const a");
1477        }
1478        if let StmtKind::Const { name, .. } = &ast.try_stmt(loop_body_stmts[1]).unwrap().kind {
1479            assert_eq!(name.name, "b");
1480        } else {
1481            panic!("expected Const b");
1482        }
1483    }
1484
1485    #[test]
1486    fn lowers_let_pattern_preserves_mutability() {
1487        // __dst is const; per-field bindings follow source kind (`let` stays `let`)
1488        let ast = pipeline("let { a } = obj;");
1489        no_patterns_left(&ast);
1490        // __dst is Const; `a` is Let.
1491        assert!(matches!(
1492            ast.try_stmt(ast.top_level[0]).unwrap().kind,
1493            StmtKind::Const { .. }
1494        ));
1495        assert!(matches!(
1496            ast.try_stmt(ast.top_level[1]).unwrap().kind,
1497            StmtKind::Let { .. }
1498        ));
1499    }
1500
1501    #[test]
1502    fn no_op_on_plain_program() {
1503        let src = "const x = 1; function main(): void { }";
1504        let before = {
1505            let mut asi = Asi::new(src, crate::FileId(0));
1506            let mut tokens: Vec<Token> = Vec::new();
1507            loop {
1508                let t = asi.next_token();
1509                let is_eof = matches!(t.kind, TokenKind::Eof);
1510                tokens.push(t);
1511                if is_eof {
1512                    break;
1513                }
1514            }
1515            let _ = asi.into_diagnostics();
1516            let (ast, _) = parse(src, tokens, crate::FileId(0));
1517            ast
1518        };
1519        let after = pipeline(src);
1520        assert_eq!(format!("{before:?}"), format!("{after:?}"));
1521    }
1522
1523    #[test]
1524    fn lowers_pattern_in_nested_block() {
1525        let src = "function main(): void { const { a } = obj; }";
1526        let ast = pipeline(src);
1527        no_patterns_left(&ast);
1528    }
1529
1530    #[test]
1531    fn idempotent() {
1532        let src = "const { a, b } = obj;";
1533        let mut ast1 = pipeline(src);
1534        let snapshot = format!("{ast1:?}");
1535        ast1 = lower(ast1).unwrap();
1536        assert_eq!(snapshot, format!("{ast1:?}"));
1537    }
1538
1539    #[test]
1540    fn binding_span_preserved() {
1541        let (ast, _) = parse(
1542            "const { a } = obj;",
1543            tokens_of("const { a } = obj;"),
1544            crate::FileId(0),
1545        );
1546        match &ast.try_stmt(ast.top_level[0]).unwrap().kind {
1547            crate::StmtKind::ConstPattern {
1548                binding: Binding::Object { span, .. },
1549                ..
1550            } => {
1551                // `{ a }` starts at offset 6 and ends at 11 (inclusive of `}`).
1552                assert_eq!(*span, crate::Span::new(crate::FileId(0), 6, 11).unwrap());
1553            }
1554            other => panic!("expected ConstPattern Object, got {other:?}"),
1555        }
1556    }
1557
1558    #[test]
1559    fn defaults_save_each_access_once_and_test_undefined() {
1560        for source in [
1561            "const { a = fallback() } = source();",
1562            "const [a = fallback()] = source();",
1563        ] {
1564            let ast = pipeline(source);
1565            assert_eq!(ast.top_level.len(), 3);
1566            let StmtKind::Const {
1567                value: saved_access,
1568                ..
1569            } = ast.try_stmt(ast.top_level[1]).unwrap().kind
1570            else {
1571                panic!("expected saved read")
1572            };
1573            assert!(matches!(
1574                ast.try_expr(saved_access).unwrap().kind,
1575                crate::ExprKind::FieldAccess { .. } | crate::ExprKind::Ternary { .. }
1576            ));
1577            let StmtKind::Const { value, .. } = ast.try_stmt(ast.top_level[2]).unwrap().kind else {
1578                panic!("expected binding")
1579            };
1580            let crate::ExprKind::Ternary { cond, then_, else_ } = ast.try_expr(value).unwrap().kind
1581            else {
1582                panic!("expected lazy default")
1583            };
1584            let crate::ExprKind::Binary {
1585                op: crate::BinOp::Eq,
1586                lhs,
1587                rhs,
1588            } = ast.try_expr(cond).unwrap().kind
1589            else {
1590                panic!("expected undefined comparison")
1591            };
1592            assert_eq!(lhs, else_);
1593            assert!(matches!(
1594                ast.try_expr(rhs).unwrap().kind,
1595                crate::ExprKind::Void { .. }
1596            ));
1597            assert!(matches!(
1598                ast.try_expr(then_).unwrap().kind,
1599                crate::ExprKind::Call { .. }
1600            ));
1601        }
1602    }
1603
1604    #[test]
1605    fn array_defaults_keep_original_indices_and_origin_metadata() {
1606        let ast = pipeline("const [, a = 1] = values;");
1607        let StmtKind::Const { value, .. } = ast.try_stmt(ast.top_level[1]).unwrap().kind else {
1608            panic!("expected saved index read")
1609        };
1610        let crate::ExprKind::Ternary {
1611            cond,
1612            then_: access,
1613            else_,
1614        } = ast.try_expr(value).unwrap().kind
1615        else {
1616            panic!("expected bounds guard")
1617        };
1618        assert!(matches!(
1619            ast.try_expr(cond).unwrap().kind,
1620            crate::ExprKind::Binary {
1621                op: crate::BinOp::Lt,
1622                ..
1623            }
1624        ));
1625        assert!(matches!(
1626            ast.try_expr(else_).unwrap().kind,
1627            crate::ExprKind::Void { .. }
1628        ));
1629        let crate::ExprKind::IndexAccess { index, .. } = ast.try_expr(access).unwrap().kind else {
1630            panic!("expected guarded index")
1631        };
1632        assert!(matches!(
1633            ast.try_expr(index).unwrap().kind,
1634            crate::ExprKind::Number(1.0)
1635        ));
1636        assert_eq!(ast.pattern_origins.get(&access).unwrap().slot_arity, 2);
1637    }
1638
1639    #[test]
1640    fn lowers_class_method_parameter_defaults() {
1641        let ast = pipeline("class C { method({ x = 1 }: T): number { return x; } }");
1642        let StmtKind::ClassDecl { ref members, .. } = ast.try_stmt(ast.top_level[0]).unwrap().kind
1643        else {
1644            panic!("expected class")
1645        };
1646        let crate::ClassMember::Method { params, body, .. } = &members[0] else {
1647            panic!("expected method")
1648        };
1649        assert!(params[0].pattern.is_none());
1650        let StmtKind::Block(ref statements) = ast.try_stmt(*body).unwrap().kind else {
1651            panic!("expected block")
1652        };
1653        assert_eq!(statements.len(), 1);
1654        assert_eq!(
1655            ast.parameter_bindings
1656                .get(&params[0].name.span)
1657                .unwrap()
1658                .len(),
1659            2
1660        );
1661    }
1662
1663    fn tokens_of(source: &str) -> Vec<Token> {
1664        let mut asi = Asi::new(source, crate::FileId(0));
1665        let mut tokens = Vec::new();
1666        loop {
1667            let t = asi.next_token();
1668            let is_eof = matches!(t.kind, TokenKind::Eof);
1669            tokens.push(t);
1670            if is_eof {
1671                break;
1672            }
1673        }
1674        let _ = asi.into_diagnostics();
1675        tokens
1676    }
1677}