Memory Semantics

Zym gives you explicit control over how values pass between scopes with four keywords: ref, val, slot, and clone.

KeywordMeaningWrites visible to caller?
refCreates an alias (reference) to the original variableYes — all writes go through
valDeep-copies the value, but refs keep the same targetNo for plain values; refs still alias the original target
slotBinds to the caller’s variable slot directlyYes — writes back to the slot
cloneCreates an independent deep copyNo — 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

ref basics
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.

ref flattening
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.

Important: 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.
val in assignment
var original = [1, 2, 3]
var copy = val original   // deep copy via assignment

copy[0] = 999
original[0]              // still 1
val as parameter
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

slot basics
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.

slot rebinding
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.

ref rebinding with slot
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.

clone vs val: The key difference is how they handle 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.

cloning lists
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.

all qualifiers together
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).

assignment syntax
// 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
parameter syntax
// 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
Key distinction: 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

ScenarioKeyword
Create an alias to an existing variablevar 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 toslot y = ...
Function modifies caller’s variablefunc f(ref x) or func f(slot x)
Function gets a safe copy of datafunc 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.

ref through a closure
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.

ref + upvalue
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.

multi-layer ref drilling
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.

slot rebinding through a function
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
Interactions: Memory qualifiers work correctly with tail-call optimization (refs survive frame reuse) and continuations (refs remain valid across capture and resume). See the respective pages for details.