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