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