Skip to content

Pointers & memory

E# makes the value/reference distinction and the mutation story explicit, then gets out of your way. You rarely think about the heap — but when you need to share, recurse, or mutate through an alias, the tools are right there.

Three levels of “can this change,” from most to least frozen:

const MAX = 1024 // compile-time constant — folded to a literal at every use
let name = "Ada" // runtime-immutable — computed once, never reassigned
var total = 0 // mutable

const must fold to a literal at compile time (const SUM = 10 + 20 is fine; const NOW = DateTime.UtcNow is not — use let). For locals, let is immutable and var reassigns. A name-first typed local (count: int = 0) is also mutable, but intentionally cannot be borrowed with &; use typed var when a local must be addressable. By convention const names are SCREAMING_SNAKE_CASE.

Browse const examples →

A struct value is copied on assignment and has no identity (see Types). That’s the default, and it’s usually what you want — no aliasing surprises. When you do want sharing, recursion, or in-place mutation, you reach for a pointer.

*T is E#‘s pointer, modeled on Go’s: nullable, aliasing, and first-class (you can store it, return it, share it). It’s how a value-shaped type reaches into recursive or shared flows.

struct Node { value: int, next: *Node } // recursive shape needs the pointer
func sum(head: *Node) -> int {
var total = 0
var cur = head
while cur != nil {
total += cur.value // auto-deref — no explicit *cur
cur = cur.next
}
return total
}

new T is already heap allocation. For an address formed with &, the carrier is the compiler’s choice: a borrow that never escapes the frame can be a plain managed pointer (ref T) with zero allocation, aliasing the caller’s storage directly; an address that escapes or goes nullable gets a small heap wrapper so it can outlive the frame. A by-ref parameter that does not escape costs nothing.

*T works for value struct (any size) and primitives (*int). It does not apply to class — that’s already a reference; pointing at it is meaningless.

Browse pointer examples →

This is worth holding onto:

new heap-allocates a fresh T and yields *T. & borrows or addresses a location that already exists.

Both expressions produce *T, but the fact that both can be stored in var pointer should not hide the difference:

SourceWhat pointer denotes
var pointer = &cellan address/borrow of the exact location already named by cell
var pointer: *Cell = new Cell { ... }a heap-allocated location with no separate Cell binding

new T { ... } heap-allocates a value struct and hands back a *T. It’s the one allocation expression in the language, and the only way to mint a fresh pointer:

let n: *Node = new Node { value: 7, next: nil }
let v: *Vec2 = new Vec2(3, 4) // positional form, for positional struct

& only ever borrows or takes an address — of an addressable variable, an explicitly location-aware property, or a function (&func):

var x = 10
var p = &x // address of an existing local → a pointer
p += 5 // writes through it — x is now 15

Here p is a local containing a pointer value; it is not another int. p and x reach the same location. Contrast that with a fresh pointer-owned value:

struct Cell { var value: int }
var cell = Cell { value: 40 }
var alias = &cell
var fresh: *Cell = new Cell { value: 40 }
alias.value += 1 // cell.value is now 41
fresh.value += 2 // only fresh is now 42

&cell does not itself allocate. If that address is returned, captured, stored, or survives await, the compiler promotes that same cell location to durable storage as needed. Use new when you want a freshly heap-allocated value; use & when another operation should borrow or alias the value you already have.

Properties keep their implementation private by default. A stored let/var property has a location contract; computed properties do not. loca => &self.storage names stable storage explicitly, while a scoped mut property can lend a temporary working value and always resume afterward. The full rules and a complete example are in Properties and initialization.

(new on a class is an error — it’s already heap-allocated; construct it bare, Connection { ... }.)

A *T parameter takes a pointer; at the call site, mark it with & or * so the mutation is visible to the reader:

func increment(counter: *int) {
counter += 1
}
var count = 0
increment(&count) // count is now 1

For a large struct you only want to read, readonly *T is a zero-copy borrow (it emits as CLR in T) — you get the pointer’s cheapness without granting mutation. And out x: T is the plain CLR out parameter, for the Try… pattern at the BCL boundary.

The same mutation works on a struct, and this is where &local versus new actually shows up:

struct Data { var value: int }
func otherFunc(d: *Data) { d.value = 99 }
var local = Data { value: 10 }
otherFunc(&local) // local.value is now 99

&local aliases the location named by local directly — no allocation unless the pointer later crosses a durable boundary. Had you written var local = new Data { value: 10 } and passed local, the value would already live in a heap cell. Same result, same callee — but the semantic choice is stronger than cost: &local shares an existing value, while new creates a fresh one. A returned &local is valid too; the compiler preserves that original location by promoting it rather than copying a new Data value.

Copy a value and overwrite a few fields, producing a new value — no mutation, no allocation:

let p1 = Point { x: 3, y: 4 }
let p2 = p1 with { x: 10 }
// p1.x == 3, p2.x == 10

with is value-only — it’s the idiomatic way to “change” an immutable struct.

Browse allocation examples →