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