Properties and object initialization
This section gives the detailed property and object-construction contract for struct and class. Namespace
properties use the same accessor rules with static storage; see Namespace hosts and initialization.
Member grammar
Section titled “Member grammar”Field = [ Visibility ] identifier ":" Type [ "=" Expression ] .Property = [ Visibility ] [ "required" ] ( "let" | "var" ) identifier ":" Type [ "=" Expression | "=>" Expression | AccessorBlock ] .AccessorBlock = "{" [ SetAccessor | LocaAccessor | MutAccessor ] "}" .SetAccessor = "set" "(" identifier ")" "=>" Expression .LocaAccessor = "loca" "=>" "&self." identifier .MutAccessor = "mut" "=>" "&self." identifier | "mut" BlockWithOneLend .BlockWithOneLend = "{" { Statement } "yield" "&" identifier { Statement } "}" .Init = [ "priv" | "protected" ] "init" "(" [ ParameterList ] ")" [ ":" ( "base" | "this" ) "(" [ ArgumentList ] ")" ] Block .PrimaryHeader = "class" TypeName "(" [ ParameterList ] ")" .Visibility = "pub" | "priv" .At member scope, the declaration keyword selects the representation before any suffix is considered. Bare
name: Type is a field; let name: Type and var name: Type are properties even when they have no { }.
Their optional initializer initializes stored property storage. => expression is a computed property and
{ ... } customizes an accessor/location protocol.
Fields and properties
Section titled “Fields and properties”| Source form | Source behavior | emitted shape |
|---|---|---|
x: T | ordinary mutable field | direct field |
let x: T | stored get/init property with implicit readonly loca | getter, construction-only write path, location companion, private backing storage |
var x: T | stored get/set property with implicit writable loca | getter, setter, location companion, private backing storage |
let x: T => e | getter recomputes e on every read; no implicit location | getter only; no backing field |
let x: T { get => e } | authored getter recomputes e; may be combined with explicit location behavior | getter only; no backing field |
var x: T { get => read() set(v) => write(v) } | authored read/write behavior | getter and setter; no backing field |
let x: T { } | stored get-only property with implicit readonly loca | getter, construction-only backing storage, location companion |
required let x: T { } | required get/init property with implicit readonly loca | getter, init-only setter, location companion, required metadata |
var x: T { } | stored get/set property with implicit writable loca | getter, setter, location companion, private backing field |
var x: T { set(v) => e } | transform incoming value; its implicit storage location requires explicit loca or mut acknowledgement before &x | getter, setter, private backing field |
Within a type body, bare x: T (or x: T = value) is the ordinary mutable direct-field spelling. Member
let and var instead reserve a property boundary, so changing a stored property into a computed, validated,
or location-aware property does not change the declaration family. A custom setter over an automatic getter
computes the value that will be stored. It must not assign to self.x, which would invoke the setter
recursively. A custom get => expression removes implicit storage. When paired with it,
set(v) => expression is evaluated as the property’s authored write effect rather than as a storage
transformation. var x: T => expression is invalid because the shorthand supplies no writable behavior; use
let, or an accessor block containing both behaviors. The
similarly spelled local declaration x: T = value is a mutable but non-addressable local only inside a
function body; see Bindings and assignment.
class Product { var stock: int let available: bool => self.stock > 0 var price: int { set(v) => if v < 0 { 0 } else { v } } required let id: Guid { }}class Session { id: int init(id: int) { self.id = id } func replaceId(value: int) { self.id = value }
var displayId: int { get => self.id + 1 set(value) => self.replaceId(value) }}The available property has no backing slot, while price stores the normalized value. A stored property is
represented by a PropertyDefinition and get_x / set_x methods; its <x>k__BackingField remains private
even when the accessors are public. The visibility prefix controls the accessor methods, not the backing
storage.
Constructor-owned fields
Section titled “Constructor-owned fields”A class constructor may declare an instance field while initializing it. The declaration is distinguished
from assignment by the explicit type after self.name:
class Meter { init(value: int) { priv self.storage: int = value }}priv self.storage: int = value declares a private int field named storage and initializes it in that
constructor. self.storage = value without : int is only assignment and never creates a member. A
visibility prefix belongs to the field declaration: in a public constructor, private storage must say
priv self.storage; omitting the prefix uses the ordinary bare-member visibility rather than inheriting the
constructor’s visibility.
Every nondelegating constructor for the class shall declare the same constructor-owned field set with
compatible types and visibility. A : this(...) constructor inherits the target constructor’s field
initialization and shall not redeclare those fields. A declaration also participates in ordinary collision
checking: it cannot silently replace a body-declared field, property, method, or another incompatible
constructor-owned declaration.
Property locations: loca
Section titled “Property locations: loca”A field on an addressable value type is an addressable storage location. A class is a reference type rather
than a source-level *T target, so &owner.field is not general class-pointer formation. A property is an
access protocol, and &owner.property is valid only when the property supplies stable location identity.
Stored member let and var properties receive an implicit location protocol; a computed property does not.
loca makes the selected storage explicit:
class Meter { init(value: int) { priv self.storage: int = value }
var value: int { loca => &self.storage }}
func increment(value: *int) { value += 1 }
func run() -> int { let meter = Meter(41) increment(&meter.value) return meter.value}loca evaluates the receiver once and returns the declared property’s location; it does not form a pointer to
the receiver. In particular, *Meter, &meter as a general class pointer, and &meter.storage for a class
field remain invalid source forms. A class-valued property may still have a loca contract: the compiler uses
an opaque property-location carrier if that location must cross a boundary. That carrier is implementation
machinery, is not an E# pointer type, and never makes *Class legal.
A local initialized from a durable property location retains the property’s protocol. let location = &owner.property is a readonly alias; var location = &owner.property is a writable alias when the property
contract permits writes. While confined to one frame, the compiler may implement that alias as a CLR managed
byref to the property value. It shall not place that managed byref in a closure display object or async state
machine. If the alias is captured or remains live across await, it is raised to an opaque carrier containing
the receiver evaluated at the declaration and the durable location behavior. The raised location continues
through the property’s location companion; it does not become an ordinary getter/setter pair and cannot
accidentally enter a scoped mut protocol. Consequently, a class-valued property location may expose the class value’s
ordinary member set without introducing *Class, and a later property write is visible through the alias.
No generated display or state-machine field may have a managed-byref type for this case.
This raising rule applies only to durable loca or direct-mut locations. A scoped mut lend is tied to its
setup/resume region and is rejected before closure conversion or async lowering if code attempts to capture it
or keep it across an await.
A property with a custom set has an additional rule. Borrowing its implicit storage would bypass validation
or normalization in the setter, so &owner.value is diagnosed unless an explicit loca or mut declares the
intended policy. An explicit declaration is the author’s acknowledgement that location writes have the stated
semantics rather than silently invoking set.
Construction is the only ordinary caller context allowed to initialize a property’s backing storage directly.
Inside init, self.value = expression establishes the stored value without applying a custom setter or
scoped mut protocol. After construction, the same spelling in any ordinary method is a property call and
shall invoke set or mut, including when the method is declared on the property-owning class. The generated
setter implementation itself may of course write its own backing storage without recursively calling itself.
Scoped property access: mut
Section titled “Scoped property access: mut”mut is one property capability, not a nest of read, modify, or borrow subproperties. Its construction
determines which of reading, assignment, readonly borrowing, mutable borrowing, and durable escape are valid.
A direct mut => &self.storage is a stable location contract; the selected field or property determines
whether the location is writable.
The block form is the scoped variant. Setup runs in the declaring type, yield &working lends exactly one
local working location to one direct borrowing call, and statements after yield always resume after that call
returns or throws:
struct Cell { value: int }
class Meter { init(value: int) { priv self.storage: int = value }
let value: Cell { mut { var working: Cell = Cell { value: self.storage } yield &working self.storage = working.value } }}
func increment(cell: *Cell) { cell.value += 1 }
func run() -> int { let meter = Meter(4) increment(&meter.value) return meter.value.value // 5}The compiler lowers that borrow through a generated lease and a CLR finally: private setup and resume remain
inside Meter, the borrower sees only the yielded Cell location, and resume executes on both normal and
exceptional exits. The scoped location cannot be returned, stored, captured, used by await, or otherwise
escaped. Those are static errors; a throw authored in the resume portion is ordinary runtime behavior and
does not change the property capability. Declaring mut suppresses automatic loca; when both are present,
ordinary/scoped access uses mut, while durable escape requires a durable mut or the separately declared
loca contract. When explicit loca and scoped mut coexist, a direct borrowing call uses the scoped lend
and resume protocol; storing, capturing, returning, or carrying the location across await uses loca.
The direct form derives its static contract from the selected location. mut => &self.mutableStorage
permits reads, assignment, readonly borrowing, mutable borrowing, and durable escape when that storage is
writable and durable. Selecting readonly storage removes assignment and mutable borrowing. A runtime check or
throw inside authored accessor code does not strengthen or weaken this static contract.
| Construction | read | assign/modify | borrow | mutable borrow | escape |
|---|---|---|---|---|---|
implicit stored let location | yes | no | readonly | no | durable |
implicit stored var location | yes | yes | yes | yes | durable |
direct mut => &readonlyLocation | yes | no | readonly | no | durable |
direct mut => &writableLocation | yes | yes | yes | yes | durable |
scoped mut { ... yield &working ... } | yes | if yielded location is writable | direct call only | direct call only | no |
For a scoped mut, capability checking happens before its setup body executes. An invalid writable operation,
multiple simultaneous scoped lends, or an attempted escape is a compile-time error; the implementation may
not enter setup and then fail dynamically. The receiver is evaluated once. Nested mutation and compound
assignment use the same single lend and resume region rather than invoking the property repeatedly.
Separate compilation and metadata
Section titled “Separate compilation and metadata”Property capability is part of the E# public contract. The compiler emits normal CLR property metadata plus
compiler-owned capability metadata describing stored, durable-location, direct-mut, scoped-mut, writable,
and custom-setter facts. A consuming E# compilation reads those facts directly from the referenced PE; it does
not need to load the producer assembly or inspect its source.
A durable location imports as the opaque E# property-location capability and calls the producer’s companion
location accessor. A scoped location imports the producer’s generated begin/lease/resume protocol and emits a
real finally in the consumer. The lease type and companion methods are ABI machinery: source, diagnostics,
semantic tooling, and method signatures shall not reinterpret them as *Class. Interfaces and separate
assemblies preserve the same read, write, borrow, scoped, and escape distinctions as a same-compilation use.
An external CLR property whose getter genuinely returns T& imports as a durable loca. A CLR ref T
getter permits readonly and writable borrow; a ref readonly T getter imports only readonly borrow, preserving
its required InAttribute modifier in the emitted call. Reading either property normally dereferences the
getter result to a T value. An ordinary CLR getter returning T does not become addressable merely because
the property also has a setter.
Required members and interfaces
Section titled “Required members and interfaces”required applies to fields and stored properties. A composite literal shall supply every required member or
is ill-formed. For a property, required let x: T { } additionally exposes an init-only setter so a composite
literal can establish the value before the object becomes immutable.
class Ticket { required let id: Guid { } required let quantity: int { }}
let ticket = Ticket { id: makeId(), quantity: 2 }An interface may require let x: T { get }, var x: T { get set }, or an init-capable property. An
implementer must provide an actual property with at least the required accessor set. A richer property may
satisfy a smaller contract. A bare field never satisfies a property requirement: writing x: T fixes field
representation, and conformance does not rewrite it into accessors. Use let x: T or var x: T; a matching
field produces ES2226.
Interface location contracts
Section titled “Interface location contracts”loca in an interface accessor list makes durable identity normative rather than an implementation detail:
interface ICounter { var value: int { get set loca }}
class Counter : ICounter { pub var value: int = 41}
func increment(value: *int) { value += 1 }
func update(counter: ICounter) -> int { increment(&counter.value) return counter.value}let value: T { get loca } promises a readonly durable location. var value: T { get set loca } promises a
writable durable location. The interface emits an abstract ref-returning getloca_value companion and
capability metadata; an implementation’s companion fills that CLR interface slot. Stored let/var
properties, explicit loca, and compatible direct mut can satisfy the corresponding contract.
A computed property, a scoped-only mut, or a custom-set property whose raw location policy was not explicitly
acknowledged cannot satisfy it. These checks occur at conformance time, before a caller can form &value.
The same rules survive metadata-only import. A consumer compiled without loading the producer assembly sees
the interface member as a property with durable readonly or writable direction and emits a virtual call to the
location companion. The companion’s T& is ABI machinery; when T is a class it still does not introduce
source *Class.
Struct restrictions
Section titled “Struct restrictions”A struct has no init constructor block. Consequently, a stored get-only let x: T { } property is
ill-formed on a struct: there is no constructor body that can establish its backing value. Use a computed
property, a required let property established by a composite literal, a mutable property, or a plain field
instead. A readonly struct makes all fields immutable and emits the corresponding CLR readonly metadata.
Class constructors
Section titled “Class constructors”An init declaration emits an instance .ctor. It is public by default; priv init emits a private
constructor and protected init emits a family constructor. Overload resolution for constructors uses arity
and parameter names, never argument types. Two constructors with the same parameter count are ill-formed.
Field defaults run before an ordinary constructor body. : base(args) selects a base constructor. : this(args)
delegates to a sibling constructor; the delegated constructor performs the base call, field defaults, and its
body before the delegating body runs. A this-delegation cycle is ill-formed.
class Connection { var host: string var port: int
init(host: string, port: int) { self.host = host self.port = port } init(host: string) : this(host, 80) { }}Primary-constructor capture
Section titled “Primary-constructor capture”class Type(params) { ... } declares a primary constructor header. Header parameters are not automatically
fields. A parameter used by an in-body method is captured into a synthesized private, init-only field. A
parameter used only by a field default or parameterless init { } epilogue remains a constructor-local.
class UserService(store: IUserStore, maxRetries: int = 3) { var tokens: int = maxRetries init { if maxRetries <= 0 { tokens = 1 } } func find(id: Guid) -> User = store.find(id)}The primary construction sequence is: base call, capture stores, field defaults, then the parameterless
init { } epilogue. A secondary init(args) on a headered class shall delegate to the primary through
: this(...); a composite literal cannot construct a headered class. A header parameter may not duplicate an
explicit field name.