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