Skip to main content

submilli_engine/
typed_ast.rs

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