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