Masterbelt

masterbelt/masterbelt

C# Code Generation

Synced from main@9490864MarkdownSource

#C# Code Generation

This document defines the C# code generation target.

#Kind

The target kind identifier is csharp.

#Options

The C# target reads the following options from its options mapping in the project configuration:

  • namespace: STRING is the C# namespace used in every generated file. Required. The value is used verbatim as the file-scoped namespace directive and must be a valid C# qualified identifier (one or more identifier segments separated by .).
  • class: STRING is the name of the shared public static partial class every module contributes to. Optional; defaults to Masterbelt. The value must be a valid C# identifier (one segment, no .).
  • 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 ADO.NET commands against a host-supplied DbConnection — see Master Data).

A missing or empty namespace is a configuration error. An invalid class value is a configuration error. A storage value other than memory or sql is a configuration error.

The C# target does not consume any other options at this stage; unknown options are silently ignored.

#File Set

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

  • One generated file per Masterbelt module. The file name is the module file stem in PascalCase followed by .cs. Each file declares one segment of the shared public static partial class (named by the class option, default Masterbelt) containing every reachable constant from that module, plus any product-type classes declared by the module as siblings at file scope.
  • A MasterbeltUnions.cs file when any constant has a union type. The file declares one sealed-abstract-class union per union type encountered across the project.
  • A MasterbeltMasterData.cs 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 MasterbeltQuery.cs file when the project declares at least one master. The file declares MasterSource, FieldRef, the IPredicate<R> / IOrdering<R> interfaces, the exported concrete predicate / ordering node classes that back the inspectable AST, the static Predicates helper with And / Or / Not, the generic field-handle classes (OrderedField<R, V>, BoolField<R>), the QueryPlan<R> value type, and the QueryRuntime.Execute helper used by every generated relation terminal; see Master Data. The file is omitted when the project declares no master.

All files share the same C# namespace declared by the namespace option and live under the configured output root.

#Reserved file name prefix

Files invented by the C# target itself, rather than derived from a Masterbelt source file name, use the reserved Masterbelt PascalCase prefix. New generator-managed files added to this target MUST use the same prefix.

#Shared Partial Class

C# does not have free-standing top-level constants. Every Masterbelt module contributes its reachable constants and re-exports to one shared public static partial class declared in the configured namespace. The class name is taken from the class option (default Masterbelt); each generated module file repeats the same public static partial class declaration with that name and adds its members.

The class is always public static partial. Its visibility does not depend on whether any of its members are public: it is the container, not a participating symbol. Because every module shares one class, every top-level identifier across the project must be unique under its C# mapping; collisions are reported as a generation diagnostic.

Product-type classes emitted from a module are declared as siblings of the partial class, at file scope rather than as nested types of the shared class.

#Reachability

The C# 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 public access modifier on its class member; a non-public constant is emitted with internal. C# identifier names are preserved verbatim because C# expresses visibility through modifiers, not through identifier case.

#Constants and Static Fields

Each Masterbelt const item maps to one C# class member:

  • A const field is used when the constant's Masterbelt type is bool, string, or any built-in fixed-width numeric type and the lowered expression is a corresponding literal. C# const fields are compile-time constants and require literal initializers. Native-width numerics (int, uint) cannot be C# const because nint/nuint are disallowed there, so they always fall back to the next bullet.
  • A static readonly field is used for every other case: null-typed constants, list and map literals, union-typed constants, and references to other constants.

Doc comment lines are emitted as /// <summary> and /// <para> lines forming a single XML documentation comment block immediately before the member.

#Type Mapping

MasterbeltC#
nullobject? (the value null lowers to the C# literal null)
boolbool
int / uintnint / nuint (C#'s native-width integers)
int8 / uint8sbyte / byte
int16 / uint16short / ushort
int32 / uint32int / uint
int64 / uint64long / ulong
stringstring
list<T>IReadOnlyList<T'> where T' is the C# mapping of T
map<K, V>IReadOnlyDictionary<K', V'> where K' and V' are the C# mappings
T1 | T2 | ...A sealed abstract class generated into MasterbeltUnions.cs (see Unions below)
{f: T, ...}public class Name { public required T' Field { get; init; } ... } emitted at file scope next to the shared partial class; see Product Types below
fn(p: T, ...): Rpublic delegate R Name(T p, ...) at file scope when reached as a named type; System.Func<...> when used inline. See Function Types below
enum Name { ... }public enum Name : Storage { Variant = value, ... } at file scope (see Enums below)

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

#Literal Mapping

  • null lowers to the C# literal null.
  • true and false lower to the C# literals true and false.
  • An integer literal lowers to its decoded value rendered in base 10.
  • A string literal lowers to a C# double-quoted string with C# escape sequences.
  • A list literal [e1, ..., eN] lowers to a C# 12 collection expression [e1', ..., eN']. The surrounding declared list type fixes the runtime collection type, so the explicit element type is omitted to keep the output free of the IDE0300: Collection initialization can be simplified warning. An empty list literal lowers to [].
  • A map literal of type map<K, V> lowers to new Dictionary<K', V'> { [k1] = v1, ..., [kN] = vN }, with entries emitted in lowering order after last-wins deduplication.
  • An identifier reference lowers to the referent constant's identifier, qualified by its declaring class when necessary.
  • An integer literal lowers to its decoded value rendered in base 10, with the suffix C# requires to bind the literal to the resolved numeric type: UL for uint64, L for int64, and U for uint32. Narrower fixed-width types (int8/uint8/int16/uint16/int32) and the native widths (int/uint) emit the bare digits because implicit conversion or target inference covers them.
  • A product literal lowers to a C# target-typed object initializer new() { Field = ..., ... }. The surrounding declared type (a product-type declaration) supplies the runtime type, so the type name is omitted at the call site to keep the output free of the IDE0090: 'new' expression can be simplified warning. Initializers carry PascalCase property names and preserve the source order of the literal.

#Product Types

A Masterbelt type declaration whose target is a product type emits a public class at file scope, alongside the shared partial class:

C#
public class Item
{
    public required string Name { get; init; }

    public required int Count { get; init; }
}

Field names are converted to PascalCase to match the C# naming convention. Field order matches the field order written in source. The required modifier ensures every field must be supplied at construction. Each property's setter accessor reflects the Masterbelt field modifier: a readonly field emits { get; init; } so it cannot be reassigned after object initialization, while a field without the modifier (or with the explicit writable modifier) emits { get; set; } and remains assignable. Anonymous nested product types written inline in a Masterbelt declaration body are normalized to named declarations by the lowering pass (see ir.md), so the C# target emits each one as a sibling class with the synthesized name.

#Unions

A union type lowers to a C# sealed abstract class. For a canonical union T1 | T2 | ... | TN:

  • The class type is named by concatenating the PascalCase member type names with Or. For primitive members, the names are Null, Bool, Int, String. The order matches the canonical order recorded in the IR.
  • The class has a private parameterless constructor so external code cannot construct arbitrary subclasses.
  • For each non-null member type, a nested public sealed class wrapper is declared that inherits from the abstract base. The wrapper exposes a public required property named Value of the member's mapped C# type. Inheritance lets is-pattern checks on a union variable identify the matched member (used by match statement lowering), and a nested class has access to the base class's private parameterless constructor so external assemblies still cannot construct arbitrary subclasses.
  • The null member does not get a wrapper. The C# null literal directly satisfies a nullable reference to the abstract base, so a union variable of static type T1Or...? holding null represents the null case.

A value of a union type lowers as new <Interface>.<Member> { Value = <expression> } for non-null members and null for null.

At this stage, only unions of primitive types are supported. A union containing a generic member is a generation error.

#Type Declarations

Each Masterbelt type declaration whose target is not a product type emits one C# file-scoped using-alias directive placed immediately after the regular using directives and before the namespace declaration:

C#
using LocalName<Params> = MappedTargetType;

When the declaration carries type parameters, the using directive carries the same parameters as a generic using-alias parameter list (<T, ...>). Generic using-aliases require C# 12 or later. C# does not distinguish public and non-public for file-scoped aliases; the directive is visible everywhere in the file.

A type declaration whose target is a product type does not emit a using-alias directive; it emits a public class Name<Params> { ... } at file scope. See the Product Types section above for the class shape; the parameter list is the same parameter list as the declaration's. At a use site, a generic class is rendered with its type arguments: LocalName<T1, ...>.

#Enums

A Masterbelt enum lowers to a public enum Name : Storage { ... } declaration at file scope alongside the shared partial class:

C#
public enum Status : sbyte
{
    Active = 0,
    Inactive = 1,
}

The storage clause uses the C# spelling defined in Type Mapping (int8sbyte, int32int, and so on). Variant names appear in source declaration order with their resolved integer values.

A member access expression Enum.Variant lowers to the C# member access Enum.Variant. When the variant is used as an initializer for a shared-class member, the variant is emitted with its enum-type qualifier.

#Functions and Methods

A top-level function declaration emits a [public|internal] static Result Name(params) { ... } method on the shared partial class. Asyncable functions are declared async and return Task<R>; cancellable functions append a CancellationToken cancellationToken parameter.

A method declared inside a product type emits a [public] Result Name(params) { ... } instance method on the owning class. Overloaded methods rely on C#'s native overloading by signature — the emitted methods all share the same name; no numeric suffix is needed. The Masterbelt implicit-receiver keyword self is rewritten to C#'s this keyword inside method bodies so the surface form maps onto C#'s idiom.

A call expression target(args) emits Target(args) (PascalCase identifier resolution for functions). A method call value.method(args) emits value.Method(args).

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

#For Statements

A Masterbelt for statement lowers to a C# foreach or for statement. The IR subject shape selects the form:

  • list<T> subject — foreach (var name in subject) { ... }. A skipped binding (_ in source) is rendered as the C# discard pattern _.
  • map<K, V> subject — foreach (var (k, v) in subject) { ... }. Either binding renders as _ when skipped. The C# target's map type (IReadOnlyDictionary<K, V>) yields KeyValuePair<K, V> entries; the foreach pattern deconstructs them via the language's built-in tuple deconstruction support on KeyValuePair.
  • range(start, end) subject — for (var i = start; i < end; i++) { ... }. A _ binding synthesizes __mbI used only to advance the counter.

A break statement lowers to C#'s break; a continue statement lowers to continue.

The lowered subject expression is evaluated exactly once. Iteration over a master's toList() lowers through the Master Data rewrite (await this.ToList(data, cancellationToken) inside the owning master's own static body, await Masterbelt.<Other>.ToList(data, cancellationToken) for a cross-master subject) and walks the returned IReadOnlyList<Record>. The await placement comes from the regular asyncable inheritance machinery: the surrounding static method becomes async Task<R> silently and the for-loop subject lifts into await ... at the C# call site.

#Master Data

A master Foo { record { ... } static { ... } } declaration follows the runtime model defined in ../masterdata/schema.md. The C# target emits per master:

  • <Master>Record — a C# class that backs one row. Field naming, modifiers, and constructor / property generation follow the regular Product Types rules.
  • <Master>Relation — a C# public sealed class declared at file scope alongside the record. The class is data-less: it carries a single private readonly QueryPlan<<Master>Record> plan field plus a default constructor that initialises a fresh plan and an internal constructor that accepts a pre-built plan 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 (ToList, AsAsyncEnumerable, FindBy, FirstOrDefault, Count, Any) take (MasterData data, CancellationToken cancellationToken) (plus primary-key arguments for FindBy) and resolve records through the master's IExecutor obtained from data.GetExecutor<Master>(). Every terminal carries the asyncable, cancellable, and failable effects per ../masterdata/schema.md — the C# target renders the async/Task<R> wrap (asyncable) and the trailing CancellationToken cancellationToken parameter (cancellable); failure surfaces through a thrown exception (failable). AsAsyncEnumerable is the streaming counterpart of ToList: it returns IAsyncEnumerable<<Master>Record> and uses [EnumeratorCancellation] on its cancellationToken parameter so cancellation flows through await foreach. Each user-declared static method becomes an instance method on the relation; it always takes (MasterData data, ...declared args, CancellationToken cancellationToken).
  • Package-level entrypoint: public static partial class Masterbelt { public static readonly <Master>Relation <Master> = new(); }. Users write Items.Where(...).ToList(data, cancellationToken) after adding using static <Namespace>.Masterbelt; (or Masterbelt.Items.Where(...) with no using directive).

Both declarations live as siblings at file scope.

Nested masters follow the same naming scheme on the flattened identifier — master User { master Friendships { ... } } emits UserFriendshipsRecord, UserFriendshipsRelation, and a Masterbelt.UserFriendships static field 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 MasterbeltMasterData.cs. It declares:

  • One private readonly IReadOnlyList<<Master>Record> <master>Records field per master (lowerCamelCase), storing the per-master record set.
  • A constructor public MasterData(IReadOnlyList<<Items>Record> items, IReadOnlyList<<Users>Record> users, ...) that takes one read-only list per master in master-declaration order and assigns each to the matching field.
  • One internal IExecutor<<Master>Record> GetExecutor<Master>() accessor per master. Every generated relation terminal calls data.GetExecutor<Master>() to reach the active backend, then awaits Execute(plan, cancellationToken) / FindByPK(plan, keys, cancellationToken) on the returned executor. Under storage: memory the accessor returns a MemoryExecutor<<Master>Record> wrapping the master's record list (and the <Master>Relation.Matches<Master>PK closure when the master declares a primary key).

The C# target never writes MasterData in code emitted from a Masterbelt source program: the identifier exists for the host application to construct and inject the dataset. MasterData stores records — not relations — because every generated relation is a data-less value-typed 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 DbConnection connection (from System.Data.Common) shared by every per-master executor; the per-master record lists are not emitted.
  • The public positional constructor is replaced by a factory public static Task<MasterData> NewSqliteMasterData(DbConnection connection, CancellationToken cancellationToken). The host application opens the connection with its preferred SQLite provider (Microsoft.Data.Sqlite, System.Data.SQLite, a Unity binding, ...) and owns its open/close lifetime; MasterData never closes it.
  • GetExecutor<Master>() returns a per-master <Master>SqlExecutor (emitted alongside the relation) that translates the relation's QueryPlan into a parameterised DbCommand via SqlTranslator.TranslatePlan / TranslatePKLookup, runs it with ExecuteReaderAsync, and materialises rows with the generated ScanRow.
  • 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 IExecutor<R> / MemoryExecutor<R> seam, the ISqlEmittable / ISqlOrderable interfaces, the SqlFragment carrier, the SqlTranslator helpers, and the SqlSupport query runner all live in MasterbeltQuery.cs. Generated code depends only on System.Data.Common; it never references a concrete SQLite provider package.

Under storage: sql, a ref<T> join and a select projection are pushed into SQL when the source relation's plan is trivial: the join becomes one INNER JOIN statement (rather than a per-row FindBy), and the projection becomes a column-narrowed SELECT. Each derived relation captures the source plan's triviality at the accessor (SqlTranslator.IsTrivial); a non-trivial source plan (a source-side Where/OrderBy/Skip/Take) or the memory backend falls back to the materialise-and-evaluate path. Both paths return identical results.

Because the factory accepts a DbConnection directly, no adapter is needed: a provider's connection type already derives from DbConnection. With Microsoft.Data.Sqlite:

C#
using Microsoft.Data.Sqlite;

await using var connection = new SqliteConnection(class="token-string">"Data Source=masterdata.db");
await connection.OpenAsync(cancellationToken);
var data = await MasterData.NewSqliteMasterData(connection, cancellationToken);
// var items = await Masterbelt.Items.Where(f => f.Count.Ge(20)).ToList(data, cancellationToken);

The host owns the connection's open/close lifetime; MasterData never disposes it. Any System.Data.Common-compatible provider (Microsoft.Data.Sqlite, System.Data.SQLite, a Unity SQLite binding) works without changes to the generated code.

The same file also declares public static MasterData LoadJson(string json), 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 System.Text.Json only:

C#
public static MasterData LoadJson(string json) {
    var raw = JsonSerializer.Deserialize<JsonShape>(json, JsonOptions);
    return new MasterData(raw.items, raw.userFriendships /* ... */);
}

private static readonly JsonSerializerOptions JsonOptions =
    new JsonSerializerOptions { PropertyNameCaseInsensitive = true };

private sealed class JsonShape {
    [JsonPropertyName(class="token-string">"items")]
    public IReadOnlyList<ItemsRecord> items { get; init; }
    [JsonPropertyName(class="token-string">"userFriendships")]
    public IReadOnlyList<UserFriendshipsRecord> userFriendships { get; init; }
    // ... one per master in declaration order
}

Each generated <Master>Record property carries a [JsonPropertyName("<surfaceName>")] attribute so the C# Id property maps to JSON key id, Name maps to name, and so on. The surface name is the master's source-level field identifier verbatim. A ref<T> field expands to the underlying primary-key fields under the surrounding field's name joined with _ (field_pk1, field_pk2, ...); the [JsonPropertyName(...)] on each expanded leaf carries the joined source name. Other product types not declared inside a master block do not receive JSON attributes.

#Static Body Rewrites

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

  • Master.toList() inside the owning master's own static body lowers to this.ToList(data, cancellationToken). 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, cancellationToken, ...args).
  • OtherMaster.toList() lowers to Masterbelt.<OtherMaster>.ToList(data, cancellationToken) against the static field on the partial class.
  • OtherMaster.X (any other cross-master reference) lowers to Masterbelt.<OtherMaster>.X(data, cancellationToken, ...args) against the static field.

Every relation method, including user-declared statics and constants, accepts (MasterData data, ...declared args, CancellationToken cancellationToken) 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 MasterData data as its first positional parameter and CancellationToken cancellationToken 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 CancellationToken cancellationToken is appended, asyncable wraps the return in Task<R>) without further interaction.

#Query API

Every master emits the chainable surface directly on <Master>Relation. The C# target uses a callback style: Where, OrderBy, and ThenBy accept a callback that receives a typed <Master>Fields instance and returns a predicate or ordering AST. Per the design doc, the predicate is a generated AST type and not an Expression<Func<...>> for this phase; future SQL-translating backends may revisit the choice. See ../masterdata/query.md for the cross-target contract.

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

  • MasterSource — a public sealed record(string Name) identifying a master by its source-level name.
  • FieldRef — a public sealed record(string Name) identifying a record field by its source-level name.
  • IPredicate<R>interface { bool Evaluate(R record); }. Parametrised by the record type so a predicate built for one master cannot be passed to another's Where callback; the compiler rejects the mismatch on R.
  • IOrdering<R>interface { int Compare(R a, R b); }.
  • 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 properties so a backend can translate the node to SQL without invoking the per-record accessor.
  • Predicates.And<R>(params IPredicate<R>[]), Predicates.Or<R>(...), Predicates.Not<R>(IPredicate<R>) — static combinators that compose predicates over a single record type. A mixed-record composition is a compile-time error.
  • OrderedField<R, V> where V : IComparable<V> and BoolField<R> — generic field-handle classes whose comparison methods return concrete predicate / ordering nodes specialised to the same record type. The constructors take (string name, Func<R, V> accessor) so each node embeds the source-level field name into its FieldRef.
  • QueryPlan<R> — the inspectable AST value type wrapping Source, Predicates, Orderings, Skip, and Take. Stage helpers (WithWhere, WithOrderBy, WithThenBy, WithSkip, WithTake) return a new plan; the original is never mutated.
  • QueryRuntime.Execute<R>(records, plan) — the shared in-memory evaluator used by every generated <Master>Relation terminal.

The per-master user-facing declarations are:

  • <Master>Fields — a public sealed class exposing one public readonly field-handle per supported record field plus a public static readonly <Master>Fields Instance singleton. 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 IPredicate<<Master>Record> / IOrdering<<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 Func<<Master>Fields, IPredicate<<Master>Record>> / Func<<Master>Fields, IOrdering<<Master>Record>>, so the compiler rejects a predicate built against a different master at the call site.
C#
using static Example.Masters.Masterbelt;
// ...
var weapons = await Items
    .Where(item => Predicates.And(item.Category.Eq(class="token-string">"weapon"), item.Level.Ge(10)))
    .OrderBy(item => item.SortOrder.Asc())
    .Take(10)
    .ToList(data, cancellationToken);

await foreach (var item in Items
    .Where(item => item.Category.Eq(class="token-string">"weapon"))
    .AsAsyncEnumerable(data, cancellationToken))
{
    // ...
}

#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 in PascalCase (genderedAdultGenderedAdult). Its parameters are the scope's declared parameters mapped through the regular Type Mapping; it takes no MasterData and no CancellationToken because a scope is effect-free and synchronous.

C#
public <Master>Relation <Scope>(<params>)

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 behaves identically under storage: memory and storage: sql. 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 C# 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 C# declarations alongside the source relation:

  • <Master><Name>Record — a public sealed class carrying the projected fields. Field order matches the source order written in the select body.
  • <Master><Name>Fields — a public sealed class exposing typed field handles for the projected record, with the same singleton Instance shape as <Master>Fields.
  • <Master><Name>Relation — a public sealed class carrying the source relation by value plus its own pair-level QueryPlan<<Master><Name>Record>. Stage methods are copy-on-write; terminals mirror the base relation's surface, parametrised on the projected record type.
  • public <Master><Name>Relation Select<Name>() — 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.

C#
var summaries = await Items
    .Where(item => item.Count.Ge(10))
    .SelectSummary()
    .OrderBy(summary => summary.Name.Asc())
    .ToList(data, cancellationToken);

#Join Operator

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

  • <Master>Join<Field>Pair — a public sealed class with public required <Master>Record Left { get; init; } and public required <Target>Record Right { get; init; } properties that aggregate the joined pair.
  • <Master>Join<Field>LeftFields / <Master>Join<Field>RightFieldspublic sealed classes each exposing typed field handles for the corresponding side's record, plus a singleton Instance static field. Each handle's accessor reads pair.Left.<Field> or pair.Right.<Field>.
  • <Master>Join<Field>Fields — a public sealed class whose Left and Right instance fields point at the per-side singletons above, plus its own singleton Instance callback target.
  • <Master>Join<Field>Relation — a public sealed 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.
  • public <Master>Join<Field>Relation Join<Field>(<Target>Relation right) — 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 await source.ToList(data, cancellationToken), then iterate the surviving left records and await right.FindBy(data, cancellationToken, leftRecord.<Field>_<Pk1>, ...) for each, calling pairs.Add(new <Master>Join<Field>Pair { Left = ..., Right = ... }) on a successful match and dropping the row on null (INNER JOIN; LEFT / RIGHT / FULL OUTER deferred). Pair-level state (predicates, orderings, skip, take) then applies through QueryRuntime.Execute before the terminal returns. Joined relations do not expose FindBy.

C#
var pairs = await B
    .JoinARecord(A)
    .Where(fields => fields.Right.Name.Eq(class="token-string">"alpha"))
    .OrderBy(fields => fields.Left.Id.Asc())
    .ToList(data, cancellationToken);

#Match Statements

A Masterbelt match statement lowers to a C# switch statement on the lowered subject expression. The C# target relies on the language's pattern matching syntax to express every arm without synthesizing helper methods.

  • A type pattern T as name lowers to a C# type pattern case T name:. A union type pattern matches against the union's nested wrapper class (case IntOrString.Int_ wrapper: followed by var name = wrapper.Value; for the user binding). A type pattern against the union's null member lowers to case null:.
  • An enum pattern lowers to a value pattern case Status.Active:.
  • A literal pattern lowers to a constant pattern case 1:, case "a":, case true:, or case null:.
  • A product pattern lowers to a recursive pattern case T { Field: pattern, ... } prefix:. Field sub-patterns reuse the same pattern lowering rules. Short field bindings emit var patterns ({ Field: var name }) so the field value is captured directly.
  • A wildcard pattern lowers to default:.

A |-separated alternative list emits one case label per alternative on the same arm body. When alternatives introduce a binding, the binding is rendered with C#'s or pattern combinator inside a single case label.

Bindings introduced by a pattern are emitted as local variables declared by the pattern itself (C# pattern matching binds directly), so the arm body uses the bound names without extra declarations. When the subject expression is a plain identifier without an explicit pattern binding, the C# target renders the narrowed binding by introducing var name = <pattern variable>; at the top of the arm body.

A guard is rendered as a when clause on the case label (case T name when condition:). A guarded arm whose guard evaluates to false is skipped per C#'s normal switch semantics; matching continues with the next case.

A wildcard arm becomes the default: label. When the match is statically exhaustive without a wildcard, the C# target omits any default: label; C#'s compiler does not require one for switch statements (it only requires exhaustiveness on switch expressions). The Masterbelt checker has already proven exhaustiveness, so no fallback throw is synthesized.

A match statement is otherwise emitted at its source position inside the surrounding C# method body and shares the method's local scope. The subject expression is evaluated exactly once at the head of the switch.

#Operator Expressions

The unary and binary operator expressions defined in language/syntax.md reach the C# 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 C# target emits the call as the corresponding native operator:

  • Numeric operands emit + - * / % == != < <= > >= & | ^ << >> directly.
  • bool operands emit && || == !=; and/or/xor lower to && || != respectively. Unary not emits !.
  • string operands emit + for add, the == / != operators for eql/neq, and String.Compare ordinal comparison results for lt/lteq/gt/gteq.
  • 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 receiver.Method(args) (C# native overloading handles overload disambiguation). 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 C# expressions:

  • string.length lowers to a call into the runtime helper MasterbeltStringLength(receiver) written into MasterbeltRuntime.cs. The helper returns the number of Unicode codepoints in the string by iterating string.EnumerateRunes(), matching the spec's codepoint count. The naive receiver.Length property is not used because it returns the UTF-16 code unit count, which diverges from the spec for any non-BMP codepoint.
  • list<T>.size lowers to receiver.Count (the IReadOnlyList<T>.Count property).
  • map<K, V>.size lowers to receiver.Count (the IReadOnlyDictionary<K, V>.Count property).

Only the string.length access pulls in the runtime file; the list and map size accesses inline directly.

#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 C# target writes alongside the generated module files. The helper file is named MasterbeltRuntime.cs (the reserved Masterbelt PascalCase prefix used by any generator-managed file) and is emitted only when at least one generated module references a helper from it.

The helper file contributes to the shared partial class declared in the configured namespace, so call sites reach the helpers as bare identifiers without any class qualifier:

  • MasterbeltListAdd<T>(IReadOnlyList<T> a, IReadOnlyList<T> b): IReadOnlyList<T> returns a fresh list containing the elements of a followed by the elements of b.
  • MasterbeltMapAdd<K, V>(IReadOnlyDictionary<K, V> a, IReadOnlyDictionary<K, V> b): IReadOnlyDictionary<K, V> returns a fresh dictionary containing every entry of a and every entry of b, with keys present in both taking the value from b. The K parameter is constrained where K : notnull to satisfy Dictionary<,>'s key constraint.

Call sites for list.add emit MasterbeltListAdd(a, b); call sites for map.add emit MasterbeltMapAdd(a, b). C#'s type inference resolves the generic type parameters from the operand types at the call site.

#Function Types

A Masterbelt function type lowers to one of two C# forms depending on where it appears.

A type declaration whose body is a function type emits a public delegate at file scope alongside the shared partial class:

C#
public delegate int BinaryOp(int left, int right);
public delegate U Mapper<T, U>(T value);
public delegate int Summer(int initial, params int[] values);

Parameter names are preserved from source and rendered after the parameter type, matching C#'s Type name parameter form. A variadic parameter prefixed with * in source lowers to a C# params T[] parameter; the element type is the parameter's declared type. The variadic-must-be-last rule defined in language/types.md is enforced before generation.

When a function type appears inline as part of another type expression (a product field's type, a generic argument, a union member, and so on), it lowers to a closed System.Func<...> constructed by listing each parameter type followed by the return type. Variadic inline function types have no Func<> representation and are rejected with the diagnostic defined for unsupported types; named-delegate form must be used instead.

Effects defined in Effects shape the rendered signature:

  • cancellable appends a CancellationToken cancellationToken parameter to the parameter list.
  • failable does not change the declared return type. The C# signature renders the success type R; the failure path is plumbed by exception propagation at the call site (see Failable Handling).
  • asyncable wraps the declared return type R in System.Threading.Tasks.Task<R>. When the named delegate or Func<...> form is used, the wrapping applies to the return type only; the C# async keyword is not synthesized at the type level (it belongs on the implementation, not the delegate signature).

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

#Cross-Module References

A reference to a symbol declared in another Masterbelt module emits the bare C# identifier of the foreign symbol. No class qualifier is added: every module's reachable symbols are members of one shared partial class, so the foreign symbol is already visible in scope at the reference site.

#Re-exports

A pub { ForeignName as LocalName } from "./other.mst" declaration that renames the foreign symbol emits a forwarding member on the shared partial class. The forwarding member is a public static readonly field whose type matches the foreign symbol's checked type and whose initializer references the foreign symbol by its bare name:

C#
public static readonly int LocalName = ForeignName;

A re-export that keeps the foreign name (no as rename) is a no-op in C#: the foreign symbol is already a member of the shared partial class under the same identifier, so emitting public static readonly T Foo = Foo; would be a self-reference. Such re-exports emit no declaration.

Re-exports keep their declaration's doc comment as an XML documentation block immediately before the field.

#Effects

Each effect defined in codegen/model maps to C# as follows:

  • cancellable adds a CancellationToken cancellationToken parameter as the last parameter of the callable.
  • failable does not change the return type. Failure is surfaced through a thrown exception (see Failable Handling); call sites do not catch it so the exception bubbles through the surrounding callable.
  • asyncable wraps the result in Task<T> (or Task for void) 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 C# 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 method still becomes an async Task<R> method, and a non-cancellable declaration that calls a cancellable method still receives and forwards the CancellationToken.

A callable that carries multiple effective effects combines all applicable transformations on its signature.

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 C# reports failure through a thrown exception. The Masterbelt surface treats failable as transparent (see language/semantics.md); the C# target uses native exception propagation so neither the method signature nor the call site needs to surface the failure path:

  • fail "message" lowers to throw new System.InvalidOperationException("message");.
  • fail value where value: Error lowers to throw new System.InvalidOperationException(value.Message);.
  • A call to a failable callable lowers to a plain method invocation; a thrown exception propagates through the surrounding failable method because the surrounding method does not catch it.

No Error class is emitted by the C# target. Match expressions cannot observe the failure path of a failable call subject because the surface type of the call is R.

#Using Directives

The C# target's emitter assembles the file's using directives from the symbols referenced during rendering. A symbol carries the C# namespace it lives in and the unqualified identifier name. The emitter:

  • Aggregates the set of referenced namespaces across every declaration written into a file.
  • Emits one using <namespace>; directive per distinct referenced namespace, in lexicographic order, before the file's namespace directive.
  • When two distinct namespaces export the same unqualified name, the second occurrence is emitted as a using alias such as using <Alias> = <Namespace>.<Name>; so each local identifier is unique.
  • Rewrites identifier renderings to use either the unqualified name (when only one namespace exports it) or the chosen alias.

The default local name of an imported symbol is its original name in the namespace. The emitter only assigns aliases when collisions require it.

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

#Determinism

Generated files are deterministic with respect to the input modules and options. Constants appear in source order within each class. Union and wrapper class declarations appear sorted by interface name in MasterbeltUnions.cs. Map literal entries appear in lowering order (first-occurrence position, last-wins value). Using directives are emitted in lexicographic order.

Specification