#Go Code Generation
This document defines the Go code generation target.
#Kind
The target kind identifier is golang.
#Options
The Go target reads the following options from its options mapping in the project configuration:
package: STRINGis the Go package name used in every generated file. Required. The value is used verbatim as the package clause and must be a valid Go package identifier.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 throughNewMasterDataorLoadJSON) andsql(a SQL-backed executor that translates queries into SQL against a host-supplied database connection — see Master Data).
A missing or empty package is a configuration error. A storage value other than memory or sql is a configuration error.
The Go target does not consume any other options at this stage; unknown options are silently ignored to leave room for future extension without breaking existing configurations.
#File Set
For a project whose lowered IR modules are m1.mst, m2.mst, ..., the Go target produces:
- One generated file per Masterbelt module. The file name is the module name with the
.mstsuffix replaced by.go. Each file contains the Go declarations corresponding to that module's constants. - A
masterbelt_unions.gofile when any constant has a union type. The file declares the sealed interface and member wrapper types for every union encountered across the project. The file is omitted when no union types are used. - A
masterbelt_masterdata.gofile when the project declares at least one master. The file declares theMasterDatastruct, theNewMasterDataconstructor, and theWith/Fromcontext helpers described in Master Data. The file is omitted when the project declares no master. - A
masterbelt_query.gofile when the project declares at least one master. The file declares theQueryPlanvalue type, thePredicate/Orderinginterfaces, the exported concrete predicate / ordering node structs that back the inspectable AST, the generic field-handle types (OrderedField[R, V],BoolField[R]), the combinators (And,Or,Not), and the in-memory executor consumed by every generated relation terminal; see Master Data. The file is omitted when the project declares no master.
All files share the same Go package and live under the configured output root.
The Go target does not emit a separate runtime file at this stage.
#Type Declarations
Each Masterbelt type declaration emits one Go type declaration. Non-product targets use the Go type-alias form so the declared name is a transparent name for the resolved body:
type LocalName[Params] = MappedTargetTypeProduct targets use a defined type so a named, addressable receiver type is available for methods and serializer hooks:
type LocalName[Params] struct { ... }When the declaration carries type parameters, the emitted Go declaration carries the same parameters as a Go type parameter list ([T any, ...]). All parameters are emitted with the any constraint at this stage. Visibility follows the source program: a pub-declared name is emitted under its public Go identifier, while a non-public name keeps the lowercase form per the visibility rule.
At a use site, a generic declaration is rendered with its type arguments: LocalName[T1, ...]. A product literal whose declared type is generic is emitted with type arguments on the literal type as well, for example Container[int]{Value: 1}.
#Cross-Module References
All modules in a project share one Go package. A reference to a symbol declared in another Masterbelt module emits the bare Go identifier of that symbol; no Go import statement is required and no module qualifier is added to the call site.
Because every module lives in the same Go package, every top-level identifier across the project must be unique under its Go mapping. The target emits no per-module prefix; collisions are reported as a generation diagnostic.
#Re-exports
A pub { ForeignName as LocalName } from "..." declaration emits a forwarding var in the current module's Go file:
var LocalName = ForeignNameThe forwarding var makes the local name visible alongside the foreign one inside the shared Go package. Go's type inference keeps the declaration's source type identical to the foreign symbol's type.
#Reserved file name prefix
Files invented by the Go target itself, rather than derived from a Masterbelt source file name, use the reserved masterbelt_ prefix. This keeps generator-managed files in a separate name space from user-named modules so that a Masterbelt file such as unions.mst does not collide with the framework's union catalog. New generator-managed files added to this target MUST use the same prefix.
#Package Clause
Each generated file begins with package <package> where <package> is the configured package option.
#Reachability
The Go target follows the default reachability policy defined in codegen/model: only constants reachable from at least one pub-declared constant are emitted. Non-public constants that are not referenced by any reachable constant are dropped from the output, including their identifier in any generated file. 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 const item carries a public flag from its pub modifier. Each Masterbelt identifier is mapped to a Go identifier whose first letter is upper case for public constants and lower case for non-public constants. The remainder of the identifier is preserved as written.
A constant whose Masterbelt identifier starts with a non-letter character (such as _) is not currently supported and is a generation error.
#Constants and Variables
Each Masterbelt const item maps to one Go declaration:
- A
constdeclaration is used when the constant's type isbool,string, or any built-in numeric type (int,int8, ...,uint64) and the lowered expression is a corresponding literal. - A
vardeclaration is used for every other case:nulltyped constants, list and map literals, union typed constants, and references to other constants.
The declared Go type follows the type mapping table below. The right-hand side is the lowered expression rendered as a Go expression.
Doc comment lines from the source are emitted as // <line> lines immediately before the declaration in source order, with one space inserted between // and the original text when the original text has no leading space.
#Type Mapping
| Masterbelt | Go |
|---|---|
null | any (the value null lowers to the Go untyped nil) |
bool | bool |
int / uint | int / uint (Go's natural integer width) |
int8 / uint8 | int8 / uint8 |
int16 / uint16 | int16 / uint16 |
int32 / uint32 | int32 / uint32 |
int64 / uint64 | int64 / uint64 |
string | string |
list<T> | []T' where T' is the Go mapping of T |
map<K, V> | map[K']V' where K' and V' are the Go mappings |
T1 | T2 | ... | Sealed interface generated into unions.go (see Unions below) |
{f: T, ...} | Defined Go struct type when reached through an alias declaration; see Product Types below |
fn(p: T, ...): R | Go function type func(p T, ...) R (see Function Types below) |
enum Name { ... } | Defined integer type type Name storage plus one const Variant Name = value per variant; see Enums below |
Type declarations are resolved before mapping; declared types do not appear in generated code.
#Literal Mapping
nulllowers to the Go untypednil.trueandfalselower to the Go literalstrueandfalse.- An integer literal lowers to its decoded value rendered in base 10.
- A string literal lowers to a Go double-quoted string with Go escape sequences.
- A list literal
[e1, ..., eN]of typelist<T>lowers to[]T'{e1', ..., eN'}. - A map literal of type
map<K, V>lowers tomap[K']V'{k1: v1, ...}with entries emitted in lowering order after last-wins deduplication. - An identifier reference lowers to the referent constant's mapped Go identifier.
- A product literal of type
Itemlowers to a Go struct literalItem{Name: ..., ...}. Field initializers preserve the source order of the literal; field name keys are upper-cased so they are reachable from any package.
#Master Data
A master Foo { record { ... } static { ... } } declaration follows the runtime model defined in ../masterdata/schema.md. The Go target emits the following declarations per master:
<Master>Record— the Go struct that backs one row. Field naming, modifiers, and constructor / getter generation follow the regular Product Types rules; the only difference is the type name suffix.<Master>Relation— a Go defined struct type that exposes the master's query surface. The relation is a data-less value type carrying only aQueryPlan[<Master>Record]field; it does not own records. Records are reached throughMasterDataat terminal time via the per-master accessor<master>Executor(*MasterData) Executor[<Master>Record]emitted in the same file. The accessor wraps the records the host supplied toNewMasterData(orLoadJSON) as amemoryExecutorand returns a never-nilExecutorso terminals can callExecute/FindByPKunconditionally.<Master>— a package-levelvarof type<Master>Relationinitialised with an empty query plan. Authoring a query starts from this value (Items.Where(...).ToSlice(ctx)), never fromdata.Items. AMasterDataaccessor for the master is intentionally absent from the public API: the data is reached implicitly throughFrom(ctx).- Chainable stages and terminals on
<Master>Relation: copy-on-writeWhere,OrderBy,ThenBy,Skip,Take, plus the terminalsToSlice,Iter,FindBy,FirstOrDefault,Count,Any. The full method shapes appear under Query API.
Both types live as siblings at file scope. The Masterbelt source identifier <Master> no longer requires a data.<Master> accessor: the package-level relation value carries the same name.
Nested masters follow the same naming scheme on the flattened identifier — master User { master Friendships { ... } } emits UserFriendshipsRecord, UserFriendshipsRelation, and a package-level UserFriendships value as siblings of the parent. See Nested Masters.
#MasterData Entry
Every project that declares at least one master emits one generator-managed file masterbelt_masterdata.go carrying the project-wide dataset entry. The file declares:
MasterData— a struct with one unexported records slice per master in the project (<master>Records []<Master>Record). MasterData owns the records, not the relations. The per-master accessor (<master>Executor(*MasterData) Executor[<Master>Record]) emitted next to each relation reads this field and wraps it as amemoryExecutorso the terminal call site never sees the storage choice; a futurestorage: sqlconfiguration will change the accessor body to return a SQL-backed executor while keeping the call site stable.NewMasterData(<master1> []<Master1>Record, <master2> []<Master2>Record, ...) *MasterData— a positional constructor that takes one record slice per master in master-declaration order and returns a fully wired*MasterData.With(ctx context.Context, data *MasterData) context.Context— attaches a*MasterDatato a context so generated terminals reached through that context can resolve the active records.From(ctx context.Context) *MasterData— retrieves the*MasterDatapreviously attached withWith, returningnilwhen no value was attached.
The Go target never writes MasterData, With, From, or NewMasterData in code emitted from a Masterbelt source program: those identifiers exist for the host application to construct and inject the dataset. Generated terminals consult From(ctx) to resolve the active records; the public chainable API never names MasterData directly.
When storage: sql is configured, the layout above changes:
MasterDatacarries a singledb SQLDBfield shared by every per-master executor; the per-master records slices are not emitted.- The positional
NewMasterData(...)constructor is replaced byNewSQLMasterData(ctx context.Context, db SQLDB) (*MasterData, error)so the host application supplies a connection rather than record slices. The constructor stores the suppliedSQLDBand returns; per-master executors fetch rows on demand through the connection. WithandFromare unchanged.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 SQLDB interface, the matching SQLRows / SQLRow row interfaces, and the translatePlan / translatePKLookup helpers live in masterbelt_query.go. A host built on database/sql wraps *sql.DB in a small adapter that satisfies SQLDB; alternative SQLite bindings can do the same without dragging a particular driver into the generated package.
The adapter is a handful of lines because *sql.DB.QueryContext returns a *sql.Rows rather than the generated SQLRows:
type sqlDBAdapter struct{ db *sql.DB }
func (a sqlDBAdapter) QueryContext(ctx context.Context, query string, args ...any) (masters.SQLRows, error) {
rows, err := a.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
return rows, nil // *sql.Rows already satisfies SQLRows
}
func (a sqlDBAdapter) QueryRowContext(ctx context.Context, query string, args ...any) masters.SQLRow {
return a.db.QueryRowContext(ctx, query, args...) // *sql.Row already satisfies SQLRow
}
// db, _ := sql.Open(class="token-string">"sqlite", class="token-string">"masterdata.db") // host registers its own driver
// data, _ := masters.NewSQLMasterData(ctx, sqlDBAdapter{db: db})*sql.Rows and *sql.Row already satisfy SQLRows / SQLRow structurally, so the only wrapping needed is the QueryContext return type. The host registers a SQLite driver (modernc.org/sqlite, github.com/mattn/go-sqlite3, ...) and owns the *sql.DB open/close lifetime; the generated code never imports a driver.
Under storage: memory the same file also declares LoadJSON(data []byte) (*MasterData, error), a helper that unmarshals the JSON document produced by the JSON exporter (see ../masterdata/export-json.md) into a fresh *MasterData. The function uses encoding/json only and is independent of any backend library:
func LoadJSON(data []byte) (*MasterData, error) {
var raw struct {
Items []ItemsRecord `json:class="token-string">"items"`
UserFriendships []UserFriendshipsRecord `json:class="token-string">"userFriendships"`
// ... one per master in declaration order
}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
return NewMasterData(raw.Items, raw.UserFriendships /* ... */), nil
}Each generated <Master>Record struct carries a json:"<surfaceName>" tag on every field so the inner record objects round-trip with the surface field names as JSON keys. The surface name is the master's source-level field identifier verbatim (id, name, userId) without any case transformation. A ref<T> field expands to the underlying primary-key fields under the surrounding field's name joined with _ (field_pk1, field_pk2, ...); the JSON tag on each expanded leaf carries the joined source name. Other product types not declared inside a master block do not receive JSON tags.
#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 tor.ToSlice(ctx)against the receiver of the surrounding relation method. The receiver is the relation value the static method was invoked on, so a caller that chains stages ontorbefore invoking the static observes those stages.Master.X(any user-declared static constant or method) inside the same owning master's body lowers tor.X(ctx)for a constant orr.X(ctx, ...)for a method.OtherMaster.toList()(a cross-master reference) lowers to<OtherMaster>.ToSlice(ctx)against the package-level relation value.OtherMaster.X(any other cross-master reference) lowers to<OtherMaster>.X(ctx, ...)against the package-level relation value.
The receiver of the surrounding relation method is named r so a chained owner-self reference never collides with the self receiver used by record-attached methods.
#Top-Level Dataset Threading
A top-level function or product-type method that transitively reaches any master static member (constant or method, including the built-in toList()) implicitly acquires the dataset and threads it through every call that needs it. The Go target piggy-backs on the existing cancellable inheritance machinery: a function that reaches a master is treated as effectively cancellable, so it receives the same ctx context.Context first parameter the cancellable transform adds and forwards it through call sites that also became effectively cancellable. Effects already in scope (failable, asyncable) compose with the synthesized cancellable transform without further interaction.
The Masterbelt source program never declares cancellable for the purpose of master access; the codegen-side inference is invisible at the surface.
#Query API
Every master emits the chainable surface directly on <Master>Relation. Predicates and orderings are constructed by methods on a package-level field handle; there is no separate <Master>Query type in the public API. See ../masterdata/query.md for the cross-target contract and ../masterdata/schema.md for how it ties into the master-data schema.
The per-master declarations are:
<Master>Fields— a package-level variable that holds one typed field handle per supported record field. The variable is exported regardless of the master's visibility because the field handles are the call-site authoring surface. The backing struct type is unexported (<master>FieldsStruct); users reach the field handles through the variable (ItemsFields.Category.Eq("weapon")) and never name the struct directly. Fields whose Go type is not one of the supported primitives (numeric, string, bool) are omitted from the field builder silently; the user-facing query API still emits for the rest of the surface.- Stage methods on
<Master>Relation(value receivers):Where(predicate Predicate[<Master>Record]) <Master>Relation,OrderBy(ordering Ordering[<Master>Record]) <Master>Relation,ThenBy(ordering Ordering[<Master>Record]) <Master>Relation,Skip(n int) <Master>Relation,Take(n int) <Master>Relation. Each method returns a freshly allocated relation whose plan field extends the receiver's plan; the receiver is never mutated, so a base relation can be shared across independent chains. - Terminal methods on
<Master>Relation:ToSlice(ctx context.Context) ([]<Master>Record, error),Iter(ctx context.Context) iter.Seq2[<Master>Record, error],FindBy(ctx context.Context, k1 T1, ...) (<Master>Record, bool, error),FirstOrDefault(ctx context.Context) (<Master>Record, bool, error),Count(ctx context.Context) (int, error),Any(ctx context.Context) (bool, error). Every terminal carries the same three effects astoList()/FindBy; the Go target renders thecancellableslot (ctx) and thefailableslot (trailingerror) explicitly.
Iter returns iter.Seq2[<Master>Record, error] from the standard iter package so user code consumes the relation with for record, err := range Items.Where(...).Iter(ctx) { ... }. The iterator consumes the same plan as ToSlice; backends that can stream a particular operation are free to do so, but the public iterator contract is stable.
FindBy honours the relation's Where predicates: a primary-key match that fails the filter chain returns the zero record with ok=false. OrderBy, ThenBy, Skip, and Take do not affect FindBy.
The QueryPlan[R] value type, the Predicate[R] / Ordering[R] interfaces, the exported concrete node structs (EqPredicate, LtPredicate, BetweenPredicate, AndPredicate, AscOrdering, ...), the generic field-handle types OrderedField[R any, V cmp.Ordered] and BoolField[R any], the combinators And(...), Or(...), Not(...), the Executor[R] seam consumed by every terminal, the memoryExecutor[R] in-memory implementation, and the iterator helpers iterMemoryPlan / iterMemoryError / iterMaterialised all live in the generator-managed masterbelt_query.go file. Backends can inspect a relation's plan structurally — every concrete node carries its field reference and operand values — without invoking the closures the in-memory evaluator uses.
weapons, err := masterdata.Items.
Where(masterdata.And(
masterdata.ItemsFields.Category.Eq(class="token-string">"weapon"),
masterdata.ItemsFields.Level.Ge(10),
)).
OrderBy(masterdata.ItemsFields.SortOrder.Asc()).
Take(10).
ToSlice(ctx)#Scope Methods
Each pub scope on a master emits an exported method on <Master>Relation; 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 ctx and no error because a scope is effect-free.
func (r <Master>Relation) <Scope>(<params>) <Master>RelationThe method returns a <Master>Relation: it extends the receiver's plan with the scope body's stages (with chained scopes inlined) and returns a fresh relation, exactly like the built-in stage methods, so a scope composes with Where / OrderBy and with other scopes (masterdata.Records.Adult().Gendered(1)). The method is backend-independent — it builds a plan and is identical 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 Go surface beyond what its pub flag implies; it only influences SQLite index generation.
#Select Projections
Each select Name { ... } section on a master (../masterdata/schema.md) emits a parallel set of declarations alongside the source relation:
<Master><Name>Record— a Go defined struct type carrying the projected fields. Field order matches the order written in the select body; field types are copied from the master's record by name.<Master><Name>Fields— the package-level field builder for the projected record, with the same shape as<Master>Fieldsbut parametrised on the projected record.<Master><Name>Relation— the projected relation type. It carries the source relation by value plus its ownQueryPlan[<Master><Name>Record]. Its stage and terminal methods mirror the source relation's surface, parametrised on the projected record type. Stage methods acceptPredicate[<Master><Name>Record]/Ordering[<Master><Name>Record]; terminals carry the same(ctx context.Context, ...) (..., error)shape as the source relation's terminals.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 against the master's record slice, 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. Predicates and orderings added through the projected relation are typed on the projected record, so authoring Where(ItemsSummaryFields.Name.Eq("alpha")) against an ItemsSummaryRelation is a compile-time match; passing a source-record predicate to the projected relation is a compile-time error.
Projected relations do not expose FindBy: the projected record does not have a primary-key concept.
summaries, err := Items.
Where(ItemsFields.Count.Ge(10)).
SelectSummary().
OrderBy(ItemsSummaryFields.Name.Asc()).
ToSlice(ctx)#Join Operator
Each ref<Target> field on a master's record (../masterdata/relations.md) emits a parallel set of declarations alongside the source relation:
<Master>Join<Field>Pair— a Go defined struct type with two exported fields,Left <Master>RecordandRight <Target>Record. The pair is the join's record-side aggregate.<Master>Join<Field>Fields— a package-level field builder for the pair record. The variable's struct exposes nestedLeftandRightsub-builders, each carrying one typed field handle per supported primitive on the corresponding side (including the expanded ref-field columns on the left). Each handle's accessor readspair.Left.<field>orpair.Right.<field>.<Master>Join<Field>Relation— the joined relation type. It carries the source relation by value, the right relation supplied at the call site, and its ownQueryPlan[<Master>Join<Field>Pair]. Its stage and terminal methods mirror the source relation's surface, parametrised on the pair record. Stage methods acceptPredicate[<Master>Join<Field>Pair]/Ordering[<Master>Join<Field>Pair]; terminals carry the same(ctx context.Context, ...) (..., error)shape as the source relation's terminals.Join<Field>(right <Target>Relation) <Master>Join<Field>Relation— a method on<Master>Relationthat returns a fresh joined relation capturing the receiver as the source side, the supplied relation as the right side, and a fresh pair-level plan.
Terminals on the joined relation first call ToSlice(ctx) on the source relation (so source-side state applies first), then iterate the surviving left records and call right.FindBy(ctx, leftRecord.<field>_<pk1>, ...) for each, emitting a <Master>Join<Field>Pair{Left: ..., Right: ...} on a successful match and dropping the row on a non-match (INNER JOIN; LEFT / RIGHT / FULL OUTER deferred). Pair-level state (predicates, orderings, skip, take) then applies to the pair slice before the terminal returns.
Joined relations do not expose FindBy: the pair record does not have a primary-key concept.
pairs, err := B.
JoinARecord(A).
Where(BJoinARecordFields.Right.Name.Eq(class="token-string">"alpha")).
OrderBy(BJoinARecordFields.Left.Id.Asc()).
ToSlice(ctx)#Product Types
A Masterbelt type declaration whose target is a product type emits a defined Go struct type rather than the type-alias form used for other targets:
type Item struct {
Name string
Count int
}The field order matches the field order written in source. A defined type is used so methods and serializer hooks can be attached to the struct receiver idiomatically.
Each field's Go visibility follows its Masterbelt modifier. A field with no modifier (or with the explicit writable modifier) is emitted as an exported field with a Title-cased name; a readonly field is emitted as an unexported field with a lower-cased name. Mutable fields stay directly assignable; readonly fields can only be set at construction.
When any field of a product declaration carries readonly, the target also emits:
- A
NewName[P](field1 T1, ...) Name[P]constructor that takes every field as a positional parameter in source order and assigns it to the corresponding struct field. The constructor is the only way external callers can populate the unexported readonly fields. - A value-receiver getter method per readonly field. The getter's name is the Title-cased form of the field's Masterbelt identifier and its return type is the field's mapped Go type.
At a use site, a product literal whose declared type carries any readonly field lowers to a NewName(...) constructor call with arguments rearranged into source order. Without any readonly field the literal continues to lower to the existing struct literal form Name{Field: ...}.
A readonly field whose generated getter name would collide with another field's exported Go name (for example, a readonly field name whose getter Name() would shadow a mutable field Name) is a generation error reported with masterbelt.codegen.golang.field_modifier_collision.
A product literal Item { name: ..., count: ... } lowers to a Go struct literal whose type is the alias name and whose field initializers use the upper-cased field names.
#Function Types
A Masterbelt function type lowers to a Go function type func(name T, ...) R. A type declaration whose body is a function type emits a Go type-alias declaration following the rule in Type Declarations:
type BinaryOp = func(left int, right int) int
type Mapper[T any, U any] = func(value T) U
type Summer = func(initial int, values ...int) intParameter names are preserved from source. A variadic parameter prefixed with * in source lowers to a Go variadic parameter prefixed with ...; the element type is the parameter's declared type. A variadic parameter is permitted only as the last parameter (the language rule defined in language/types.md is enforced before generation).
Effects defined in Effects shape the rendered signature:
cancellableinserts acontext.Contextparameter at the beginning of the parameter list, before any declared parameters.failableappends anerrorresult, yielding a(R, error)return tuple whereRis the declared return type.asyncabledoes not change the rendered signature; see Effects for the rationale.
A function type that combines multiple effects applies every transformation listed above.
#Enums
A Masterbelt enum lowers to a Go defined integer type plus one const declaration per variant. The defined-type form (no =) preserves the enum's nominal identity in Go: the enum type is not interchangeable with its storage type without an explicit Storage(variant) conversion at the call site.
type Status int8
const Active Status = 0
const Inactive Status = 1Variant identifiers are emitted at package scope under their source names. A variant whose value was explicit in source uses that value; a variant without an explicit value uses 0 for the first variant and the previous variant's value plus one thereafter (the language rule defined in language/types.md).
A member access expression Enum.Variant lowers to the bare Go identifier for the variant. The surface-level dot is not preserved because each variant is already accessible as a package-scope identifier; the type system carries the enum type through the call site.
#Functions and Methods
A top-level function declaration emits a Go func Name(params) Result { ... } at package scope. The function name is rendered with the C# convention's capitalization rule: public functions become exported PascalCase identifiers; non-public functions stay lowerCamelCase. Effect modifiers reshape the signature per the Effects section.
A method declared inside a product type emits a Go receiver function func (self Owner) Method(params) Result { ... } against the owning struct type. The receiver is named self in the emitted Go so a Masterbelt method body that references the implicit self keyword maps one-to-one onto the Go receiver. Methods that share a name (overloaded methods) are disambiguated by a 1-based numeric suffix: the first overload keeps the original name, the next is Method1, then Method2, and so on. Call sites carry the same suffix so the dispatch is decided at the call site by the checker's overload resolution.
A call expression target(args) emits the Go call form target(args). A method call value.method(args) emits value.Method(args) (with the overload suffix when applicable).
A function literal fn(params): R { ... } emits a Go function literal func(params) R { ... } inline at its surrounding expression position.
A return statement emits the Go return statement, optionally followed by the value expression.
#For Statements
A Masterbelt for statement lowers to a Go for statement. The IR subject shape selects the form:
list<T>subject —for _, name := range list { ... }. A skipped value binding (_in source) is rendered as Go's_.map<K, V>subject —for k, v := range m { ... }. Either binding renders as_when skipped.range(start, end)subject —for i := start; i < end; i++ { ... }withinamed after the source binding. A_binding still requires a loop variable; the Go target synthesizes a local named__mbIwhose only purpose is to advance the counter.
A break statement lowers to the Go break keyword; a continue statement lowers to the Go continue keyword. The Go compiler enforces the rule that both must appear inside a for body; the Masterbelt checker has already established that, so the generated code passes the check trivially.
The lowered subject expression is evaluated exactly once (Go's range and counted-for forms both have a single evaluation point), matching the language's once-only semantics. Iteration over a Master.toList() subject lowers through the Master Data rewrite: Items.ToSlice(ctx) against the package-level relation for a cross-master subject and r.ToSlice(ctx) when the iteration appears inside the owning master's own static body. Because toList() is failable, the surrounding callable inherits the same effect (the Go signature gains a trailing error result) and the for-loop subject is lifted into a __mbRecords, __mbErr := <call> short variable declaration plus the standard failable propagation guard before the for _, x := range __mbRecords { ... } loop — same shape the Failable Handling section defines for let x = <failable call>.
#Master Static Members
A master's static section (../masterdata/schema.md) emits its members on the master's <Master>Relation type (see Master Data):
- A
static const Name: T = valueemits as a value-receiver methodfunc (r *<Master>Relation) Name() T { return value }. The constant body has the same dataset-rewriting rules as a method body so a constant initializer that calls another master's static member resolves throughFrom(ctx).Other.X(ctx). - A
static fn name(params): R { body }emits as a value-receiver methodfunc (r *<Master>Relation) Name(ctx context.Context, params) R { body }. The first parameter is alwaysctx context.Context; the rest of the parameter list and the return type follow the regular function-type mapping including the effect-driven signature transforms (failable,asyncable) described in Effects.
A pub static member emits a Title-cased Go method name; a non-public member emits a lower-cased name. 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 r.X, and the cross-master case resolves to From(ctx).<Other>.X (with (ctx, args) when called).
#Match Statements
A Masterbelt match statement lowers to one of three Go forms depending on the subject's resolved type:
- Union subject lowers to a Go type switch on the lowered subject. Each arm becomes one
casewhose case type is the Go form of the arm's pattern type (the wrapper type for a non-null union member, ornilfor the union's null member). Bindings introduced by the pattern are emitted as local variables in the case body; a type pattern binding extracts the wrapper'sValuefield for non-null members, and uses the subject directly for thenilcase. A product pattern emits field accesses on the matched value to populate its short-form field bindings. The narrowing of the original subject identifier (when the subject is a plain identifier) emits an additionalname := <extracted>assignment at the top of the case body using the same narrowed expression. When the match is statically exhaustive nodefaultcase is emitted; a wildcard arm becomes adefaultcase. - Enum or literal subject lowers to a Go value switch on the lowered subject. Each arm becomes one
casewhose case expression list is the arm's enum-pattern or literal-pattern alternatives. A wildcard arm becomes thedefaultcase. - Subject with mixed arm kinds (for example a union of primitives where some arms are literals and some are type patterns) lowers to an
if/else ifchain. Each arm's condition combines a type-assertion check (for type and product patterns) with literal equality checks; bindings are introduced inside theifbody using the same extraction rules described above.
Guards lower as an if check wrapping the arm body. When a guarded arm's guard evaluates to false, control falls through the type switch (Go's case does not fall through by default, so the synthesized fallthrough is achieved by structuring guarded arms as if/else if chains nested inside the case).
The Go target emits no panic or default arm to absorb unhandled cases when the checker has proven exhaustiveness. A wildcard arm explicitly written in source lowers to default: (for switch forms) or a trailing else { ... } (for the if-chain form).
A match statement is otherwise emitted at its source position inside the surrounding Go function body and shares the function's local scope. The lowered subject is evaluated exactly once at the head of the switch (or assigned to a fresh local at the top of the if-chain).
#Operator Expressions
The unary and binary operator expressions defined in language/syntax.md reach the Go 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 Go target emits the call as the corresponding native Go operator instead of a method invocation:
- Numeric operands emit
+ - * / % == != < <= > >= & | ^ << >>directly. booloperands emit&& || == !=; the bitwise-shaped methodsand/or/xoronboollower 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 receiver.Method(args) (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 Go expressions:
string.lengthlowers toutf8.RuneCountInString(receiver)(with theunicode/utf8import added automatically). The result is the number of Unicode codepoints in the string, matching the spec semantics forstring.length.list<T>.sizeandmap<K, V>.sizelower tolen(receiver). The Golenbuilt-in returns the slice length and the map entry count respectively, both of which agree with the spec's element / entry count semantics.
No runtime helper is emitted for these fields; the lowering inlines the call at the access 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 Go target writes alongside the generated module files. The helper file is named masterbelt_runtime.go (the same reserved masterbelt_ prefix used by the union file) and is emitted only when at least one generated module references a helper from it.
The helper file declares the package functions used at call sites:
masterbeltListAdd[T any](a, b []T) []Treturns a fresh slice containing the elements ofafollowed by the elements ofb.masterbeltMapAdd[K comparable, V any](a, b map[K]V) map[K]Vreturns a fresh map containing every entry ofaand every entry ofb, with keys present in both taking the value fromb.
Call sites for list.add emit masterbeltListAdd(a, b); call sites for map.add emit masterbeltMapAdd(a, b). The helpers live in the same Go package as the rest of the generated code, so the call site uses the bare identifier without any import qualifier.
#Unions
A union type lowers to a Go sealed interface. For a canonical union T1 | T2 | ... | TN:
- The interface type is named by concatenating the title-cased member type names with
Or. For primitive members, the title-cased names areNull,Bool,Int,String. The order matches the canonical order recorded in the IR, which is the lexicographic order of each member's textual type spelling. - The interface declares one unexported method whose name is
is<Interface>so only generated types satisfy it. - For each non-null member type, the file declares a Go wrapper type named
<Interface><Member>with a single exportedValuefield of the Go type for that member, and implementsis<Interface>on the wrapper. - The
nullmember does not get a wrapper type. The Go untypednilis the zero value of any interface and serves as the null member's representation directly; an interface variable holdingnilcompares equal tonil.
A value of a union type lowers as a wrapper struct literal <Interface><Member>{Value: <expression>} for non-null members, and as the Go literal nil for null.
At this stage, only unions of primitive types are supported. A union containing a generic member is a generation error.
#Effects
Each effect defined in codegen/model maps to Go as follows:
cancellableadds acontext.Contextparameter as the first parameter of the callable.failableadds a trailingerrorreturn value to the callable.asyncablehas no idiomatic Go signature transform at this stage; a function type carryingasyncableis rendered as if the effect were absent. The lack of mapping is a known limitation: Go has no language-level future/promise type, and the target reserves an explicit choice (such as introducing a runtime channel helper) for a later design pass.
Every effect is inferred along the call graph: the Go 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 callable that carries multiple effective effects combines all applicable transformations on its signature; the parameter ordering is context.Context first, then declared parameters; the return ordering is declared results followed by error.
A caller invokes the resulting Go callable with the inherited context and propagates the returned error using ordinary Go control flow. The Go target does not invent helper macros or wrappers around the call site; effect propagation is recorded by the effective-effect inference and applied during signature rendering and call-site lowering.
#Failable Handling
A function whose effective effect set contains failable carries an error second result in its Go signature (see Effects). The effective set is the union of the declared effects and the effects of every function the body transitively calls; a non-failable declaration whose body calls a failable function still renders as (T, error) in Go. The surface program never has to acknowledge this transport (see language/semantics.md); the Go target plumbs it transparently:
fail "message"emitsreturn zero, errors.New("message")wherezerois the success-typed zero value (*new(R)).fail errwhereerris anErrorvalue emitsreturn zero, errors.New(err.Message).- A call whose callee is effectively failable, inside a body that is also effectively failable, is rewritten to receive both results and to short-circuit on a non-nil error. For a binding
let x = f()the emission isx, __mbErr := f(); if __mbErr != nil { return zero, __mbErr }; forreturn f()it is the analogous tuple form. The user never writes the guard.
A match expression cannot observe the Error path of a failable call subject because the surface type of the call is R; the synthesized local for the failure value is internal to this lowering.
#Imports
The Go target's emitter is responsible for assembling the file's import block from the symbols referenced during rendering. A symbol carries the Go import path of the package it lives in and the unqualified identifier name. The emitter:
- Aggregates the set of referenced import paths across every declaration written into a file.
- Picks a default alias from the path's last segment (skipping a trailing
/vNmajor-version suffix). - When two distinct paths share the same default alias, the emitter renames later occurrences by appending a numeric suffix until uniqueness is reached.
- Writes a Go
import (...)block listing every referenced path; unused references produce no import. - Rewrites identifier renderings as
alias.Namefor every external reference.
The default alias of the local package is empty: symbols declared in the same generated file reference each other without qualification.
Targets MUST express external references through this emitter mechanism. Inlining raw import paths 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 module file. Union and wrapper type declarations appear sorted by interface name in masterbelt_unions.go. Map literal entries appear in lowering order (first occurrence position, last-wins value). Import blocks are emitted in lexicographic path order.