Skip to main content

submilli_engine/
typed_ast.rs

1use std::collections::BTreeMap;
2
3use crate::arena::{self, ArenaError, ArenaKind};
4use crate::typechecker::infer::narrowing::{CastInfo, ReferencePath};
5use crate::{BinOp, BindingKind, ExprId, Ident, MangledName, Span, StmtId, Type, UnOp};
6
7#[derive(Clone, Debug, PartialEq)]
8pub struct TypedExpr {
9    pub kind: TypedExprKind,
10    pub span: Span,
11    pub ty: Type,
12}
13
14#[derive(Clone, Debug, PartialEq)]
15pub enum TypedExprKind {
16    Number(f64),
17    /// bigint literal — no `n` suffix, no sign; negatives are `Unary { Neg, … }`.
18    BigInt(String),
19    String(String),
20    Boolean(bool),
21    Null,
22    Undefined,
23    /// `this` inside a class method or constructor body. A leaf; `TypedExpr.ty`
24    /// carries the enclosing class's `Type::ClassRef`. Codegen lowers it to
25    /// `local.get <this-slot>` (the receiver param for methods, the allocated
26    /// instance local for constructors).
27    This,
28    Regex {
29        source: String,
30        flags: String,
31    },
32    LocalRef {
33        ident: Ident,
34        boxed: bool,
35    },
36    /// Narrowed shadow of a `LocalRef`/`GlobalRef`. `binding` holds the synthetic
37    /// `#narrow_<N>` ident so tooling can recover `path` without string-matching the
38    /// `#` prefix. No `boxed` field — shadows are region-scoped and never mutated.
39    LocalNarrowRef {
40        binding: Ident,
41        path: crate::typechecker::infer::narrowing::ReferencePath,
42    },
43    GlobalRef {
44        mangled: MangledName,
45        name: Ident,
46    },
47    /// Distinct from `GlobalRef` — codegen lowers to a closure adapter, not `global.get`.
48    FunctionRef {
49        mangled: MangledName,
50        name: Ident,
51    },
52    /// Evaluate `effect`, discard its value, then yield `result`.
53    ///
54    /// The `void` operator lowers to this with an undefined result. It also
55    /// lets a fold that decides an expression's *value* statically keep the
56    /// computation that produced the operand: `typeof f() ===
57    /// "number"` has a constant answer when `f`'s return type decides the tag,
58    /// but JS evaluates the operand either way.
59    EffectThen {
60        effect: ExprId,
61        result: ExprId,
62    },
63    /// Run `stmts` in order, then yield `result`. An assignment used as a value
64    /// lowers to this: its assignment statement, then a read of what it assigned.
65    /// `stmts` declares no binding that outlives the expression.
66    Sequence {
67        stmts: Vec<StmtId>,
68        result: ExprId,
69    },
70    Binary {
71        op: BinOp,
72        lhs: ExprId,
73        rhs: ExprId,
74    },
75    Unary {
76        op: UnOp,
77        operand: ExprId,
78    },
79    /// Static dispatch. Closure-through-value invocation goes through `CallClosure` instead.
80    Call {
81        mangled: MangledName,
82        args: Vec<ExprId>,
83        /// Type guard predicate from callee's signature; codegen ignores it.
84        type_predicate: Option<Box<crate::TypePredicate>>,
85    },
86    /// `call_ref` dispatch; distinct from `Call` (direct `call` instruction).
87    CallClosure {
88        callee: ExprId,
89        args: Vec<ExprId>,
90    },
91    /// `@mcp/<server>.<tool>(args)` or `@tools.<tool>(args)`: dispatched through
92    /// the package's single host fn (`submilli:mcp.call`, `submilli:tools.call`)
93    /// rather than a per-tool import. Carries the target and tool names directly,
94    /// so codegen needs no mangled-name parsing. Known-return calls are wrapped in
95    /// a normal `Cast` by typecheck.
96    VirtualCall {
97        target: crate::VirtualDispatch,
98        tool: String,
99        args: Vec<ExprId>,
100    },
101    /// Callee signature has bare `Type::TypeVar` slots. Per-position `is_generic` flags
102    /// and `return_cast` drive box/cast at the call boundary. Always statically dispatched
103    /// (no generic closures).
104    GenericCall {
105        mangled: MangledName,
106        type_args: Vec<Type>,
107        args: Vec<GenericArgument>,
108        /// `Some(T)` when the unsubstituted return is a bare `TypeVar`; codegen emits a
109        /// cast to materialize the call-site type. `None` when return is already concrete.
110        return_cast: Option<crate::Type>,
111        type_predicate: Option<Box<crate::TypePredicate>>,
112    },
113    /// Intrinsic recognised by name in the inferer; bypasses the normal `Call` path.
114    IntrinsicCall {
115        kind: Intrinsic,
116        args: Vec<ExprId>,
117    },
118    /// Interface method dispatch. Codegen composes `<iface>#<name>` to find the
119    /// direct-dispatch thunk, falling back to vtable dispatch.
120    MethodCall {
121        receiver: ExprId,
122        iface: MangledName,
123        name: Ident,
124        args: Vec<ExprId>,
125        type_predicate: Option<Box<crate::TypePredicate>>,
126    },
127    /// `super(...)` in a subclass constructor: a direct call of the parent's
128    /// constructor *init* fn on the current `this` (self-first ABI). Codegen
129    /// pushes `this` then the args. Type is `Void`.
130    SuperCtorCall {
131        parent: MangledName,
132        args: Vec<ExprId>,
133    },
134    /// `super.method(...)`: a direct call of the parent body that *declares* the
135    /// method (`owner`), skipping vtable dispatch so it doesn't re-resolve to the
136    /// override. Codegen pushes `this` then the args.
137    SuperMethodCall {
138        owner: MangledName,
139        name: Ident,
140        args: Vec<ExprId>,
141    },
142    /// `MethodSig` has a bare `TypeVar` in args or return. HOF methods (`map`, `filter`, …)
143    /// with `TypeVar` only inside composites stay as plain `MethodCall`. Variadic trailing
144    /// args are pre-packed by the typechecker, so `args.len()` always matches `params.len()` 1:1.
145    GenericMethodCall {
146        receiver: ExprId,
147        iface: MangledName,
148        name: Ident,
149        /// The method's own type arguments, for a runtime-generic host method
150        /// (`Response#json<T>`) whose call passes their descriptors in a
151        /// trailing `$types` environment; empty for every other method.
152        type_args: Vec<Type>,
153        args: Vec<GenericArgument>,
154        return_cast: Option<crate::Type>,
155        /// Guard predicate after type-arg substitution; codegen ignores it.
156        type_predicate: Option<Box<crate::TypePredicate>>,
157    },
158    /// `fields` records the static shape in canonical order. `members` retains
159    /// source evaluation order. In a spread literal, ordinary properties become
160    /// singleton object sources so each source can be copied immediately.
161    ObjectLiteral {
162        /// Every expression the literal evaluates, in source order, as JavaScript
163        /// evaluates them. A value a later member overwrites is still here: it is
164        /// evaluated for its effects and then discarded. Each field's source
165        /// refers to one of these.
166        members: Vec<TypedObjectMember>,
167        fields: Vec<TypedObjectFieldOrigin>,
168    },
169    /// `element_ty` lets codegen pick the wrapper struct's element type without re-running inference.
170    ArrayLiteral {
171        elements: Vec<TypedArrayElement>,
172        element_ty: Type,
173    },
174    /// Same source syntax as `ArrayLiteral`; emitted when the expected type is `Type::Tuple`.
175    /// `element_types` drives per-slot boxing in codegen.
176    TupleLiteral {
177        elements: Vec<ExprId>,
178        element_types: Vec<Type>,
179    },
180    FieldAccess {
181        receiver: ExprId,
182        name: Ident,
183    },
184    /// Distinct from `FieldAccess` — codegen composes `<iface>#<name>` rather than a struct field offset.
185    InterfacePropertyAccess {
186        receiver: ExprId,
187        iface: MangledName,
188        name: Ident,
189    },
190    /// Enum name is a compile-time namespace with no runtime representation; `value` is
191    /// stored inline so codegen emits `i32.const N`.
192    NumberEnumMember {
193        enum_mangled: MangledName,
194        variant: Ident,
195        value: f64,
196    },
197    /// Codegen emits a string-pool reference rather than `i32.const`.
198    StringEnumMember {
199        enum_mangled: MangledName,
200        variant: Ident,
201        value: String,
202    },
203    IndexAccess {
204        receiver: ExprId,
205        index: ExprId,
206    },
207    /// `captured` is empty until the capture pass runs; codegen reads entries in order as env struct slots.
208    Closure {
209        runtime_generics: Vec<String>,
210        params: Vec<TypedParam>,
211        return_type: Type,
212        body: ClosureBody,
213        captured: Vec<CapturedVar>,
214    },
215    /// `typeof x === "<tag>"` check. Distinct from `Is` so primitive type-test codegen
216    /// stays free of vtable/shape-disjunction logic (`Object` and `Function` need both).
217    TypeofTag {
218        value: ExprId,
219        tag: TypeofTagKind,
220    },
221    /// Wraps the dependent side of `&&`/`||`/ternary. See `docs/narrowing.md` primitive #6.
222    Narrowed {
223        path: ReferencePath,
224        source: ExprId,
225        binding: Ident,
226        cast_info: CastInfo,
227        inner: ExprId,
228    },
229    /// Narrowing on `cond` flows into `then_` (true env) and `else_` (false env).
230    Ternary {
231        cond: ExprId,
232        then_: ExprId,
233        else_: ExprId,
234    },
235    /// `a ?? b`. Result type is `union(strip_null(lhs.ty), rhs.ty)`.
236    NullishCoalesce {
237        lhs: ExprId,
238        rhs: ExprId,
239    },
240    /// Each part carries `result_ty` so codegen steps through without re-running inference.
241    /// Containing `TypedExpr.ty` is `union(tail_result_ty, Null)`.
242    OptionalChain {
243        base: ExprId,
244        parts: Vec<TypedChainPart>,
245    },
246    /// Statement-position uses are desugared into `AssignLocal`/`AssignGlobal`/etc. before
247    /// codegen; only expression-position uses reach here.
248    PostfixUnary {
249        op: crate::PostfixOp,
250        target: PostfixTarget,
251    },
252    /// `value!` — runtime-checked non-null assertion. Throws `Error` on `null`.
253    NonNullAssert {
254        value: ExprId,
255    },
256    /// `value as target_ty`. `check` carries the structural shape to validate at runtime
257    /// (the target with interfaces reduced to object shapes) when the source isn't a static
258    /// subtype of the target; `None` for a statically-proven upcast (repr-only narrow, no
259    /// runtime test).
260    Cast {
261        value: ExprId,
262        target_ty: Type,
263        check: Option<Box<Type>>,
264    },
265    /// `x instanceof Foo` — lowers to `ref.test (ref $Foo)`. `class` is the resolved
266    /// `Type::ClassRef`; the result type is `boolean`.
267    InstanceOf {
268        value: ExprId,
269        class: Type,
270    },
271}
272
273#[derive(Clone, Debug, PartialEq)]
274pub enum PostfixTarget {
275    /// `boxed` is set by Capture when an inner closure references this binding.
276    Local {
277        ident: Ident,
278        boxed: bool,
279        target_ty: Type,
280    },
281    /// Only `let` globals; `const` global targets are rejected by the inferer.
282    Global {
283        name: Ident,
284        mangled: MangledName,
285        target_ty: Type,
286    },
287    /// Receiver is cached in an anonymous local to avoid double-evaluating side effects.
288    Field {
289        receiver: ExprId,
290        name: Ident,
291        target_ty: Type,
292    },
293    /// Both `receiver` and `index` are cached in anonymous locals to avoid double-evaluation.
294    Index {
295        receiver: ExprId,
296        index: ExprId,
297        elem_ty: Type,
298    },
299}
300
301/// `optional` drives short-circuit codegen; `result_ty` is the type after this part, without `| null`.
302#[derive(Clone, Debug, PartialEq)]
303pub enum TypedChainPart {
304    /// Representation-only view through a proven generic constraint. Unlike
305    /// `NonNull`, this preserves nullish values for a following optional step.
306    Constraint { result_ty: Type, span: Span },
307    Field {
308        name: Ident,
309        optional: bool,
310        result_ty: Type,
311        span: Span,
312    },
313    InterfaceProperty {
314        iface: MangledName,
315        name: Ident,
316        optional: bool,
317        result_ty: Type,
318        span: Span,
319    },
320    Index {
321        idx: ExprId,
322        optional: bool,
323        result_ty: Type,
324        span: Span,
325    },
326    Call {
327        args: Vec<ExprId>,
328        optional: bool,
329        result_ty: Type,
330        /// The one signature a union of function types is called through,
331        /// which the receiver is cast up to; `None` when the receiver is a
332        /// function type.
333        union_signature: Option<Type>,
334        span: Span,
335    },
336    MethodCall {
337        iface: MangledName,
338        name: Ident,
339        args: Vec<ExprId>,
340        optional: bool,
341        result_ty: Type,
342        span: Span,
343    },
344    /// `!` applied to the step before it. A runtime-checked narrowing that throws
345    /// `TypeError` on null, matching the non-chain `NonNullAssert` — not TS's
346    /// unchecked assertion.
347    NonNull { result_ty: Type, span: Span },
348}
349
350impl TypedChainPart {
351    /// The type this step yields, which is the next step's receiver.
352    pub fn result_ty(&self) -> &Type {
353        match self {
354            TypedChainPart::Field { result_ty, .. }
355            | TypedChainPart::InterfaceProperty { result_ty, .. }
356            | TypedChainPart::Index { result_ty, .. }
357            | TypedChainPart::Call { result_ty, .. }
358            | TypedChainPart::MethodCall { result_ty, .. }
359            | TypedChainPart::NonNull { result_ty, .. }
360            | TypedChainPart::Constraint { result_ty, .. } => result_ty,
361        }
362    }
363
364    /// Replaces the type this step yields. Used when a chain step's path turns
365    /// out to be narrowed: the step reads at the narrowed type, not the declared
366    /// one, and the next step's receiver follows.
367    pub fn set_result_ty(&mut self, ty: Type) {
368        match self {
369            TypedChainPart::Field { result_ty, .. }
370            | TypedChainPart::InterfaceProperty { result_ty, .. }
371            | TypedChainPart::Index { result_ty, .. }
372            | TypedChainPart::Call { result_ty, .. }
373            | TypedChainPart::MethodCall { result_ty, .. }
374            | TypedChainPart::NonNull { result_ty, .. }
375            | TypedChainPart::Constraint { result_ty, .. } => *result_ty = ty,
376        }
377    }
378
379    /// Where this step is written.
380    pub fn span(&self) -> crate::Span {
381        match self {
382            TypedChainPart::Field { span, .. }
383            | TypedChainPart::InterfaceProperty { span, .. }
384            | TypedChainPart::Index { span, .. }
385            | TypedChainPart::Call { span, .. }
386            | TypedChainPart::MethodCall { span, .. }
387            | TypedChainPart::NonNull { span, .. }
388            | TypedChainPart::Constraint { span, .. } => *span,
389        }
390    }
391
392    /// Whether this step short-circuits on a null receiver (`?.`).
393    pub fn is_optional(&self) -> bool {
394        match self {
395            TypedChainPart::Field { optional, .. }
396            | TypedChainPart::InterfaceProperty { optional, .. }
397            | TypedChainPart::Index { optional, .. }
398            | TypedChainPart::Call { optional, .. }
399            | TypedChainPart::MethodCall { optional, .. } => *optional,
400            TypedChainPart::NonNull { .. } | TypedChainPart::Constraint { .. } => false,
401        }
402    }
403}
404
405/// `is_generic` = the callee's unsubstituted slot was a bare `TypeVar`; codegen emits `cast::emit_box` when set.
406#[derive(Clone, Debug, PartialEq)]
407pub struct GenericArgument {
408    pub expr: ExprId,
409    pub is_generic: bool,
410}
411
412/// `boxed: true` — env stores `(ref $box T)`, mutations go through box (let/param captures).
413/// `boxed: false` — env stores the value directly (const captures, copied at construction).
414#[derive(Clone, Debug, PartialEq)]
415pub struct CapturedVar {
416    pub name: Ident,
417    pub ty: Type,
418    pub boxed: bool,
419}
420
421#[derive(Clone, Debug, PartialEq)]
422pub enum ClosureBody {
423    Expr(ExprId),
424    Block(StmtId),
425}
426
427#[derive(Clone, Debug, PartialEq)]
428pub struct TypedObjectLiteralField {
429    pub name: Ident,
430    pub value: ExprId,
431}
432
433#[derive(Clone, Debug, PartialEq)]
434pub struct TypedObjectFieldOrigin {
435    pub name: Ident,
436    pub source: TypedObjectFieldSource,
437    /// Carried so the shape collector and codegen build the object's struct
438    /// with the right optional flags — an absent optional must read back
439    /// `undefined` and be omitted by `JSON.stringify`.
440    pub optional: bool,
441    /// The field's declared type, not the source value's type. They diverge for
442    /// an `undefined`-filled optional field (value is `Type::Undefined`, declared type is the
443    /// real type) and for a literal widened to its hint. The shape collector and
444    /// codegen must build the object's struct — and its `TypeInfo` — from this so
445    /// `JSON.stringify` serializes by the declared type, not the fill value.
446    pub ty: Type,
447}
448
449/// The structural type an object literal of type `ty` is built with. Its fields
450/// come from `fields`, which hold the `undefined`-filled optional fields and the
451/// declared field types a literal's own type can leave out or narrow. A type
452/// with an index signature (from a spread) is already the layout.
453pub fn object_literal_layout(ty: &Type, fields: &[TypedObjectFieldOrigin]) -> Type {
454    if let Type::Object { index: Some(_), .. } = ty {
455        return ty.clone();
456    }
457    let fields = fields
458        .iter()
459        .map(|field| {
460            (
461                field.name.name.clone(),
462                crate::ObjectField {
463                    ty: field.ty.clone(),
464                    optional: field.optional,
465                    readonly: false,
466                    method: false,
467                },
468            )
469        })
470        .collect();
471    Type::Object {
472        index: None,
473        fields,
474    }
475}
476
477#[derive(Clone, Debug, PartialEq)]
478pub enum TypedObjectFieldSource {
479    Literal(ExprId),
480    Absent(ExprId),
481    /// `source_index` counts the literal's `Spread` members; `source_ty` is the
482    /// spread's `Type::Object` so codegen can index by BTreeMap order.
483    Spread {
484        source_index: usize,
485        field_name: String,
486        source_ty: Type,
487        /// Set when the source's field is optional and an earlier member wrote
488        /// the same field: an absent field leaves that earlier value in place,
489        /// as in JavaScript, where `{ a: 1, ...{} }` keeps `a: 1`.
490        fallback: Option<Box<TypedObjectFieldSource>>,
491    },
492}
493
494impl TypedObjectFieldSource {
495    pub fn literal_expr_id(&self) -> Option<ExprId> {
496        match self {
497            TypedObjectFieldSource::Literal(id) | TypedObjectFieldSource::Absent(id) => Some(*id),
498            TypedObjectFieldSource::Spread { .. } => None,
499        }
500    }
501}
502
503/// One member of an object literal, in source order.
504#[derive(Clone, Copy, Debug, PartialEq)]
505pub enum TypedObjectMember {
506    Computed {
507        key: ExprId,
508        value: ExprId,
509    },
510    /// A field's value.
511    Value(ExprId),
512    /// A spread's source object. `by_name` is set when the source has no one
513    /// layout — its type is a union of object types, or it is a conditional
514    /// whose branches differ — so each field is found by name at run time, and
515    /// one the object lacks reads as absent.
516    Spread {
517        source: ExprId,
518        by_name: bool,
519    },
520}
521
522impl TypedObjectMember {
523    pub fn expressions(self) -> impl Iterator<Item = ExprId> {
524        let key = match self {
525            Self::Computed { key, .. } => Some(key),
526            _ => None,
527        };
528        key.into_iter().chain(std::iter::once(self.expr_id()))
529    }
530
531    pub fn expr_id(self) -> ExprId {
532        match self {
533            TypedObjectMember::Value(id)
534            | TypedObjectMember::Spread { source: id, .. }
535            | TypedObjectMember::Computed { value: id, .. } => id,
536        }
537    }
538}
539
540#[derive(Clone, Debug, PartialEq)]
541pub enum TypedArrayElement {
542    Value(ExprId),
543    Spread(ExprId),
544}
545
546impl TypedArrayElement {
547    pub fn expr_id(&self) -> ExprId {
548        match self {
549            TypedArrayElement::Value(id) | TypedArrayElement::Spread(id) => *id,
550        }
551    }
552}
553
554#[derive(Copy, Clone, Debug, PartialEq, Eq)]
555pub enum TypeofTagKind {
556    Undefined,
557    BigInt,
558    Number,
559    String,
560    Boolean,
561    /// `"object"` — includes `null` (the JS quirk) and arrays; excludes functions.
562    Object,
563    /// `"function"` — vtable equality check against shared `closure_vtable` global.
564    Function,
565}
566
567#[derive(Copy, Clone, Debug, PartialEq, Eq)]
568pub enum Intrinsic {
569    /// `assert(cond, msg)` — `unreachable` on false; will throw `Error` post-exceptions.
570    Assert,
571    /// `JSON.stringify(x)` — only the call form is accepted; bare `JSON` ref is rejected.
572    JsonStringify,
573    /// `JSON.parse(s)` — returns `unknown`; use `as T` for runtime validation.
574    JsonParse,
575    /// `BigInt.fromString(s)` — decimal parse via `submilli:bigint.fromString`; throws on failure.
576    BigIntFromString,
577    /// A template interpolation whose value may be `null`, `undefined` or of a
578    /// type only known at run time — JavaScript's ToString, as `${x}` performs.
579    ToString,
580    /// A `{}` value flowing into an object type: a primitive or array becomes
581    /// the object JavaScript reads its properties from, and an object stays
582    /// itself.
583    ToObject,
584}
585
586impl Intrinsic {
587    pub fn from_name(name: &str) -> Option<Self> {
588        match name {
589            "assert" => Some(Intrinsic::Assert),
590            _ => None,
591        }
592    }
593
594    /// `JsonStringify` uses `Type::Error` as the value slot — typechecker accepts any
595    /// non-`void` arg; only diagnostic formatting reads this.
596    pub fn params(self) -> Vec<crate::Param> {
597        use crate::{Param, Type};
598        match self {
599            Intrinsic::Assert => vec![
600                Param::new("condition", Type::Boolean),
601                Param {
602                    name: "message".to_string(),
603                    optional: true,
604                    ty: Type::String,
605                    default: Some(crate::DefaultValue::String("assertion failed".to_string())),
606                    rest: false,
607                },
608            ],
609            Intrinsic::BigIntFromString => vec![Param::new("value", Type::String)],
610            Intrinsic::JsonStringify => vec![Param::new("value", Type::Error)],
611            Intrinsic::JsonParse => vec![Param::new("text", Type::String)],
612            Intrinsic::ToString | Intrinsic::ToObject => {
613                vec![Param::new("value", Type::Unknown)]
614            }
615        }
616    }
617
618    /// `JsonParse` returns `unknown`.
619    pub fn ret(self) -> crate::Type {
620        match self {
621            Intrinsic::Assert => crate::Type::Void,
622            Intrinsic::BigIntFromString => crate::Type::BigInt,
623            Intrinsic::JsonStringify => crate::Type::String,
624            Intrinsic::JsonParse => crate::Type::Unknown,
625            Intrinsic::ToString => crate::Type::String,
626            Intrinsic::ToObject => crate::Type::Unknown,
627        }
628    }
629
630    pub fn name(self) -> &'static str {
631        match self {
632            Intrinsic::Assert => "assert",
633            Intrinsic::BigIntFromString => "BigInt.fromString",
634            Intrinsic::JsonStringify => "JSON.stringify",
635            Intrinsic::JsonParse => "JSON.parse",
636            Intrinsic::ToString => "String",
637            Intrinsic::ToObject => "Object",
638        }
639    }
640}
641
642#[derive(Clone, Debug, PartialEq)]
643pub struct TypedStmt {
644    pub kind: TypedStmtKind,
645    pub span: Span,
646}
647
648/// Resolved at typecheck time so desugar doesn't re-run assignability. `Iterable` subsumes
649/// `Map`/`Set` and any structurally-iterable interface.
650#[derive(Clone, Copy, Debug, PartialEq, Eq)]
651pub enum ForOfKind {
652    Array,
653    Iterator,
654    Iterable,
655}
656
657#[derive(Clone, Debug, PartialEq)]
658pub enum TypedStmtKind {
659    /// Function-local only; top-level `let` is in `TypedAst::globals`.
660    /// `ty` is the annotation when present, else the inferred type — annotation wins on mismatch.
661    Let {
662        name: Ident,
663        ty: Type,
664        value: ExprId,
665        boxed: bool,
666        doc: Option<crate::DocComment>,
667    },
668    /// Replace the box in `ident`'s slot with a fresh one holding the same value,
669    /// so closures created from here on capture a cell nothing before them shares.
670    ///
671    /// Only the `for` lowering emits this, for the per-iteration binding a `let`
672    /// head has in JS (ECMA-262 §14.7.4.4 CreatePerIterationEnvironment): each
673    /// pass gets its own copy, which is why `for (let i …) fns.push(() => i)`
674    /// yields `0,1,2` rather than three views of one slot. `ident` must name a
675    /// *boxed* binding — an unboxed one is captured by value, so there is
676    /// nothing to separate.
677    ReboxLocal {
678        ident: Ident,
679        ty: Type,
680    },
681    /// Captured `const` bindings are copied into the closure env (no `boxed` field).
682    /// Function-local only — see `Let`.
683    Const {
684        name: Ident,
685        ty: Type,
686        value: ExprId,
687        doc: Option<crate::DocComment>,
688    },
689    If {
690        condition: ExprId,
691        then_block: StmtId,
692        else_block: Option<StmtId>,
693    },
694    While {
695        condition: ExprId,
696        body: StmtId,
697    },
698    /// Pre-desugar; codegen never sees this variant.
699    For {
700        init: Vec<StmtId>,
701        condition: Option<ExprId>,
702        update: Option<StmtId>,
703        body: StmtId,
704    },
705    /// Pre-desugar; codegen never sees this variant. `kind` lets desugar dispatch
706    /// without re-running assignability.
707    ForOf {
708        binding_kind: BindingKind,
709        name: Ident,
710        element_ty: Type,
711        iter: ExprId,
712        body: StmtId,
713        kind: ForOfKind,
714    },
715    /// Pre-desugar; lowers to a `while`-true whose head tests `cond` on every
716    /// pass but the first, so a `continue` still reaches the test.
717    DoWhile {
718        body: StmtId,
719        condition: ExprId,
720    },
721    /// Not lowered to if/else — first-class through desugar and codegen.
722    /// `discriminant_ty` drives dispatch strategy. Case bodies are wrapped in
723    /// `NarrowRegion` by the inferer for discriminated-union narrowing.
724    Switch {
725        discriminant: ExprId,
726        discriminant_ty: Type,
727        cases: Vec<TypedSwitchCase>,
728        default: Option<StmtId>,
729    },
730    Break,
731    /// Desugar injects the update step before the jump in `for`/`for-of`.
732    /// A switch frame on the loop-contexts stack is skipped so `continue` inside
733    /// `switch` reaches the enclosing loop.
734    Continue,
735    Return(Option<ExprId>),
736    /// `throw <expr>`. Value must be `Error`; diverges in control-flow analysis.
737    Throw {
738        value: ExprId,
739    },
740    /// `catches` dispatch in declaration order on the thrown value's nominal
741    /// class; an unmatched error re-raises. Codegen duplicates `finally` on
742    /// normal/caught/uncaught paths.
743    Try {
744        body: StmtId,
745        catches: Vec<TypedCatchClause>,
746        finally: Option<StmtId>,
747    },
748    Expr(ExprId),
749    Block(Vec<StmtId>),
750    /// Function-scope binding assignment. `target_ty` is the slot's declared type;
751    /// codegen coerces primitives into ref-typed slots (e.g. `number | null` boxes f64).
752    AssignLocal {
753        ident: Ident,
754        target_ty: Type,
755        value: ExprId,
756        boxed: bool,
757        /// When set, codegen materializes a shadow Wasm local of this narrowed type
758        /// alongside the slot write. Subsequent `LocalNarrowRef` reads resolve to the
759        /// shadow; it disappears at block exit.
760        narrowed_shadow_ty: Option<Type>,
761    },
762    /// `const` and function targets are rejected. `target_ty` mirrors `AssignLocal.target_ty` for codegen coercion.
763    AssignGlobal {
764        ident: Ident,
765        mangled: MangledName,
766        target_ty: Type,
767        value: ExprId,
768    },
769    AssignField {
770        receiver: ExprId,
771        name: Ident,
772        value: ExprId,
773    },
774    /// `elem_ty` is carried so codegen picks the boxing path without re-walking
775    /// the receiver's `Type`.
776    AssignIndex {
777        receiver: ExprId,
778        index: ExprId,
779        value: ExprId,
780        elem_ty: Type,
781    },
782    /// Wraps then/else blocks and switch case bodies. `path` is for diagnostics; codegen
783    /// consults only `source`, `cast_info`, `binding`, and `body`.
784    NarrowRegion {
785        path: ReferencePath,
786        source: ExprId,
787        binding: Ident,
788        cast_info: CastInfo,
789        body: StmtId,
790    },
791}
792
793/// `ty` is the binding's class type — the root `Error` class for untyped
794/// `catch (e)`, or the annotated `Error` subclass (which filters at runtime);
795/// `boxed` mirrors `Let.boxed` from the Capture pass.
796#[derive(Clone, Debug, PartialEq)]
797pub struct TypedCatchClause {
798    pub binding: Ident,
799    pub ty: Type,
800    pub body: StmtId,
801    pub boxed: bool,
802    pub span: Span,
803}
804
805/// Multiple entries in `values` when consecutive `case` labels share a body.
806/// `body` is wrapped in `NarrowRegion` by the inferer for discriminated unions.
807#[derive(Clone, Debug, PartialEq)]
808pub struct TypedSwitchCase {
809    pub values: Vec<TypedSwitchValue>,
810    pub body: StmtId,
811    pub span: Span,
812}
813
814impl TypedSwitchCase {
815    /// The run-time comparisons of the clause's labels that aren't literals, which
816    /// run before any clause body does.
817    pub fn label_comparisons(&self) -> impl Iterator<Item = ExprId> + '_ {
818        self.values.iter().filter_map(|value| match value {
819            TypedSwitchValue::Expr { comparison, .. } => Some(*comparison),
820            _ => None,
821        })
822    }
823}
824
825/// A `case` label. `span` anchors fallthrough and duplicate-case diagnostics.
826#[derive(Clone, Debug, PartialEq)]
827pub enum TypedSwitchValue {
828    /// A label that isn't a literal, such as `case one:`, compared at run time.
829    /// Walkers visit `comparison`, which holds `label`.
830    Expr {
831        label: ExprId,
832        /// `discriminant === label`. The discriminant is read from the
833        /// temporary the inferer binds it to, so the label can't re-run it.
834        comparison: ExprId,
835        /// The one value the label can have, when its type is a single literal:
836        /// it then counts toward exhaustiveness as a literal label would.
837        literal: Option<crate::typechecker::infer::narrowing::LiteralValue>,
838        span: Span,
839    },
840    String {
841        value: String,
842        span: Span,
843    },
844    Number {
845        value: f64,
846        span: Span,
847    },
848    Boolean {
849        value: bool,
850        span: Span,
851    },
852    Null {
853        span: Span,
854    },
855    Undefined {
856        span: Span,
857    },
858    /// `value` is the lowered runtime representation.
859    Enum {
860        enum_name: MangledName,
861        member: Ident,
862        value: EnumVariantPayload,
863        span: Span,
864    },
865}
866
867/// Distinct from the top-level `Number`/`String` variants so the inferer can attach enum-identity diagnostics.
868#[derive(Clone, Debug, PartialEq)]
869pub enum EnumVariantPayload {
870    Number(f64),
871    String(String),
872}
873
874#[derive(Clone, Debug, PartialEq)]
875pub struct TypedParam {
876    pub name: Ident,
877    /// Whether the caller may omit this parameter.
878    pub optional: bool,
879    pub ty: Type,
880    /// Set by Capture when an inner closure captures this param. The Wasm signature
881    /// is unaffected — codegen emits a body prologue that wraps the arg into
882    /// `(ref $box T)` and shadows it; all subsequent reads/writes go through the box.
883    pub boxed: bool,
884    /// Callee sees `T[]`; call sites pre-pack trailing args. Only valid on the last param.
885    pub rest: bool,
886    /// Resolved default for an omitted argument, carried so `PackageDeclaration`
887    /// can export it — without it, cross-module calls can't omit the arg.
888    pub default: Option<crate::DefaultValue>,
889}
890
891/// A function-local binding that holds a read which may have found no value;
892/// see [`TypedAst::maybe_missing_locals`].
893#[derive(Clone, Debug)]
894pub struct MaybeMissingLocal {
895    /// What a use at `T` throws when the value is missing; it names the read.
896    pub message: String,
897    /// Whether codegen keeps a present flag beside the binding: one typed
898    /// with a type parameter, whose instantiation may hold `undefined`, can't
899    /// tell a missing read from a present `undefined` by its value. Recorded
900    /// here because wide-slot erasure may type the binding `unknown` before
901    /// codegen sees it.
902    pub has_present_flag: bool,
903}
904
905/// Compiler intermediate representation, produced by type inference and consumed
906/// by checking, capture analysis, desugaring, and code generation.
907///
908/// # Phase input contract
909///
910/// Public phase APIs accept compiler-produced trees at the appropriate pipeline
911/// stage. Only continue compilation when inference and checking report no errors.
912/// Public fields support compiler transformations and inspection; they do not
913/// make arbitrary hand-built trees supported phase inputs. Transformations must
914/// preserve the compiler's type, binding, and metadata invariants.
915///
916/// In particular, every embedded [`Type`] must stay within
917/// [`crate::compiler_limits::MAX_TYPE_DEPTH`] and
918/// [`crate::compiler_limits::MAX_TYPE_NODES`], including types in parameters,
919/// declarations, shapes, captures, predicates, and runtime metadata. Inference
920/// bounds annotation resolution, inferred expression types, and substitutions
921/// before handing the tree to later phases. Replacing a payload invalidates that
922/// guarantee; [`crate::type_size::check`] checks a replacement type's bounds, not
923/// the semantic validity of the containing tree.
924///
925/// [`crate::tree_height::check_typed`] checks arena structure, not these payload
926/// invariants. Clone, formatting, and destruction can recurse through payloads,
927/// including destruction when an owning phase returns an error. Keep their
928/// bounds valid before transferring ownership, and perform phase work and
929/// destruction on a [`crate::compiler_limits::COMPILER_STACK_BYTES`] stack.
930#[derive(Default, Clone, Debug)]
931pub struct TypedAst {
932    /// Positional reads from a collected call-argument array return undefined for missing slots.
933    pub call_spread_index_reads: std::collections::BTreeSet<ExprId>,
934    /// Function-local `let`/`const` declarations, by name span, that hold a
935    /// read which may have found no value, from their initializer or a later
936    /// assignment, and the temporaries of assignment expressions that pass
937    /// such a read on. The binding is stored as `T | undefined`, and a read
938    /// of it that the typechecker left at `T` throws `MissingValueError` when
939    /// the value is missing.
940    pub maybe_missing_locals: std::collections::BTreeMap<Span, MaybeMissingLocal>,
941    /// Every read that may find no value and reaches a check or a place
942    /// that holds `undefined`, with its written type: the type tsc reports,
943    /// before the typechecker widened it with `undefined` (unchanged where it
944    /// already held `undefined`). Tools show the written type (see
945    /// [`TypedAst::written_type`]); codegen takes a read listed here as a
946    /// check of a maybe-missing local, and as a value that may clear a
947    /// present flag. A use not listed tests the value, except a use of a
948    /// local without a present flag typed with room for `undefined`, which
949    /// takes the value as it is.
950    pub widened_missing_reads: std::collections::BTreeMap<ExprId, Type>,
951    /// Generic reads (`o[k]` typed `T[K]`) whose missing key reads
952    /// `undefined`: every instantiation's property holds it (an optional
953    /// field among `K`'s, or an unbounded `T`). Their type stays tsc's;
954    /// codegen reads `undefined` for a missing key, as for a read typed with
955    /// room for it.
956    pub missing_reads_undefined: std::collections::BTreeSet<ExprId>,
957    /// Declarations and assignment targets, by span, whose recorded type is
958    /// the `T | undefined` storage of an unannotated local holding a
959    /// maybe-missing read, though the local has the type `T`. Only tools that
960    /// show types read it, the conformance harness among them; see
961    /// [`TypedAst::written_binding_type`].
962    pub widened_bindings: std::collections::BTreeSet<Span>,
963    /// Typed operands written as spread arguments to a rest parameter.
964    pub call_spread_values: std::collections::BTreeSet<ExprId>,
965    /// Tuple sources expanded into positional call arguments; codegen evaluates each once.
966    pub tuple_call_spread_sources: std::collections::BTreeSet<ExprId>,
967    /// Number of parameter-initialization statements before a wrapped body.
968    pub parameter_default_prologues: std::collections::BTreeMap<StmtId, usize>,
969    /// Receiver types for ordinary function expressions; arrows capture their receiver.
970    pub closure_this: std::collections::BTreeMap<ExprId, Type>,
971    /// Named function-expression bindings, scoped to their closure body.
972    pub closure_names: std::collections::BTreeMap<ExprId, Ident>,
973    /// The closures nested function declarations become, by declared name, so
974    /// a diagnostic about one can name it as the function it is.
975    pub nested_function_names: std::collections::BTreeMap<ExprId, Ident>,
976    /// Closures with empty bodies that hold a nested function's binding until
977    /// its real closure is assigned. The typechecker rejects every use before
978    /// then, so they are never called: a non-void one traps if it is.
979    pub placeholder_closures: std::collections::BTreeSet<ExprId>,
980    /// Arguments before omitted defaults and rest packing, keyed by call span.
981    pub authored_arguments: std::collections::BTreeMap<(u32, u32, u32), Vec<ExprId>>,
982    /// Parameter declaration types before optional/default ABI input widening.
983    pub authored_parameter_types: std::collections::BTreeMap<(u32, u32, u32), Type>,
984    /// The type a binding's postfix `++`/`--` reads it at, keyed by the name's
985    /// span: its narrowed type, where the target records the declared one.
986    pub postfix_operand_types: std::collections::BTreeMap<(u32, u32, u32), Type>,
987    /// Synthetic reads of incoming arguments before a parameter becomes initialized.
988    pub parameter_inputs: std::collections::BTreeSet<ExprId>,
989    /// Callee prologue statements that initialize parameters and pattern bindings.
990    pub parameter_initializations: std::collections::BTreeSet<StmtId>,
991    /// Authored expression types retained by runtime-value lowering for member
992    /// selection. Physical slot types live on the lowered expressions.
993    pub runtime_source_types: std::collections::BTreeMap<ExprId, Type>,
994    pub runtime_chain_types: std::collections::BTreeMap<ExprId, Vec<Type>>,
995    /// For each spread source read by name, the fields its value is checked
996    /// against: every field one of its object types names, with the union of
997    /// the types they give it, an index signature's value type included.
998    pub spread_mask_fields:
999        std::collections::BTreeMap<ExprId, std::collections::BTreeMap<String, Type>>,
1000    /// For each spread source of an object rest, the names the rest leaves out
1001    /// whatever their value.
1002    pub spread_omitted_fields:
1003        std::collections::BTreeMap<ExprId, std::collections::BTreeSet<String>>,
1004    /// Static member reads and calls named through a subclass that inherits
1005    /// the member, with that subclass: `Sub.hello()` calls `Base`'s method but
1006    /// still uses `Sub`, which may not be initialized yet.
1007    pub inherited_static_receivers: std::collections::BTreeMap<ExprId, MangledName>,
1008    /// The discriminants of the `switch`es without a `default` whose cases the
1009    /// typechecker found match every value the discriminant can hold.
1010    pub exhaustive_switches: std::collections::BTreeSet<ExprId>,
1011    /// Module name used for mangling. Defaults to `USER_PACKAGE` (`"main"`).
1012    pub package_name: String,
1013    exprs: Vec<TypedExpr>,
1014    stmts: Vec<TypedStmt>,
1015    /// Slot definitions only; initializers are `AssignGlobal` in `top_level_statements`.
1016    pub globals: Vec<TypedGlobal>,
1017    /// The globals code can rebind after the package loads that this package
1018    /// declares or imports, each with how the source names it: a module `let`,
1019    /// its own or one another package exports, and a static field not declared
1020    /// `readonly`, as `Class.field`. Every static field is a `GlobalKind::Const`
1021    /// global, so `globals` alone cannot tell.
1022    pub rebindable_globals: std::collections::BTreeMap<MangledName, String>,
1023    /// Top-level function declarations. Hoisted — codegen doesn't depend on source order.
1024    pub functions: Vec<TypedFunction>,
1025    /// [`crate::Ast::unparsed_main`]: a `main` was declared but did not parse.
1026    pub unparsed_main: bool,
1027    /// Source-order `_start` body — currently one `AssignGlobal` per global initializer.
1028    pub top_level_statements: Vec<StmtId>,
1029    /// Where each top-level class is declared: the index into
1030    /// `top_level_statements` of the first statement that runs after it.
1031    pub class_declaration_points: std::collections::BTreeMap<MangledName, usize>,
1032    /// Type declarations (`interface`, `enum`) — no Wasm representation, no body.
1033    /// Stored here so post-inference passes see member spans/types without re-exposing
1034    /// the inferer's `TypeNamespace`.
1035    pub types: Vec<TypedTypeDecl>,
1036    /// Distinct anonymous shapes (`Object`, `Array`, `Union`) reachable from the module.
1037    /// Built at end of inference; consumers read this instead of re-walking the AST.
1038    pub shapes: Vec<crate::Shape>,
1039    /// Public surface: one entry per `export`-marked declaration. Single source of
1040    /// truth for visibility — `TypedFunction`/`TypedGlobal` carry no `is_exported`
1041    /// flag. Built but not yet consumed for surface projection in single-file mode.
1042    pub exports: Vec<ExportEntry>,
1043    /// Root module path. Package rule checking requires it to report module
1044    /// diagnostics in path order; a script has `None`.
1045    pub package_root_module: Option<crate::ModulePath>,
1046    /// External package names that were resolved from explicit imports in this
1047    /// module. This is deliberately package-level, not symbol-level: embedders
1048    /// use it to install only the package modules the typed program imports.
1049    pub imported_packages: std::collections::BTreeSet<String>,
1050    /// What each non-entry module of a package exports, by module path. Package
1051    /// inference fills it and package compilation takes it before any later
1052    /// pass; only the package's own tests may import these.
1053    pub internal_modules: BTreeMap<crate::ModulePath, InternalModuleExports>,
1054    /// Exports a test build of a package adds to `exports`: the functions,
1055    /// globals and class statics of its non-entry modules, under their
1056    /// module-qualified mangled names. Empty outside a test build.
1057    pub test_exports: Vec<ExportEntry>,
1058    /// The check codegen emits for the argument of the package's valid
1059    /// `finalizer`, in the hidden export embedders call to restore it; `None`
1060    /// without one.
1061    pub finalizer_argument: Option<FinalizerArgumentCheck>,
1062    /// Concrete runtime validators resolved while inference still has access to
1063    /// interface declarations and generic substitutions. Codegen consults this
1064    /// for narrowed reads instead of re-deriving type structure.
1065    pub runtime_type_tests: std::collections::BTreeMap<Type, FieldNarrowingTest>,
1066    /// Substituted data fields for generic-class runtime validation.
1067    pub runtime_class_fields: std::collections::BTreeMap<Type, Type>,
1068    pub runtime_class_parameters: std::collections::BTreeMap<MangledName, Vec<String>>,
1069    pub runtime_class_contexts: std::collections::BTreeMap<Type, Vec<InstanceTypeContext>>,
1070    pub runtime_field_guards: std::collections::BTreeMap<Type, Vec<InstantiatedFieldGuard>>,
1071}
1072
1073/// A finalizer's value type and the shape its argument is checked against,
1074/// as a `Cast` carries them.
1075#[derive(Clone, Debug, PartialEq)]
1076pub struct FinalizerArgumentCheck {
1077    pub target: Type,
1078    pub check: Type,
1079}
1080
1081#[derive(Clone, Debug)]
1082pub struct InstanceTypeContext {
1083    pub declaration: MangledName,
1084    pub args: Vec<Type>,
1085}
1086
1087/// The surface of one non-entry package module, as a sibling module sees it.
1088#[derive(Clone, Debug, Default, PartialEq)]
1089pub struct InternalModuleExports {
1090    pub values: BTreeMap<String, crate::ValueSymbol>,
1091    pub types: BTreeMap<String, crate::TypeSymbol>,
1092    /// Names the module declares without exporting, to tell "add `export`"
1093    /// apart from a misspelling.
1094    pub unexported: std::collections::BTreeSet<String>,
1095}
1096
1097/// A public export: maps a package-public mangled name onto the internal symbol
1098/// it refers to. In single-file packages `public_name == target`.
1099#[derive(Clone, Debug, PartialEq)]
1100pub struct ExportEntry {
1101    pub public_name: MangledName,
1102    pub target: MangledName,
1103    pub kind: ExportKind,
1104    /// Span of the `export` keyword (or re-export statement) that produced this entry.
1105    pub span: Span,
1106}
1107
1108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1109pub enum ExportKind {
1110    Function,
1111    Global,
1112    Type,
1113}
1114
1115#[derive(Clone, Debug, PartialEq)]
1116pub struct TypedGlobal {
1117    pub name: Ident,
1118    pub mangled_name: MangledName,
1119    pub ty: Type,
1120    pub kind: GlobalKind,
1121    pub doc: Option<crate::DocComment>,
1122    pub span: Span,
1123}
1124
1125/// Codegen treats both identically at the Wasm-global level; the inferer enforces the `const` write diagnostic.
1126#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1127pub enum GlobalKind {
1128    Let,
1129    Const,
1130}
1131
1132#[derive(Clone, Debug, PartialEq)]
1133pub struct TypedFunction {
1134    pub name: Ident,
1135    pub mangled_name: MangledName,
1136    /// Body is type-checked under `Type::GenericParam` instantiations then erased
1137    /// to `Type::TypeVar` for the external view.
1138    pub generics: crate::GenericParameters,
1139    pub params: Vec<TypedParam>,
1140    pub return_type: Type,
1141    /// Type guard predicate. When set, `return_type` is always `Type::Boolean`.
1142    pub type_predicate: Option<crate::TypePredicate>,
1143    pub body: StmtId,
1144    pub doc: Option<crate::DocComment>,
1145    pub span: Span,
1146}
1147
1148/// Enums split by representation kind so the typed AST can't represent a mixed-kind enum.
1149#[derive(Clone, Debug, PartialEq)]
1150pub enum TypedTypeDecl {
1151    Interface(TypedInterfaceDecl),
1152    Class(TypedClassDecl),
1153    NumberEnum(TypedNumberEnumDecl),
1154    StringEnum(TypedStringEnumDecl),
1155    Alias(TypedTypeAliasDecl),
1156}
1157
1158/// A typechecked class. Method/constructor bodies are checked in the class-body
1159/// pass; codegen (SUB-480+) consumes the field layout, body IDs, and signatures.
1160#[derive(Clone, Debug, PartialEq)]
1161pub struct TypedClassDecl {
1162    pub name: Ident,
1163    pub fields: Vec<TypedClassField>,
1164    /// Static member visibility retained for analyses that run after package
1165    /// declarations have been reduced to their runtime surface.
1166    pub static_methods: BTreeMap<String, crate::Visibility>,
1167    /// Static field signatures are lowered to globals, but their source-level
1168    /// visibility and callable types remain relevant to public-surface analysis.
1169    pub static_fields: BTreeMap<String, crate::FieldSig>,
1170    pub constructor: Option<TypedClassConstructor>,
1171    /// The signature a class with no `constructor` of its own exposes, taken
1172    /// from the nearest ancestor that declares one with the `extends` clause's
1173    /// type arguments already substituted. Empty when `constructor` is `Some`.
1174    ///
1175    /// This is what the package declaration exports and what a cross-package
1176    /// consumer imports, so the emitted constructor must use it rather than
1177    /// re-deriving the parent's unsubstituted parameters — the two disagree the
1178    /// moment a subclass fixes a generic parent's type argument.
1179    pub inherited_ctor_params: Vec<TypedParam>,
1180    pub methods: Vec<TypedClassMethod>,
1181    /// Accessor functions (`get`/`set`), one entry each — getter and setter are
1182    /// symmetric, neither is required (a property may be get-only, set-only, or
1183    /// both). They are *not* in `methods`; codegen synthesizes the vtable method
1184    /// per entry (see `codegen::classes`).
1185    pub accessors: Vec<TypedClassAccessor>,
1186    pub extends: Option<crate::MangledName>,
1187    pub implements: Vec<crate::MangledName>,
1188    pub mangled_name: crate::MangledName,
1189    pub doc: Option<crate::DocComment>,
1190}
1191
1192/// One accessor function. Getter and setter are independent: the read type
1193/// (`ret_ty`) and write type (`param.ty`) need not match (TS 4.3+).
1194#[derive(Clone, Debug, PartialEq)]
1195pub enum TypedClassAccessor {
1196    Getter {
1197        name: Ident,
1198        ret_ty: Type,
1199        visibility: crate::Visibility,
1200        body: StmtId,
1201    },
1202    Setter {
1203        name: Ident,
1204        /// The setter parameter; `param.ty` is the property's write type.
1205        param: TypedParam,
1206        visibility: crate::Visibility,
1207        body: StmtId,
1208    },
1209}
1210
1211impl TypedClassAccessor {
1212    pub fn name(&self) -> &Ident {
1213        match self {
1214            TypedClassAccessor::Getter { name, .. } | TypedClassAccessor::Setter { name, .. } => {
1215                name
1216            }
1217        }
1218    }
1219
1220    pub fn body(&self) -> StmtId {
1221        match self {
1222            TypedClassAccessor::Getter { body, .. } | TypedClassAccessor::Setter { body, .. } => {
1223                *body
1224            }
1225        }
1226    }
1227
1228    pub fn visibility(&self) -> crate::Visibility {
1229        match self {
1230            TypedClassAccessor::Getter { visibility, .. }
1231            | TypedClassAccessor::Setter { visibility, .. } => *visibility,
1232        }
1233    }
1234}
1235
1236#[derive(Clone, Debug, PartialEq)]
1237pub struct TypedClassField {
1238    pub name: Ident,
1239    pub ty: Type,
1240    pub visibility: crate::Visibility,
1241    pub readonly: bool,
1242    pub optional: bool,
1243    pub initializer: Option<ExprId>,
1244    /// `true` for a parameter property (`constructor(public x: T)`): the field is
1245    /// assigned from the constructor parameter, so it's exempt from the
1246    /// definite-assignment check.
1247    pub auto_assigned: bool,
1248    /// Declared as a bodyless optional method (`f?(): T;`), which, as in
1249    /// TypeScript, emits nothing: it neither initializes nor resets the slot.
1250    pub method_signature: bool,
1251    /// Set when this declaration *narrows* an inherited one. See
1252    /// [`FieldNarrowingCheck`]. Boxed because every class field carries this and
1253    /// almost none of them narrow.
1254    pub narrowing_check: Option<Box<FieldNarrowingCheck>>,
1255    pub doc: Option<crate::DocComment>,
1256}
1257
1258/// A read guard for a field whose subclass declaration narrows the inherited
1259/// one. The two declarations share one storage slot, so a write that goes
1260/// through the parent's — an inherited method, a parent-typed reference, the
1261/// parent's constructor — can leave the slot holding a value the subclass's
1262/// declaration does not admit. The language accepts the narrowing anyway
1263/// (spec.md §Classes, matching TypeScript), so the read is what has to check.
1264///
1265/// Without this the read's bare `ref.cast` raises an uncatchable `cast failure`
1266/// naming nothing the author wrote; with it, the read runs `test` first —
1267/// presence for a narrowing that only strips `null`, structural conformance
1268/// otherwise — and throws `message` as a catchable `TypeError`.
1269#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
1270pub struct FieldNarrowingCheck {
1271    #[serde(default)]
1272    pub declaration: Option<MangledName>,
1273    pub test: FieldNarrowingTest,
1274    /// A concrete declaration type whose ancestor alternatives need only a
1275    /// presence or representation check. Other read types still validate fully.
1276    #[serde(default)]
1277    pub minimal_test_target: Option<Type>,
1278    pub message: String,
1279}
1280
1281#[derive(Clone, Debug, PartialEq)]
1282pub struct InstantiatedFieldGuard {
1283    pub field: String,
1284    pub target: Type,
1285    pub check: FieldNarrowingCheck,
1286}
1287
1288/// What a *redeclared* field's read guard verifies before it casts. The
1289/// typechecker selects the most precise test codegen can lower, falling back to
1290/// a read-time substituted test for erased class parameters.
1291#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
1292pub enum FieldNarrowingTest {
1293    /// The two declarations differ only in admitting `null`, so presence is the
1294    /// whole check. Worth its own case: the structural walk below is O(size of
1295    /// the stored value), and this is both the commonest narrowing and the one
1296    /// where walking proves nothing. It also has no shape to lower, so it guards
1297    /// types `Shape` cannot — a recursive one, an interface with methods.
1298    ///
1299    /// Whether the test actually runs is settled at the read, not here: an
1300    /// erased type parameter's `v: T` is `string | null` at `Sub<string | null>`,
1301    /// where a `null` is legal, and `string` at `Sub<string>`, where it is not.
1302    /// `emit_narrowed_field_read` asks the substituted read type and skips the
1303    /// test when it admits `null`.
1304    NonNull,
1305    /// Structural conformance to this shape, which is restricted to what
1306    /// `cast_check::emit_structural_test` can lower.
1307    Shape(Type),
1308    /// Runtime member check for an interface. Methods are read through the
1309    /// object-shape getter (which can surface class vtable slots), while data
1310    /// properties use the ordinary accessor-aware conformance walk. The method
1311    /// set is empty for a data-only interface.
1312    Interface(InterfaceNarrowingTest),
1313    /// The declaration contains an erased class type parameter, so its concrete
1314    /// runtime shape is available only at the read. Codegen tests the
1315    /// substituted `result_ty` rather than silently falling back to a bare cast.
1316    Substituted,
1317    /// Only the target Wasm representation can be established. This still
1318    /// makes the following cast safe and turns mismatches into `TypeError`.
1319    Representation,
1320}
1321
1322#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
1323pub struct InterfaceNarrowingTest {
1324    pub index: Option<crate::IndexSignature>,
1325    pub members: std::collections::BTreeMap<String, crate::ObjectField>,
1326    pub methods: std::collections::BTreeSet<String>,
1327    pub non_shape_carriers: std::collections::BTreeSet<InterfaceCarrier>,
1328    /// Whether an ordinary `$ObjectShape` may satisfy the interface. Direct
1329    /// host dispatch requires its canonical carrier; vtable interfaces remain
1330    /// structurally implementable by user objects.
1331    #[serde(default = "interface_shape_allowed_default")]
1332    pub shape_allowed: bool,
1333    pub nullable: bool,
1334    #[serde(default)]
1335    pub undefined: bool,
1336}
1337
1338fn interface_shape_allowed_default() -> bool {
1339    true
1340}
1341
1342#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
1343pub enum InterfaceCarrier {
1344    Number,
1345    Boolean,
1346    String,
1347    BigInt,
1348    Uint8Array,
1349    ArrayAny,
1350    Array(Type),
1351    MapAny,
1352    Map(Type, Type),
1353    SetAny,
1354    Set(Type),
1355    RegExp,
1356    RegExpMatch,
1357    RegExpMatchArray,
1358    TemporalInstant,
1359    TemporalDuration,
1360    TemporalZonedDateTime,
1361    TemporalPlainDate,
1362    TemporalPlainTime,
1363    TemporalPlainDateTime,
1364    TemporalPlainYearMonth,
1365    TemporalPlainMonthDay,
1366    ObjectShape,
1367    Url,
1368    FsStat,
1369    FsPeek,
1370    FsDirEntry,
1371    FsInfo,
1372    FsMountInfo,
1373    FsFileWriter,
1374    HttpResponse,
1375    HttpDownloadResult,
1376    SessionEntry,
1377    SessionPage,
1378}
1379
1380/// Whether codegen can validate every value admitted by `ty` with
1381/// `cast_check::emit_structural_test`. Keeping the allowlist beside the
1382/// serialized narrowing-test model gives typechecking and codegen one answer.
1383pub(crate) fn runtime_type_is_testable(ty: &Type) -> bool {
1384    runtime_type_is_testable_inner(ty, false)
1385}
1386
1387/// Field redeclaration guards can root generated validators at recursive alias
1388/// or interface back-edges. General expression descriptors cannot: polymorphic
1389/// recursion can grow its type arguments forever while enumerating every
1390/// expression type.
1391pub(crate) fn field_runtime_type_is_testable(ty: &Type) -> bool {
1392    runtime_type_is_testable_inner(ty, true)
1393}
1394
1395fn runtime_type_is_testable_inner(ty: &Type, allow_recursive_ref: bool) -> bool {
1396    match ty.peel() {
1397        Type::Null
1398        | Type::Undefined
1399        | Type::Void
1400        | Type::Number
1401        | Type::NumberLiteral(_)
1402        | Type::Boolean
1403        | Type::BooleanLiteral(_)
1404        | Type::String
1405        | Type::StringLiteral(_)
1406        | Type::BigInt
1407        | Type::Uint8Array
1408        | Type::Unknown
1409        | Type::Function { .. }
1410        | Type::NumberEnum { .. }
1411        | Type::StringEnum { .. }
1412        | Type::ClassRef { .. } => true,
1413        Type::AliasRef { .. } | Type::InterfaceRef { .. } => allow_recursive_ref,
1414        Type::Array(elem) => runtime_type_is_testable_inner(elem, allow_recursive_ref),
1415        Type::Tuple(elems) => elems
1416            .iter()
1417            .all(|elem| runtime_type_is_testable_inner(elem, allow_recursive_ref)),
1418        Type::Union(elems) => elems
1419            .iter()
1420            .all(|elem| runtime_type_is_testable_inner(elem, allow_recursive_ref)),
1421        Type::Object { fields, index } => {
1422            index
1423                .as_ref()
1424                .is_none_or(|i| runtime_type_is_testable_inner(&i.value, allow_recursive_ref))
1425                && fields
1426                    .values()
1427                    .all(|field| runtime_type_is_testable_inner(&field.ty, allow_recursive_ref))
1428        }
1429        _ => false,
1430    }
1431}
1432
1433impl TypedClassDecl {
1434    /// The constructor signature this class exposes: its own if it declares
1435    /// one, otherwise the inherited signature. Callers should prefer this over
1436    /// reading either field directly — `inherited_ctor_params` is meaningful
1437    /// only when `constructor` is `None`.
1438    pub fn effective_ctor_params(&self) -> &[TypedParam] {
1439        match &self.constructor {
1440            Some(c) => &c.params,
1441            None => &self.inherited_ctor_params,
1442        }
1443    }
1444}
1445
1446#[derive(Clone, Debug, PartialEq)]
1447pub struct TypedClassConstructor {
1448    pub doc: Option<Box<crate::DocComment>>,
1449    pub span: Span,
1450    pub params: Vec<TypedParam>,
1451    pub body: StmtId,
1452}
1453
1454#[derive(Clone, Debug, PartialEq)]
1455pub struct TypedClassMethod {
1456    pub name: Ident,
1457    pub generics: crate::GenericParameters,
1458    pub params: Vec<TypedParam>,
1459    pub return_type: Type,
1460    pub body: StmtId,
1461    pub visibility: crate::Visibility,
1462    pub doc: Option<crate::DocComment>,
1463}
1464
1465#[derive(Clone, Debug, PartialEq)]
1466pub struct TypedInterfaceDecl {
1467    /// Complete property names, including inherited properties, for payload validation.
1468    pub property_names: std::collections::BTreeSet<String>,
1469    pub index: Option<crate::IndexSignature>,
1470    pub name: Ident,
1471    pub generics: crate::GenericParameters,
1472    pub members: Vec<TypedInterfaceMember>,
1473    pub doc: Option<crate::DocComment>,
1474}
1475
1476#[derive(Clone, Debug, PartialEq)]
1477pub enum TypedInterfaceMember {
1478    Method {
1479        name: Ident,
1480        generics: crate::GenericParameters,
1481        params: Vec<TypedParam>,
1482        return_type: Type,
1483        doc: Option<crate::DocComment>,
1484    },
1485    Property {
1486        name: Ident,
1487        ty: Type,
1488        readonly: bool,
1489        /// Reads widen to `T | null`; may be omitted at construction.
1490        optional: bool,
1491        doc: Option<crate::DocComment>,
1492    },
1493}
1494
1495#[derive(Clone, Debug, PartialEq)]
1496pub struct TypedNumberEnumDecl {
1497    pub name: Ident,
1498    pub members: Vec<TypedNumberEnumMember>,
1499    pub doc: Option<crate::DocComment>,
1500}
1501
1502#[derive(Clone, Debug, PartialEq)]
1503pub struct TypedNumberEnumMember {
1504    pub name: Ident,
1505    pub value: f64,
1506    pub doc: Option<crate::DocComment>,
1507}
1508
1509#[derive(Clone, Debug, PartialEq)]
1510pub struct TypedStringEnumDecl {
1511    pub name: Ident,
1512    pub members: Vec<TypedStringEnumMember>,
1513    pub doc: Option<crate::DocComment>,
1514}
1515
1516#[derive(Clone, Debug, PartialEq)]
1517pub struct TypedStringEnumMember {
1518    pub name: Ident,
1519    pub value: String,
1520    pub doc: Option<crate::DocComment>,
1521}
1522
1523/// `ty` is fully resolved; `Type::TypeVar(name)` placeholders correspond to `generics` entries.
1524#[derive(Clone, Debug, PartialEq)]
1525pub struct TypedTypeAliasDecl {
1526    pub name: Ident,
1527    pub generics: crate::GenericParameters,
1528    pub ty: Type,
1529    pub doc: Option<crate::DocComment>,
1530}
1531
1532impl TypedAst {
1533    pub(crate) fn has_string_index(&self, ty: &Type) -> bool {
1534        match ty.peel() {
1535            Type::Object { index, .. } => index.is_some(),
1536            Type::Union(members) => members.iter().any(|member| self.has_string_index(member)),
1537            Type::InterfaceRef { .. } => matches!(
1538                self.runtime_type_tests.get(ty.peel()),
1539                Some(FieldNarrowingTest::Interface(interface)) if interface.index.is_some()
1540            ),
1541            _ => false,
1542        }
1543    }
1544
1545    pub fn new() -> Self {
1546        Self {
1547            package_name: crate::mangle::USER_PACKAGE.to_string(),
1548            ..Self::default()
1549        }
1550    }
1551
1552    pub fn with_package(package_name: impl Into<String>) -> Self {
1553        Self {
1554            package_name: package_name.into(),
1555            ..Self::default()
1556        }
1557    }
1558
1559    pub fn record_authored_arguments(&mut self, span: Span, args: Vec<ExprId>) {
1560        self.authored_arguments
1561            .insert((span.file.0, span.start, span.end), args);
1562    }
1563
1564    pub fn authored_call_arguments(&self, span: Span) -> Option<&Vec<ExprId>> {
1565        self.authored_arguments
1566            .get(&(span.file.0, span.start, span.end))
1567    }
1568
1569    /// Checked allocation; failure leaves the arena unchanged.
1570    pub fn try_push_expr(&mut self, expr: TypedExpr) -> Result<ExprId, ArenaError> {
1571        arena::push(&mut self.exprs, expr, ArenaKind::TypedExpressions).map(ExprId)
1572    }
1573
1574    /// Checked allocation; failure leaves the arena unchanged.
1575    pub fn try_push_stmt(&mut self, stmt: TypedStmt) -> Result<StmtId, ArenaError> {
1576        arena::push(&mut self.stmts, stmt, ArenaKind::TypedStatements).map(StmtId)
1577    }
1578
1579    pub fn try_expr(&self, id: ExprId) -> Result<&TypedExpr, ArenaError> {
1580        arena::get(&self.exprs, id.0, ArenaKind::TypedExpressions)
1581    }
1582
1583    pub fn try_stmt(&self, id: StmtId) -> Result<&TypedStmt, ArenaError> {
1584        arena::get(&self.stmts, id.0, ArenaKind::TypedStatements)
1585    }
1586
1587    pub fn try_expr_mut(&mut self, id: ExprId) -> Result<&mut TypedExpr, ArenaError> {
1588        arena::get_mut(&mut self.exprs, id.0, ArenaKind::TypedExpressions)
1589    }
1590
1591    pub fn try_stmt_mut(&mut self, id: StmtId) -> Result<&mut TypedStmt, ArenaError> {
1592        arena::get_mut(&mut self.stmts, id.0, ArenaKind::TypedStatements)
1593    }
1594
1595    /// Snapshot of allocated expression IDs, usable while appending new nodes.
1596    pub fn expr_ids(&self) -> Result<impl DoubleEndedIterator<Item = ExprId> + use<>, ArenaError> {
1597        Ok(arena::ids(self.exprs.len(), ArenaKind::TypedExpressions)?.map(ExprId))
1598    }
1599
1600    /// Snapshot of allocated statement IDs, usable while appending new nodes.
1601    pub fn stmt_ids(&self) -> Result<impl DoubleEndedIterator<Item = StmtId> + use<>, ArenaError> {
1602        Ok(arena::ids(self.stmts.len(), ArenaKind::TypedStatements)?.map(StmtId))
1603    }
1604
1605    /// Body `StmtId`s of every class constructor + method — additional codegen
1606    /// roots alongside `functions` (the string/bigint/closure/box collection
1607    /// passes must walk these too).
1608    pub fn class_body_roots(&self) -> Vec<StmtId> {
1609        let mut roots = Vec::new();
1610        for decl in &self.types {
1611            if let TypedTypeDecl::Class(c) = decl {
1612                if let Some(ctor) = &c.constructor {
1613                    roots.push(ctor.body);
1614                }
1615                roots.extend(c.methods.iter().map(|m| m.body));
1616                roots.extend(c.accessors.iter().map(TypedClassAccessor::body));
1617            }
1618        }
1619        roots
1620    }
1621
1622    /// Field-initializer expressions across all classes — `expr` roots that live
1623    /// outside any statement body, so analysis/erasure passes must visit them too.
1624    pub fn class_field_initializers(&self) -> Vec<ExprId> {
1625        let mut inits = Vec::new();
1626        for decl in &self.types {
1627            if let TypedTypeDecl::Class(c) = decl {
1628                inits.extend(c.fields.iter().filter_map(|f| f.initializer));
1629            }
1630        }
1631        inits
1632    }
1633
1634    /// The type `id` has as written, before the typechecker widened a read
1635    /// that may find no value with `undefined` (see
1636    /// [`TypedAst::widened_missing_reads`]).
1637    pub fn written_type(&self, id: ExprId) -> Result<&Type, ArenaError> {
1638        let expr = self.try_expr(id)?;
1639        Ok(self.widened_missing_reads.get(&id).unwrap_or(&expr.ty))
1640    }
1641
1642    /// The type a binding declared or assigned at `span` has as written, given
1643    /// the type its statement records: an unannotated local holding a
1644    /// maybe-missing read is stored as `T | undefined` but has the type `T`.
1645    pub fn written_binding_type(&self, span: Span, ty: &Type) -> Type {
1646        if !self.widened_bindings.contains(&span) {
1647            return ty.clone();
1648        }
1649        match ty.peel() {
1650            Type::Union(members) => Type::union(
1651                members
1652                    .iter()
1653                    .filter(|member| !matches!(member.peel(), Type::Undefined))
1654                    .cloned()
1655                    .collect(),
1656            ),
1657            _ => ty.clone(),
1658        }
1659    }
1660
1661    pub fn source_type(&self, id: ExprId) -> Result<&Type, ArenaError> {
1662        let expr = self.try_expr(id)?;
1663        Ok(self.runtime_source_types.get(&id).unwrap_or(&expr.ty))
1664    }
1665
1666    /// Used by post-inference passes to iterate just-pushed IDs (e.g. GenericParam erasure).
1667    pub fn exprs_len(&self) -> usize {
1668        self.exprs.len()
1669    }
1670
1671    pub fn stmts_len(&self) -> usize {
1672        self.stmts.len()
1673    }
1674
1675    /// Whether evaluating `id` can be skipped without changing what the program
1676    /// does. Deliberately a short whitelist of leaves: everything else — a call,
1677    /// a field read that may hit an accessor, an operator that may throw — says
1678    /// `false`, so a caller that folds an expression's value away and relies on
1679    /// this to decide whether to keep the computation errs toward keeping it.
1680    pub fn is_effect_free(&self, id: ExprId) -> Result<bool, ArenaError> {
1681        Ok(matches!(
1682            self.try_expr(id)?.kind,
1683            TypedExprKind::Number(_)
1684                | TypedExprKind::BigInt(_)
1685                | TypedExprKind::String(_)
1686                | TypedExprKind::Boolean(_)
1687                | TypedExprKind::Null
1688                | TypedExprKind::Undefined
1689                | TypedExprKind::This
1690                | TypedExprKind::Regex { .. }
1691                | TypedExprKind::LocalRef { .. }
1692                | TypedExprKind::LocalNarrowRef { .. }
1693                | TypedExprKind::GlobalRef { .. }
1694                | TypedExprKind::FunctionRef { .. }
1695                | TypedExprKind::NumberEnumMember { .. }
1696                | TypedExprKind::StringEnumMember { .. }
1697        ))
1698    }
1699}
1700
1701#[cfg(test)]
1702mod tests {
1703    use super::{TypedAst, TypedExpr, TypedExprKind, TypedParam, TypedStmt, TypedStmtKind};
1704    use crate::{BinOp, Ident, Span, Type, UnOp};
1705
1706    #[test]
1707    fn missing_read_side_tables_are_ordered() {
1708        let spans = [
1709            Span::new(crate::FileId(0), 1, 2).unwrap(),
1710            Span::new(crate::FileId(0), 1, 3).unwrap(),
1711            Span::new(crate::FileId(0), 4, 4).unwrap(),
1712            Span::at(crate::FileId(1)),
1713        ];
1714        let ids = [
1715            crate::ExprId(0),
1716            crate::ExprId(1),
1717            crate::ExprId(2),
1718            crate::ExprId(3),
1719        ];
1720        let mut forward = TypedAst::new();
1721        let mut reverse = TypedAst::with_package("main");
1722        for (ast, order) in [(&mut forward, [0, 1, 2, 3]), (&mut reverse, [3, 2, 1, 0])] {
1723            for index in order {
1724                let span = spans[index];
1725                ast.maybe_missing_locals.insert(
1726                    span,
1727                    super::MaybeMissingLocal {
1728                        message: format!("missing read {index}"),
1729                        has_present_flag: index == 0,
1730                    },
1731                );
1732                ast.widened_bindings.insert(span);
1733                ast.widened_missing_reads.insert(ids[index], Type::Number);
1734            }
1735        }
1736        assert_eq!(
1737            forward
1738                .maybe_missing_locals
1739                .keys()
1740                .copied()
1741                .collect::<Vec<_>>(),
1742            spans
1743        );
1744        assert_eq!(
1745            forward
1746                .widened_missing_reads
1747                .keys()
1748                .copied()
1749                .collect::<Vec<_>>(),
1750            ids
1751        );
1752        assert_eq!(
1753            forward.widened_bindings.iter().copied().collect::<Vec<_>>(),
1754            spans
1755        );
1756        assert_eq!(format!("{forward:?}"), format!("{reverse:?}"));
1757    }
1758
1759    #[test]
1760    fn arena_round_trip_for_typed_exprs() {
1761        let mut ast = TypedAst::new();
1762        let id = ast
1763            .try_push_expr(TypedExpr {
1764                kind: TypedExprKind::Number(42.0),
1765                span: Span::new(crate::FileId(0), 0, 2).unwrap(),
1766                ty: Type::Number,
1767            })
1768            .unwrap();
1769        assert_eq!(id.0, 0);
1770        let e = ast.try_expr(id).unwrap();
1771        assert_eq!(e.kind, TypedExprKind::Number(42.0));
1772        assert_eq!(e.span, Span::new(crate::FileId(0), 0, 2).unwrap());
1773        assert_eq!(e.ty, Type::Number);
1774    }
1775
1776    #[test]
1777    fn arena_round_trip_for_typed_stmts() {
1778        let mut ast = TypedAst::new();
1779        let expr_id = ast
1780            .try_push_expr(TypedExpr {
1781                kind: TypedExprKind::Boolean(true),
1782                span: Span::new(crate::FileId(0), 0, 4).unwrap(),
1783                ty: Type::Boolean,
1784            })
1785            .unwrap();
1786        let stmt_id = ast
1787            .try_push_stmt(TypedStmt {
1788                kind: TypedStmtKind::Expr(expr_id),
1789                span: Span::new(crate::FileId(0), 0, 5).unwrap(),
1790            })
1791            .unwrap();
1792        assert_eq!(stmt_id.0, 0);
1793        let s = ast.try_stmt(stmt_id).unwrap();
1794        assert_eq!(s.span, Span::new(crate::FileId(0), 0, 5).unwrap());
1795        assert!(matches!(s.kind, TypedStmtKind::Expr(_)));
1796    }
1797
1798    #[test]
1799    fn build_typed_binary_tree() {
1800        let mut ast = TypedAst::new();
1801        let lhs = ast
1802            .try_push_expr(TypedExpr {
1803                kind: TypedExprKind::Number(1.0),
1804                span: Span::new(crate::FileId(0), 0, 1).unwrap(),
1805                ty: Type::Number,
1806            })
1807            .unwrap();
1808        let rhs = ast
1809            .try_push_expr(TypedExpr {
1810                kind: TypedExprKind::Number(2.0),
1811                span: Span::new(crate::FileId(0), 4, 5).unwrap(),
1812                ty: Type::Number,
1813            })
1814            .unwrap();
1815        let sum = ast
1816            .try_push_expr(TypedExpr {
1817                kind: TypedExprKind::Binary {
1818                    op: BinOp::Add,
1819                    lhs,
1820                    rhs,
1821                },
1822                span: Span::new(crate::FileId(0), 0, 5).unwrap(),
1823                ty: Type::Number,
1824            })
1825            .unwrap();
1826
1827        let outer = ast.try_expr(sum).unwrap();
1828        assert_eq!(outer.ty, Type::Number);
1829        let TypedExprKind::Binary { op, lhs, rhs } = outer.kind else {
1830            panic!("expected Binary");
1831        };
1832        assert_eq!(op, BinOp::Add);
1833        assert_eq!(ast.try_expr(lhs).unwrap().ty, Type::Number);
1834        assert_eq!(ast.try_expr(rhs).unwrap().ty, Type::Number);
1835    }
1836
1837    #[test]
1838    fn build_typed_function_with_return() {
1839        let mut ast = TypedAst::new();
1840        let one = ast
1841            .try_push_expr(TypedExpr {
1842                kind: TypedExprKind::Number(1.0),
1843                span: Span::new(crate::FileId(0), 31, 32).unwrap(),
1844                ty: Type::Number,
1845            })
1846            .unwrap();
1847        let ret = ast
1848            .try_push_stmt(TypedStmt {
1849                kind: TypedStmtKind::Return(Some(one)),
1850                span: Span::new(crate::FileId(0), 24, 33).unwrap(),
1851            })
1852            .unwrap();
1853        let body = ast
1854            .try_push_stmt(TypedStmt {
1855                kind: TypedStmtKind::Block(vec![ret]),
1856                span: Span::new(crate::FileId(0), 22, 35).unwrap(),
1857            })
1858            .unwrap();
1859        ast.functions.push(crate::TypedFunction {
1860            name: Ident {
1861                name: "f".to_string(),
1862                span: Span::new(crate::FileId(0), 9, 10).unwrap(),
1863            },
1864            mangled_name: crate::mangle::package_symbol("main", "f"),
1865            generics: Default::default(),
1866            params: vec![],
1867            return_type: Type::Number,
1868            type_predicate: None,
1869            body,
1870            doc: None,
1871            span: Span::new(crate::FileId(0), 0, 35).unwrap(),
1872        });
1873
1874        let f = &ast.functions[0];
1875        assert_eq!(f.return_type, Type::Number);
1876        assert!(matches!(
1877            ast.try_stmt(f.body).unwrap().kind,
1878            TypedStmtKind::Block(_)
1879        ));
1880    }
1881
1882    #[test]
1883    fn clone_and_equality() {
1884        let kind = TypedExprKind::Unary {
1885            op: UnOp::Neg,
1886            operand: crate::ExprId(0),
1887        };
1888        assert_eq!(kind.clone(), kind);
1889    }
1890
1891    #[test]
1892    fn typed_param_construction() {
1893        let p = TypedParam {
1894            optional: false,
1895            name: Ident {
1896                name: "param".to_string(),
1897                span: Span::new(crate::FileId(0), 10, 15).unwrap(),
1898            },
1899            ty: Type::String,
1900            boxed: false,
1901            rest: false,
1902            default: None,
1903        };
1904        assert_eq!(p.name.name, "param");
1905        assert_eq!(p.name.span, Span::new(crate::FileId(0), 10, 15).unwrap());
1906        assert_eq!(p.ty, Type::String);
1907        assert!(!p.boxed);
1908    }
1909
1910    #[test]
1911    fn local_ref_carries_name_and_boxed_flag() {
1912        let mut ast = TypedAst::new();
1913        let id = ast
1914            .try_push_expr(TypedExpr {
1915                kind: TypedExprKind::LocalRef {
1916                    ident: Ident {
1917                        name: "x".to_string(),
1918                        span: Span::new(crate::FileId(0), 0, 1).unwrap(),
1919                    },
1920                    boxed: false,
1921                },
1922                span: Span::new(crate::FileId(0), 0, 1).unwrap(),
1923                ty: Type::Number,
1924            })
1925            .unwrap();
1926        match ast.try_expr(id).unwrap().kind {
1927            TypedExprKind::LocalRef { ref ident, boxed } => {
1928                assert_eq!(ident.name, "x");
1929                assert_eq!(ident.span, Span::new(crate::FileId(0), 0, 1).unwrap());
1930                assert!(!boxed);
1931            }
1932            _ => panic!("expected LocalRef"),
1933        }
1934    }
1935
1936    #[test]
1937    fn global_ref_carries_name_and_mangled() {
1938        let mut ast = TypedAst::new();
1939        let mangled = crate::mangle::package_symbol("main", "y");
1940        let id = ast
1941            .try_push_expr(TypedExpr {
1942                kind: TypedExprKind::GlobalRef {
1943                    mangled: mangled.clone(),
1944                    name: Ident {
1945                        name: "y".to_string(),
1946                        span: Span::new(crate::FileId(0), 2, 3).unwrap(),
1947                    },
1948                },
1949                span: Span::new(crate::FileId(0), 2, 3).unwrap(),
1950                ty: Type::Number,
1951            })
1952            .unwrap();
1953        match ast.try_expr(id).unwrap().kind {
1954            TypedExprKind::GlobalRef {
1955                ref name,
1956                ref mangled,
1957            } => {
1958                assert_eq!(name.name, "y");
1959                assert_eq!(name.span, Span::new(crate::FileId(0), 2, 3).unwrap());
1960                assert_eq!(mangled.as_str(), "main#y");
1961            }
1962            _ => panic!("expected GlobalRef"),
1963        }
1964    }
1965}