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