Declarations
Compilation unit
Section titled “Compilation unit”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.
In this specification
Section titled “In this specification”- Namespace hosts and initialization specifies namespace-level state,
static properties,
init { }, and the CLR type-initialization boundary. - Properties and object initialization specifies the
field/property distinction, accessor contracts,
initconstructors, and primary-constructor capture.
Namespace initialization
Section titled “Namespace initialization”namespace Service
let initialEnv = AppConfig.Environmentvar 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:
| Spelling | Namespace-host member |
|---|---|
x: T = expr | direct mutable static field |
let x = expr | inferred stored static property, construction-only write path |
var x = expr | inferred stored static property, getter/setter |
let x: T => expr | computed 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.
struct
Section titled “struct”StructDecl = [ "readonly" ] "struct" TypeName [ Generics ] ( "{" [ FieldList ] "}" | "(" [ ParamList ] ")" ) .FieldList = Field { ( "," | newline ) Field } .Field = [ "pub" | "priv" ] [ "required" ] [ "let" | "var" ] identifier ":" Type [ "=" Expr ] | [ "pub" ] [ "*" ] TypeName . // embedded (anonymous) fieldGenerics = "<" 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
initblock — 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].
Field & property visibility
Section titled “Field & property visibility”A field or property carries a visibility prefix — pub, 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:
| Prefix | CLR accessibility | Meaning |
|---|---|---|
pub | public | exposed to external consumers and across the assembly |
| (bare) | assembly (internal) | the default — visible within the assembly, not to external consumers |
priv | private | visible 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 methods — pub 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 } // oklet bad = Span { lo: 2 } // ES2189: required field 'hi' not setThe 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 DeconstructAn 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 firstMember = Field | Property | Init | Method | EventDecl | ConstDecl | ReturnsClause | TypeDecl . // nested type declarationInit = [ "priv" | "protected" ] "init" "(" [ ParamList ] ")" [ ":" ( "base" | "this" ) "(" [ ArgList ] ")" ] Block .Vis = "pub" | "priv" | "protected" . // the ':' override marker leads itMethod = ( ":" [ Vis ] | [ Vis ] [ "virtual" | "abstract" ] ) "func" identifier "(" [ ParamList ] ")" [ ReturnType ] ( Block | "=" Expr | ε ) .ReturnsClause = "returns" Type . // class-level default returnTypeDecl = StructDecl | ClassDecl | UnionDecl | RefUnionDecl | EnumDecl | InterfaceDecl | DelegateFuncDecl . // emits as CLR nested typeA 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 ExceptionProperties
Section titled “Properties”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):
| Suffix | Meaning |
|---|---|
no suffix / = expr | stored property, optionally with an initial value |
=> expr | computed getter, no backing field |
{ } | accessor block (property) |
Accessor sets derive from the keyword:
| Spelling | Kind | Accessors | Notes |
|---|---|---|---|
x: int / x: int = 0 | field (mutable) | storage — no accessor methods | bare field, default optional |
let x: int | stored property | getter + construction-only write path | compiler-managed storage |
var x: int | stored property | getter + setter | compiler-managed storage |
let area: float => self.r * self.r * PI | computed property | get only, no backing field | recomputed on every read |
let area: float { get => self.r * self.r * PI } | custom getter | get only, no backing field | may be combined with an explicit location protocol |
var display: string { get => format(self.id) set(v) => self.apply(v) } | behavioral property | custom get + custom set, no backing field | setter expression performs the write effect |
let x: int { } | property | get-only | set in init (class) |
required let x: int { } | property | get + init | init-only setter |
var x: int { } | property | get + set (auto-backed) | |
var price: float { set(v) => guard(v) } | property | get auto, custom set | setter 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 explicitly — set(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) }}The accessor forms
Section titled “The accessor forms”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 readselffreely:class Rect {var w: floatvar h: floatlet area: float => self.w * self.h // recomputed each read, no storagelet 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. Aletproperty may use only the getter; avarproperty must pair it withsetormut. -
Auto get-only —
let x: int { }. A getter over aprivate initonlybacking field. On aclassthe value is written once, ininit, through the backing field; thereafter it is read-only:class Connection {let host: string { } // get-only; set once in initlet port: int { }init(host: string, port: int) {self.host = host // writes the backing field through the propertyself.port = port}} -
Get + init —
required let id: Guid { }. A getter plus an init-only setter (one carryingmodreq(IsExternalInit)): the value is supplied by a composite literal and is immutable thereafter, exactly C#‘s{ get; init; }. Therequiredmarker forces every composite literal to set it (ES2189):class User {required let id: Guid { } // get + init — set by the composite literalrequired 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.vis 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 0var name: string { set(v) => v.Trim() } // normalize on store}A read of
p.pricereturns the stored (already-clamped) value; a writep.price = -5.0stores0.0. The setter is an ordinary value expression — neverself.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 illegal — ES2193 — 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 methods — pub 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
/ init — pub 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" } .Nested types
Section titled “Nested types”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: pub → NestedPublic, bare (internal) →
NestedAssembly, priv → NestedPrivate. 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 qualifier — Arm 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.
Constructors (init, class only)
Section titled “Constructors (init, class only)”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 secondaryinit(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.
union / ref union
Section titled “union / ref union”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).
Value union — reified shape
Section titled “Value union — reified shape”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 { … }}Reference union — reified shape
Section titled “Reference union — reified shape”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 … }}Recursive and generic unions
Section titled “Recursive and generic unions”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> cellsA 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()).
Flag sets
Section titled “Flag sets”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.
interface
Section titled “interface”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.
Functions, static facets, delegate func
Section titled “Functions, static facets, delegate func”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 allowedSep = "," | newline .Param = ( [ "out" ] [ "readonly" ] identifier ":" Type | identifier ":" "*" Type ) [ "=" Expr ] . // optional default valueA 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 facets
Section titled “Static facets”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.
Default and named arguments
Section titled “Default and named arguments”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.
const and derive
Section titled “const and derive”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)).
| Trait | Generates | Also |
|---|---|---|
equality | Equals(other), GetHashCode(), ==, != — structural, field-by-field | implements IEquatable<T> (the closed instantiation for a generic type) |
debug | ToString() — a readable rendering of the type and its fields | — |
record | the whole CLR record protocol (below) | implements IEquatable<T>; subsumes equality and debug |
flags | [FlagsAttribute] — which is also what makes Enum.ToString() name the set bits | enum 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.
The record protocol
Section titled “The record protocol”@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.
| Member | On a struct | On a class |
|---|---|---|
Equals(T) / Equals(object) / GetHashCode() / == / != | field-by-field | field-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 |
EqualityContract | — | Type — 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.
Delimited regions at namespace scope
Section titled “Delimited regions at namespace scope”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 noneNothing 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.
Attributes
Section titled “Attributes”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.
On members
Section titled “On members”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 form | CLR member | Attribute lands on |
|---|---|---|
name: T | field | the field |
let name: T · var name: T | property over a private backing field | the property |
let name: T => expr | property, no storage | the property |
event name: D | event over a private backing field | the event |
const NAME = v | literal field | the field |
init(…) | constructor | the constructor |
| an enum case | literal field | the 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.
Assembly attributes
Section titled “Assembly attributes”An attribute written with the assembly: target at compilation-unit scope attaches to the emitted
assembly rather than to a declaration:
namespace Appusing "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.