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.
The mutation triad: const / let / var
Section titled “The mutation triad: const / let / var”Three levels of “can this change,” from most to least frozen:
const MAX = 1024 // compile-time constant — folded to a literal at every uselet name = "Ada" // runtime-immutable — computed once, never reassignedvar total = 0 // mutableconst 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.
Value by default
Section titled “Value by default”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 — the pointer
Section titled “*T — the 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.
new vs. & — the clean seam
Section titled “new vs. & — the clean seam”This is worth holding onto:
newallocates 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 = 10var p = &x // address of an existing local → a pointerp += 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 { ... }.)
Passing by reference
Section titled “Passing by reference”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 = 0increment(&count) // count is now 1For 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.
with — non-destructive update
Section titled “with — non-destructive update”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 == 10with is value-only — it’s the idiomatic way to “change” an immutable data.
Allocation, handled for you
Section titled “Allocation, handled for you”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.