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