# TypeScript Code Generation

This document defines the TypeScript code generation target.

## Kind

The target kind identifier is `typescript`.

## Options

The TypeScript target reads the following options from its `options` mapping in the project configuration:

- `storage: STRING` selects the master-data backend baked into the generated package. Optional; defaults to `memory`. Accepted values are `memory` (the in-memory executor that consumes records supplied through the `MasterData` constructor / `loadJson`) and `sql` (a SQL-backed executor that translates queries into SQL against a host-supplied `SQLiteDatabase` adapter — see [Master Data](#master-data)).

A `storage` value other than `memory` or `sql` is a configuration error. Unknown options are silently ignored to leave room for future extension.

## File Set

For a project whose lowered IR modules are `m1.mst`, `m2.mst`, ..., the TypeScript target produces:

- One generated file per Masterbelt module. The file name is the module name with the `.mst` suffix replaced by `.ts`. Each file contains the TypeScript declarations corresponding to that module's constants.
- A `masterbelt_masterdata.ts` file when the project declares at least one master. The file declares the `MasterData` class described in [Master Data](#master-data). The file is omitted when the project declares no master.
- A `masterbelt_query.ts` file when the project declares at least one master. The file declares `MasterSource`, `FieldRef`, the `Predicate<R>` / `Ordering<R>` interfaces, the exported concrete predicate / ordering classes that back the inspectable AST, the `and` / `or` / `not` combinators, the generic field-handle classes (`OrderedField<R, V>`, `BoolField<R>`), the `QueryPlan<R>` value class, and the `executePlan<R>` executor used by every generated relation terminal; see [Master Data](#master-data). The file is omitted when the project declares no master.

Native TypeScript union types remove the need for a separate sealed-interface file. Future generator-managed files added to this target MUST use the reserved `masterbelt_` file name prefix to avoid collisions with user-named modules.

## Module Declaration

Each generated file is a TypeScript ES module. Public declarations carry the `export` keyword.

## Reachability

The TypeScript target follows the default reachability policy defined in `codegen/model`: only constants reachable from at least one `pub`-declared constant are emitted. Identifier references resolve against the surviving set, so a `pub const A = helper` declaration keeps `helper` even though `helper` itself is not public.

## Visibility

A Masterbelt constant declared with `pub` is emitted with the `export` keyword. A non-public constant is emitted without it. Identifier names are preserved verbatim because TypeScript's visibility is expressed by the keyword, not by the identifier's case.

## Constants

Each Masterbelt const item maps to one TypeScript `const` declaration. Doc comment lines are emitted as `// <line>` comments immediately before the declaration in source order.

## Type Mapping

| Masterbelt        | TypeScript                                                |
|-------------------|-----------------------------------------------------------|
| `null`            | `null`                                                    |
| `bool`            | `boolean`                                                 |
| `int`, `uint`, `int8`, `uint8`, `int16`, `uint16`, `int32`, `uint32`, `int64`, `uint64` | `number` (TypeScript's only numeric type; integers above 2^53 lose precision) |
| `string`          | `string`                                                  |
| `list<T>`         | `readonly T'[]` where `T'` is the mapping of `T`          |
| `map<string, V>`  | `Readonly<Record<string, V'>>`                            |
| `map<K, V>`       | unsupported at this stage; reports a generation error     |
| `T1 \| T2 \| ...` | A native TypeScript union of each member's mapping        |
| `{f: T, ...}`     | `{ readonly field: T'; ... }` structural object type      |
| `fn(p: T, ...): R`| Arrow type `(p: T', ...) => R'` (see Function Types below)|
| `enum Name { ... }`| `[export] const enum Name { Variant = value, ... }` (see Enums below) |

Type declarations are resolved before mapping; declared types do not appear in generated code.

The TypeScript target restricts map key types to `string` for this iteration. Other key types do not have a clean object-literal representation in TypeScript and require a `ReadonlyMap`-shaped runtime; they will be added in a follow-up.

## Literal Mapping

- `null` lowers to the literal `null`.
- `true` and `false` lower to the literals `true` and `false`.
- An integer literal lowers to its decoded value rendered in base 10.
- A string literal lowers to a TypeScript double-quoted string with TypeScript escape sequences.
- A list literal `[e1, ..., eN]` lowers to `[e1', ..., eN']`.
- A map literal of type `map<string, V>` lowers to an object literal `{ "k1": v1, ..., "kN": vN }`, with keys quoted and entries emitted in lowering order after last-wins deduplication.
- An identifier reference lowers to the referent constant's identifier.
- A product literal lowers to a TypeScript object literal `{ name: value, ... }`. Field names appear as bare identifiers and entries preserve the source order of the literal.

## Master Data

A `master Foo { record { ... } static { ... } }` declaration follows the runtime model defined in [../masterdata/schema.md](../masterdata/schema.md#runtime-model). The TypeScript target emits per master:

- `<Master>Record` — a TypeScript `type` alias that backs one row, following the regular [Product Types](#product-types) rules.
- `<Master>Relation` — an exported TypeScript `class` that exposes the master's chainable query surface. The class is data-less: it carries a single `private readonly plan: QueryPlan<<Master>Record>` field with a default constructor that builds a fresh plan and an optional `plan?` constructor argument for copy-on-write chaining. Stage methods (`where`, `orderBy`, `thenBy`, `skip`, `take`) return a freshly-allocated `<Master>Relation` whose plan extends the receiver's plan; the receiver is never mutated. Terminals (`toArray`, `findBy`, `firstOrDefault`, `count`, `any`) take `(data: MasterData, signal: AbortSignal)` (plus primary-key arguments for `findBy`) and resolve records through the master's `Executor` obtained from `data.getExecutor<Master>()`. Every terminal carries the `asyncable`, `cancellable`, and `failable` effects per [../masterdata/schema.md](../masterdata/schema.md#runtime-model) — the TypeScript target renders the `async`/`Promise<R>` wrap (asyncable) and the trailing `signal: AbortSignal` parameter (cancellable); failure surfaces through a thrown exception (failable). Each user-declared static method becomes an instance method on the class; it always takes `(data: MasterData, ...declared args, signal: AbortSignal)`.
- Module-level entrypoint: `export const <master>: <Master>Relation = new <Master>Relation();`. Users write `items.where(...).toArray(data, signal)` against the const directly.

Both declarations live as siblings at module scope. The Masterbelt source identifier `<Master>` lowers to the module-level `<master>` const named after the master in camelCase.

Nested masters follow the same naming scheme on the flattened identifier — `master User { master Friendships { ... } }` emits `UserFriendshipsRecord`, `UserFriendshipsRelation`, and a module-level `userFriendships` const as siblings of the parent. See [Nested Masters](../masterdata/schema.md#nested-masters).

### MasterData Entry

A project that declares at least one master emits one `MasterData` class declaration. The class lives in a generator-managed file named `masterbelt_masterdata.ts`. It declares:

- One `private readonly <master>Records: readonly <Master>Record[]` field per master (lowerCamelCase) storing the per-master record set.
- A constructor `constructor(items: readonly <Items>Record[], users: readonly <Users>Record[], ...)` that takes one record array per master in master-declaration order and assigns each to the matching field.
- One `getExecutor<Master>(): Executor<<Master>Record>` accessor per master. Every generated relation terminal calls `data.getExecutor<Master>()` to reach the active backend, then invokes `execute(plan, signal)` / `findByPK(plan, keys, signal)` on the returned executor. Under `storage: memory` the accessor returns a `MemoryExecutor` that wraps the master's record array (and the `matches<Master>PK` closure when the master declares a primary key).

The TypeScript target never writes `MasterData` or its construction in code emitted from a Masterbelt source program: those identifiers exist for the host application to construct and inject the dataset. MasterData stores records — not relations — because every generated relation is a data-less plan; the dataset is supplied at every terminal call site.

When `storage: sql` is configured, the layout above changes:

- `MasterData` carries a single `private readonly database: SQLiteDatabase` adapter shared by every per-master executor; the per-master record arrays are not emitted.
- The constructor takes the `SQLiteDatabase` instead of record arrays. The host application wraps its preferred SQLite binding (`better-sqlite3`, `node:sqlite`, `bun:sqlite`, `sql.js`, ...) in the `SQLiteDatabase` interface (exported from `masterbelt_query.ts`) and passes it.
- `getExecutor<Master>()` returns a per-master `<Master>SqlExecutor` (emitted alongside the relation in the master's module file) that translates the relation's `QueryPlan` into SQL via `translatePlan` / `translatePKLookup`, runs it through the adapter, and materialises rows with the generated `scan<Master>Row` function.
- `loadJSON` is not emitted under `storage: sql`. Hosts that want to seed a SQL backend from the JSON exporter should round-trip the data through the SQLite exporter (see [../masterdata/export-sqlite.md](../masterdata/export-sqlite.md)) and open the resulting database.

The `SQLiteDatabase` / `SQLiteRow` adapter interfaces, the `Executor<R>` / `MemoryExecutor<R>` seam, and the `translatePlan` / `translatePKLookup` helpers all live in `masterbelt_query.ts`.

The generated core never imports a concrete SQLite binding. The host wraps its runtime's binding in the `SQLiteDatabase` interface. The supported example adapter wraps Node's built-in `node:sqlite` (Node 22+); other bindings (`better-sqlite3`, `bun:sqlite`, `sql.js`) follow the same shape:

```ts
import { DatabaseSync } from "node:sqlite";
import type { SQLiteDatabase, SQLiteRow } from "./masterbelt_query";

export function nodeSqliteAdapter(db: DatabaseSync): SQLiteDatabase {
  return {
    all(sql, params): readonly SQLiteRow[] {
      return db.prepare(sql).all(...params) as SQLiteRow[];
    },
    get(sql, params): SQLiteRow | undefined {
      return db.prepare(sql).get(...params) as SQLiteRow | undefined;
    },
  };
}

// const data = new MasterData(nodeSqliteAdapter(new DatabaseSync("masterdata.db")));
```

The host constructs the SQL-backed dataset with `new MasterData(adapter)` (the same constructor entry the memory backend uses, with the adapter in place of record arrays) and threads it into every terminal call as the first argument.

The same file also declares `loadJSON(data: string | object): MasterData`, a helper that consumes the JSON document produced by the JSON exporter (see [../masterdata/export-json.md](../masterdata/export-json.md)) and returns a freshly wired `MasterData`. The helper uses the platform `JSON.parse` only and is independent of any backend library:

```ts
export function loadJSON(data: string | object): MasterData {
  const raw = typeof data === "string" ? JSON.parse(data) : data;
  return new MasterData(
    (raw.items ?? []) as ItemsRecord[],
    (raw.userFriendships ?? []) as UserFriendshipsRecord[],
    // ... one per master in declaration order
  );
}
```

Each generated `<Master>Record` type alias keeps its field names verbatim from source so the structural cast above is sufficient: TypeScript's structural typing makes a plain object with the right field shape interchangeable with one produced by a typed constructor. No decorators or runtime metadata are emitted on the record type itself.

### Static Body Rewrites

A user-declared static method's body is rewritten so the planner-side master references resolve against the module-level relation const values and the threaded dataset:

- `Master.toList()` inside the owning master's own static body lowers to `this.toArray(data, signal)`. The receiver is the data-less relation value the static method was invoked on, so a caller that chained stages before invoking the static observes those stages.
- `Master.X` (any user-declared constant or method) inside the same owning master's body lowers to `this.X(data, ...args, signal)`.
- `OtherMaster.toList()` lowers to `<otherMaster>.toArray(data, signal)` against the module-level const, automatically imported when the call appears in a different module from where the master is declared.
- `OtherMaster.X` (any other cross-master reference) lowers to `<otherMaster>.X(data, ...args, signal)`.

Every relation method, including user-declared statics and constants, accepts `(data: MasterData, ...declared args, signal: AbortSignal)` uniformly so the call-site rewrite stays straightforward; methods that do not declare a body-level need for either argument still take both.

### Top-Level Dataset Threading

A top-level function or constant that transitively reaches any master static member (constant or method, including the built-in `toList()`) acquires the dataset as an explicit parameter: it receives `data: MasterData` as its first positional parameter and `signal: AbortSignal` as a trailing parameter, and forwards both to every call that also requires them. The Masterbelt source program never writes the parameters.

The dataset-threading parameter is added before any other parameters declared by the function. It composes with the effect-driven signature transforms (`cancellable` ensures `signal: AbortSignal` is appended, `asyncable` wraps the return in `Promise<R>`) without further interaction.

### Query API

Every master emits the chainable surface directly on `<Master>Relation`. The TypeScript target uses a callback style: `where`, `orderBy`, and `thenBy` accept a callback that receives a typed `<Master>Fields` builder and returns a predicate or ordering AST. See [../masterdata/query.md](../masterdata/query.md) for the cross-target contract.

The runtime types live in the generator-managed `masterbelt_query.ts` file (one file per project, emitted whenever the project declares at least one master):

- `MasterSource` — a class with a single `readonly name` field identifying a master by its source-level name.
- `FieldRef` — a class with a single `readonly name` field identifying a record field by its source-level name.
- `Predicate<R>` — `interface { evaluate(record: R): boolean }`. Parametrised by the record type so a predicate built for one master cannot be passed to another's `where` callback; the structural check fails on `R`.
- `Ordering<R>` — `interface { compare(a: R, b: R): number }`.
- Concrete predicate / ordering classes (`EqPredicate<R, V>`, `NePredicate`, `LtPredicate`, `LePredicate`, `GtPredicate`, `GePredicate`, `InPredicate`, `BetweenPredicate`, `BoolEqPredicate<R>`, `BoolNePredicate<R>`, `BoolInPredicate<R>`, `AndPredicate<R>`, `OrPredicate<R>`, `NotPredicate<R>`, `AscOrdering<R, V>`, `DescOrdering<R, V>`) that carry the operator-relevant metadata (`field`, `value`, `low` / `high`, `operands`) as public readonly fields so a backend can translate the node to SQL without invoking the per-record accessor.
- `and<R>(...preds: readonly Predicate<R>[]): Predicate<R>`, `or<R>(...): Predicate<R>`, `not<R>(p: Predicate<R>): Predicate<R>` — combinators over a single record type. A mixed-record composition is a compile-time error.
- `OrderedField<R, V extends Ordered>` and `BoolField<R>` — generic field-handle classes whose constructors take `(name: string, accessor: (record: R) => V)` so each node embeds the source-level field name into its `FieldRef`. Their comparison methods return the concrete predicate / ordering classes specialised to the owning record type.
- `QueryPlan<R>` — the inspectable AST value class wrapping `source`, `predicates`, `orderings`, `skip`, and `take`. Stage helpers (`withWhere`, `withOrderBy`, `withThenBy`, `withSkip`, `withTake`) return a new plan; the original is never mutated.
- `executePlan<R>(records, plan)` — the shared in-memory evaluator used by every generated `<Master>Relation` terminal.
- `stableSort<R>` — internal helper used by `executePlan`.

The per-master user-facing declarations are:

- `<Master>Fields` — an exported TypeScript `class` exposing one typed field-handle property per supported record field. Field handles for ordered values (numeric, string) expose `eq`, `ne`, `lt`, `le`, `gt`, `ge`, `in`, `between`, `asc`, and `desc`; field handles for bool values expose `eq`, `ne`, and `in` only. Each handle's comparison method returns the concrete predicate / ordering class specialised to the owning master, statically typed as `Predicate<<Master>Record>` / `Ordering<<Master>Record>` for the callback's return type.
- `<Master>Relation` — see [Master Data](#master-data) for the relation class itself. Its `where` / `orderBy` / `thenBy` callbacks are typed `(fields: <Master>Fields) => Predicate<<Master>Record>` / `Ordering<<Master>Record>`, so the compiler rejects a predicate built against a different master at the call site.

```ts
import { and } from "./masterbelt_query";
import { items } from "./data";

const weapons = await items
  .where(item => and(item.category.eq("weapon"), item.level.ge(10)))
  .orderBy(item => item.sortOrder.asc())
  .take(10)
  .toArray(data, signal);
```

### Scope Methods

Each `pub` [scope](../masterdata/schema.md#scope-section) on a master emits an instance method on the `<Master>Relation` class; a non-`pub` scope is internal to Masterbelt source and is not emitted. The method name is the source scope name verbatim in camelCase (`genderedAdult` stays `genderedAdult`). Its parameters are the scope's declared parameters mapped through the regular [Type Mapping](#type-mapping); it takes no `data` and no `signal` because a scope is effect-free and synchronous.

```ts
<scope>(<params>): <Master>Relation
```

The method returns a freshly-allocated `<Master>Relation` whose plan extends the receiver's plan with the scope body's stages (chained scopes inlined), exactly like the built-in stage methods, so a scope composes with `where` / `orderBy` and with other scopes (`records.adult().gendered(1)`). The method builds a plan only, so it is identical regardless of the configured storage backend. A call to a non-`pub` sibling scope is inlined into the method body (with its parameters substituted), because only `pub` scopes are emitted as methods; a call to a `pub` sibling scope is a method call. An `indexed` scope adds no TypeScript surface beyond its `pub` flag; it only influences SQLite index generation.

### Select Projections

Each `select Name { ... }` section on a master ([../masterdata/schema.md](../masterdata/schema.md#select-section)) emits a parallel set of TypeScript declarations alongside the source relation:

- `<Master><Name>Record` — an exported `type` alias carrying the projected fields. Field order matches the source order written in the select body.
- `<Master><Name>Fields` — an exported `class` exposing typed field-handle properties for the projected record.
- `<Master><Name>Relation` — an exported `class` carrying the source relation by value plus its own per-projection plan. Stage methods are copy-on-write; terminals mirror the source relation's surface, parametrised on the projected record type.
- `select<Name>(): <Master><Name>Relation` — a method on `<Master>Relation` that returns a fresh projected relation capturing the receiver's source-side plan.

Terminals on the projected relation first apply the source-side plan, then project each surviving record into a `<Master><Name>Record` by copying the named fields, and finally apply the projected plan to the projected slice. Projected relations do not expose `findBy`.

```ts
const summaries = await items
  .where(item => item.count.ge(10))
  .selectSummary()
  .orderBy(summary => summary.name.asc())
  .toArray(data, signal);
```

### Join Operator

Each `ref<Target>` field on a master's record ([../masterdata/relations.md](../masterdata/relations.md)) emits a parallel set of TypeScript declarations alongside the source relation:

- `<Master>Join<Field>Pair` — an exported `type` alias `{ readonly left: <Master>Record; readonly right: <Target>Record; }` that aggregates the joined pair.
- `<Master>Join<Field>LeftFields` / `<Master>Join<Field>RightFields` — exported `class`es exposing typed field handles for each side's record. Each handle's accessor reads `pair.left.<field>` or `pair.right.<field>` so predicates and orderings type-check against the pair.
- `<Master>Join<Field>Fields` — an exported `class` whose `left` and `right` properties hold the per-side field-handle classes above. The pair relation's `where` / `orderBy` / `thenBy` callbacks receive an instance of this class.
- `<Master>Join<Field>Relation` — an exported `class` carrying the source relation by value, the right relation supplied at the call site, and its own pair-level plan. Stage and terminal methods mirror the source relation's surface, parametrised on the pair record.
- `join<Field>(right: <Target>Relation): <Master>Join<Field>Relation` — a method on `<Master>Relation` that returns a fresh joined relation capturing the receiver as the left source, the supplied relation as the right source, and a fresh pair-level plan.

Terminals on the joined relation first call `toArray(data, signal)` on the source relation, then iterate the surviving left records and `await this.right.findBy(data, signal, leftRecord.<field>_<pk1>, ...)` for each, pushing the pair on a successful match and dropping the row on `undefined` (INNER JOIN; `LEFT` / `RIGHT` / `FULL OUTER` deferred). Pair-level state (predicates, orderings, skip, take) then applies before the terminal returns. Joined relations do not expose `findBy`.

```ts
const pairs = await b
  .joinARecord(a)
  .where(fields => fields.right.name.eq("alpha"))
  .orderBy(fields => fields.left.id.asc())
  .toArray(data, signal);
```

## Product Types

A Masterbelt product type lowers to a TypeScript object type literal with `readonly` modifiers on every field:

```ts
export type Item = { readonly name: string; readonly count: number; };
```

Field order in the emitted type matches the field order written in source. A field's `readonly` modifier renders as the TypeScript `readonly` keyword on that field; fields without the modifier (or with the explicit `writable` modifier) emit no keyword and are assignable through the structural type.

## Unions

A union type lowers to the native TypeScript union of each member's mapping. Member ordering matches the canonical order recorded in the IR, which is lexicographic by member type spelling. No wrapper types, marker methods, or generated files are introduced; a value of a union type lowers to the unwrapped TypeScript value because the TypeScript union directly accepts every member.

For example, `bool | int` lowers to the type `boolean | number`, and a value `1` of that union lowers to the literal `1`.

## Function Types

A Masterbelt function type lowers to a TypeScript arrow type `(name: T, ...) => R`. A type declaration whose body is a function type emits a TypeScript `type` alias following the rule in [Type Declarations](#type-declarations):

```ts
export type BinaryOp = (left: number, right: number) => number;
export type Mapper<T, U> = (value: T) => U;
export type Summer = (initial: number, ...values: readonly number[]) => number;
```

Parameter names are preserved from source. A variadic parameter prefixed with `*` in source lowers to a TypeScript rest parameter prefixed with `...`; the parameter type is rendered as a `readonly` array of the declared element type because TypeScript requires variadic parameters to be array-typed.

Effects defined in [Effects](#effects) shape the rendered signature:

- `cancellable` appends an `AbortSignal` parameter named `signal` to the parameter list.
- `failable` does not change the declared return type. The TypeScript signature renders the success type `R` only; the failure path is plumbed internally by the call-site lowering (see [Failable Handling](#failable-handling)). When combined with `asyncable` the wrapping is `Promise<R>`.
- `asyncable` wraps the return type in `Promise<R>` where `R` is the declared return type.

A function type that combines multiple effects applies every transformation listed above.

## Enums

A Masterbelt enum lowers to a TypeScript `const enum` declaration:

```ts
export const enum Status {
  Active = 0,
  Inactive = 1,
}
```

Variant names appear in source declaration order, each with its resolved integer value as the right-hand side. The `const enum` form keeps every member's compile-time value inlined at call sites so emitted code carries no runtime object for the enum.

A member access expression `Enum.Variant` lowers to the literal `Enum.Variant` reference, which TypeScript resolves to the variant's integer literal at compile time.

## Functions and Methods

A top-level function declaration emits an `[export] function name(params): Return { ... }`. When the function's signature carries `asyncable`, the declaration is `async` and the return type is wrapped in `Promise<R>`. Other effects follow the rules in [Effects](#effects).

Methods declared inside a product type are emitted as free-standing module functions rather than members of the type. The TypeScript target represents product types as `type` aliases (not classes), which do not carry methods; methods become functions named `OwnerType_method[Index](self, args)` where `Index` is a 1-based numeric suffix on overloaded methods (the first overload omits the suffix). The receiver value is passed as the first argument named `self`, matching the Masterbelt implicit-receiver keyword so `self.field` inside a method body maps directly onto the synthesized parameter.

A call expression `target(args)` emits `target(args)`. A method call `value.method(args)` emits the free-standing form `OwnerType_method(value, args)` with the overload suffix when applicable.

A function literal `fn(params): R { ... }` is reserved for a follow-up; the current target does not synthesize TypeScript arrow expressions at expression positions.

## For Statements

A Masterbelt for statement lowers to a TypeScript `for` statement. The IR subject shape selects the form:

- `list<T>` subject — `for (const name of subject) { ... }`. A skipped binding (`_` in source) renders as `_` (a TypeScript identifier the compiler accepts as a regular local; the destructuring case below uses TypeScript's `_` convention too).
- `map<K, V>` subject — `for (const [k, v] of Object.entries(subject)) { ... }`. Either binding renders as `_` when skipped. Map keys are always strings in the TypeScript target (see [Type Mapping](#type-mapping)); `Object.entries` produces the matching `[string, V]` tuples.
- `range(start, end)` subject — `for (let i = start; i < end; i++) { ... }`. A `_` binding synthesizes `__mbI`, used only to advance the counter.

A `break` statement lowers to TypeScript's `break`; a `continue` statement lowers to `continue`.

The lowered subject expression is evaluated exactly once (TypeScript's `for-of` and counted-for forms both have a single evaluation point). Iteration over `Master.toList()` lowers through the [Master Data](#master-data) rewrite: `await <otherMaster>.toArray(data, signal)` against the module-level relation const for a cross-master subject and `await this.toArray(data, signal)` when the iteration appears inside the owning master's own static body.

## Master Static Members

A master's static section ([../masterdata/schema.md](../masterdata/schema.md#static-section)) emits its members on the master's `<Master>Relation` class (see [Master Data](#master-data)):

- A `static const Name: T = value` emits as an instance method `Name(data: MasterData, signal: AbortSignal): T { return value }` on the relation. The body has the same dataset-rewriting rules as a method body so a constant initializer that calls another master's static member resolves through `<otherMaster>.X(data, signal)`.
- A `static fn name(params): R { body }` emits as `name(data: MasterData, ...params, signal: AbortSignal): R { body }` on the relation; the body is rewritten per the [Static Body Rewrites](#static-body-rewrites) above.

A `pub` modifier on a static member emits the method as public on the class; a non-public member emits as `private`. Visibility is independent from the master's own `pub` modifier.

The source-level access `Items.X` inside any callable's body lowers per the [Master Data](#master-data) rewrite rules: the owner-self case resolves to `this.X(data, signal)` (with `(data, ...args, signal)` when called with user arguments), and the cross-master case resolves to `<otherMaster>.X(data, signal)` (with `(data, ...args, signal)` when called with user arguments) against the module-level relation const.

## Match Statements

A Masterbelt match statement lowers to a TypeScript `if`/`else if` chain whose subject is bound once to a fresh local. Each arm contributes one branch:

- A **type pattern** whose type is a primitive emits a `typeof` check (`number`, `string`, `boolean`) or a `value === null` check for the `null` type. A type pattern whose type is a user-declared product type emits a generated type predicate call `is<Type>(value)`; the predicate is emitted into `masterbelt_runtime.ts` once per product type referenced by any match statement and the calling module imports it via the standard cross-module import machinery.
- An **enum pattern** lowers to a strict equality check against the enum variant (`value === Status.Active`).
- A **literal pattern** lowers to a strict equality check against the literal value.
- A **product pattern** combines a `is<Type>` predicate call with strict-equality or sub-pattern checks for every named field. Field bindings inside the branch destructure the matched value (`const field = value.field`).
- A **wildcard pattern** lowers to an unconditional `else` branch.

Bindings introduced by a pattern are emitted as `const` declarations at the top of the branch body. When the subject expression is a plain identifier without an explicit pattern binding, the narrowed local shadows the original name by a `const name = value as NarrowedType;` declaration so that downstream uses see the narrowed type. The original identifier remains untouched outside the match.

When the match is statically exhaustive, the chain ends with the spec's `else { value satisfies never; }` tail so TypeScript's type system records the exhaustiveness without inserting a runtime throw. A wildcard arm replaces this tail with the user's body. When a guarded arm appears, its `if` condition is the AND of the pattern check and the guard expression; the failing-guard path falls through to the next arm.

A match statement is otherwise emitted at its source position inside the surrounding TypeScript function body and shares the function's local scope. The subject expression is evaluated exactly once.

## Operator Expressions

The unary and binary operator expressions defined in [language/syntax.md](../language/syntax.md) reach the TypeScript target as method calls on the operand's type (see [language/builtins.md](../language/builtins.md)). When the receiver is a built-in primitive type (or an alias whose target resolves to a primitive), the TypeScript target emits the call as the corresponding native operator:

- Numeric operands emit `+ - * / % == != < <= > >= & | ^ << >>` directly. Equality is rendered as strict `===` / `!==`.
- `bool` operands emit `&& || === !==`; `and`/`or`/`xor` lower to `&& || !==` respectively. Unary `not` emits `!`.
- `string` operands emit `+` for `add` and `===` / `!==` / `< <= > >=` for the comparisons.
- Unary `plus`, `minus`, and `not` emit `+`, `-`, and `!`.

A user product type that declares one of the operator method names continues to lower through the regular method call path: the call site emits the free-standing `OwnerType_method(receiver, args)` form (with the overload suffix when applicable). The native-operator rewrite applies only when the receiver's resolved type is a primitive.

### Built-in Field Accesses

The fields defined in [language/builtins.md](../language/builtins.md) on built-in primitive and generic types lower to native TypeScript expressions:

- `string.length` lowers to `[...receiver].length`. The spread iterates the string by codepoint (JavaScript's string iterator yields codepoints, not UTF-16 code units), so the resulting array length matches the spec's codepoint count. The naive `receiver.length` property is **not** used because JavaScript's string `length` returns the UTF-16 code unit count, which diverges from the spec for any non-BMP codepoint.
- `list<T>.size` lowers to the array `length` property.
- `map<K, V>.size` lowers to `Object.keys(receiver).length`. Maps are emitted as plain object literals at this stage, so there is no `Map`-style `size` getter to reuse.

No runtime helper is emitted for these fields; the lowering inlines the access expression at the use site.

### Built-in Generic Operator Methods

`list<T>.add(other)` and `map<K, V>.add(other)` defined in [language/builtins.md](../language/builtins.md) lower to a call into a runtime helper that the TypeScript target writes alongside the generated module files. The helper file is named `masterbelt_runtime.ts` (the reserved `masterbelt_` prefix as for any generator-managed file) and is emitted only when at least one generated module references a helper from it.

The helper file declares the exported functions used at call sites:

- `masterbeltListAdd<T>(a: readonly T[], b: readonly T[]): readonly T[]` returns a fresh array containing the elements of `a` followed by the elements of `b`.
- `masterbeltMapAdd<K extends string, V>(a: ReadonlyRecord<K, V>, b: ReadonlyRecord<K, V>): ReadonlyRecord<K, V>` returns a fresh object containing every entry of `a` and every entry of `b`, with keys present in both taking the value from `b`.

Call sites import the helper by relative path from the calling module (`./masterbelt_runtime`) using the emitter's normal cross-module import machinery, then emit `masterbeltListAdd(a, b)` or `masterbeltMapAdd(a, b)` as the call expression.

## Type Declarations

Each Masterbelt type declaration emits one TypeScript `type` declaration:

```ts
export type LocalName<Params> = MappedTargetType
```

Public declarations carry the `export` keyword; non-public declarations omit it. When the declaration carries type parameters, the emitted TypeScript declaration carries the same parameters as a TypeScript generic type parameter list (`<T, ...>`). Parameters have no constraint at this stage.

At a use site, a generic declaration is rendered with its type arguments: `LocalName<T1, ...>`.

## Cross-Module References

A reference to a symbol declared in another Masterbelt module emits the imported identifier name. The emitter adds the corresponding `import { ... } from "..."` declaration automatically; the module specifier is derived from the foreign module's canonical project-relative path by stripping the `.mst` suffix and computing the path relative to the importing file's directory, prefixed with `./` when the relative form would otherwise be unqualified.

## Re-exports

A `pub { ForeignName as LocalName } from "./other.mst"` declaration emits one ES module re-export per declared module specifier:

```ts
export { ForeignName as LocalName } from "./other"
```

Multiple re-exports targeting the same foreign module are coalesced into one `export { ... } from "..."` statement. When `ForeignName` and `LocalName` are equal the `as` clause is omitted. A re-export keeps its declaration's doc comment as `// ...` lines immediately before the statement.

## Effects

Each effect defined in `codegen/model` maps to TypeScript as follows:

- `cancellable` adds an `AbortSignal` parameter named `signal` as the last named parameter of the callable.
- `failable` does not change the declared return type. The TypeScript signature renders the success type `R`; the failure path is plumbed internally by the call-site lowering (see [Failable Handling](#failable-handling)). When combined with `asyncable`, the result wraps as `Promise<R>`.
- `asyncable` wraps the result in `Promise<T>` and the callable is declared `async`. A call site whose callee is asyncable is wrapped in `await`.

Every effect is inferred along the call graph: the TypeScript target computes an effective effect set per callable by walking from the declared effects and propagating through every transitive call site to a fixed point. A function whose effective set differs from its declared set still renders with the inferred shape — a non-asyncable surface declaration whose body calls an asyncable function still becomes an `async function` returning `Promise<R>`, and a non-cancellable declaration that calls a cancellable function still receives and forwards the `AbortSignal`.

The target reports an explicit diagnostic when a program requires an effect that has no defined mapping rather than emitting code that silently drops the obligation.

## Failable Handling

A `failable` function in TypeScript reports failure through a thrown exception. The Masterbelt surface treats `failable` as transparent (see [language/semantics.md](../language/semantics.md#failable-handling)); the TypeScript target uses native exception propagation so neither the function signature nor the call site need to widen:

- `fail "message"` lowers to `throw new Error("message")`.
- `fail value` where `value: Error` lowers to `throw new Error(value.message)`.
- A call to a `failable` callable lowers to a plain call expression; a thrown `Error` propagates through the surrounding `failable` function because the surrounding function does not catch it. No `try`/`catch` is emitted at call sites.

The platform `Error` constructor is sufficient; the target does not emit a separate Error type declaration.

Match expressions cannot observe the Error path of a `failable` call subject because the surface type of the call is `R`.

## Imports

The TypeScript target's emitter assembles the file's `import { ... } from '...'` block from the symbols referenced during rendering. A symbol carries the module specifier it lives in and the unqualified identifier name. The emitter:

- Aggregates the set of referenced (module specifier, name) pairs.
- When two distinct module specifiers export the same name, later occurrences are imported with `import { Name as Name2 } from '...'` so each local identifier is unique.
- Writes one `import { ... } from '...'` statement per distinct module specifier, with bindings sorted lexicographically by their original name.
- Rewrites identifier renderings as the chosen local name.

Targets MUST express external references through this emitter mechanism. Inlining raw module specifiers into source bypasses collision handling and is forbidden.

## Determinism

Generated files are deterministic with respect to the input modules. Constants appear in source order within each module file. Map literal entries appear in lowering order (first-occurrence position, last-wins value). Union members and import bindings are emitted in lexicographic order; import statements are emitted in lexicographic module-specifier order.
