Memory Semantics
Zym gives you explicit control over how values pass between scopes with four keywords: ref, val, slot, and clone.
| Keyword | Meaning | Writes visible to caller? |
|---|---|---|
ref | Creates an alias (reference) to the original variable | Yes — all writes go through |
val | Deep-copies the value, but refs keep the same target | No for plain values; refs still alias the original target |
slot | Binds to the caller’s variable slot directly | Yes — writes back to the slot |
clone | Creates an independent deep copy | No — completely isolated |
ref — References
The ref keyword creates an alias to an existing variable. Any writes through the ref are visible to the original, and any writes to the original are visible through the ref.
Basic ref
var x = 42 var y = ref x // y is an alias for x y = 100 // x is now 100 x = 200 // y is now 200
Multiple refs
Multiple refs can point to the same variable. All refs and the original stay in sync.
var orig = 5 var r1 = ref orig var r2 = ref orig r1 = 10 // orig == 10, r1 == 10, r2 == 10 r2 = 15 // orig == 15, r1 == 15, r2 == 15
Ref flattening
Creating a ref of a ref does not create a double-indirection. Refs are flattened — all levels point directly to the original variable.
var base = 1 var mid = ref base var top = ref mid // flattened — points to base, not mid top = 999 // base == 999, mid == 999, top == 999 // Even deeper chains flatten var a = 111 var b = ref a var c = ref b var d = ref c d = 222 // a == 222, b == 222, c == 222, d == 222
Ref as function parameter
When a function parameter is declared ref, the function receives an alias to the caller’s variable.
func increment(ref n) { n = n + 1 } var x = 10 increment(x) // x is now 11
Ref with collections
Refs work with lists, maps, and structs. Changes to elements through a ref are visible to the original.
var list = [1, 2, 3] var alias = ref list alias[0] = 99 // list[0] is now 99 var m = { a: 1 } var mr = ref m mr.a = 42 // m.a is now 42
val — Value Copies
The val keyword creates a deep copy of the value. Changes to the copy do not affect the original, and vice versa. It can be used both as a function parameter qualifier and as a target qualifier in assignment.
val does not isolate references created with ref. When you val-copy a value that contains a ref, the copy gets its own ref that still points to the same target as the original. If you need true isolation — where even refs are fully independent — use clone instead.var original = [1, 2, 3] var copy = val original // deep copy via assignment copy[0] = 999 original[0] // still 1
func safe(val data) { data[0] = 999 // local copy — caller unchanged } var arr = [1, 2, 3] safe(arr) arr[0] // still 1
Deep copy semantics
val copies nested structures all the way down. Even deeply nested lists and maps are fully independent.
func modify(val nested) { nested[0][0] = 999 } var deep = [[1, 2], [3, 4]] modify(deep) deep[0][0] // still 1 — deep copy protects all levels
Val with structs
struct Point { x; y } func move(val p) { p.x = p.x + 100 return p } var origin = Point(0, 0) var moved = move(origin) origin.x // 0 — original unchanged moved.x // 100 — copy was modified
slot — Slot Binding
The slot keyword binds directly to the caller’s variable slot. Assignments write back to the caller’s original variable. The key distinction from ref is that slot can rebind what a variable points to, including rebinding refs.
Basic slot
func reset(slot target) { target = 0 } var counter = 42 reset(counter) // counter is now 0
Slot as a statement
slot can also be used as a statement to directly rebind a variable’s slot. This is essential for rebinding refs.
var a = 5 slot a = 6 // direct slot assignment // a is now 6 // Changing the type via slot var x = 10 slot x = "hello" // x is now a string
Ref rebinding (core use case)
The primary power of slot is rebinding refs to point to different targets. A normal assignment to a ref writes through it; slot replaces what the ref points to.
var a = 5 var b = 10 var r = ref a r = 100 // writes THROUGH ref → a is now 100 slot r = ref b // REBIND r to point to b instead r = 200 // writes through new ref → b is now 200 // a is still 100
Multiple rebindings
var x = 1, y = 2, z = 3 var r = ref x r = 10 // x is now 10 slot r = ref y r = 20 // y is now 20, x still 10 slot r = ref z r = 30 // z is now 30, x still 10, y still 20
Slot function parameter
As a function parameter qualifier, slot lets the function rebind the caller’s variable entirely.
func swapToRef(slot target, newSource) { slot target = ref newSource } var a = 100 var b = 200 var r = ref a swapToRef(r, b) r = 999 // b is now 999 (r was rebound to ref b) // a is still 100
clone — Deep Copy (True Isolation)
The clone keyword creates a fully independent deep copy of any value. It works with all types: primitives, lists, maps, structs, and nested combinations.
ref. val copies a ref but keeps the same target — writes through the copied ref still reach the original. clone duplicates both the ref and its target, creating true isolation where no writes can reach the original.Primitives
Cloning primitives works as expected — the clone is independent.
var num = 42 var copy = clone num copy = 99 // num is still 42 var str = "hello" var strCopy = clone str strCopy = "world" // str is still "hello"
Lists
Clone performs a deep copy of lists, including all nested elements.
var list = [1, 2, 3] var copy = clone list copy[0] = 99 // list[0] is still 1 // Deep nested lists var nested = [[1, 2], [3, 4]] var deep = clone nested deep[0][0] = 999 // nested[0][0] is still 1 — fully independent
Maps
var orig = { a: 1, b: { nested: 2 } } var copy = clone orig copy.a = 99 copy.b.nested = 99 // orig.a is still 1, orig.b.nested is still 2
Structs
struct Point { x; y } var p1 = Point(10, 20) var p2 = clone p1 p2.x = 99 // p1.x is still 10
Clone as function parameter
clone can be used as a parameter qualifier, similar to val. The function receives a deep copy of the argument.
func process(clone data) { data[0] = 999 return data } var original = [1, 2, 3] var result = process(original) original[0] // still 1 result[0] // 999
Combined Example
All four qualifiers can be used together in a single function signature.
func mix(slot ext, ref mirror, val snap) { ext = ext + 1 // writes back to caller's variable mirror = ext // writes through to caller's ref snap[0] = 999 // local copy only — caller unchanged } var x = 10 var y = 0 var arr = [5, 6, 7] mix(x, y, arr) x // 11 — slot wrote back y // 11 — ref wrote through arr[0] // 5 — val kept caller safe
When to Use What
General syntax
Memory qualifiers can be used in two contexts: assignment (qualifying the target) and function parameters (qualifying how arguments are received).
// General form: <var|slot> <name> = <ref|val|clone> <target> var alias = ref original // alias points to original var copy = val original // deep copy (refs keep same target) var isolated = clone original // fully independent copy // slot on the storage side (rebind the variable itself) slot alias = ref other // rebind alias to point to other slot x = 42 // direct slot assignment
// All four can be used as function parameter qualifiers func f1(ref x) { ... } // alias to caller's variable func f2(val x) { ... } // deep copy of argument func f3(slot x) { ... } // bind to caller's variable slot func f4(clone x) { ... } // fully isolated copy of argument
slot is used on the storage side (before the variable name) — it targets the variable that acts as storage, never the target value itself. ref, val, and clone are used on the target side (after the =) to qualify how the target value is accessed or copied.Scenario guide
| Scenario | Keyword |
|---|---|
| Create an alias to an existing variable | var y = ref x |
| Deep-copy a value (refs keep same target) | var y = val x |
| Fully isolated copy (refs cloned too) | var y = clone x |
| Rebind what a variable/ref points to | slot y = ... |
| Function modifies caller’s variable | func f(ref x) or func f(slot x) |
| Function gets a safe copy of data | func f(val x) or func f(clone x) |
ref vs slot
ref creates an alias — reads and writes flow through to the original. slot can rebind the underlying variable entirely, including switching a ref to point to a different target. In assignments, ref goes on the target side (var y = ref x) while slot goes on the storage side (slot y = ...).
val vs clone
Both create deep copies and can be used as assignment qualifiers (var y = val x, var y = clone x) or function parameter qualifiers (func f(val x), func f(clone x)). The difference is how they handle ref: val copies a ref but keeps the same target, while clone duplicates both the ref and its target for true isolation.
Advanced Patterns
Refs through closures
Closures that accept ref parameters work as expected — the ref connects back to the caller’s variable regardless of how many function layers are in between.
func makeIncrementer() { return func(ref x) { x = x + 100 } } var inc = makeIncrementer() var v = 5 inc(v) // v is 105 — ref drilled through the closure
Upvalues and ref parameters
A closure can capture upvalues and accept ref parameters. The ref modifies the caller’s variable while the upvalue maintains its own independent state.
func makeAccumulator() { var callCount = 0 func add(ref x, amount) { x = x + amount callCount = callCount + 1 } func getCalls() { return callCount } return [add, getCalls] } var fns = makeAccumulator() var total = 10 fns[0](total, 5) // total is 15 fns[0](total, 3) // total is 18 fns[1]() // 2 — upvalue tracked independently
Nested function drilling
Refs can be passed through multiple layers of nested function calls. Each layer maintains the connection back to the original variable.
func outer(ref x) { func middle(ref y) { func inner(ref z) { z = z + 1 } inner(y) } middle(x) } var deep = 0 outer(deep) // deep is 1 — ref drilled through three function layers
Slot through function boundaries
A slot parameter lets a function rebind the caller’s variable entirely — including switching a ref to point to a different target, even across closure boundaries.
func redirectRef(slot r, newTarget) { slot r = ref newTarget } var a = 100 var b = 200 var r = ref a redirectRef(r, b) r = 999 // b is 999 (r was rebound to b) // a is still 100