Skip to content

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.

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.

Source formSource behavioremitted shape
x: Tordinary mutable fielddirect field
let x: Tstored get/init property with implicit readonly locagetter, construction-only write path, location companion, private backing storage
var x: Tstored get/set property with implicit writable locagetter, setter, location companion, private backing storage
let x: T => egetter recomputes e on every read; no implicit locationgetter only; no backing field
let x: T { get => e }authored getter recomputes e; may be combined with explicit location behaviorgetter only; no backing field
var x: T { get => read() set(v) => write(v) }authored read/write behaviorgetter and setter; no backing field
let x: T { }stored get-only property with implicit readonly locagetter, construction-only backing storage, location companion
required let x: T { }required get/init property with implicit readonly locagetter, init-only setter, location companion, required metadata
var x: T { }stored get/set property with implicit writable locagetter, 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 &xgetter, 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.

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.

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.

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.

Constructionreadassign/modifyborrowmutable borrowescape
implicit stored let locationyesnoreadonlynodurable
implicit stored var locationyesyesyesyesdurable
direct mut => &readonlyLocationyesnoreadonlynodurable
direct mut => &writableLocationyesyesyesyesdurable
scoped mut { ... yield &working ... }yesif yielded location is writabledirect call onlydirect call onlyno

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.

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 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.

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.

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.

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) { }
}

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.