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