#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: STRINGselects the master-data backend baked into the generated package. Optional; defaults tomemory. Accepted values arememory(the in-memory executor that consumes records supplied through theMasterDataconstructor /loadJson) andsql(a SQL-backed executor that translates queries into SQL against a host-suppliedSQLiteDatabaseadapter — 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
.mstsuffix replaced by.ts. Each file contains the TypeScript declarations corresponding to that module's constants. - A
masterbelt_masterdata.tsfile when the project declares at least one master. The file declares theMasterDataclass described in Master Data. The file is omitted when the project declares no master. - A
masterbelt_query.tsfile when the project declares at least one master. The file declaresMasterSource,FieldRef, thePredicate<R>/Ordering<R>interfaces, the exported concrete predicate / ordering classes that back the inspectable AST, theand/or/notcombinators, the generic field-handle classes (OrderedField<R, V>,BoolField<R>), theQueryPlan<R>value class, and theexecutePlan<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
| 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
nulllowers to the literalnull.trueandfalselower to the literalstrueandfalse.- 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 TypeScripttypealias that backs one row, following the regular Product Types rules.<Master>Relation— an exported TypeScriptclassthat exposes the master's chainable query surface. The class is data-less: it carries a singleprivate readonly plan: QueryPlan<<Master>Record>field with a default constructor that builds a fresh plan and an optionalplan?constructor argument for copy-on-write chaining. Stage methods (where,orderBy,thenBy,skip,take) return a freshly-allocated<Master>Relationwhose 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 forfindBy) and resolve records through the master'sExecutorobtained fromdata.getExecutor<Master>(). Every terminal carries theasyncable,cancellable, andfailableeffects per ../masterdata/schema.md — the TypeScript target renders theasync/Promise<R>wrap (asyncable) and the trailingsignal: AbortSignalparameter (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 writeitems.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 callsdata.getExecutor<Master>()to reach the active backend, then invokesexecute(plan, signal)/findByPK(plan, keys, signal)on the returned executor. Understorage: memorythe accessor returns aMemoryExecutorthat wraps the master's record array (and thematches<Master>PKclosure 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:
MasterDatacarries a singleprivate readonly database: SQLiteDatabaseadapter shared by every per-master executor; the per-master record arrays are not emitted.- The constructor takes the
SQLiteDatabaseinstead of record arrays. The host application wraps its preferred SQLite binding (better-sqlite3,node:sqlite,bun:sqlite,sql.js, ...) in theSQLiteDatabaseinterface (exported frommasterbelt_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'sQueryPlaninto SQL viatranslatePlan/translatePKLookup, runs it through the adapter, and materialises rows with the generatedscan<Master>Rowfunction.loadJSONis not emitted understorage: 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:
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:
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 tothis.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 tothis.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 singlereadonly namefield identifying a master by its source-level name.FieldRef— a class with a singlereadonly namefield 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'swherecallback; the structural check fails onR.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>andBoolField<R>— generic field-handle classes whose constructors take(name: string, accessor: (record: R) => V)so each node embeds the source-level field name into itsFieldRef. Their comparison methods return the concrete predicate / ordering classes specialised to the owning record type.QueryPlan<R>— the inspectable AST value class wrappingsource,predicates,orderings,skip, andtake. 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>Relationterminal.stableSort<R>— internal helper used byexecutePlan.
The per-master user-facing declarations are:
<Master>Fields— an exported TypeScriptclassexposing one typed field-handle property per supported record field. Field handles for ordered values (numeric, string) exposeeq,ne,lt,le,gt,ge,in,between,asc, anddesc; field handles for bool values exposeeq,ne, andinonly. Each handle's comparison method returns the concrete predicate / ordering class specialised to the owning master, statically typed asPredicate<<Master>Record>/Ordering<<Master>Record>for the callback's return type.<Master>Relation— see Master Data for the relation class itself. Itswhere/orderBy/thenBycallbacks 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.
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.
<scope>(<params>): <Master>RelationThe 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 exportedtypealias carrying the projected fields. Field order matches the source order written in the select body.<Master><Name>Fields— an exportedclassexposing typed field-handle properties for the projected record.<Master><Name>Relation— an exportedclasscarrying 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>Relationthat 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.
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 exportedtypealias{ readonly left: <Master>Record; readonly right: <Target>Record; }that aggregates the joined pair.<Master>Join<Field>LeftFields/<Master>Join<Field>RightFields— exportedclasses exposing typed field handles for each side's record. Each handle's accessor readspair.left.<field>orpair.right.<field>so predicates and orderings type-check against the pair.<Master>Join<Field>Fields— an exportedclasswhoseleftandrightproperties hold the per-side field-handle classes above. The pair relation'swhere/orderBy/thenBycallbacks receive an instance of this class.<Master>Join<Field>Relation— an exportedclasscarrying 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>Relationthat 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.
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:
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:
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:
cancellableappends anAbortSignalparameter namedsignalto the parameter list.failabledoes not change the declared return type. The TypeScript signature renders the success typeRonly; the failure path is plumbed internally by the call-site lowering (see Failable Handling). When combined withasyncablethe wrapping isPromise<R>.asyncablewraps the return type inPromise<R>whereRis 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:
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.entriesproduces 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 = valueemits as an instance methodName(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 asname(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
typeofcheck (number,string,boolean) or avalue === nullcheck for thenulltype. A type pattern whose type is a user-declared product type emits a generated type predicate callis<Type>(value); the predicate is emitted intomasterbelt_runtime.tsonce 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
elsebranch.
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===/!==. booloperands emit&& || === !==;and/or/xorlower to&& || !==respectively. Unarynotemits!.stringoperands emit+foraddand===/!==/< <= > >=for the comparisons.- Unary
plus,minus, andnotemit+,-, 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.lengthlowers 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 naivereceiver.lengthproperty is not used because JavaScript's stringlengthreturns the UTF-16 code unit count, which diverges from the spec for any non-BMP codepoint.list<T>.sizelowers to the arraylengthproperty.map<K, V>.sizelowers toObject.keys(receiver).length. Maps are emitted as plain object literals at this stage, so there is noMap-stylesizegetter 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 ofafollowed by the elements ofb.masterbeltMapAdd<K extends string, V>(a: ReadonlyRecord<K, V>, b: ReadonlyRecord<K, V>): ReadonlyRecord<K, V>returns a fresh object containing every entry ofaand every entry ofb, with keys present in both taking the value fromb.
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:
export type LocalName<Params> = MappedTargetTypePublic 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:
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:
cancellableadds anAbortSignalparameter namedsignalas the last named parameter of the callable.failabledoes not change the declared return type. The TypeScript signature renders the success typeR; the failure path is plumbed internally by the call-site lowering (see Failable Handling). When combined withasyncable, the result wraps asPromise<R>.asyncablewraps the result inPromise<T>and the callable is declaredasync. A call site whose callee is asyncable is wrapped inawait.
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 tothrow new Error("message").fail valuewherevalue: Errorlowers tothrow new Error(value.message).- A call to a
failablecallable lowers to a plain call expression; a thrownErrorpropagates through the surroundingfailablefunction because the surrounding function does not catch it. Notry/catchis 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.