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}
559
560impl Intrinsic {
561    pub fn from_name(name: &str) -> Option<Self> {
562        match name {
563            "assert" => Some(Intrinsic::Assert),
564            _ => None,
565        }
566    }
567
568    /// `JsonStringify` uses `Type::Error` as the value slot — typechecker accepts any
569    /// non-`void` arg; only diagnostic formatting reads this.
570    pub fn params(self) -> Vec<crate::Param> {
571        use crate::{Param, Type};
572        match self {
573            Intrinsic::Assert => vec![
574                Param::new("condition", Type::Boolean),
575                Param {
576                    name: "message".to_string(),
577                    optional: true,
578                    ty: Type::String,
579                    default: Some(crate::DefaultValue::String("assertion failed".to_string())),
580                    rest: false,
581                },
582            ],
583            Intrinsic::BigIntFromString => vec![Param::new("value", Type::String)],
584            Intrinsic::JsonStringify => vec![Param::new("value", Type::Error)],
585            Intrinsic::JsonParse => vec![Param::new("text", Type::String)],
586            Intrinsic::ToString => vec![Param::new("value", Type::Unknown)],
587        }
588    }
589
590    /// `JsonParse` returns `unknown`.
591    pub fn ret(self) -> crate::Type {
592        match self {
593            Intrinsic::Assert => crate::Type::Void,
594            Intrinsic::BigIntFromString => crate::Type::BigInt,
595            Intrinsic::JsonStringify => crate::Type::String,
596            Intrinsic::JsonParse => crate::Type::Unknown,
597            Intrinsic::ToString => crate::Type::String,
598        }
599    }
600
601    pub fn name(self) -> &'static str {
602        match self {
603            Intrinsic::Assert => "assert",
604            Intrinsic::BigIntFromString => "BigInt.fromString",
605            Intrinsic::JsonStringify => "JSON.stringify",
606            Intrinsic::JsonParse => "JSON.parse",
607            Intrinsic::ToString => "String",
608        }
609    }
610}
611
612#[derive(Clone, Debug, PartialEq)]
613pub struct TypedStmt {
614    pub kind: TypedStmtKind,
615    pub span: Span,
616}
617
618/// Resolved at typecheck time so desugar doesn't re-run assignability. `Iterable` subsumes
619/// `Map`/`Set` and any structurally-iterable interface.
620#[derive(Clone, Copy, Debug, PartialEq, Eq)]
621pub enum ForOfKind {
622    Array,
623    Iterator,
624    Iterable,
625}
626
627#[derive(Clone, Debug, PartialEq)]
628pub enum TypedStmtKind {
629    /// Function-local only; top-level `let` is in `TypedAst::globals`.
630    /// `ty` is the annotation when present, else the inferred type — annotation wins on mismatch.
631    Let {
632        name: Ident,
633        ty: Type,
634        value: ExprId,
635        boxed: bool,
636        doc: Option<crate::DocComment>,
637    },
638    /// Replace the box in `ident`'s slot with a fresh one holding the same value,
639    /// so closures created from here on capture a cell nothing before them shares.
640    ///
641    /// Only the `for` lowering emits this, for the per-iteration binding a `let`
642    /// head has in JS (ECMA-262 §14.7.4.4 CreatePerIterationEnvironment): each
643    /// pass gets its own copy, which is why `for (let i …) fns.push(() => i)`
644    /// yields `0,1,2` rather than three views of one slot. `ident` must name a
645    /// *boxed* binding — an unboxed one is captured by value, so there is
646    /// nothing to separate.
647    ReboxLocal {
648        ident: Ident,
649        ty: Type,
650    },
651    /// Captured `const` bindings are copied into the closure env (no `boxed` field).
652    /// Function-local only — see `Let`.
653    Const {
654        name: Ident,
655        ty: Type,
656        value: ExprId,
657        doc: Option<crate::DocComment>,
658    },
659    If {
660        condition: ExprId,
661        then_block: StmtId,
662        else_block: Option<StmtId>,
663    },
664    While {
665        condition: ExprId,
666        body: StmtId,
667    },
668    /// Pre-desugar; codegen never sees this variant.
669    For {
670        init: Option<StmtId>,
671        condition: Option<ExprId>,
672        update: Option<StmtId>,
673        body: StmtId,
674    },
675    /// Pre-desugar; codegen never sees this variant. `kind` lets desugar dispatch
676    /// without re-running assignability.
677    ForOf {
678        binding_kind: BindingKind,
679        name: Ident,
680        element_ty: Type,
681        iter: ExprId,
682        body: StmtId,
683        kind: ForOfKind,
684    },
685    /// Pre-desugar; lowers to a `while`-true whose head tests `cond` on every
686    /// pass but the first, so a `continue` still reaches the test.
687    DoWhile {
688        body: StmtId,
689        condition: ExprId,
690    },
691    /// Not lowered to if/else — first-class through desugar and codegen.
692    /// `discriminant_ty` drives dispatch strategy. Case bodies are wrapped in
693    /// `NarrowRegion` by the inferer for discriminated-union narrowing.
694    Switch {
695        discriminant: ExprId,
696        discriminant_ty: Type,
697        cases: Vec<TypedSwitchCase>,
698        default: Option<StmtId>,
699    },
700    Break,
701    /// Desugar injects the update step before the jump in `for`/`for-of`.
702    /// A switch frame on the loop-contexts stack is skipped so `continue` inside
703    /// `switch` reaches the enclosing loop.
704    Continue,
705    Return(Option<ExprId>),
706    /// `throw <expr>`. Value must be `Error`; diverges in control-flow analysis.
707    Throw {
708        value: ExprId,
709    },
710    /// `catches` dispatch in declaration order on the thrown value's nominal
711    /// class; an unmatched error re-raises. Codegen duplicates `finally` on
712    /// normal/caught/uncaught paths.
713    Try {
714        body: StmtId,
715        catches: Vec<TypedCatchClause>,
716        finally: Option<StmtId>,
717    },
718    Expr(ExprId),
719    Block(Vec<StmtId>),
720    /// Function-scope binding assignment. `target_ty` is the slot's declared type;
721    /// codegen coerces primitives into ref-typed slots (e.g. `number | null` boxes f64).
722    AssignLocal {
723        ident: Ident,
724        target_ty: Type,
725        value: ExprId,
726        boxed: bool,
727        /// When set, codegen materializes a shadow Wasm local of this narrowed type
728        /// alongside the slot write. Subsequent `LocalNarrowRef` reads resolve to the
729        /// shadow; it disappears at block exit.
730        narrowed_shadow_ty: Option<Type>,
731    },
732    /// `const` and function targets are rejected. `target_ty` mirrors `AssignLocal.target_ty` for codegen coercion.
733    AssignGlobal {
734        ident: Ident,
735        mangled: MangledName,
736        target_ty: Type,
737        value: ExprId,
738    },
739    AssignField {
740        receiver: ExprId,
741        name: Ident,
742        value: ExprId,
743    },
744    /// `elem_ty` is carried so codegen picks the boxing path without re-walking
745    /// the receiver's `Type`.
746    AssignIndex {
747        receiver: ExprId,
748        index: ExprId,
749        value: ExprId,
750        elem_ty: Type,
751    },
752    /// Wraps then/else blocks and switch case bodies. `path` is for diagnostics; codegen
753    /// consults only `source`, `cast_info`, `binding`, and `body`.
754    NarrowRegion {
755        path: ReferencePath,
756        source: ExprId,
757        binding: Ident,
758        cast_info: CastInfo,
759        body: StmtId,
760    },
761}
762
763/// `ty` is the binding's class type — the root `Error` class for untyped
764/// `catch (e)`, or the annotated `Error` subclass (which filters at runtime);
765/// `boxed` mirrors `Let.boxed` from the Capture pass.
766#[derive(Clone, Debug, PartialEq)]
767pub struct TypedCatchClause {
768    pub binding: Ident,
769    pub ty: Type,
770    pub body: StmtId,
771    pub boxed: bool,
772    pub span: Span,
773}
774
775/// Multiple entries in `values` when consecutive `case` labels share a body.
776/// `body` is wrapped in `NarrowRegion` by the inferer for discriminated unions.
777#[derive(Clone, Debug, PartialEq)]
778pub struct TypedSwitchCase {
779    pub values: Vec<TypedSwitchValue>,
780    pub body: StmtId,
781    pub span: Span,
782}
783
784impl TypedSwitchCase {
785    /// The run-time comparisons of the clause's labels that aren't literals, which
786    /// run before any clause body does.
787    pub fn label_comparisons(&self) -> impl Iterator<Item = ExprId> + '_ {
788        self.values.iter().filter_map(|value| match value {
789            TypedSwitchValue::Expr { comparison, .. } => Some(*comparison),
790            _ => None,
791        })
792    }
793}
794
795/// A `case` label. `span` anchors fallthrough and duplicate-case diagnostics.
796#[derive(Clone, Debug, PartialEq)]
797pub enum TypedSwitchValue {
798    /// A label that isn't a literal, such as `case one:`, compared at run time.
799    /// Walkers visit `comparison`, which holds `label`.
800    Expr {
801        label: ExprId,
802        /// `discriminant === label`. The discriminant is read from the
803        /// temporary the inferer binds it to, so the label can't re-run it.
804        comparison: ExprId,
805        /// The one value the label can have, when its type is a single literal:
806        /// it then counts toward exhaustiveness as a literal label would.
807        literal: Option<crate::typechecker::infer::narrowing::LiteralValue>,
808        span: Span,
809    },
810    String {
811        value: String,
812        span: Span,
813    },
814    Number {
815        value: f64,
816        span: Span,
817    },
818    Boolean {
819        value: bool,
820        span: Span,
821    },
822    Null {
823        span: Span,
824    },
825    Undefined {
826        span: Span,
827    },
828    /// `value` is the lowered runtime representation.
829    Enum {
830        enum_name: MangledName,
831        member: Ident,
832        value: EnumVariantPayload,
833        span: Span,
834    },
835}
836
837/// Distinct from the top-level `Number`/`String` variants so the inferer can attach enum-identity diagnostics.
838#[derive(Clone, Debug, PartialEq)]
839pub enum EnumVariantPayload {
840    Number(f64),
841    String(String),
842}
843
844#[derive(Clone, Debug, PartialEq)]
845pub struct TypedParam {
846    pub name: Ident,
847    /// Whether the caller may omit this parameter.
848    pub optional: bool,
849    pub ty: Type,
850    /// Set by Capture when an inner closure captures this param. The Wasm signature
851    /// is unaffected — codegen emits a body prologue that wraps the arg into
852    /// `(ref $box T)` and shadows it; all subsequent reads/writes go through the box.
853    pub boxed: bool,
854    /// Callee sees `T[]`; call sites pre-pack trailing args. Only valid on the last param.
855    pub rest: bool,
856    /// Resolved default for an omitted argument, carried so `PackageDeclaration`
857    /// can export it — without it, cross-module calls can't omit the arg.
858    pub default: Option<crate::DefaultValue>,
859}
860
861/// Compiler intermediate representation, produced by type inference and consumed
862/// by checking, capture analysis, desugaring, and code generation.
863///
864/// # Phase input contract
865///
866/// Public phase APIs accept compiler-produced trees at the appropriate pipeline
867/// stage. Only continue compilation when inference and checking report no errors.
868/// Public fields support compiler transformations and inspection; they do not
869/// make arbitrary hand-built trees supported phase inputs. Transformations must
870/// preserve the compiler's type, binding, and metadata invariants.
871///
872/// In particular, every embedded [`Type`] must stay within
873/// [`crate::compiler_limits::MAX_TYPE_DEPTH`] and
874/// [`crate::compiler_limits::MAX_TYPE_NODES`], including types in parameters,
875/// declarations, shapes, captures, predicates, and runtime metadata. Inference
876/// bounds annotation resolution, inferred expression types, and substitutions
877/// before handing the tree to later phases. Replacing a payload invalidates that
878/// guarantee; [`crate::type_size::check`] checks a replacement type's bounds, not
879/// the semantic validity of the containing tree.
880///
881/// [`crate::tree_height::check_typed`] checks arena structure, not these payload
882/// invariants. Clone, formatting, and destruction can recurse through payloads,
883/// including destruction when an owning phase returns an error. Keep their
884/// bounds valid before transferring ownership, and perform phase work and
885/// destruction on a [`crate::compiler_limits::COMPILER_STACK_BYTES`] stack.
886#[derive(Default, Clone, Debug)]
887pub struct TypedAst {
888    /// Number of parameter-initialization statements before a wrapped body.
889    pub parameter_default_prologues: std::collections::BTreeMap<StmtId, usize>,
890    /// Receiver types for ordinary function expressions; arrows capture their receiver.
891    pub closure_this: std::collections::BTreeMap<ExprId, Type>,
892    /// Named function-expression bindings, scoped to their closure body.
893    pub closure_names: std::collections::BTreeMap<ExprId, Ident>,
894    /// The closures nested function declarations become, by declared name, so
895    /// a diagnostic about one can name it as the function it is.
896    pub nested_function_names: std::collections::BTreeMap<ExprId, Ident>,
897    /// Closures with empty bodies that hold a nested function's binding until
898    /// its real closure is assigned. The typechecker rejects every use before
899    /// then, so they are never called: a non-void one traps if it is.
900    pub placeholder_closures: std::collections::BTreeSet<ExprId>,
901    /// Arguments before omitted defaults and rest packing, keyed by call span.
902    pub authored_arguments: std::collections::BTreeMap<(u32, u32, u32), Vec<ExprId>>,
903    /// Parameter declaration types before optional/default ABI input widening.
904    pub authored_parameter_types: std::collections::BTreeMap<(u32, u32, u32), Type>,
905    /// Synthetic reads of incoming arguments before a parameter becomes initialized.
906    pub parameter_inputs: std::collections::BTreeSet<ExprId>,
907    /// Callee prologue statements that initialize parameters and pattern bindings.
908    pub parameter_initializations: std::collections::BTreeSet<StmtId>,
909    /// Authored expression types retained by runtime-value lowering for member
910    /// selection. Physical slot types live on the lowered expressions.
911    pub runtime_source_types: std::collections::BTreeMap<ExprId, Type>,
912    pub runtime_chain_types: std::collections::BTreeMap<ExprId, Vec<Type>>,
913    /// For each spread source read by name, the fields its value is checked
914    /// against: every field one of its object types names, with the union of
915    /// the types they give it, an index signature's value type included.
916    pub spread_mask_fields:
917        std::collections::BTreeMap<ExprId, std::collections::BTreeMap<String, Type>>,
918    /// For each spread source of an object rest, the names the rest leaves out
919    /// whatever their value.
920    pub spread_omitted_fields:
921        std::collections::BTreeMap<ExprId, std::collections::BTreeSet<String>>,
922    /// The discriminants of the `switch`es without a `default` whose cases the
923    /// typechecker found match every value the discriminant can hold.
924    pub exhaustive_switches: std::collections::BTreeSet<ExprId>,
925    /// Module name used for mangling. Defaults to `USER_PACKAGE` (`"main"`).
926    pub package_name: String,
927    exprs: Vec<TypedExpr>,
928    stmts: Vec<TypedStmt>,
929    /// Slot definitions only; initializers are `AssignGlobal` in `top_level_statements`.
930    pub globals: Vec<TypedGlobal>,
931    /// The globals code can rebind after the package loads that this package
932    /// declares or imports, each with how the source names it: a module `let`,
933    /// its own or one another package exports, and a static field not declared
934    /// `readonly`, as `Class.field`. Every static field is a `GlobalKind::Const`
935    /// global, so `globals` alone cannot tell.
936    pub rebindable_globals: std::collections::BTreeMap<MangledName, String>,
937    /// Top-level function declarations. Hoisted — codegen doesn't depend on source order.
938    pub functions: Vec<TypedFunction>,
939    /// Source-order `_start` body — currently one `AssignGlobal` per global initializer.
940    pub top_level_statements: Vec<StmtId>,
941    /// Where each top-level class is declared: the index into
942    /// `top_level_statements` of the first statement that runs after it.
943    pub class_declaration_points: std::collections::BTreeMap<MangledName, usize>,
944    /// Type declarations (`interface`, `enum`) — no Wasm representation, no body.
945    /// Stored here so post-inference passes see member spans/types without re-exposing
946    /// the inferer's `TypeNamespace`.
947    pub types: Vec<TypedTypeDecl>,
948    /// Distinct anonymous shapes (`Object`, `Array`, `Union`) reachable from the module.
949    /// Built at end of inference; consumers read this instead of re-walking the AST.
950    pub shapes: Vec<crate::Shape>,
951    /// Public surface: one entry per `export`-marked declaration. Single source of
952    /// truth for visibility — `TypedFunction`/`TypedGlobal` carry no `is_exported`
953    /// flag. Built but not yet consumed for surface projection in single-file mode.
954    pub exports: Vec<ExportEntry>,
955    /// External package names that were resolved from explicit imports in this
956    /// module. This is deliberately package-level, not symbol-level: embedders
957    /// use it to install only the package modules the typed program imports.
958    pub imported_packages: std::collections::BTreeSet<String>,
959    /// Concrete runtime validators resolved while inference still has access to
960    /// interface declarations and generic substitutions. Codegen consults this
961    /// for narrowed reads instead of re-deriving type structure.
962    pub runtime_type_tests: std::collections::BTreeMap<Type, FieldNarrowingTest>,
963    /// Substituted data fields for generic-class runtime validation.
964    pub runtime_class_fields: std::collections::BTreeMap<Type, Type>,
965    pub runtime_class_parameters: std::collections::BTreeMap<MangledName, Vec<String>>,
966    pub runtime_class_contexts: std::collections::BTreeMap<Type, Vec<InstanceTypeContext>>,
967    pub runtime_field_guards: std::collections::BTreeMap<Type, Vec<InstantiatedFieldGuard>>,
968}
969
970#[derive(Clone, Debug)]
971pub struct InstanceTypeContext {
972    pub declaration: MangledName,
973    pub args: Vec<Type>,
974}
975
976/// A public export: maps a package-public mangled name onto the internal symbol
977/// it refers to. In single-file packages `public_name == target`.
978#[derive(Clone, Debug, PartialEq)]
979pub struct ExportEntry {
980    pub public_name: MangledName,
981    pub target: MangledName,
982    pub kind: ExportKind,
983    /// Span of the `export` keyword (or re-export statement) that produced this entry.
984    pub span: Span,
985}
986
987#[derive(Clone, Copy, Debug, PartialEq, Eq)]
988pub enum ExportKind {
989    Function,
990    Global,
991    Type,
992}
993
994#[derive(Clone, Debug, PartialEq)]
995pub struct TypedGlobal {
996    pub name: Ident,
997    pub mangled_name: MangledName,
998    pub ty: Type,
999    pub kind: GlobalKind,
1000    pub doc: Option<crate::DocComment>,
1001    pub span: Span,
1002}
1003
1004/// Codegen treats both identically at the Wasm-global level; the inferer enforces the `const` write diagnostic.
1005#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1006pub enum GlobalKind {
1007    Let,
1008    Const,
1009}
1010
1011#[derive(Clone, Debug, PartialEq)]
1012pub struct TypedFunction {
1013    pub name: Ident,
1014    pub mangled_name: MangledName,
1015    /// Body is type-checked under `Type::GenericParam` instantiations then erased
1016    /// to `Type::TypeVar` for the external view.
1017    pub generics: Vec<String>,
1018    pub params: Vec<TypedParam>,
1019    pub return_type: Type,
1020    /// Type guard predicate. When set, `return_type` is always `Type::Boolean`.
1021    pub type_predicate: Option<crate::TypePredicate>,
1022    pub body: StmtId,
1023    pub doc: Option<crate::DocComment>,
1024    pub span: Span,
1025}
1026
1027/// Enums split by representation kind so the typed AST can't represent a mixed-kind enum.
1028#[derive(Clone, Debug, PartialEq)]
1029pub enum TypedTypeDecl {
1030    Interface(TypedInterfaceDecl),
1031    Class(TypedClassDecl),
1032    NumberEnum(TypedNumberEnumDecl),
1033    StringEnum(TypedStringEnumDecl),
1034    Alias(TypedTypeAliasDecl),
1035}
1036
1037/// A typechecked class. Method/constructor bodies are checked in the class-body
1038/// pass; codegen (SUB-480+) consumes the field layout, body IDs, and signatures.
1039#[derive(Clone, Debug, PartialEq)]
1040pub struct TypedClassDecl {
1041    pub name: Ident,
1042    pub fields: Vec<TypedClassField>,
1043    /// Static member visibility retained for analyses that run after package
1044    /// declarations have been reduced to their runtime surface.
1045    pub static_methods: BTreeMap<String, crate::Visibility>,
1046    /// Static field signatures are lowered to globals, but their source-level
1047    /// visibility and callable types remain relevant to public-surface analysis.
1048    pub static_fields: BTreeMap<String, crate::FieldSig>,
1049    pub constructor: Option<TypedClassConstructor>,
1050    /// The signature a class with no `constructor` of its own exposes, taken
1051    /// from the nearest ancestor that declares one with the `extends` clause's
1052    /// type arguments already substituted. Empty when `constructor` is `Some`.
1053    ///
1054    /// This is what the package declaration exports and what a cross-package
1055    /// consumer imports, so the emitted constructor must use it rather than
1056    /// re-deriving the parent's unsubstituted parameters — the two disagree the
1057    /// moment a subclass fixes a generic parent's type argument.
1058    pub inherited_ctor_params: Vec<TypedParam>,
1059    pub methods: Vec<TypedClassMethod>,
1060    /// Accessor functions (`get`/`set`), one entry each — getter and setter are
1061    /// symmetric, neither is required (a property may be get-only, set-only, or
1062    /// both). They are *not* in `methods`; codegen synthesizes the vtable method
1063    /// per entry (see `codegen::classes`).
1064    pub accessors: Vec<TypedClassAccessor>,
1065    pub extends: Option<crate::MangledName>,
1066    pub implements: Vec<crate::MangledName>,
1067    pub mangled_name: crate::MangledName,
1068    pub doc: Option<crate::DocComment>,
1069}
1070
1071/// One accessor function. Getter and setter are independent: the read type
1072/// (`ret_ty`) and write type (`param.ty`) need not match (TS 4.3+).
1073#[derive(Clone, Debug, PartialEq)]
1074pub enum TypedClassAccessor {
1075    Getter {
1076        name: Ident,
1077        ret_ty: Type,
1078        visibility: crate::Visibility,
1079        body: StmtId,
1080    },
1081    Setter {
1082        name: Ident,
1083        /// The setter parameter; `param.ty` is the property's write type.
1084        param: TypedParam,
1085        visibility: crate::Visibility,
1086        body: StmtId,
1087    },
1088}
1089
1090impl TypedClassAccessor {
1091    pub fn name(&self) -> &Ident {
1092        match self {
1093            TypedClassAccessor::Getter { name, .. } | TypedClassAccessor::Setter { name, .. } => {
1094                name
1095            }
1096        }
1097    }
1098
1099    pub fn body(&self) -> StmtId {
1100        match self {
1101            TypedClassAccessor::Getter { body, .. } | TypedClassAccessor::Setter { body, .. } => {
1102                *body
1103            }
1104        }
1105    }
1106
1107    pub fn visibility(&self) -> crate::Visibility {
1108        match self {
1109            TypedClassAccessor::Getter { visibility, .. }
1110            | TypedClassAccessor::Setter { visibility, .. } => *visibility,
1111        }
1112    }
1113}
1114
1115#[derive(Clone, Debug, PartialEq)]
1116pub struct TypedClassField {
1117    pub name: Ident,
1118    pub ty: Type,
1119    pub visibility: crate::Visibility,
1120    pub readonly: bool,
1121    pub optional: bool,
1122    pub initializer: Option<ExprId>,
1123    /// `true` for a parameter property (`constructor(public x: T)`): the field is
1124    /// assigned from the constructor parameter, so it's exempt from the
1125    /// definite-assignment check.
1126    pub auto_assigned: bool,
1127    /// Set when this declaration *narrows* an inherited one. See
1128    /// [`FieldNarrowingCheck`]. Boxed because every class field carries this and
1129    /// almost none of them narrow.
1130    pub narrowing_check: Option<Box<FieldNarrowingCheck>>,
1131    pub doc: Option<crate::DocComment>,
1132}
1133
1134/// A read guard for a field whose subclass declaration narrows the inherited
1135/// one. The two declarations share one storage slot, so a write that goes
1136/// through the parent's — an inherited method, a parent-typed reference, the
1137/// parent's constructor — can leave the slot holding a value the subclass's
1138/// declaration does not admit. The language accepts the narrowing anyway
1139/// (spec.md §Classes, matching TypeScript), so the read is what has to check.
1140///
1141/// Without this the read's bare `ref.cast` raises an uncatchable `cast failure`
1142/// naming nothing the author wrote; with it, the read runs `test` first —
1143/// presence for a narrowing that only strips `null`, structural conformance
1144/// otherwise — and throws `message` as a catchable `TypeError`.
1145#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
1146pub struct FieldNarrowingCheck {
1147    #[serde(default)]
1148    pub declaration: Option<MangledName>,
1149    pub test: FieldNarrowingTest,
1150    /// A concrete declaration type whose ancestor alternatives need only a
1151    /// presence or representation check. Other read types still validate fully.
1152    #[serde(default)]
1153    pub minimal_test_target: Option<Type>,
1154    pub message: String,
1155}
1156
1157#[derive(Clone, Debug, PartialEq)]
1158pub struct InstantiatedFieldGuard {
1159    pub field: String,
1160    pub target: Type,
1161    pub check: FieldNarrowingCheck,
1162}
1163
1164/// What a *redeclared* field's read guard verifies before it casts. The
1165/// typechecker selects the most precise test codegen can lower, falling back to
1166/// a read-time substituted test for erased class parameters.
1167#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
1168pub enum FieldNarrowingTest {
1169    /// The two declarations differ only in admitting `null`, so presence is the
1170    /// whole check. Worth its own case: the structural walk below is O(size of
1171    /// the stored value), and this is both the commonest narrowing and the one
1172    /// where walking proves nothing. It also has no shape to lower, so it guards
1173    /// types `Shape` cannot — a recursive one, an interface with methods.
1174    ///
1175    /// Whether the test actually runs is settled at the read, not here: an
1176    /// erased type parameter's `v: T` is `string | null` at `Sub<string | null>`,
1177    /// where a `null` is legal, and `string` at `Sub<string>`, where it is not.
1178    /// `emit_narrowed_field_read` asks the substituted read type and skips the
1179    /// test when it admits `null`.
1180    NonNull,
1181    /// Structural conformance to this shape, which is restricted to what
1182    /// `cast_check::emit_structural_test` can lower.
1183    Shape(Type),
1184    /// Runtime member check for an interface. Methods are read through the
1185    /// object-shape getter (which can surface class vtable slots), while data
1186    /// properties use the ordinary accessor-aware conformance walk. The method
1187    /// set is empty for a data-only interface.
1188    Interface(InterfaceNarrowingTest),
1189    /// The declaration contains an erased class type parameter, so its concrete
1190    /// runtime shape is available only at the read. Codegen tests the
1191    /// substituted `result_ty` rather than silently falling back to a bare cast.
1192    Substituted,
1193    /// Only the target Wasm representation can be established. This still
1194    /// makes the following cast safe and turns mismatches into `TypeError`.
1195    Representation,
1196}
1197
1198#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
1199pub struct InterfaceNarrowingTest {
1200    pub index: Option<crate::IndexSignature>,
1201    pub members: std::collections::BTreeMap<String, crate::ObjectField>,
1202    pub methods: std::collections::BTreeSet<String>,
1203    pub non_shape_carriers: std::collections::BTreeSet<InterfaceCarrier>,
1204    /// Whether an ordinary `$ObjectShape` may satisfy the interface. Direct
1205    /// host dispatch requires its canonical carrier; vtable interfaces remain
1206    /// structurally implementable by user objects.
1207    #[serde(default = "interface_shape_allowed_default")]
1208    pub shape_allowed: bool,
1209    pub nullable: bool,
1210    #[serde(default)]
1211    pub undefined: bool,
1212}
1213
1214fn interface_shape_allowed_default() -> bool {
1215    true
1216}
1217
1218#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
1219pub enum InterfaceCarrier {
1220    Number,
1221    Boolean,
1222    String,
1223    BigInt,
1224    Uint8Array,
1225    ArrayAny,
1226    Array(Type),
1227    MapAny,
1228    Map(Type, Type),
1229    SetAny,
1230    Set(Type),
1231    RegExp,
1232    RegExpMatch,
1233    TemporalInstant,
1234    TemporalDuration,
1235    TemporalZonedDateTime,
1236    TemporalPlainDate,
1237    TemporalPlainTime,
1238    TemporalPlainDateTime,
1239    TemporalPlainYearMonth,
1240    TemporalPlainMonthDay,
1241    ObjectShape,
1242    Url,
1243    FsStat,
1244    FsPeek,
1245    FsDirEntry,
1246    FsInfo,
1247    FsMountInfo,
1248    FsFileWriter,
1249    HttpResponse,
1250    HttpDownloadResult,
1251    SessionEntry,
1252    SessionPage,
1253}
1254
1255/// Whether codegen can validate every value admitted by `ty` with
1256/// `cast_check::emit_structural_test`. Keeping the allowlist beside the
1257/// serialized narrowing-test model gives typechecking and codegen one answer.
1258pub(crate) fn runtime_type_is_testable(ty: &Type) -> bool {
1259    runtime_type_is_testable_inner(ty, false)
1260}
1261
1262/// Field redeclaration guards can root generated validators at recursive alias
1263/// or interface back-edges. General expression descriptors cannot: polymorphic
1264/// recursion can grow its type arguments forever while enumerating every
1265/// expression type.
1266pub(crate) fn field_runtime_type_is_testable(ty: &Type) -> bool {
1267    runtime_type_is_testable_inner(ty, true)
1268}
1269
1270fn runtime_type_is_testable_inner(ty: &Type, allow_recursive_ref: bool) -> bool {
1271    match ty.peel() {
1272        Type::Null
1273        | Type::Undefined
1274        | Type::Void
1275        | Type::Number
1276        | Type::NumberLiteral(_)
1277        | Type::Boolean
1278        | Type::BooleanLiteral(_)
1279        | Type::String
1280        | Type::StringLiteral(_)
1281        | Type::BigInt
1282        | Type::Uint8Array
1283        | Type::Unknown
1284        | Type::Function { .. }
1285        | Type::NumberEnum { .. }
1286        | Type::StringEnum { .. }
1287        | Type::ClassRef { .. } => true,
1288        Type::AliasRef { .. } | Type::InterfaceRef { .. } => allow_recursive_ref,
1289        Type::Array(elem) => runtime_type_is_testable_inner(elem, allow_recursive_ref),
1290        Type::Tuple(elems) => elems
1291            .iter()
1292            .all(|elem| runtime_type_is_testable_inner(elem, allow_recursive_ref)),
1293        Type::Union(elems) => elems
1294            .iter()
1295            .all(|elem| runtime_type_is_testable_inner(elem, allow_recursive_ref)),
1296        Type::Object { fields, index } => {
1297            index
1298                .as_ref()
1299                .is_none_or(|i| runtime_type_is_testable_inner(&i.value, allow_recursive_ref))
1300                && fields
1301                    .values()
1302                    .all(|field| runtime_type_is_testable_inner(&field.ty, allow_recursive_ref))
1303        }
1304        _ => false,
1305    }
1306}
1307
1308impl TypedClassDecl {
1309    /// The constructor signature this class exposes: its own if it declares
1310    /// one, otherwise the inherited signature. Callers should prefer this over
1311    /// reading either field directly — `inherited_ctor_params` is meaningful
1312    /// only when `constructor` is `None`.
1313    pub fn effective_ctor_params(&self) -> &[TypedParam] {
1314        match &self.constructor {
1315            Some(c) => &c.params,
1316            None => &self.inherited_ctor_params,
1317        }
1318    }
1319}
1320
1321#[derive(Clone, Debug, PartialEq)]
1322pub struct TypedClassConstructor {
1323    pub doc: Option<Box<crate::DocComment>>,
1324    pub span: Span,
1325    pub params: Vec<TypedParam>,
1326    pub body: StmtId,
1327}
1328
1329#[derive(Clone, Debug, PartialEq)]
1330pub struct TypedClassMethod {
1331    pub name: Ident,
1332    pub generics: Vec<String>,
1333    pub params: Vec<TypedParam>,
1334    pub return_type: Type,
1335    pub body: StmtId,
1336    pub visibility: crate::Visibility,
1337    pub doc: Option<crate::DocComment>,
1338}
1339
1340#[derive(Clone, Debug, PartialEq)]
1341pub struct TypedInterfaceDecl {
1342    /// Complete property names, including inherited properties, for payload validation.
1343    pub property_names: std::collections::BTreeSet<String>,
1344    pub index: Option<crate::IndexSignature>,
1345    pub name: Ident,
1346    pub generics: Vec<String>,
1347    pub members: Vec<TypedInterfaceMember>,
1348    pub doc: Option<crate::DocComment>,
1349}
1350
1351#[derive(Clone, Debug, PartialEq)]
1352pub enum TypedInterfaceMember {
1353    Method {
1354        name: Ident,
1355        generics: Vec<String>,
1356        params: Vec<TypedParam>,
1357        return_type: Type,
1358        doc: Option<crate::DocComment>,
1359    },
1360    Property {
1361        name: Ident,
1362        ty: Type,
1363        readonly: bool,
1364        /// Reads widen to `T | null`; may be omitted at construction.
1365        optional: bool,
1366        doc: Option<crate::DocComment>,
1367    },
1368}
1369
1370#[derive(Clone, Debug, PartialEq)]
1371pub struct TypedNumberEnumDecl {
1372    pub name: Ident,
1373    pub members: Vec<TypedNumberEnumMember>,
1374    pub doc: Option<crate::DocComment>,
1375}
1376
1377#[derive(Clone, Debug, PartialEq)]
1378pub struct TypedNumberEnumMember {
1379    pub name: Ident,
1380    pub value: f64,
1381    pub doc: Option<crate::DocComment>,
1382}
1383
1384#[derive(Clone, Debug, PartialEq)]
1385pub struct TypedStringEnumDecl {
1386    pub name: Ident,
1387    pub members: Vec<TypedStringEnumMember>,
1388    pub doc: Option<crate::DocComment>,
1389}
1390
1391#[derive(Clone, Debug, PartialEq)]
1392pub struct TypedStringEnumMember {
1393    pub name: Ident,
1394    pub value: String,
1395    pub doc: Option<crate::DocComment>,
1396}
1397
1398/// `ty` is fully resolved; `Type::TypeVar(name)` placeholders correspond to `generics` entries.
1399#[derive(Clone, Debug, PartialEq)]
1400pub struct TypedTypeAliasDecl {
1401    pub name: Ident,
1402    pub generics: Vec<String>,
1403    pub ty: Type,
1404    pub doc: Option<crate::DocComment>,
1405}
1406
1407impl TypedAst {
1408    pub(crate) fn has_string_index(&self, ty: &Type) -> bool {
1409        match ty.peel() {
1410            Type::Object { index, .. } => index.is_some(),
1411            Type::Union(members) => members.iter().any(|member| self.has_string_index(member)),
1412            Type::InterfaceRef { .. } => matches!(
1413                self.runtime_type_tests.get(ty.peel()),
1414                Some(FieldNarrowingTest::Interface(interface)) if interface.index.is_some()
1415            ),
1416            _ => false,
1417        }
1418    }
1419
1420    pub fn new() -> Self {
1421        Self {
1422            package_name: crate::mangle::USER_PACKAGE.to_string(),
1423            ..Self::default()
1424        }
1425    }
1426
1427    pub fn with_package(package_name: impl Into<String>) -> Self {
1428        Self {
1429            package_name: package_name.into(),
1430            ..Self::default()
1431        }
1432    }
1433
1434    pub fn record_authored_arguments(&mut self, span: Span, args: Vec<ExprId>) {
1435        self.authored_arguments
1436            .insert((span.file.0, span.start, span.end), args);
1437    }
1438
1439    pub fn authored_call_arguments(&self, span: Span) -> Option<&Vec<ExprId>> {
1440        self.authored_arguments
1441            .get(&(span.file.0, span.start, span.end))
1442    }
1443
1444    /// Checked allocation; failure leaves the arena unchanged.
1445    pub fn try_push_expr(&mut self, expr: TypedExpr) -> Result<ExprId, ArenaError> {
1446        arena::push(&mut self.exprs, expr, ArenaKind::TypedExpressions).map(ExprId)
1447    }
1448
1449    /// Checked allocation; failure leaves the arena unchanged.
1450    pub fn try_push_stmt(&mut self, stmt: TypedStmt) -> Result<StmtId, ArenaError> {
1451        arena::push(&mut self.stmts, stmt, ArenaKind::TypedStatements).map(StmtId)
1452    }
1453
1454    pub fn try_expr(&self, id: ExprId) -> Result<&TypedExpr, ArenaError> {
1455        arena::get(&self.exprs, id.0, ArenaKind::TypedExpressions)
1456    }
1457
1458    pub fn try_stmt(&self, id: StmtId) -> Result<&TypedStmt, ArenaError> {
1459        arena::get(&self.stmts, id.0, ArenaKind::TypedStatements)
1460    }
1461
1462    pub fn try_expr_mut(&mut self, id: ExprId) -> Result<&mut TypedExpr, ArenaError> {
1463        arena::get_mut(&mut self.exprs, id.0, ArenaKind::TypedExpressions)
1464    }
1465
1466    pub fn try_stmt_mut(&mut self, id: StmtId) -> Result<&mut TypedStmt, ArenaError> {
1467        arena::get_mut(&mut self.stmts, id.0, ArenaKind::TypedStatements)
1468    }
1469
1470    /// Snapshot of allocated expression IDs, usable while appending new nodes.
1471    pub fn expr_ids(&self) -> Result<impl DoubleEndedIterator<Item = ExprId> + use<>, ArenaError> {
1472        Ok(arena::ids(self.exprs.len(), ArenaKind::TypedExpressions)?.map(ExprId))
1473    }
1474
1475    /// Snapshot of allocated statement IDs, usable while appending new nodes.
1476    pub fn stmt_ids(&self) -> Result<impl DoubleEndedIterator<Item = StmtId> + use<>, ArenaError> {
1477        Ok(arena::ids(self.stmts.len(), ArenaKind::TypedStatements)?.map(StmtId))
1478    }
1479
1480    /// Body `StmtId`s of every class constructor + method — additional codegen
1481    /// roots alongside `functions` (the string/bigint/closure/box collection
1482    /// passes must walk these too).
1483    pub fn class_body_roots(&self) -> Vec<StmtId> {
1484        let mut roots = Vec::new();
1485        for decl in &self.types {
1486            if let TypedTypeDecl::Class(c) = decl {
1487                if let Some(ctor) = &c.constructor {
1488                    roots.push(ctor.body);
1489                }
1490                roots.extend(c.methods.iter().map(|m| m.body));
1491                roots.extend(c.accessors.iter().map(TypedClassAccessor::body));
1492            }
1493        }
1494        roots
1495    }
1496
1497    /// Field-initializer expressions across all classes — `expr` roots that live
1498    /// outside any statement body, so analysis/erasure passes must visit them too.
1499    pub fn class_field_initializers(&self) -> Vec<ExprId> {
1500        let mut inits = Vec::new();
1501        for decl in &self.types {
1502            if let TypedTypeDecl::Class(c) = decl {
1503                inits.extend(c.fields.iter().filter_map(|f| f.initializer));
1504            }
1505        }
1506        inits
1507    }
1508
1509    pub fn source_type(&self, id: ExprId) -> Result<&Type, ArenaError> {
1510        let expr = self.try_expr(id)?;
1511        Ok(self.runtime_source_types.get(&id).unwrap_or(&expr.ty))
1512    }
1513
1514    /// Used by post-inference passes to iterate just-pushed IDs (e.g. GenericParam erasure).
1515    pub fn exprs_len(&self) -> usize {
1516        self.exprs.len()
1517    }
1518
1519    pub fn stmts_len(&self) -> usize {
1520        self.stmts.len()
1521    }
1522
1523    /// Whether evaluating `id` can be skipped without changing what the program
1524    /// does. Deliberately a short whitelist of leaves: everything else — a call,
1525    /// a field read that may hit an accessor, an operator that may throw — says
1526    /// `false`, so a caller that folds an expression's value away and relies on
1527    /// this to decide whether to keep the computation errs toward keeping it.
1528    pub fn is_effect_free(&self, id: ExprId) -> Result<bool, ArenaError> {
1529        Ok(matches!(
1530            self.try_expr(id)?.kind,
1531            TypedExprKind::Number(_)
1532                | TypedExprKind::BigInt(_)
1533                | TypedExprKind::String(_)
1534                | TypedExprKind::Boolean(_)
1535                | TypedExprKind::Null
1536                | TypedExprKind::Undefined
1537                | TypedExprKind::This
1538                | TypedExprKind::Regex { .. }
1539                | TypedExprKind::LocalRef { .. }
1540                | TypedExprKind::LocalNarrowRef { .. }
1541                | TypedExprKind::GlobalRef { .. }
1542                | TypedExprKind::FunctionRef { .. }
1543                | TypedExprKind::NumberEnumMember { .. }
1544                | TypedExprKind::StringEnumMember { .. }
1545        ))
1546    }
1547}
1548
1549#[cfg(test)]
1550mod tests {
1551    use super::{TypedAst, TypedExpr, TypedExprKind, TypedParam, TypedStmt, TypedStmtKind};
1552    use crate::{BinOp, Ident, Span, Type, UnOp};
1553
1554    #[test]
1555    fn arena_round_trip_for_typed_exprs() {
1556        let mut ast = TypedAst::new();
1557        let id = ast
1558            .try_push_expr(TypedExpr {
1559                kind: TypedExprKind::Number(42.0),
1560                span: Span::new(crate::FileId(0), 0, 2).unwrap(),
1561                ty: Type::Number,
1562            })
1563            .unwrap();
1564        assert_eq!(id.0, 0);
1565        let e = ast.try_expr(id).unwrap();
1566        assert_eq!(e.kind, TypedExprKind::Number(42.0));
1567        assert_eq!(e.span, Span::new(crate::FileId(0), 0, 2).unwrap());
1568        assert_eq!(e.ty, Type::Number);
1569    }
1570
1571    #[test]
1572    fn arena_round_trip_for_typed_stmts() {
1573        let mut ast = TypedAst::new();
1574        let expr_id = ast
1575            .try_push_expr(TypedExpr {
1576                kind: TypedExprKind::Boolean(true),
1577                span: Span::new(crate::FileId(0), 0, 4).unwrap(),
1578                ty: Type::Boolean,
1579            })
1580            .unwrap();
1581        let stmt_id = ast
1582            .try_push_stmt(TypedStmt {
1583                kind: TypedStmtKind::Expr(expr_id),
1584                span: Span::new(crate::FileId(0), 0, 5).unwrap(),
1585            })
1586            .unwrap();
1587        assert_eq!(stmt_id.0, 0);
1588        let s = ast.try_stmt(stmt_id).unwrap();
1589        assert_eq!(s.span, Span::new(crate::FileId(0), 0, 5).unwrap());
1590        assert!(matches!(s.kind, TypedStmtKind::Expr(_)));
1591    }
1592
1593    #[test]
1594    fn build_typed_binary_tree() {
1595        let mut ast = TypedAst::new();
1596        let lhs = ast
1597            .try_push_expr(TypedExpr {
1598                kind: TypedExprKind::Number(1.0),
1599                span: Span::new(crate::FileId(0), 0, 1).unwrap(),
1600                ty: Type::Number,
1601            })
1602            .unwrap();
1603        let rhs = ast
1604            .try_push_expr(TypedExpr {
1605                kind: TypedExprKind::Number(2.0),
1606                span: Span::new(crate::FileId(0), 4, 5).unwrap(),
1607                ty: Type::Number,
1608            })
1609            .unwrap();
1610        let sum = ast
1611            .try_push_expr(TypedExpr {
1612                kind: TypedExprKind::Binary {
1613                    op: BinOp::Add,
1614                    lhs,
1615                    rhs,
1616                },
1617                span: Span::new(crate::FileId(0), 0, 5).unwrap(),
1618                ty: Type::Number,
1619            })
1620            .unwrap();
1621
1622        let outer = ast.try_expr(sum).unwrap();
1623        assert_eq!(outer.ty, Type::Number);
1624        let TypedExprKind::Binary { op, lhs, rhs } = outer.kind else {
1625            panic!("expected Binary");
1626        };
1627        assert_eq!(op, BinOp::Add);
1628        assert_eq!(ast.try_expr(lhs).unwrap().ty, Type::Number);
1629        assert_eq!(ast.try_expr(rhs).unwrap().ty, Type::Number);
1630    }
1631
1632    #[test]
1633    fn build_typed_function_with_return() {
1634        let mut ast = TypedAst::new();
1635        let one = ast
1636            .try_push_expr(TypedExpr {
1637                kind: TypedExprKind::Number(1.0),
1638                span: Span::new(crate::FileId(0), 31, 32).unwrap(),
1639                ty: Type::Number,
1640            })
1641            .unwrap();
1642        let ret = ast
1643            .try_push_stmt(TypedStmt {
1644                kind: TypedStmtKind::Return(Some(one)),
1645                span: Span::new(crate::FileId(0), 24, 33).unwrap(),
1646            })
1647            .unwrap();
1648        let body = ast
1649            .try_push_stmt(TypedStmt {
1650                kind: TypedStmtKind::Block(vec![ret]),
1651                span: Span::new(crate::FileId(0), 22, 35).unwrap(),
1652            })
1653            .unwrap();
1654        ast.functions.push(crate::TypedFunction {
1655            name: Ident {
1656                name: "f".to_string(),
1657                span: Span::new(crate::FileId(0), 9, 10).unwrap(),
1658            },
1659            mangled_name: crate::mangle::package_symbol("main", "f"),
1660            generics: vec![],
1661            params: vec![],
1662            return_type: Type::Number,
1663            type_predicate: None,
1664            body,
1665            doc: None,
1666            span: Span::new(crate::FileId(0), 0, 35).unwrap(),
1667        });
1668
1669        let f = &ast.functions[0];
1670        assert_eq!(f.return_type, Type::Number);
1671        assert!(matches!(
1672            ast.try_stmt(f.body).unwrap().kind,
1673            TypedStmtKind::Block(_)
1674        ));
1675    }
1676
1677    #[test]
1678    fn clone_and_equality() {
1679        let kind = TypedExprKind::Unary {
1680            op: UnOp::Neg,
1681            operand: crate::ExprId(0),
1682        };
1683        assert_eq!(kind.clone(), kind);
1684    }
1685
1686    #[test]
1687    fn typed_param_construction() {
1688        let p = TypedParam {
1689            optional: false,
1690            name: Ident {
1691                name: "param".to_string(),
1692                span: Span::new(crate::FileId(0), 10, 15).unwrap(),
1693            },
1694            ty: Type::String,
1695            boxed: false,
1696            rest: false,
1697            default: None,
1698        };
1699        assert_eq!(p.name.name, "param");
1700        assert_eq!(p.name.span, Span::new(crate::FileId(0), 10, 15).unwrap());
1701        assert_eq!(p.ty, Type::String);
1702        assert!(!p.boxed);
1703    }
1704
1705    #[test]
1706    fn local_ref_carries_name_and_boxed_flag() {
1707        let mut ast = TypedAst::new();
1708        let id = ast
1709            .try_push_expr(TypedExpr {
1710                kind: TypedExprKind::LocalRef {
1711                    ident: Ident {
1712                        name: "x".to_string(),
1713                        span: Span::new(crate::FileId(0), 0, 1).unwrap(),
1714                    },
1715                    boxed: false,
1716                },
1717                span: Span::new(crate::FileId(0), 0, 1).unwrap(),
1718                ty: Type::Number,
1719            })
1720            .unwrap();
1721        match ast.try_expr(id).unwrap().kind {
1722            TypedExprKind::LocalRef { ref ident, boxed } => {
1723                assert_eq!(ident.name, "x");
1724                assert_eq!(ident.span, Span::new(crate::FileId(0), 0, 1).unwrap());
1725                assert!(!boxed);
1726            }
1727            _ => panic!("expected LocalRef"),
1728        }
1729    }
1730
1731    #[test]
1732    fn global_ref_carries_name_and_mangled() {
1733        let mut ast = TypedAst::new();
1734        let mangled = crate::mangle::package_symbol("main", "y");
1735        let id = ast
1736            .try_push_expr(TypedExpr {
1737                kind: TypedExprKind::GlobalRef {
1738                    mangled: mangled.clone(),
1739                    name: Ident {
1740                        name: "y".to_string(),
1741                        span: Span::new(crate::FileId(0), 2, 3).unwrap(),
1742                    },
1743                },
1744                span: Span::new(crate::FileId(0), 2, 3).unwrap(),
1745                ty: Type::Number,
1746            })
1747            .unwrap();
1748        match ast.try_expr(id).unwrap().kind {
1749            TypedExprKind::GlobalRef {
1750                ref name,
1751                ref mangled,
1752            } => {
1753                assert_eq!(name.name, "y");
1754                assert_eq!(name.span, Span::new(crate::FileId(0), 2, 3).unwrap());
1755                assert_eq!(mangled.as_str(), "main#y");
1756            }
1757            _ => panic!("expected GlobalRef"),
1758        }
1759    }
1760}