#Types
This document defines the currently implemented Masterbelt type checking, inference, assignability, and overload behavior.
The type system is intentionally minimal at this stage. Future type additions must extend this document before or together with implementation changes.
Built-in types are defined in builtins.md.
#Type Expressions
A type expression denotes a type. Type expressions appear in const item type annotations and in type declarations.
type_expression = union_type | primary_type ;
union_type = primary_type "|" primary_type { "|" primary_type } ;
primary_type = named_type | generic_type ;
named_type = identifier | reserved_type_identifier ;
generic_type = identifier "<" type_expression { "," type_expression } ">" ;A named type references a built-in type or a user-declared type declaration by name.
A generic type applies type arguments to a built-in generic type constructor. User-declared generic type constructors are not implemented at this stage.
A union type denotes a value that has any one of its member types. Union types are flat: nested unions are flattened. Union members are deduplicated by type identity, and duplicate members are not an error.
A union type with fewer than two distinct members is invalid syntax. A type expression with only one type denotation must be written without |.
#Type Identity
Two types are the same type when they are structurally identical.
- Two named types are identical when they have the same name.
- Two generic types are identical when they have the same constructor and identical type arguments in order.
- Two union types are identical when they have the same set of member types.
- Two function types are identical when they have the same parameter types in order, the same variadic flag on the last parameter, the same return type, and the same effect set.
- Two enum types are identical when they originate from the same enum declaration. Enum types are nominal: an enum type is not identical to any other enum type even when their storage types and variant lists coincide.
Declared-type resolution applies before identity is determined. A declared type is identical to its resolved target type. An enum type is not unwrapped by declared-type resolution; it is its own type.
Parameter names are not part of a function type's identity: fn(a: int): int and fn(b: int): int denote the same type.
#Literal Types
Literal expressions have the following types:
nullhas typenull.trueandfalsehave typebool.- String literals have type
string.
Integer literals are typed contextually:
- With a numeric target type pushed down from an annotation or another contextual position (such as a list element type), the literal adopts that target's type. Aliases of numeric types are preserved at the use site so a literal annotated with
type ID = int32types asID. - Without a numeric target, the literal defaults to
int.
A literal whose magnitude does not fit the resolved numeric type's value range is rejected at lowering time with masterbelt.lowering.integer_out_of_range.
#Cast Expressions
A cast expression T(value) converts the inner value to the named target type. The target type T must resolve to a numeric type (a built-in numeric or an alias whose resolved body is numeric); a non-numeric target is a type checking error reported as masterbelt.checker.cast_non_numeric_target. The inner value must itself resolve to a numeric type; a non-numeric value is reported as masterbelt.checker.cast_non_numeric_value.
The cast expression's result type is the cast target with its surface form preserved: int32(42) types as int32 and Level(10) where type Level = int64 types as Level. The inner expression is type-checked against the cast's target so integer literals inside the cast adopt the target's numeric kind through the normal pushdown mechanism.
Cast expressions cannot widen the value beyond the target's range: the same integer_out_of_range rule that applies to annotated literals applies to a literal inside a cast.
#Collection Literal Types
Collection literals are type-checked contextually. When a target type is known from a type annotation, the checker pushes the target down into the literal and checks each element. When no target is known, the checker infers a type from the elements.
#List Literals
For a list literal [e1, e2, ..., eN]:
- With target type
list<U>: each element type must be assignable toU. The result type islist<U>. - Without a target type: the result type is
list<Union(type(e1), type(e2), ..., type(eN))>.
#Map Literals
For a map literal [k1: v1, k2: v2, ..., kN: vN]:
- With target type
map<K, V>: eachkimust be assignable toKand eachvimust be assignable toV.Kmust be a Comparable type. The result type ismap<K, V>. - Without a target type: the result key type is
Union(type(k1), ..., type(kN))and the result value type isUnion(type(v1), ..., type(vN)). The key union must be Comparable. The result type ismap<KeyUnion, ValueUnion>.
#Empty Collection Literals
A literal [] is an empty collection literal.
- With target type
list<U>: the result type islist<U>. - With target type
map<K, V>:Kmust be Comparable. The result type ismap<K, V>. - With any other target type, or without a target type: the empty collection literal is a type checking error.
#Pushdown and Generic Variance
Generic types remain invariant for general assignability. Pushdown into a literal is a contextual rule on the literal expression itself: when an element of [1, 2] is checked against int | string because the surrounding annotation is list<int | string>, each element succeeds by assignability to int | string. The resulting type takes the annotation, not the narrower element union.
Pushdown applies only when the target type is the same generic shape as the literal (list<U> for a list literal, map<K, V> for a map literal). When the target type is a union or otherwise does not directly match the literal shape, the checker falls back to inferring the literal type independently and then checking assignability through union membership.
#Comparable Types
A type is Comparable when it is one of:
- The primitive types
null,bool, orstring. - Any built-in numeric type listed in builtins.md (signed or unsigned, native or fixed width).
- A union type whose every member is Comparable.
- A type declaration that resolves to a Comparable type.
Generic types are not Comparable at this stage. Map key types must be Comparable.
#Type Annotations
A type annotation names the expected type of the annotated declaration item.
The annotation form is a type expression.
An unknown named type is a type checking error.
A built-in primitive type with type arguments is a type checking error.
A generic type with the wrong number of type arguments is a type checking error.
A non-generic named type with type arguments is a type checking error.
#Reserved Built-In Names
Built-in type names cannot be used as type declaration names. Declaring a type declaration whose name matches a built-in primitive or generic constructor is a type checking error. After such an error, references to the name continue to resolve to the built-in.
#Source Order Preservation
The type checker stores union member types in a canonical order. Tooling that renders type expressions back to source, such as the formatter, preserves the order written in source instead of the canonical order.
#Type Declarations
A type declaration introduces a new type binding for an existing type expression.
type ID = int
type Number = int | string
type Names = list<string>
A type declaration name is a single identifier. Type declaration names live in the type binding space defined in names.md.
A type declaration's right-hand side may reference other type declarations. The checker resolves declared names by chasing through the chain to the final non-declaration target.
A reference to a declared type retains the declaration's surface name at the reference site. Assignability and equality unwrap the surface wrapper so a value typed by the declared name is interchangeable with a value typed by its resolved target. Downstream consumers (code generation, indexes) use the surface name to render output that prefers the declared form over its resolved target.
A type declaration that directly or indirectly references itself is a type checking error.
A type declaration may be declared pub. Public type declarations follow the same visibility rules as pub const declarations.
When a union type lists declared-type members, the canonical union form strips the surface wrappers and keeps only the underlying targets. Surface names survive only outside union construction. The union int | ID where type ID = int canonicalizes to the single type int.
#Generic Type Declarations
A type declaration may declare a list of type parameters using the <T, ...> syntax described in syntax.md. Type parameters belong to the type declaration itself rather than to any specific body shape; the declared body may reference them through any nested type expression — product, union, generic, or any combination thereof.
type Box<T> = { value: T }
type Pair<A, B> = { first: A, second: B }
type Maybe<T> = T | null
type Items<T> = list<T>
A type parameter introduces an identifier in the type binding space that is visible only inside the body of the declaration that introduced it. Inside the body, a parameter name resolves to a distinct type variable; the variable is not a primitive, not equal to any user-declared type with the same surface name, and not assignable to any other type. Outside the declaration, the parameter is not in scope.
A generic declaration with N parameters must be applied with exactly N type arguments at every use site, written Name<arg, ...> with the same syntax used by built-in generic constructors such as list<int>. A bare reference to a generic declaration without arguments is a type checking error. A non-generic declaration applied with type arguments is a type checking error.
Applying a generic declaration substitutes each parameter with the corresponding argument throughout the declared body and yields the substituted target type. Equality and assignability of the resulting type follow the structural rules above on the substituted form; the declaration's surface name and argument list are preserved at the use site so downstream consumers (code generation, indexes) can render the surface form Container<int> instead of the substituted body.
A generic declaration may reference itself only by way of substitution at a use site. A directly or indirectly self-referential body (without an intervening type argument applied) is a type checking error in the same way as for non-generic declarations.
#Function Types
A function type denotes a callable signature. Its surface form is described in syntax.md; this section describes its semantics under the type system.
A function type carries:
- An ordered list of parameter types.
- A boolean flag on the last parameter indicating whether it is variadic.
- A return type.
- A set of effect modifiers.
Parameter names appear in source for documentation and tooling purposes only. They participate in scope and uniqueness checks at the declaration site (see below) but do not affect type identity or assignability.
#Parameter Name Uniqueness
Within a single function type, every parameter name must be distinct. Declaring two parameters with the same name is a type checking error reported as masterbelt.checker.duplicate_function_parameter.
#Variadic Position
A function type may carry at most one variadic parameter, and it must be the last parameter in the list. A *-prefixed parameter in any earlier position is a type checking error reported as masterbelt.checker.variadic_not_last. The element type of a variadic parameter is the type expression written after the colon; the function accepts zero or more arguments of that element type at that position.
#Effects
The effect set on a function type is canonical: source order is not significant, and duplicates collapse. Two function types whose effect sets contain the same members are identical for the purposes of Type Identity. The semantics of each effect are defined in codegen/model.md and their target mappings are defined in each target's specification.
#Methods on Product Types
A product type may carry an ordered list of methods alongside its fields. A method is a named callable bound to the product type; the method's signature is a function type (parameters, return type, effects). Methods do not contribute to the product type's structural identity: two product types whose fields agree but whose method sets differ are still the same type for the purposes of Type Identity and Assignability.
A product type may declare multiple methods with the same name; the methods must differ in their parameter shapes. Two methods with the same name and identical parameter signatures are a type checking error reported as masterbelt.checker.method_duplicate_signature.
A method is accessed as an instance member through value.method and invoked as value.method(args). Overload resolution selects an applicable method:
- Every applicable overload's parameter types must accept the call's argument types under Assignability.
- An exact-type match (every argument's type equals the corresponding parameter type) is preferred over an assignable-but-not-exact match.
- When no overload applies, the call is reported as
masterbelt.checker.overload_no_match. - When multiple overloads apply and no exact match disambiguates, the call is reported as
masterbelt.checker.overload_ambiguous.
The result type of a method call is the chosen overload's return type.
#Implicit Receiver self
Inside a method body the identifier self is in scope and refers to the implicit receiver value. Its checked type is the owning product type, so self.field resolves to one of the type's fields and self.method(args) invokes a method on the same type. Each target language renders the receiver according to its idiom: Go and TypeScript expose the receiver under the name self; C# rewrites self to its native this keyword.
The identifier self is reserved at every declaration position. It is allowed in value-position identifier references and as the target of a member access expression solely so the implicit receiver remains writable inside a method body. A use of self outside a method body is a type checking error reported through the usual unknown-name diagnostic because no scope binds the name.
#Functions as Values
A function declaration introduces a value of its function type into the value binding space. Calls against a function-typed value follow the same parameter-by-parameter assignability rule used for method calls, with no overload set: a function declaration produces exactly one signature.
A function literal fn(params)[: R] body produces a function-typed value with no name. Parameter and return types may be inferred from a surrounding contextual type (the const annotation, an argument position, and so on). Without a contextual type the parameter and return types must be written explicitly; otherwise the checker reports masterbelt.checker.function_parameter_missing_type or masterbelt.checker.function_missing_return_type.
#Generic Function Types
A function type may appear on the right-hand side of a generic type declaration. Type parameters introduced by the declaration are in scope within every part of the function type's signature: parameter types, the variadic element type, and the return type may all reference them. Applying the declaration substitutes each parameter throughout the function type's signature.
type Mapper<T, U> = fn(value: T): U
#Enum Types
An enum type denotes a finite, ordered set of named variants. Each variant is backed by a numeric integer value fixed at compile time.
The surface form of an enum declaration is described in syntax.md; this section describes its semantics under the type system.
An enum type carries:
- A declaration-site name.
- A storage type that is a built-in numeric type.
- An ordered list of variants. Each variant has a name and an integer value.
#Storage Type
The storage type is the type expression that follows the enum declaration's : clause. The type expression is resolved through declared-type resolution; the resolved target must be a built-in numeric type. A non-numeric resolved target is a type checking error reported as masterbelt.checker.enum_non_numeric_storage.
When the storage clause is omitted, the storage type is int8.
#Variant Values
Variant values are assigned in source declaration order by the rule in syntax.md. Every assigned value must fit the storage type's value range. A value outside the range is a lowering-time error reported with the standard integer-out-of-range diagnostic for the storage type.
Variant values are recorded as part of the enum type and exposed to downstream consumers (code generation, indexes). Two enum types with the same variant names but different values are distinct types under nominal identity.
#Type Identity
Enum types are nominal: an enum type is identical only to itself (the type produced by the same enum declaration). It is not identical to its storage type and not identical to any other enum type, including ones with structurally identical variant lists. The nominal identity rule lifts to Assignability below.
#Assignability
An enum type is not assignable to any other type, including its storage type. To produce a value of the storage type from a variant, a cast expression Storage(EnumName.Variant) is required.
Conversely, a value of the storage type is not assignable to an enum type. A program that needs to construct an enum value from an integer value writes the variant by name; integer-to-enum conversion is not provided.
#Member Access
Every type exposes a (possibly empty) set of named members. A member belongs to one of two kinds:
- An instance member is accessed through a value of the owning type:
value.member. Instance members have a member type that is the type of the result. - A static member is accessed through the type itself:
Type.member. Static members similarly carry the type of the resulting value.
The members exposed by each type at this stage are:
- A product type exposes one instance member per declared field. The member's name is the field name; the member's type is the field type.
- An enum type exposes one static member per declared variant. The member's name is the variant name; the member's type is the enum type itself (a variant value is itself a value of the enum).
- Every other built-in or user-declared type exposes no members at this stage.
A type declaration's resolved body provides the member set: an alias chases through to its target for instance-member lookup, so a value typed by an alias of a product type accesses fields through the alias surface name. Enum types are nominal and are not unwrapped; their static members are accessed directly through the enum's declaration name.
#Member Access Expression Resolution
The expression Target.Member is resolved by:
- Looking up
Targetfirst in the value binding space. If found, the access is an instance member access against the binding's checked type. - Otherwise, looking up
Targetin the type binding space. If found, the access is a static member access against the named type. - If
Targetresolves in neither space, the reference is reported as an unknown name.
The checker then looks up Member on the resolved type. A member name with no matching declaration is reported as masterbelt.checker.unknown_member. Accessing an instance member through a type or a static member through a value is also reported as masterbelt.checker.unknown_member: a type's instance members do not exist as static members and vice versa.
The result type of a member access expression is the member's declared type.
#Identifier Reference Types
An identifier reference expression has the checked type of its resolved value binding.
References are checked in source order. A reference whose binding has not yet been checked is a type checking error; this corresponds to the forward reference restriction defined in names.md.
#Const Type Checking
A const item initializer expression determines the initializer type.
If a const item has no type annotation, the const item type is inferred from the initializer type.
If a const item has a type annotation, the initializer type must be assignable to the annotated type.
const A = true // A has type bool
const B: bool = true // valid
const C: string = 1 // type error
const D: bool | int = 1 // valid; int is assignable to bool | int
type ID = int
const E: ID = 1 // valid; ID resolves to int
Grouped const declarations type check each item independently.
#Failable Handling
A function declared with the failable effect produces values of its declared success type R at the type level. Error is not visible at the type level of a failable call site; calls to a failable function are typed R, identical to calls to a non-failable function with the same success type. See semantics.md for the user-visible meaning.
Error is the built-in product type defined in builtins.md. It appears in the type system only as a permitted argument to the fail statement.
#fail Statement
fail is valid only inside a function whose declared effect set contains failable. Outside, the checker reports masterbelt.checker.fail_outside_failable.
The argument is assignable to one of:
string— the runtime constructsError { message: <argument> }.Error— the value is used directly.
Other argument types are reported as masterbelt.checker.fail_unsupported_argument. The argument is type-checked once with the union of permitted types as the contextual target.
#Effect Inheritance
Every effect — failable, asyncable, cancellable — is inherited silently at call sites. A call to a function whose effect set is non-empty is always valid regardless of the surrounding function's declared effect set. The type checker emits no diagnostic for missing effect declarations; each codegen target observes the call graph and lifts the obligation into the rendered signature.
The surface program may still declare effect modifiers on a function; they participate in type identity and in code generation the same way an inherited effect does. The declaration is a hint, not an obligation: a planner who writes pub fn foo() and calls an asyncable function inside is rendering the same target code as if pub asyncable fn foo() had been written.
#Return Type
A failable function's body is checked against its declared return type R: return value requires value to be assignable to R, and the body type-checks as if there were no failure path. The fail statement is independent of R; it always completes the function with an Error and does not contribute to the return-position type check.
#For Statements
The for statement is type checked against the static type of its subject expression. Type checking decides the binding count and element types, validates the loop body under a fresh scope, and tracks break/continue reachability.
#Subject Type
After declared-type resolution the subject must satisfy one of:
- A
list<T>shape — one binding, typedT. - A
map<K, V>shape — two bindings, typedKandVin source order. - A
Relation<M>shape — one binding, typedM's record type. Iterating a relation yields its post-filter records in plan order, equivalent to iterating the relation'stoList(). This branch is reached by thetable/selfbinding of a validationallrule (see Scope Bodies).
A subject whose resolved type is none of these shapes is reported as masterbelt.checker.for_subject_not_iterable. The diagnostic also fires for union subjects: a value typed list<T> | null must be narrowed before iteration.
The standard-library helper range(start, end) returns a list<int> and therefore reaches the list<T> branch automatically. Master-collection iteration relies on the master's all() method, which the checker resolves through its declared signature.
#Binding Count and Type
The number of declared bindings must match the subject's shape: a list<T> requires exactly one, a map<K, V> requires exactly two. Any other count is reported as masterbelt.checker.for_binding_count_mismatch carrying the expected and actual counts.
Each binding is added to the loop's fresh scope with the matching element type. A binding written as _ is omitted from the scope but still consumes its position for counting purposes. Reusing a binding name that shadows an enclosing scope follows the regular local redeclaration rule.
#Body Scope
The loop body is checked under a fresh scope that contains the bindings introduced above. Statements inside the body share the surrounding function's effect set and return type; a return inside the body terminates the surrounding function as usual.
#Break and Continue
break and continue are valid only inside a for body. Using either outside any loop is reported as masterbelt.checker.break_outside_loop or masterbelt.checker.continue_outside_loop. Nested loops are handled by stacking the loop context: each statement resolves to the innermost enclosing for.
#Match Statements
The match statement is type checked against the static type of its subject expression. Type checking ensures that every arm's pattern can match a value of the subject's type, that no two arms match the same value without a guard intervening, and that the arm list covers every value of the subject's type.
#Subject Type
The subject expression's checked type is computed by the standard expression type checking rule. After declared-type resolution, the subject type is treated as a (possibly singleton) union: a non-union subject type is equivalent to a one-member union for the purposes of the rules below.
#Pattern Checking
Each arm's pattern is checked against the subject type.
- A type pattern
T(optionallyT as name) is valid whenTis identical to one or more members of the subject's union after declared-type resolution. ATthat is unrelated to every union member is reported asmasterbelt.checker.match_pattern_unrelated. Theas nameclause introduces a new local binding of typeTvisible inside the arm; reusing a name that shadows an outer binding is reported asmasterbelt.checker.local_redeclaration. - An enum pattern
E.Vis valid when the subject type contains the enum typeEandVis one ofE's declared variants. Otherwise the pattern is reported asmasterbelt.checker.match_pattern_unrelated. - A literal pattern is valid when the literal's type is assignable to the subject type under the regular Assignability rule, and the subject type is one of
null,bool,string, or a numeric type (or a union of those). A literal pattern against any other subject type is reported asmasterbelt.checker.match_pattern_unrelated. - A product pattern
T { field: pattern, ... }(optionally... as name) is valid whenTis identical to one or more members of the subject's union after declared-type resolution and resolves to a product type. Every field name listed in the pattern must be declared by that product type; an unknown field is reported asmasterbelt.checker.match_pattern_unknown_field. Each field sub-pattern is checked against the field's declared type. A short field formfieldis shorthand forfield: field: it introduces a binding named after the field with the field's declared type. The optionalas nameclause introduces an additional binding of the product typeT. - A wildcard pattern is valid against any subject type.
A |-separated alternative list at one arm position is valid when every alternative is valid in isolation, every alternative is the same surface pattern kind (type, enum, literal, product, or wildcard), and the set of bindings introduced by each alternative agrees in name and type. A mismatched binding set is reported as masterbelt.checker.match_alternative_bindings_mismatch.
#Narrowed Types
A matched pattern narrows the subject type for the duration of the arm's guard and body:
- A type pattern
Tnarrows the subject type toT. - A
|-separated list of type patterns narrows the subject type to the union of the listed types. - An enum pattern narrows the subject type to the enum type that the variant belongs to.
- A product pattern narrows the subject type to its prefix type
T. - A literal pattern narrows the subject type to the literal's static type (
null,bool,string, or the contextual numeric type). - A wildcard pattern does not narrow the subject type.
When the subject expression is a plain identifier and the pattern introduces no explicit binding for that identifier, the identifier is rebound inside the arm to its narrowed type. The original binding is restored on exit from the arm. When the subject expression is not an identifier, an as name (or product { ... } as name) clause is required to make the narrowed value reachable; without a clause the pattern still matches, but no narrowed binding is introduced.
#Guards
A guard expression is type checked against the narrowed scope of its arm. The guard expression must be assignable to bool. A non-bool guard is reported as masterbelt.checker.match_guard_non_bool.
An arm with a guard does not contribute to exhaustiveness: the checker assumes a guard may evaluate to false, so a guarded arm cannot, on its own, prove that a subject-type member is covered.
#Reachability
An arm is unreachable when every value its pattern could match is already covered by an earlier unguarded arm. An unreachable arm is reported as masterbelt.checker.match_unreachable_arm. Two arms whose patterns match exactly the same set of values are unreachable past the first; the second occurrence is reported.
#Exhaustiveness
The set of arm patterns must cover every value of the subject's static type. The checker computes the set of subject-type members not covered by any unguarded arm and reports remaining members through masterbelt.checker.match_non_exhaustive. The diagnostic carries the comma-separated list of the uncovered member types or enum variants.
A wildcard pattern (_) covers every value of the subject type and therefore makes the match exhaustive. The checker reports masterbelt.checker.match_wildcard_loosens at warning severity when a wildcard pattern appears, since the wildcard widens the set of accepted values beyond what the static type can guarantee. The warning is informational and does not prevent the source from being accepted.
A pattern alternative list (P1 | P2 | ...) covers the union of the values its alternatives cover. Literal alternatives in a single arm must all share the same static literal kind (every alternative is a bool literal, or every alternative is a string literal, or every alternative is a numeric literal); mixed-kind alternatives are reported as masterbelt.checker.match_alternative_mixed_kinds.
#Validation Blocks
A master's validation section is type checked against the master's record type. The implicit bindings introduced inside a rule body depend on the rule's scope; the surface form is defined in masterdata/validation.md.
#Implicit Bindings
- In an
eachrule,rowandselfboth have the master's record type (a product type).row.Fieldresolves to a record field through the normal Member Access rules. - In an
allrule,tableandselfboth have typeRelation<M>for the surrounding masterM(see Scope Bodies). Afor row in tablestatement iterates the relation's post-filter records in plan order — equivalent to iteratingtable.toList()— and bindsrowto the master's record type. Because the binding is a relation, anallrule may also call the master's scopes and stage operators ontable(or its aliasself).
The bindings row, table, and self are immutable: they name the record or collection supplied by the validation evaluator and are not reassignable. self is bound the same value as row (in each) or table (in all).
#assert Statement
An assert statement's condition must be assignable to bool. A non-bool condition is reported as masterbelt.checker.assert_condition_non_bool. assert is valid only inside a validation rule body; outside, the checker reports masterbelt.checker.assert_outside_validation.
#return
return inside a validation block is a type error reported as masterbelt.checker.return_in_validation. A validation block has no return value; it reports failures through assert.
#Scope Bodies
A master's scope section declares a named relation query. Each scope is type checked against the declaring master M.
#Relation<M>
Relation<M> is the type of the master's query surface. It is reached at the source level only through scopes and the validation all binding; it is distinct from the master's record type and from list<Record>. The master's surface name denotes the base Relation<M> entrypoint. Relation<M> exposes the stage methods where, orderBy, thenBy, skip, take and the master's user-declared scopes; each returns a fresh Relation<M>. It does not expose terminal operators or projection / join switches at the source level. The full operator vocabulary and the field-handle callback DSL are defined in query.md.
#Implicit self
Inside a scope body self is the receiver Relation<M>. self is a reserved identifier in every context (see lexical.md); declaring or assigning it is rejected by the lexical reserved-identifier rule (masterbelt.parser.reserved_identifier). When a scope is reached through a scope chain, self is the relation produced by the earlier stages.
#Return Type
A scope returns Relation<M> for the declaring master. A block body must return such a value; a missing return is reported as masterbelt.checker.scope_missing_return. An arrow body's expression must produce such a value. A returned expression of any other type — list<Record>, a nullable, a projected relation, a joined relation, or an unrelated master's relation — is reported as masterbelt.checker.scope_return_type_mismatch. A scope never declares an explicit return type and never carries type parameters.
#Effects
A scope body is effect-free: it builds a plan and must not inherit failable, cancellable, or asyncable. A scope body — or a query callback inside it — that calls a user function, static, or const carrying one of those effects is reported as masterbelt.checker.scope_forbidden_effect. A scope is therefore never failable / cancellable / asyncable, and terminal relation operators (which carry those effects) cannot be reached from a scope body. The effect rule extends Effect Inheritance by treating any inherited forbidden effect inside a scope as an error rather than propagating it.
#Calls and Cycles
A scope call resolves against the declaring master's Relation<M> receiver only; a call on a receiver that does not expose the scope (another master's relation, a record, or a list) is reported as masterbelt.checker.unknown_member. Argument count and types are checked like a regular call and a mismatch is reported through the general call diagnostics. Scope resolution does not depend on source order, so a scope may chain a scope declared later in the same master. A scope that references itself directly or transitively is reported as masterbelt.checker.cyclic_scope; recursive scopes are forbidden.
#Assignability
A source type is assignable to a target type when:
- The source type and the target type are identical.
- The target type is a union type and the source type is assignable to one of the target's member types.
Generic types are invariant in their type arguments at this stage. list<int> is not assignable to list<int | string>.
Assignability between declared types is determined after declared-type resolution.