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). let is immutable at runtime. var reassigns. By convention const names are SCREAMING_SNAKE_CASE.

Browse const examples →

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

data 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
}

The semantic is fixed; the representation is the compiler’s choice. A whole-module escape analysis decides per binding: a pointer that never escapes the frame becomes a plain managed pointer (ref T) — zero allocation, aliasing the caller’s storage directly; one that escapes or goes nullable gets a small heap wrapper so it can outlive the frame. A by-ref parameter that doesn’t escape costs nothing.

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

Browse pointer examples →

This is worth holding onto:

new allocates something that doesn’t exist yet. & takes the address of something that already does.

new T { ... } heap-allocates a value data 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 data

& only ever takes addresses — of a variable (&x), or of 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

(new on a ref data 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.

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

You don’t hand-tune struct-vs-class. The compiler measures each data type and silently picks the CLR form — small/simple stays a struct, large/reference-heavy/heavily-copied becomes a class — while preserving the value contract. The one case it won’t paper over is genuine recursion (data Node { next: Node }): that’s physically impossible as a value type and is a hard error, with the fix (*Node) named in the message.

Browse allocation examples →