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