Masterbelt

masterbelt/masterbelt

TypeScript Code Generation

Synced from main@9490864MarkdownSource

#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).

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. 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. 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

MasterbeltTypeScript
nullnull
boolboolean
int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64number (TypeScript's only numeric type; integers above 2^53 lose precision)
stringstring
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, ...): RArrow 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. The TypeScript target emits per master:

  • <Master>Record — a TypeScript type alias that backs one row, following the regular 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 — 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 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) 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:

TypeScript
import { DatabaseSync } from class="token-string">"node:sqlite";
import type { SQLiteDatabase, SQLiteRow } from class="token-string">"./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(class="token-string">"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) and returns a freshly wired MasterData. The helper uses the platform JSON.parse only and is independent of any backend library:

TypeScript
export function loadJSON(data: string | object): MasterData {
  const raw = typeof data === class="token-string">"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 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 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.
TypeScript
import { and } from class="token-string">"./masterbelt_query";
import { items } from class="token-string">"./data";

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

#Scope Methods

Each pub scope 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; it takes no data and no signal because a scope is effect-free and synchronous.

TypeScript
<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) 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.

TypeScript
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) 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 classes 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.

TypeScript
const pairs = await b
  .joinARecord(a)
  .where(fields => fields.right.name.eq(class="token-string">"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:

TypeScript
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:

TypeScript
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 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). 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:

TypeScript
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.

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); 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 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) emits its members on the master's <Master>Relation class (see 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 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 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 reach the TypeScript target as method calls on the operand's type (see 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 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 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:

TypeScript
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:

TypeScript
export { ForeignName as LocalName } from class="token-string">"./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). 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); 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.

Specification