#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: STRINGis the C# namespace used in every generated file. Required. The value is used verbatim as the file-scopednamespacedirective and must be a valid C# qualified identifier (one or more identifier segments separated by.).class: STRINGis the name of the sharedpublic static partial classevery module contributes to. Optional; defaults toMasterbelt. The value must be a valid C# identifier (one segment, no.).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 ADO.NET commands against a host-suppliedDbConnection— 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 sharedpublic static partial class(named by theclassoption, defaultMasterbelt) containing every reachable constant from that module, plus any product-type classes declared by the module as siblings at file scope. - A
MasterbeltUnions.csfile when any constant has a union type. The file declares one sealed-abstract-class union per union type encountered across the project. - A
MasterbeltMasterData.csfile 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
MasterbeltQuery.csfile when the project declares at least one master. The file declaresMasterSource,FieldRef, theIPredicate<R>/IOrdering<R>interfaces, the exported concrete predicate / ordering node classes that back the inspectable AST, the staticPredicateshelper withAnd/Or/Not, the generic field-handle classes (OrderedField<R, V>,BoolField<R>), theQueryPlan<R>value type, and theQueryRuntime.Executehelper 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
constfield is used when the constant's Masterbelt type isbool,string, or any built-in fixed-width numeric type and the lowered expression is a corresponding literal. C#constfields are compile-time constants and require literal initializers. Native-width numerics (int,uint) cannot be C#constbecausenint/nuintare disallowed there, so they always fall back to the next bullet. - A
static readonlyfield 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
| Masterbelt | C# |
|---|---|
null | object? (the value null lowers to the C# literal null) |
bool | bool |
int / uint | nint / nuint (C#'s native-width integers) |
int8 / uint8 | sbyte / byte |
int16 / uint16 | short / ushort |
int32 / uint32 | int / uint |
int64 / uint64 | long / ulong |
string | string |
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, ...): R | public 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
nulllowers to the C# literalnull.trueandfalselower to the C# literalstrueandfalse.- 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 theIDE0300: Collection initialization can be simplifiedwarning. An empty list literal lowers to[]. - A map literal of type
map<K, V>lowers tonew 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:
ULforuint64,Lforint64, andUforuint32. 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 theIDE0090: 'new' expression can be simplifiedwarning. 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:
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 areNull,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 classwrapper is declared that inherits from the abstract base. The wrapper exposes apublic requiredproperty namedValueof the member's mapped C# type. Inheritance letsis-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
nullmember does not get a wrapper. The C#nullliteral directly satisfies a nullable reference to the abstract base, so a union variable of static typeT1Or...?holdingnullrepresents 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:
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:
public enum Status : sbyte
{
Active = 0,
Inactive = 1,
}The storage clause uses the C# spelling defined in Type Mapping (int8 → sbyte, int32 → int, 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>) yieldsKeyValuePair<K, V>entries; the foreach pattern deconstructs them via the language's built-in tuple deconstruction support onKeyValuePair.range(start, end)subject —for (var i = start; i < end; i++) { ... }. A_binding synthesizes__mbIused 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 classdeclared at file scope alongside the record. The class is data-less: it carries a singleprivate readonly QueryPlan<<Master>Record> planfield plus a default constructor that initialises a fresh plan and aninternalconstructor that accepts a pre-built plan 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 (ToList,AsAsyncEnumerable,FindBy,FirstOrDefault,Count,Any) take(MasterData data, CancellationToken cancellationToken)(plus primary-key arguments forFindBy) and resolve records through the master'sIExecutorobtained fromdata.GetExecutor<Master>(). Every terminal carries theasyncable,cancellable, andfailableeffects per ../masterdata/schema.md — the C# target renders theasync/Task<R>wrap (asyncable) and the trailingCancellationToken cancellationTokenparameter (cancellable); failure surfaces through a thrown exception (failable).AsAsyncEnumerableis the streaming counterpart ofToList: it returnsIAsyncEnumerable<<Master>Record>and uses[EnumeratorCancellation]on itscancellationTokenparameter so cancellation flows throughawait 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 writeItems.Where(...).ToList(data, cancellationToken)after addingusing static <Namespace>.Masterbelt;(orMasterbelt.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>Recordsfield 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 callsdata.GetExecutor<Master>()to reach the active backend, thenawaitsExecute(plan, cancellationToken)/FindByPK(plan, keys, cancellationToken)on the returned executor. Understorage: memorythe accessor returns aMemoryExecutor<<Master>Record>wrapping the master's record list (and the<Master>Relation.Matches<Master>PKclosure 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:
MasterDatacarries a singleprivate readonly DbConnection connection(fromSystem.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;MasterDatanever closes it. GetExecutor<Master>()returns a per-master<Master>SqlExecutor(emitted alongside the relation) that translates the relation'sQueryPlaninto a parameterisedDbCommandviaSqlTranslator.TranslatePlan/TranslatePKLookup, runs it withExecuteReaderAsync, and materialises rows with the generatedScanRow.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 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:
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:
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 tothis.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 tothis.X(data, cancellationToken, ...args).OtherMaster.toList()lowers toMasterbelt.<OtherMaster>.ToList(data, cancellationToken)against the static field on the partial class.OtherMaster.X(any other cross-master reference) lowers toMasterbelt.<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— apublic sealed record(string Name)identifying a master by its source-level name.FieldRef— apublic 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'sWherecallback; the compiler rejects the mismatch onR.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>andBoolField<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 itsFieldRef.QueryPlan<R>— the inspectable AST value type wrappingSource,Predicates,Orderings,Skip, andTake. 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>Relationterminal.
The per-master user-facing declarations are:
<Master>Fields— apublic sealed classexposing onepublic readonlyfield-handle per supported record field plus apublic static readonly <Master>Fields Instancesingleton. 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 asIPredicate<<Master>Record>/IOrdering<<Master>Record>for the callback's return type.<Master>Relation— see Master Data for the relation class itself. ItsWhere/OrderBy/ThenBycallbacks are typedFunc<<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.
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 (genderedAdult → GenderedAdult). 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.
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— apublic sealed classcarrying the projected fields. Field order matches the source order written in the select body.<Master><Name>Fields— apublic sealed classexposing typed field handles for the projected record, with the same singletonInstanceshape as<Master>Fields.<Master><Name>Relation— apublic sealed classcarrying the source relation by value plus its own pair-levelQueryPlan<<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>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.
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— apublic sealed classwithpublic required <Master>Record Left { get; init; }andpublic required <Target>Record Right { get; init; }properties that aggregate the joined pair.<Master>Join<Field>LeftFields/<Master>Join<Field>RightFields—public sealed classes each exposing typed field handles for the corresponding side's record, plus a singletonInstancestatic field. Each handle's accessor readspair.Left.<Field>orpair.Right.<Field>.<Master>Join<Field>Fields— apublic sealed classwhoseLeftandRightinstance fields point at the per-side singletons above, plus its own singletonInstancecallback target.<Master>Join<Field>Relation— apublic sealed classcarrying 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>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 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.
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 namelowers to a C# type patterncase T name:. A union type pattern matches against the union's nested wrapper class (case IntOrString.Int_ wrapper:followed byvar name = wrapper.Value;for the user binding). A type pattern against the union's null member lowers tocase 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:, orcase 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 emitvarpatterns ({ 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. booloperands emit&& || == !=;and/or/xorlower to&& || !=respectively. Unarynotemits!.stringoperands emit+foradd, the==/!=operators foreql/neq, andString.Compareordinal comparison results forlt/lteq/gt/gteq.- 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 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.lengthlowers to a call into the runtime helperMasterbeltStringLength(receiver)written intoMasterbeltRuntime.cs. The helper returns the number of Unicode codepoints in the string by iteratingstring.EnumerateRunes(), matching the spec's codepoint count. The naivereceiver.Lengthproperty is not used because it returns the UTF-16 code unit count, which diverges from the spec for any non-BMP codepoint.list<T>.sizelowers toreceiver.Count(theIReadOnlyList<T>.Countproperty).map<K, V>.sizelowers toreceiver.Count(theIReadOnlyDictionary<K, V>.Countproperty).
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 ofafollowed by the elements ofb.MasterbeltMapAdd<K, V>(IReadOnlyDictionary<K, V> a, IReadOnlyDictionary<K, V> b): IReadOnlyDictionary<K, V>returns a fresh dictionary containing every entry ofaand every entry ofb, with keys present in both taking the value fromb. TheKparameter is constrainedwhere K : notnullto satisfyDictionary<,>'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:
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:
cancellableappends aCancellationToken cancellationTokenparameter to the parameter list.failabledoes not change the declared return type. The C# signature renders the success typeR; the failure path is plumbed by exception propagation at the call site (see Failable Handling).asyncablewraps the declared return typeRinSystem.Threading.Tasks.Task<R>. When the named delegate orFunc<...>form is used, the wrapping applies to the return type only; the C#asynckeyword 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:
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:
cancellableadds aCancellationToken cancellationTokenparameter as the last parameter of the callable.failabledoes 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.asyncablewraps the result inTask<T>(orTaskforvoid) and the callable is declaredasync. A call site whose callee is asyncable is wrapped inawait.
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 tothrow new System.InvalidOperationException("message");.fail valuewherevalue: Errorlowers tothrow new System.InvalidOperationException(value.Message);.- A call to a
failablecallable lowers to a plain method invocation; a thrown exception propagates through the surroundingfailablemethod 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'snamespacedirective. - When two distinct namespaces export the same unqualified name, the second occurrence is emitted as a
usingalias such asusing <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.