Skip to content

Pointer values and allocation

*T has one source-level meaning: a nullable, aliasing reference to a location holding a value of type T. new T creates a heap-owned location and yields that pointer. &place forms an address or borrow of an already-existing location. This page specifies those operations and the CLR representations used to carry an address when it does not need, or can no longer remain, a transient managed borrow.

Type = "*" Type | NamedType | TupleType | FuncPtrType .
NewExpr = "new" TypeName ( "{" [ FieldInitList ] "}" | "(" [ ArgList ] ")" ) .
AddressOfExpr = "&" Expr .

* nests according to Type, so *int and *Node are ordinary pointer types. A program shall not form *Class: a class already carries reference identity and *Class is ES2003. Pointers to value structs and primitives are valid. new requires a value struct; new on another type is ES2144.

A *T may be nil, stored in a field or collection, returned from a function, and shared among callers. Member access automatically dereferences its target, so a pointer reads like the value it names:

struct Node { value: int, next: *Node }
func push(head: *Node, value: int) -> *Node =
new Node { value: value, next: head }
func sum(head: *Node) -> int {
var total = 0
var current = head
while current != nil {
total += current.value
current = current.next
}
return total
}

current.value is an automatic dereference. There is no source spelling equivalent to C’s (*current).value, and there is no unmanaged pointer arithmetic. A pointer is to a location, not a separately observable object identity for the pointed-to value.

The type and memory model use three deliberately separate ideas:

ThingExampleMeaning
valueCell { value: 40 }one Cell value, copied by ordinary value assignment and argument passing
locationvar cell = Cell { value: 40 }a named, addressable place currently holding one Cell value
pointer valuevar pointer = &cella *Cell address/borrow that aliases that particular location

The name pointer in the third row is itself just a normal local binding which stores a pointer value. It does not make cell a pointer, does not create a second Cell, and does not add another level of source indirection to cell. The following two writes reach the same location:

struct Cell { var value: int }
func addressAliasesExistingLocation() -> int {
var cell = Cell { value: 40 }
var pointer = &cell
pointer.value += 1
cell.value += 1
return pointer.value // 42
}

Source reads and writes through a non-null *T automatically dereference. That makes pointer.value read pleasantly like a Cell access, but the type is still *Cell: it may be nil, may be copied as an alias, and carries location-sharing semantics rather than Cell copy semantics. & inherits the mutability contract of the place addressed: it can borrow a writable var location for mutation, or a readonly let location for observation only.

An address-of operand must be an addressable place, not merely an expression that happens to produce a T. var locals are writable places; let locals are readonly-addressable places; a bare typed local (cell: Cell = expression) is mutable by default but intentionally has no addressable representation. Properties participate only when their loca or mut contract supplies a location. An array element &arr[i] is a place in its own right, emitted as ldelema: the CLR has no read-only array, so an element is always writable storage, and a let binding of the array fixes the reference rather than the contents. Because an element is heap-rooted, a pointer to one is not the non-escaping ref T form and may outlive the frame that formed it. These rules keep addressability a deliberate representation capability rather than an accidental consequence of mutability.

& and new both produce *T, but they answer opposite questions:

FormQuestion answeredResult
&place“Where is this value already stored?”an address/borrow of the existing location named by place
new T { ... }“Heap-allocate a new T and give me its *T.”a heap-owned pointer to fresh, independent storage initialized with T
struct Cell { var value: int }
func allocationIsIndependentFromAddressOf() -> int {
var cell = Cell { value: 40 }
var alias = &cell
var fresh: *Cell = new Cell { value: 40 }
alias.value += 1
fresh.value += 2
return cell.value * 100 + fresh.value // 4142
}

alias and cell observe one location. fresh observes another location, even though its initial value has the same shape. new is therefore not shorthand for & and & is not a delayed spelling of new. new is the source-level heap-allocation operation; & is the source-level identity-preserving address/borrow operation.

This distinction is semantic, not a promise about a particular register, stack slot, or object layout. A program should choose &cell because another operation must alias or borrow cell, and choose new Cell { ... } because it is allocating a fresh, pointer-owned value. The latter has a fixed heap allocation meaning; the former intentionally says nothing about the physical residence of cell.

new T always allocates its __Ptr_T heap carrier. By contrast, &place begins as an address/borrow of an existing location. The compiler chooses the following carrier for that address according to its lifetime:

Address-of useCLR representationConsequence
escapes its frame; is captured, returned, stored, or nullable__Ptr_T, a heap reference cellallocation; the cell owns the shared location
provably does not escape and is non-nullablemanaged pointer T& (ref T)aliases existing storage without allocation

The compiler shall insert conversions wherever those carriers meet. This is why an address of a local can flow to a *T parameter and a returned *T can still outlive its creator: the former can be a managed borrow, while the latter uses the heap cell. A program cannot test which carrier was selected for &, or make its behavior depend on it.

Managed pointers are GC-tracked. They cannot dangle and cannot escape the frame that owns their storage; the escape analysis is what enforces that restriction. The heap-cell form is ordinary GC-reachable state and remains alive while a holder can reach it. A compiler path that fails to realize a managed alias before it reaches a durable boundary must report ES2030 at the source location; it must not defer the failure to malformed IL or a run-time exception.

&cell does not request allocation, but its borrow can be made durable in the source language. A *T is first-class, so an address of a local may be returned, captured, stored, or passed through a durable API. In that case the compiler promotes the same location to its durable heap carrier; it does not copy the current Cell into an unrelated allocation.

struct Cell { var value: int }
func makeCell() -> *Cell {
var cell = Cell { value: 40 }
return &cell
}
func usePromotedAddress() -> int {
let pointer = makeCell()
pointer.value += 2
return pointer.value // 42
}

The local cell names the location at the point &cell is formed. The returned pointer keeps that location alive through a compiler-selected durable carrier. This is why “address of a local” is the right source description, while “stack address” is not a stable semantic description.

await splits one source activation into an initial execution and one or more state-machine resumptions. If a local location is both live after a suspension and participates in a durable pointer operation, the state machine stores that local’s shared __Ptr_T carrier, not a copied T. This includes an awaited call whose *T parameter is itself durable. The callee, the caller after resumption, captured holders, and generic containers must therefore all observe the same location:

struct Cell { var value: int }
func bump(cell: *Cell) -> Task<int> {
await Task.Delay(1)
cell.value += 1
return cell.value
}
func observe() -> Task<int> {
var cell = Cell { value: 41 }
let result = await bump(&cell)
return cell.value * 100 + result // 4242
}

The representation remains unobservable: observe has not changed the type or meaning of cell, and a non-escaping address use still remains eligible for the allocation-free managed-pointer representation.

new heap-allocates a location that did not exist and produces its *T. & borrows or takes the address of a location that already exists:

struct Data { var value: int }
func change(data: *Data) { data.value = 99 }
func existingLocationAlias() -> int {
var value = Data { value: 10 }
change(&value)
return value.value
}
func heapCell() -> int {
let value = new Data { value: 10 }
change(value)
return value.value
}

The first call may emit ldloca and pass Data&, with no allocation. The second allocates __Ptr_Data and passes its underlying location. Both calls satisfy the same source signature and observe the same mutation. new is the only allocation expression; & itself never requests allocation, although an address that later escapes must be represented safely as a heap cell.

*T is a type constructor over value-disciplined T; it is not an unsafe native address type and it does not turn T itself into a reference type. The distinction matters at several boundaries:

  • T is an ordinary value. Assigning Cell to another Cell copies its value.
  • T? is an optional value. It records absence but does not create a shared mutable location.
  • *T is an optional alias to a location holding T; copying the pointer copies the alias. Operations through an address formed with & remain subject to the addressed place’s writable or readonly contract.
  • class C already has CLR reference identity. *C is forbidden; class-valued property locations are an opaque property protocol, not a loophole for source pointers to classes.
  • List<*T>, a closure capture, a returned pointer, and an async state machine are durable contexts. They preserve *T identity by selecting a heap cell when a managed byref would be illegal.

For example, var other = pointer copies the pointer value and therefore aliases the same Cell; it does not copy the cell. Conversely, var other = cell copies the Cell value and gives other independent storage. This difference is the practical reason the type spelling is visible in APIs.

The CLR’s ref/in/out parameter machinery is an implementation route for some *T operations, not a second source-level pointer family. By-ref calls and method sets specifies those call boundaries; Memory model specifies copying, initialization, and visibility; properties and initialization specifies the explicit property-location boundary.

The CLR forbids a managed pointer as a generic type argument. Therefore *T in a generic-argument, field, collection, capture, or return context normalizes to the heap-cell representation. This is an implementation constraint, not a second source type: an E# generic API still declares and receives *T.