Skip to content

By-ref calls and method sets

E# exposes the CLR’s by-reference family through three source forms. They all designate a location, but they differ in direction and mutability. This page specifies their syntax, call sites, and interaction with receiver methods.

Param = ( [ "out" ] [ "readonly" ] identifier ":" Type
| identifier ":" "*" Type ) [ "=" Expr ] .
Arg = [ "out" | "&" | "*" ] Expr | identifier ":" Expr .
AddressOf = "&" Expr .

The complete type grammar admits readonly *T; it is the read-only borrow form. out x: T places out before the name, while a normal pointer parameter is x: *T. The & and * call-site markers make by-reference passage visible. An implementation shall reject a call whose marker and parameter direction do not agree.

Source parameterCLR shapeContractTypical call
x: TTcopied inputf(x)
x: *TT& or pointer cellaliasing input/output locationf(&x) or f(*x)
x: readonly *Tin T / [In] T&zero-copy, no write through xf(&x)
out x: T[Out] T&callee supplies a result locationf(out x) / f(out var x)

The ref, in, and out forms are the same CIL by-reference type; parameter metadata carries the direction. A pointer parameter may use the managed-pointer or heap-cell representation from Pointer values and allocation. At its source boundary it always means an aliasing *T location.

struct Vec { var x: int, var y: int }
func translate(v: *Vec, dx: int, dy: int) {
v.x += dx
v.y += dy
}
func lengthSquared(v: readonly *Vec) -> int = v.x * v.x + v.y * v.y

Writing through v in lengthSquared is ill-formed. The restriction is on that borrow, not on the underlying variable: after the readonly call, a mutable borrow may still modify the original location. readonly *T is a parameter form, distinct from readonly func (v: T), which is a read-only method receiver.

An out parameter is a write-through result slot. Reads and assignments in the callee dereference that slot automatically; a caller may provide an existing location or introduce one in the call:

func tryParse(text: string, out value: int) -> bool {
value = int.Parse(text)
return true
}
func parse() -> int {
if tryParse("42", out var value) {
return value
}
return 0
}

The binding introduced by out var is in scope for the statement and what follows. out interoperates directly with CLR out methods: no wrapper, tuple, or allocation is inserted.

&name addresses an eligible local, parameter, field, or property location; it lowers to the matching location instruction (ldloca, ldarga, ldflda, or a property location accessor). A bare typed local (name: T = expression) is mutable but deliberately not eligible: use var name: T = expression for a writable borrow or let name: T = expression for a readonly borrow. A local initialized from an address is a ref local, and ordinary reads and writes through that binding dereference transparently.

A class receiver is never a *T target. &owner.property is therefore not a pointer to owner; it is valid only for a property whose loca or mut contract supplies a location. Class fields and computed properties cannot be addressed directly. A scoped mut location is valid solely as the direct argument to its borrowing call and is rejected before any escape or pointer-representation analysis if it is returned, stored, captured, or otherwise allowed to outlive resume.

A durable property ref local uses the binding keyword’s mutability: let location = &owner.property permits reads, while var location = &owner.property may write through when the property location is writable. A class-valued property location exposes the class value’s ordinary methods and members through that local, but the local is still an opaque property location rather than *Class. Capture and async suspension raise it to receiver-plus-protocol machinery; they never put a managed byref in a heap object or state-machine field. The same prohibition is checked after generic substitution: a declaration containing *T cannot be instantiated with a class type argument to manufacture *Class indirectly.

The binder first resolves &name as a function address. If name is a function, &name is a function pointer; if it is storage, it is an address-of expression. The distinction comes from resolution, not a second token. Function-pointer types and calls are specified in Function values.

*T has its own method set. A value receiver, func (v: T), is callable on both T and *T; a pointer receiver, func (v: *T), is callable only on *T. The pointer receiver operates on the actual location:

struct Counter { var value: int }
func (c: *Counter) increment() { c.value += 1 }
func run() -> int {
var counter = Counter { value: 0 }
(&counter).increment()
return counter.value
}

When only a pointer receiver satisfies a declared interface, the compiler creates a __Ptr_T wrapper that implements that interface and forwards to the underlying hosts. The wrapper is generated only when *T is used as that interface, avoiding an unconditional boxing cost. The source conformance remains nominal: the type shall explicitly declare the interface.