Skip to content

Declarations

SourceFile = NamespaceDecl { Using } { Declaration } .
NamespaceDecl = "namespace" QualifiedName .
QualifiedName = identifier { "." identifier } .
Using = "using" string_lit
| "using" "static" string_lit
| "using" identifier "=" string_lit .
Declaration = [ "pub" ] ( StructDecl | ClassDecl | UnionDecl | RefUnionDecl
| EnumDecl | InterfaceDecl | DelegateFuncDecl
| StaticFacetDecl | FuncDecl | NamespaceFieldDecl | StateDecl | NamespaceInitDecl
| ConstDecl | DeriveDecl ) .
NamespaceFieldDecl = identifier ":" Type [ "=" Expr ] .
StateDecl = ( "let" | "var" ) identifier
( [ ":" Type ] "=" Expr
| ":" Type ( "=>" Expr | AccessorBlock ) ) .
NamespaceInitDecl = "init" Block .

Every source file begins with exactly one NamespaceDecl. Name resolution and the semantics of Using are specified in Names & resolution. A declaration is internal unless prefixed with pub.

namespace Service
let initialEnv = AppConfig.Environment
var starts = 0
init {
starts += 1
registerServices()
}

A namespace may declare exactly one init { ... } block across all files in the compilation (ES2206). It has no visibility modifier or attributes (ES2205). Its direct body is synchronous: return is ES2207, while await, await for, and async let are ES2208; yield remains ES2131.

The : Type annotation on namespace let / var is optional; without it, the field type is inferred from the initializer exactly as for a local binding. Their initializers run first, in declaration order; the explicit init body runs afterward. The block may call functions declared later or in another file of the same namespace.

Namespace state uses the same representation distinction as class members:

SpellingNamespace-host member
x: T = exprdirect mutable static field
let x = exprinferred stored static property, construction-only write path
var x = exprinferred stored static property, getter/setter
let x: T => exprcomputed static property; getter only, no storage
let x: T { }stored get-only static property; its backing value may be assigned in namespace init
var x: T { }stored get/set static property
var x: T { set(v) => expr }stored static property with a transforming setter

Property forms without an initializer require : Type, because they have no initializer from which to infer storage/result type. Namespace let/var are properties even in the initialized form; their accessor and backing-field semantics are otherwise the same as class properties, except that all accessors and storage are static on the namespace host. Execution is lazy and once-only on first use of the namespace host. A thrown exception follows CLR type-initialization behavior. This is not a CLR module initializer and does not run merely because the assembly was loaded. See Programs → initialization order.

Type = QualifiedName [ TypeArgs ] [ "?" ] // named type, optional generic args, optional nullable
| "*" Type // pointer
| "readonly" "*" Type // read-only by-ref (parameter position)
| "(" TupleElem { "," TupleElem } ")" // tuple, elements optionally labeled
| FuncPtrType .
FuncPtrType = "&" "(" [ TypeList "->" ] Type ")" . // &(int, int -> int) , &(-> bool)
TypeArgs = "<" TypeList ">" .
TypeList = Type { "," Type } .
TupleElem = [ identifier ":" ] Type . // (x: int, y: int) , (count: int, int)

A type name in declaration position is PascalCase by convention, not by rule. *T is a pointer; *Class is ill-formed (ES2003). T? denotes an optional (see Types → nullable). Interface names conventionally add the .NET I prefix (IDrawable, IMap<K, V>); this convention does not change the grammar or interface identity.

StructDecl = [ "readonly" ] "struct" TypeName [ Generics ]
( "{" [ FieldList ] "}" | "(" [ ParamList ] ")" ) .
FieldList = Field { ( "," | newline ) Field } .
Field = [ "pub" | "priv" ] [ "required" ] [ "let" | "var" ] identifier ":" Type [ "=" Expr ]
| [ "pub" ] [ "*" ] TypeName . // embedded (anonymous) field
Generics = "<" identifier { "," identifier } ">" .

A struct introduces a value type: assignment copies it, and two values are equal iff their fields are equal. The following are ill-formed:

  • a field whose type contains the enclosing type by value — ES2002; break the cycle with *T.
  • an init block — ES3012; construct with a composite literal or factory.

Fields use the bare declaration form and are mutable by default. A field may have a default (count: int = 42), applied when a value is constructed. Member let and var instead reserve property representation. readonly struct makes every bare field immutable and emits [IsReadOnly].

A field or property carries a visibility prefixpub, priv, or none. It is the first token of the member, ahead of required and the let / var mutability keyword, and is orthogonal to both:

PrefixCLR accessibilityMeaning
pubpublicexposed to external consumers and across the assembly
(bare)assembly (internal)the default — visible within the assembly, not to external consumers
privprivatevisible only to the declaring type

The bare form is internal, not public: a member with no prefix is reachable from every type compiled into the same assembly but is invisible to a referencing assembly — the same default a C# member without an access modifier takes. pub is the only prefix that crosses the assembly boundary, so the surface a C# consumer sees is exactly the pub members. The prefix composes with the rest of the field grammar in either order of concern — mutability (let / var), the required marker, and a default value:

struct Account {
pub balance: int = 0 // public mutable field, defaulted
priv let salt: long // private read/init property
region: int // bare → internal, mutable
pub var displayBalance: int // public read/write property
pub required let id: Guid // public required init property
}

On a value struct the visibility of a bare declaration governs the emitted field directly: pub x: int emits a public mutable field and bare region: int emits an assembly field. Member let and var follow the property rules instead, so priv let salt: long controls private accessor methods around private storage. A required member additionally emits [RequiredMember] regardless of visibility.

For a property (let or var, below) the visibility applies to the synthesized get_ / set_ accessor methodspub var x: int { } emits public accessors and an unprefixed var x: int emits assembly accessors. The <x>k__BackingField behind a stored property stays private regardless of the prefix — the prefix never widens the backing storage, only the accessors that gate it. The full property surface, including the accessor forms, is in Properties; the exact CLR accessibility each lowers to is in CLR mapping → field & property visibility.

A field may be marked required (a contextual marker, valid in both struct and class field position): a composite literal over the type must supply every required field — omitting one is ES2189. Non-required fields keep their silent zero-defaulting. required composes with visibility and mutability (pub required var name: string, required let id: int) and emits [RequiredMember] on both the field and the type, so a C# consumer’s object initializer enforces the same coverage. It is a literal-coverage marker, not constructor surface: a struct stays init-free (ES3012).

struct Span {
required lo: int
required hi: int
label: string // optional as before
}
let s = Span { lo: 2, hi: 9 } // ok
let bad = Span { lo: 2 } // ES2189: required field 'hi' not set

The positional form struct Vec2(x: int, y: int) declares the same fields and adds positional construction (Vec2(3, 4)) alongside the composite-literal form — the synthesized constructor’s fields are public. A positional struct also emits a synthesized void Deconstruct(out T1, out T2, …) — one out per field in declaration order — so let (a, b) = value destructures it (and C# var (x, y) = vec works against the same method); the deconstruction arity must match the field count. Tuples keep their .ItemN path, and a body-form (non-positional) struct gets no Deconstruct. A positional header on a class means something different (a primary constructor whose parameters are not fields — see Constructors below); the struct positional form is the field-synthesizing DTO shape.

struct Vec2(x: int, y: int)
let v = Vec2(3, 4)
let (x, y) = v // x == 3, y == 4 — via synthesized Deconstruct

An embedded field — a bare type name in the body — promotes the embedded type’s fields and members into the enclosing type: in struct Widget { Base label: string }, Base’s fields are reached directly (w.x) and the composite literal flattens them (Widget { x: 10, y: 20, label: "…" }). A *T embedding (*Base) embeds by pointer. Generic type parameters are reified.

ClassDecl = [ "open" | "abstract" ] "class" TypeName [ Generics ]
[ "(" [ ParamList ] ")" ] // primary-constructor (capture) header
[ ":" BaseList ] "{" { Member } "}" .
BaseList = TypeName { "," TypeName } . // base class, if any, is first
Member = Field | Property | Init | Method | EventDecl | ConstDecl | ReturnsClause
| TypeDecl . // nested type declaration
Init = [ "priv" | "protected" ] "init" "(" [ ParamList ] ")"
[ ":" ( "base" | "this" ) "(" [ ArgList ] ")" ] Block .
Vis = "pub" | "priv" | "protected" . // the ':' override marker leads it
Method = ( ":" [ Vis ] | [ Vis ] [ "virtual" | "abstract" ] ) "func" identifier
"(" [ ParamList ] ")" [ ReturnType ] ( Block | "=" Expr | ε ) .
ReturnsClause = "returns" Type . // class-level default return
TypeDecl = StructDecl | ClassDecl | UnionDecl | RefUnionDecl
| EnumDecl | InterfaceDecl | DelegateFuncDecl . // emits as CLR nested type

A class is a CLR class (identity, reference equality). class is a reserved keyword and the reference/identity kind; there is no sealed modifier — a class is sealed by default (emitted as a sealed class), open makes it inheritable, abstract makes it non-instantiable and inheritable. open is the only shape that is both instantiable and inheritable; E#‘s OO stance is that a class is either abstract or sealed, so open is slated to become an explicit .esproj opt-in, disabled by default. Field defaults run before the constructor body. Inheritance — the member forms, the : func role inference, : base(...), and the ES2120–2128 diagnostics — is specified in the guide page Inheritance. EventDecl is specified in Delegates & events.

The base named in BaseList may be an external CLR class. An E# class then derives from a framework type, : base(...) selects a base constructor by the ordinary arity-and-name rule, and the base’s public instance members are available on the derived receiver — a property, field, or method resolved up the runtime base chain, not only the E# base chain. A derived-class member call therefore reaches an inherited BCL member with no redeclaration:

pub class ModelFileException : Exception {
init(message: string) : base(message) {}
init(message: string, inner: Exception) : base(message, inner) {}
}
func describe(ex: ModelFileException) -> string = ex.Message // inherited from Exception

A property is a member backed by accessor methods rather than a storage slot directly. The let or var keyword is the discriminator; braces are optional for stored properties and customize their access protocol when present. A bare name: Type declaration is a direct field.

Property = [ "pub" | "priv" ] [ "required" ] ( "let" | "var" ) identifier ":" Type
[ "=" Expr | "=>" Expr | AccessorBlock ] .
AccessorBlock = "{" { GetAccessor | SetAccessor | LocaAccessor | MutAccessor } "}" .
GetAccessor = "get" "=>" Expr .
SetAccessor = "set" "(" identifier ")" "=>" Expr .

The suffixes after a property type are distinct and unambiguous (finite lookahead, LL):

SuffixMeaning
no suffix / = exprstored property, optionally with an initial value
=> exprcomputed getter, no backing field
{ }accessor block (property)

Accessor sets derive from the keyword:

SpellingKindAccessorsNotes
x: int / x: int = 0field (mutable)storage — no accessor methodsbare field, default optional
let x: intstored propertygetter + construction-only write pathcompiler-managed storage
var x: intstored propertygetter + settercompiler-managed storage
let area: float => self.r * self.r * PIcomputed propertyget only, no backing fieldrecomputed on every read
let area: float { get => self.r * self.r * PI }custom getterget only, no backing fieldmay be combined with an explicit location protocol
var display: string { get => format(self.id) set(v) => self.apply(v) }behavioral propertycustom get + custom set, no backing fieldsetter expression performs the write effect
let x: int { }propertyget-onlyset in init (class)
required let x: int { }propertyget + initinit-only setter
var x: int { }propertyget + set (auto-backed)
var price: float { set(v) => guard(v) }propertyget auto, custom setsetter binds value explicitly

Only a bare member declaration creates a field. let and var denote properties at member scope even without an accessor suffix: let selects the read/construction form and var the read/write form. There is no readonly let — the grammar does not accept the readonly modifier on a property. Read-only-receiver is readonly func (see Functions → methods).

The computed-property shorthand is let x: T => expr. get => expr authors the getter inside an accessor block, which permits it to be paired with set, loca, or mut. Both getter forms recompute and have no implicit backing storage. var x: T => expr is ES2227: var promises writable behavior, while the shorthand supplies only a getter. A var custom getter must also declare a custom setter or mut write behavior (ES2228).

The setter binds its incoming value explicitlyset(v) => <body> — consistent with E#‘s explicit self; there is no contextual value name. The setter body is a value expression whose result is the value stored in the backing field, not an assignment statement: set(v) => guard(v) stores guard(v), and set(v) => self.x = … is wrong.

When set(v) => expr accompanies an automatic getter, expr is a storage transformation and its result is stored in the property’s private backing field. When it accompanies get => expr, the property is behavioral and has no backing field: the setter expression is evaluated for its authored write effect.

class Session {
id: int
func replaceId(value: int) { self.id = value }
var displayId: int {
get => self.id + 1
set(value) => self.replaceId(value)
}
}

Each form derives a precise accessor set, and the keyword (let / var) plus the required marker is what selects it:

  • Computed — let area: float => expr. A getter only, with no backing field; the body is re-evaluated on every read. It is the property analogue of an expression-bodied method, and the canonical way to derive a value from other fields or forward to an inner object. A computed property may read self freely:

    class Rect {
    var w: float
    var h: float
    let area: float => self.w * self.h // recomputed each read, no storage
    let isSquare: bool => self.w == self.h
    }
  • Custom getter — get => expr. The getter is recomputed exactly like the shorthand form, but lives inside the accessor protocol so it can be combined with authored write or location behavior. A let property may use only the getter; a var property must pair it with set or mut.

  • Auto get-only — let x: int { }. A getter over a private initonly backing field. On a class the value is written once, in init, through the backing field; thereafter it is read-only:

    class Connection {
    let host: string { } // get-only; set once in init
    let port: int { }
    init(host: string, port: int) {
    self.host = host // writes the backing field through the property
    self.port = port
    }
    }
  • Get + init — required let id: Guid { }. A getter plus an init-only setter (one carrying modreq(IsExternalInit)): the value is supplied by a composite literal and is immutable thereafter, exactly C#‘s { get; init; }. The required marker forces every composite literal to set it (ES2189):

    class User {
    required let id: Guid { } // get + init — set by the composite literal
    required let name: string { }
    }
    let u = User { id: newId(), name: "ada" } // both required → both supplied
  • Auto get + set — var x: int { }. A getter and a setter over a backing field, the plain mutable property; reads and writes both route through accessors.

  • Custom setter — var price: float { set(v) => expr }. A synthesized getter plus a setter whose body is the value to store. v is the incoming value; the expression’s result is written to the backing field. This is the validation/normalization seam — clamp, round, or reject — with no separate field to keep in sync:

    class Product {
    var price: float { set(v) => if v < 0.0 { 0.0 } else { v } } // clamp negatives to 0
    var name: string { set(v) => v.Trim() } // normalize on store
    }

    A read of p.price returns the stored (already-clamped) value; a write p.price = -5.0 stores 0.0. The setter is an ordinary value expression — never self.price = …, which would recurse through the setter rather than store.

Struct vs. class. On a class, let x { } is get-only and the value is set in init. On a value struct (no constructor) a stored let x { } is illegalES2193 — because a struct has no init to write through it; use a computed property (let x => expr), required let x { } (set by composite literal), or var x { } (set by composite literal) instead.

class Circle {
var r: float
let area: float => self.r * self.r * 3.14159 // computed — no backing field
var name: string { } // get + set, auto-backed
var price: float { set(v) => if v >= 0.0 { v } else { 0.0 } } // custom setter — body is the value to store
required let id: Guid { } // get + init
}
class AsyncStreamEnumerator<T> : IAsyncEnumerator<T> {
inner: IAsyncEnumerator<T>
pub let Current: T => self.inner.Current // computed property forwarding to inner
}

Visibility. A property takes the same pub / priv / bare prefix as a field, and it lands on the accessor methodspub var x: int { } emits public get_x / set_x, an unprefixed var x: int emits assembly (internal) accessors, and priv emits private accessors. The <x>k__BackingField is private no matter the prefix, so the prefix widens the gate (the accessors), never the storage behind it. The pub let Current: T => … on AsyncStreamEnumerator<T> above is a public computed getter; an un-prefixed computed property is internal. The full visibility table is in Field & property visibility.

An individual accessor may carry its own pub / priv inside the accessor block, ahead of get / set / initpub var version: int { priv set } emits a public get_version and a private set_version over the still-private backing field, C#‘s { get; private set; }. A per-accessor modifier may only narrow the property’s visibility relative to its declared prefix; a wider accessor (priv var x { pub set }) is ES2229.

CLR emission. A property emits as get_<name> / set_<name> accessor methods (specialname) plus a PropertyDefinition and (for stored) a private <name>k__BackingField — byte-identical to what C# emits. An init-only setter carries modreq(System.Runtime.CompilerServices.IsExternalInit). A C# consumer sees an ordinary C# property; an init-only one behaves exactly as a C# init accessor. See CLR mapping → properties.

Interface property requirements. An interface may declare property requirements; the minimum accessor set required of any implementer:

interface IShape {
let area: float { get } // implementer must expose at least a getter
var name: string { get set } // implementer must expose get and set
func draw() -> void
}

The requirement names the minimum accessor set; an implementer may offer more. A var x { } (get + set) satisfies a let x { get } (get-only) requirement, because get-and-set is a superset of get-only — but the reverse is not true, a get-only member does not satisfy a { get set } requirement. Conformance to the member is exact in name, type, and the required accessor presence; visibility on the implementer must be at least pub for the interface method-impl to bind across the assembly boundary.

A field never satisfies a property requirement and conformance never synthesizes accessors over one. The implementer must spell the member with let or var, making its source representation and CLR ABI explicit. A same-named field is ES2226, even when its type and mutability would otherwise match:

interface INamed {
let name: string { get } // get-only requirement
var label: string { get set } // get + set requirement
}
class Tag : INamed {
pub let name: string { } // get-only property — satisfies `let name { get }`
pub var label: string { } // get + set property — satisfies `var label { get set }`
}
struct Point : INamed {
pub let name: string // property — fills the getter slot
pub var label: string // property — fills getter and setter slots
x: int
y: int
}

An implementer may also satisfy a get-only requirement with a computed or custom-getter property, since each emits a real getter:

class Origin : INamed {
pub let name: string => "origin" // computed getter satisfies `let name { get }`
pub var label: string { }
}

The interface production is extended:

InterfaceMember = "func" identifier "(" [ ParamList ] ")" [ ReturnType ]
| EventDecl
| [ "let" | "var" ] identifier ":" Type "{" InterfaceAccessors "}" .
InterfaceAccessors = ( "get" | "set" | "init" | "loca" )
{ "get" | "set" | "init" | "loca" } .

A class or static body may declare nested types. What can nest: enum, struct, class, union, ref union, interface, and delegate func — the full type-kind set, the same TypeDecl production that appears at top level. A nested type may itself carry generics, conform to interfaces, hold its own members, and (for a class) declare further nested types. The nesting hosts are a class body and a static body; a value struct body holds fields and methods, not nested type declarations.

Naming & emission. A nested type emits as a CLR nested type whose metadata name is Outer/Inner (rendered Outer.Inner in source and by reflection). It is reachable by reflection and typeof(Outer.Inner), and carries the full type metadata of its kind — a nested enum is a real System.Enum, a nested class a real sealed class, a nested union the same tag-enum-plus-struct (or abstract-base-plus-subclasses for ref union) a top-level union emits. A nested type’s accessibility uses the CLR nested visibility flags — never the top-level public / not-public: pubNestedPublic, bare (internal) → NestedAssembly, privNestedPrivate. The default for a nested type is private (NestedPrivate, the C# nested default) — the inverse of a top-level declaration’s internal default — so a nested type without a prefix is reachable only from the enclosing type.

static ChanSelect {
enum Kind { Recv, Send, Timeout, Default }
pub class Arm {
required let kind: Kind { } // references the sibling nested enum, unqualified
var payload: object? { }
}
delegate func OnFire(arm: Arm) // a nested delegate type — ChanSelect.OnFire
}
class Parser {
enum State { Start, InIdent, InNumber, Done } // private to Parser (no prefix → NestedPrivate)
pub struct Token { // public nested struct — reachable as Parser.Token
required let kind: State { }
required let text: string { }
}
func next(s: State) -> State = match s {
.Start { .InIdent }
.InIdent { .Done }
default { .Done }
}
}

Resolution. A nested type is visible to the enclosing scope without a qualifierArm references its sibling Kind directly, and Parser’s methods name State and Token bare. An external caller uses the dotted path (ChanSelect.Kind, ChanSelect.Arm, Parser.Token), and reaches only the pub (and, within the assembly, the bare-internal) nested types; a NestedPrivate one (the default) is invisible outside its enclosing type. This is the encapsulation lever: expose the nested types that are part of the type’s contract with pub, leave the implementation-detail ones bare or priv.

A nested type shall not carry the name of the type that encloses it — that is ES2154. The name would mean the enclosing type at one step of resolution and the nested type at the next. C# bars the same shape for the same reason. Rename one of the two. This rule is about the enclosing name only. A nested type may share a name with an unrelated type elsewhere in the module.

A TypeDecl (see grammar above) is a valid member position in both class and static bodies. The exact per-kind emission is in CLR mapping → nested types.

An init block is a .ctor. It is a class feature: on struct an init is ES3012. A class may declare multiple init blocks. Overloads are resolved by arity and argument names — never by argument types; two inits with the same parameter count are ES2185 (differ the arity, or merge with a parameter default).

init is public by default. priv init emits a private .ctor; the contextual protected init emits a family .ctor (meaningful with an open/abstract base). The visibility prefix scopes only the constructor — fields and methods keep their own rules.

Method visibility. A body method takes the same three prefixes: pub func emits a public method, priv func a private one, and the contextual protected func a family one. Each composes with the inheritance forms — protected virtual func and protected abstract func. For an override the : marker comes first, ahead of visibility: : protected func, so that a member’s participation in the base chain reads before its access level. The reversed spelling is ES2129. A bare func inherits the enclosing type’s visibility, so a pub class publishes its un-prefixed methods; this is the one member form that does not follow the bare-is-internal rule a bare field gets. An implicit interface implementation is always emitted public whatever the prefix says — the CLR maps it into the interface slot, and a narrower method cannot fill it.

Overriding an external base. The : marker fills a slot on the inheritance chain whether the base was declared here or in another assembly — class Worker : BackgroundService { : func ExecuteAsync(...) } is the same construct as an override of an E# base. The match is by name and parameter count up the whole chain, so a virtual declared several levels up is reachable; a slot that is sealed lower down is not, and naming one is ES2122 like any other missing slot.

An override fills the slot, so it must have the slot’s return type. The parent is located by name and parameter count, which is enough to find a slot and not enough to fill one: a mismatched return emits a new method beside the slot rather than into it, so every call through a base reference runs the base body. Naming a different return type is ES2133. Whether either body awaits is not part of a signature and does not enter the comparison — a -> Task slot is filled by any -> Task override. This is the rule an interface requirement already follows.

An override does not declare its accessibility — it inherits the slot’s, and widens to it if the member was written narrower. This is not a convenience: the CLR refuses a derived method that reduces access, so a : func over a public or protected slot has exactly one legal answer, and restating it could only ever be wrong. It is the same rule an implicit interface implementation already follows above.

An external base also contributes its members: a type deriving from a framework class sees that class’s public and protected surface as its own, so self.Run(...) and w.StopAsync(...) bind through the base exactly as they would in C#. A nested external base is named with its source spelling (SeamService.SeamServiceBase), which the resolver reconciles with the CLR’s + nesting form.

: self(...) delegation. Between the parameter list and the body an init may write : self(args) to delegate to the sibling init whose arity matches the argument count, the dual of : base(args). The delegate runs first — its base call, field defaults, and body — then the delegating body runs; a delegating init performs no base call and no field-default initialization of its own (those already ran in the delegate). A delegation cycle, including an init that delegates to itself, is ES2187; a : this/: base whose arity matches no sibling/base init is ES2128.

class Conn {
var host: string
var port: int
init(host: string, port: int) { self.host = host self.port = port }
init(host: string) : self(host, 80) { } // delegates to the 2-arg init
}
class Singleton {
var v: int
priv init(v: int) { self.v = v }
init() : self(7) { }
}

Primary-constructor capture (class Foo(params))

Section titled “Primary-constructor capture (class Foo(params))”

A positional parameter list on a class is the capture header — the primary constructor. Its parameters are not fields:

class UserService(store: IUserStore, cache: ICache, maxRetries: int = 3) {
var tokens: int = maxRetries // a stored-property default may read a header param
init { if maxRetries <= 0 { tokens = 1 } } // param-less init — the primary's epilogue
func lookup(id: Guid) -> User = cache.get(id) ?? store.find(id) // cache/store captured on use
}
  • Capture is on-use. A header param referenced by an in-body method becomes a synthesized private readonly field of the same name, stored once by the primary constructor right after the base call. A param used only in init { } or a member default stays a constructor-local — no field is emitted. A method may read a captured param, never assign it.
  • Primary-constructor order: base call → capture stores → member defaults (which may read header params) → the init { } epilogue body.
  • The param-less init { } is the primary’s epilogue, not a constructor of its own. A secondary init(args) on a headered class must delegate with : self(...) (ES2186) — only the primary builds the object; a secondary whose arity collides with the header is ES2185.
  • A header param sharing a name with an explicit field is ES2188. A composite literal over a headered class is ES2190 — it constructs only through the primary.
  • Promoted free functions (func m(self: Foo, …)) are outside header scope: they see the receiver parameter only.

The struct Vec2(x: int, y: int) positional form is unrelated — on struct it synthesizes public fields and a constructor; the capture header exists only on class.

UnionDecl = "union" TypeName [ Generics ] "{" { Case } "}" .
RefUnionDecl = "ref" "union" TypeName [ Generics ] "{" { Case } "}" .
Case = identifier [ "(" ParamList ")" ] .

A union is a tagged union — a value is exactly one case, each carrying zero or more named payload fields. Cases are constructed by factory (T.case(args)) or dot-case shorthand (.case(args) where the target type is known) and consumed by match, which checks exhaustiveness. A union has value semantics; a ref union has reference semantics. Generic unions are reified.

A union is not a bespoke runtime construct. Before the rest of compilation runs it desugars into the primitives already specified on this page — an enum, a struct (or a class hierarchy), and static factory funcs — and from that point rides the ordinary type, generic, pointer, and match machinery. The subsections below are the shape it becomes; nothing about a union’s layout or ABI is special-cased. The desugared names are load-bearing: they are the ABI a referenced assembly’s union is recognised by (CLR mapping → types).

A value union becomes a tag enum plus a struct (always a struct, whatever the payloads) carrying a Tag discriminant and one field per payload, plus one static factory per case. Given:

union Shape {
circle(radius: float)
rect(w: float, h: float)
empty
}

the reified shape is equivalent to these hand-written declarations:

enum Shape_Tag { circle, rect, empty } // literals in declaration order, int-backed
struct Shape { // value type; Tag is always the first field
Tag: Shape_Tag
circle_radius: float // payload fields, named {case}_{payload}
rect_w: float
rect_h: float
}
static Shape { // one factory per case
func circle(radius: float) -> Shape = Shape { Tag: .circle, circle_radius: radius }
func rect(w: float, h: float) -> Shape = Shape { Tag: .rect, rect_w: w, rect_h: h }
func empty() -> Shape = Shape { Tag: .empty }
}

Shape.circle(2.0) is a call to that factory; .circle(2.0) is the same call where the target type is known from context. All payload fields coexist in the struct — there is no overlapped FieldOffset union in the C sense — and the fields of cases not taken are simply left zero for a given value. The tag is the sole source of truth for which case is live, and the discipline is enforced by match, not by layout. A match loads Tag, switches on it, and reads the matched case’s {case}_{payload} fields:

match s {
.circle(r) { … r … } // r ← s.circle_radius, reached only when s.Tag == .circle
.rect(w, h) { … }
.empty { … }
}

A ref union becomes an abstract base class under the union’s name plus one sealed subclass per case, named Union_case. Payload fields live on the subclass, named by the payload (no {case}_ prefix — the reference ABI). Given:

ref union Expr {
literal(value: int)
add(left: Expr, right: Expr)
neg(inner: Expr)
}

the reified shape is equivalent to:

abstract class Expr { } // a union value is typed as the base
class Expr_literal : Expr { value: int init(value: int) { self.value = value } }
class Expr_add : Expr { left: Expr right: Expr init(left: Expr, right: Expr) { self.left = left self.right = right } }
class Expr_neg : Expr { inner: Expr init(inner: Expr) { self.inner = inner } }

Because the cases are reference types, a payload may hold the union directly (left: Expr) — no pointer, unlike a value union. .add(a, b) constructs via newobj Expr_add(a, b); the subclass is also directly nameable, so Expr_add { left: …, right: … } is an equivalent object-literal construction. A match dispatches by testing the subclass type (isinst) and reading its fields:

match (e: Expr) {
.literal(l) { … l.value … } // reached when e is an Expr_literal, bound as l
.add(a) { … a.left … a.right … }
.neg(n) { … n.inner … }
}

A value union that refers to itself does so through *T — a value type cannot contain itself by value (ES2002). A *Union payload is stored in the reified struct’s field, so it always escapes and takes the __Ptr_T heap-cell representation (Pointers → two representations); for a generic union the self-pointer becomes a generic heap cell, closed per instantiation. Given:

union Tree<T> {
leaf(value: T)
node(left: *Tree<T>, right: *Tree<T>)
}

the reified shape is equivalent to:

enum Tree_Tag { leaf, node }
struct Tree<T> {
Tag: Tree_Tag
leaf_value: T
node_left: *Tree<T> // a __Ptr_Tree<T> heap cell, closed as __Ptr_Tree<int> at Tree<int>
node_right: *Tree<T>
}
static Tree<T> {
func leaf(value: T) -> Tree<T> = Tree<T> { Tag: .leaf, leaf_value: value }
func node(left: *Tree<T>, right: *Tree<T>) -> Tree<T> =
Tree<T> { Tag: .node, node_left: left, node_right: right }
}

At Tree<int> the reified struct is a distinct closed value type: leaf_value is laid out as int, and node_left / node_right are __Ptr_Tree<int> cells — never erased to object (Generics → reification). Building a tree borrows each child with &, which is coerced into the heap cell the factory parameter demands:

var a = Tree<int>.leaf(1)
var b = Tree<int>.leaf(2)
var root = Tree<int>.node(&a, &b) // &a, &b coerced into __Ptr_Tree<int> cells

A ref union recurses without a pointer (add(left: Expr, right: Expr) above): each case is already a reference, so a case may name the base type directly.

EnumDecl = "enum" TypeName [ ":" IntegralType ] "{" { EnumCase } "}" .
EnumCase = identifier [ "=" int_lit ] .
IntegralType = "byte" | "sbyte" | "short" | "ushort" | "int" | "uint" | "long" | "ulong" .

Emits as a CLR System.Enum. The underlying type is int32 by default, or the integral primitive named by the optional : T annotation — enum Codec: byte { … } emits a byte-backed enum whose value__ field and case constants are byte. The colon is unambiguous (an enum has no base class, so : T is the annotation colon naming the underlying integral type); a non-integral T is ES2127. A case without an explicit value takes the previous value + 1 (the first defaults to 0). Cases are constructed with a trailing () (Dir.north()).

An enum that declares itself a flag set carries the bitwise operators |, &, ^, and ~. It is one rule with two spellings, because one of them is C#‘s. @derive(flags) emits [Flags], and an enum that carries [Flags] is a flag set. The attribute counts whether the source wrote it or an external assembly declared it. Both spellings on one enum mark it once.

@derive(flags)
enum Access: byte { none = 0, read = 1, write = 2, execute = 4 }
let rw = Access.read() | Access.write()
let readable = rw & Access.read() != Access.none()

Both operands shall be the SAME enum type, and the result is that enum type — not its underlying integer, so the value passes straight to a parameter declared as the enum. Mixing two enum types is ES2292, and an integer operand is not admitted: the enum/integer conversions stay explicit (Access(4)), exactly as everywhere else. The shift operators are excluded — a shifted flag set names nothing.

The operators are absent on an enum that is not a flag set, so Dir.north() | Dir.south() is ES2292 rather than a value naming no case. Compound assignment derives from the binary operator by the ordinary rule, so acc |= Access.write() follows.

InterfaceDecl = "interface" TypeName [ Generics ] "{" { InterfaceMember } "}" .
InterfaceMember = "func" identifier "(" [ ParamList ] ")" [ ReturnType ]
| EventDecl
| [ "let" | "var" ] identifier ":" Type "{" InterfaceAccessors "}" .
InterfaceAccessors = ( "get" | "set" | "init" | "loca" )
{ "get" | "set" | "init" | "loca" } .

Emits as a CLR interface. Conformance is nominal: a type conforms only if it names the interface after :, and the match is exact (name, parameter types, return type). A type that would satisfy an undeclared interface raises ES2153. Where only the pointer method set conforms, the __Ptr_T wrapper implements the interface (see Pointers).

An interface may declare property requirements: let x: T { get } requires at minimum a getter; var x: T { get set } requires both. An implementer may offer a richer accessor set, but must declare an actual let/var property; a bare field never acquires property accessors through conformance. Adding loca requires a durable property location as part of the interface contract: let x: T { get loca } is readonly-addressable and var x: T { get set loca } is writable-addressable. See Properties and object initialization for conformance and separate-compilation rules.

FuncDecl = "func" ( identifier [ Generics ] | OperatorName ) "(" [ ParamList ] ")" [ ReturnType ]
( Block | "=" Expr ) .
ReturnType = "->" Type .
StaticFacetDecl = "static" TypeName [ Generics ] "{" { StaticMember } "}" .
StaticMember = ConstDecl | "let" identifier [ ":" Type ] "=" Expr
| "var" identifier [ ":" Type ] "=" Expr | FuncDecl | ReturnsClause .
DelegateFuncDecl = "delegate" "func" TypeName "(" [ ParamList ] ")" [ ReturnType ] .
ParamList = Param { Sep Param } [ Sep ] . // Sep is "," or newline; trailing Sep allowed
Sep = "," | newline .
Param = ( [ "out" ] [ "readonly" ] identifier ":" Type
| identifier ":" "*" Type ) [ "=" Expr ] . // optional default value

A free function’s name is camelCase by convention; a method (a function with a receiver block) and a static facet member are conventionally PascalCase. None of this is enforced — casing carries no meaning (Lexical → identifiers). A function written with a receiver block is a method on that type and shall be called as recv.f(...); calling it free is ES2142. Receivers, by-ref/out parameters, function pointers, and the returns default-return clause are specified in Functions and Pointers & by-ref. A static body shall contain only fields, const, and functions; a bare statement is ES1010. A delegate func mints a nominal delegate type (Delegates & events).

Types are keyed by name and arity, mirroring the CLR (Foo2). A generic struct Foo<A, B>and a non-genericstatic Footherefore coexist under one name — the standard library uses exactly this to pair theResult<TValue, TError>value type with aResult static factory class (Result.Ok/Result.Error). A non-generic class Fooandstatic Fooare a compatible pair: they contribute instance and static members respectively to the **same** emitted CLRFootype, soFoo(…)construction andFoo.helper() static dispatch remain available together. Two generic types of the same name but different arity (PairandPair<A, B>`) likewise coexist. Only a genuine same-name and same-arity redeclaration is an error (ES2152).

static Foo { ... } is an explicit static facet of the type identity Foo at that generic arity. It is a deliberately declared member surface, not a modifier inferred from methods elsewhere. When it stands alone, Foo is static-only. When class Foo (or struct Foo) and static Foo both exist, they are instance and static facets of one emitted CLR type.

class Counter { var value: int }
static Counter {
var total: int = 0
}
func (c: static Counter) resetTotal() -> int {
c.total = 0
return c.total
}
let total = Counter.resetTotal()

The static receiver is a compile-time alias for the facet, never an object or hidden CLR parameter. If Counter has only a static facet, ordinary func (c: Counter) selects it automatically. If both facets exist, ordinary Counter selects the instance facet and static Counter selects the static facet. Naming static Counter when no static declaration exists is ES2211: declare static Counter { ... }, or attach the method as an instance method without the static keyword. A static receiver cannot be readonly or a pointer.

A fixed symbolic FuncDecl such as func +(left: Vec, right: Vec) -> Vec is valid only in the companion static facet of an operand class/struct, including an attachment through static Vec. It is intrinsically static and is called only through an operator expression. See Methods and static facets → operator functions.

A func, method, init, or class-header parameter may carry a default with = expr. The default shall be a constant shape — a foldable literal, nil, or a composite-literal / dot-case / Result construction over such constants — otherwise ES2180. An omitted argument materializes the default inline at the call site; a literal default additionally stamps [Optional] and a .param constant in metadata, so a C# caller sees the optional too.

At any call, positional arguments come first, then named arguments in any order (f(1, c: 3, b: 2)). A named argument after a positional that should have filled its slot — i.e. a named argument followed by a positional — is ES2181; a name matching no parameter is ES2182; too few non-default arguments or too many is ES2183; the same parameter filled twice is ES2184. Two normative rules govern the model: overloads resolve by arity and argument names, never by argument types, and arguments always evaluate in parameter order, not the order they are written. Calling a C# method that omits trailing optionals works — the metadata default constant is loaded. Named arguments are not available on union / enum case construction. The full resolution and evaluation rules are in Functions → default and named arguments.

ConstDecl = "const" identifier [ ":" Type ] "=" Expr .
DerivePrefix = "@" "derive" "(" DeriveInvocation { "," DeriveInvocation } ")" .
DeriveInvocation = identifier [ "(" [ DeriveArg { "," DeriveArg } ] ")" ] .
DeriveArg = identifier ":" ConstExpr .

A const initializer shall fold to a compile-time literal; otherwise ES1011 (use let). const is valid at namespace, static, class, and function-body scope.

At namespace scope, bare typed declarations, let, var, and const declare module state — with no enclosing func or type body — and become static members of the namespace host class (Programs → assembly layout). Bare x: T is a direct mutable static field; let and var are static properties using the rules above, and const is an inlined literal. Stored state initializes per the CLR’s type-initialization order (Memory model → initialization order).

@derive(...) precedes a declaration — a type (struct, class, interface, union, enum), a field or property, or a method or free function — and synthesizes members at compile time: real metadata, callable from E# and C#, generated before the type’s own members (a hand-written member of the same name wins). Whatever the site, what is generated lands on the enclosing declaration, and the annotated declaration itself is never rewritten. The built-in traits below apply to a type; a referenced pack may define derives for the other sites. The outer list selects derives and each inner list configures one, so @derive(record, Json(naming: "snake")) is unambiguous — a derive is chosen positionally by name, not by the named-boolean shape the other compiler directives use (@floatMode(contractFma: true)).

TraitGeneratesAlso
equalityEquals(other), GetHashCode(), ==, != — structural, field-by-fieldimplements IEquatable<T> (the closed instantiation for a generic type)
debugToString() — a readable rendering of the type and its fields
recordthe whole CLR record protocol (below)implements IEquatable<T>; subsumes equality and debug
flags[FlagsAttribute] — which is also what makes Enum.ToString() name the set bitsenum only — admits | & ^ ~ (flag sets)

@derive(equality) is what gives a value struct its structural-equality semantics (Type system → equality); without it a struct falls back to CLR-default equality.

The trait set is closed by default and opened by reference: the names above are what a project with no provider pack recognizes, and a referenced pack adds its own (Compile-time providers). An unrecognized name — one no built-in and no loaded provider claims — is ES2242, arguments to a trait that takes none are ES2243, a non-constant argument is ES2246, and pairing record with a trait it subsumes is ES2244 — each member has exactly one definition. A derive is named by what its author declares, not by the type that implements it.

@derive(record) makes the type a CLR record: the member set a .NET consumer binds against for value semantics, byte-compatible with what C# emits for record / record struct. That compatibility is the point — with in C# is admitted only against a type declaring <Clone>$, so the shape is not a matter of taste.

MemberOn a structOn a class
Equals(T) / Equals(object) / GetHashCode() / == / !=field-by-fieldfield-by-field, plus a reference short-circuit, a null guard, and the EqualityContract check
PrintMembers(StringBuilder) -> bool · ToString()prints Name { f = v, … }same, and an extending record prepends its base’s members
EqualityContractType — the runtime record kind, so two records with identical fields are unequal
<Clone>$() · copy constructor— (assignment already copies)the copy a with expression applies its changes to

A class that is open or abstract publishes EqualityContract, PrintMembers, and <Clone>$ as protected-virtual so a derived record extends them; a sealed one keeps them private. A record extending a record overrides them, chains its Equals, GetHashCode, PrintMembers, and copy constructor into the base’s, and returns its own type from <Clone>$ (a covariant return).

record does not rewrite the type’s storage: E# fields stay fields, and the generated members read whatever the type declares. A field-backed record is still a record.

A delimited region is a declaration form: at namespace scope, @sigil { … } yields whatever declarations its template produced, and they are ordinary declarations from that point on — bound in this file’s scope, against this file’s names, with spans in this file.

@doc { … } // `doc` is whatever sigil a referenced pack claims; E# defines none

Nothing after the parse knows a region was written. A region whose template produces a non-declaration here is ES2943, and one whose template does not accept declaration position is ES2946 — both at the region, not downstream of it.

An attribute is written in square brackets immediately before a declaration, with optional positional/named arguments:

[Serializable]
struct Point { x: int, y: int }
[Obsolete("use parseV2")]
func parse(s: string) -> int { … }

Most attributes are pass-through — emitted onto the corresponding CLR metadata unchanged, meaning to a .NET consumer exactly what they mean in C# ([Serializable], [Obsolete(...)], [StructLayout(...)], any framework/user attribute). The name resolves like any other type.

An attribute whose type or constructor does not resolve is ES1015. It is not dropped: a dropped attribute means the build succeeds and the consumer reads the wrong wire name, the wrong layout, or no marker at all.

Every declaration takes an attribute, members included — a field, a property, an event, a const, an init constructor, and a method:

readonly struct OrderBracket {
[JsonPropertyName("type")]
pub let legType: int
pub let ticks: int
}

The attribute lands on the CLR member the declaration produces, which is the pass-through rule above applied to a member. Which member that is follows from the declaration form:

Declaration formCLR memberAttribute lands on
name: Tfieldthe field
let name: T · var name: Tproperty over a private backing fieldthe property
let name: T => exprproperty, no storagethe property
event name: Devent over a private backing fieldthe event
const NAME = vliteral fieldthe field
init(…)constructorthe constructor
an enum caseliteral fieldthe field

A union emits a struct, or an abstract class plus one sealed subclass per case. Its attribute lands on the type the declaration named — the struct, or the base class. The tag enum and the per-case types come from the lowering, and carry nothing the source did not write about them.

A let is a property, so its attribute is a statement about the property. Nothing reads a private backing field — not a serializer, not a designer, not a C# consumer — so an attribute on <name>k__BackingField would mean nothing to anyone. [JsonPropertyName("type")] above is the whole reason the rule has to be stated: type is a keyword, the member cannot be named for the wire, and the attribute is the only spelling that produces the required name.

An attribute in a type body that is followed by something which is not a declaration — a returns clause is the only such position — is ES1014. There is no member for it to attach to, and saying so beats dropping it.

An attribute written with the assembly: target at compilation-unit scope attaches to the emitted assembly rather than to a declaration:

namespace App
using "System.Runtime.CompilerServices"
[assembly: InternalsVisibleTo("App.Tests")]

The target is what makes it unambiguous. Without one, an attribute belongs to whatever declaration follows; a target says it belongs to something else, so the two never compete for the same syntax. The form is legal only outside every declaration — inside one, “not this declaration” has no other referent and the attribute is ES2286.

Assembly attributes are collected from every unit of the compilation and written once, so the same attribute repeated across files produces one metadata row rather than several. An attribute whose type or constructor cannot be resolved is ES2287 rather than a silent omission.

This is the only way to reach assembly-level metadata from source: [assembly: InternalsVisibleTo] keeps a library’s test seams internal instead of forcing them public, and [assembly: AssemblyMetadata] and friends are otherwise unreachable.