4.3. Very safe context

Daslang prioritizes runtime performance and development speed over absolute safety. Outside of unsafe blocks the language enforces safety checks that prevent common programming errors, but certain fundamentally unsafe patterns cannot be eliminated without sacrificing performance.

options very_safe_context mitigates the most dangerous of these patterns.

4.3.1. The problem: aliased references

Some expressions produce multiple references into the same container, where an earlier reference can be invalidated by a later operation. Consider the following example:

options gen2
options very_safe_context

struct Foo {
    a : int
}

def move_data(var a, b : Foo) {
    b <- a
}

def good_index(var a : array<Foo>; index : int) : int {
    if (index >= length(a)) {
        resize(a, index + 1)
    }
    return index
}

[export]
def main() {
    var data : array<Foo>
    data[good_index(data, 5)].a = 42
    move_data(data[good_index(data, 5)], data[good_index(data, 100)])
    print("data = {data}\n")
}

Both data[5] and data[100] must share the same lifetime, but 5 is evaluated before the resize and 100 after it. No order of operations can make this code correct with unboxed containers and pass-by-reference semantics. Equivalent C++ code exhibits the same behavior.

The issue is even more apparent with tables, where the compiler rejects the expression outright (see Tables):

var tab : table<string; int>
let key1 = "one"
let key2 = "two"
tab[key1] <- tab[key2]  // error[30250]: may rehash the table, invalidating the key1 reference

4.3.2. What very_safe_context does

When options very_safe_context is enabled, array and table memory is not freed on resize. Instead, the old buffer is left for the garbage collector to reclaim later. This prevents most crashes and often produces correct results because existing references continue to point at valid — though possibly stale — copies of the data.

Completely eliminating this class of bugs would require either boxing all containers or introducing a borrow checker, neither of which is practical for the language’s performance goals.

Some of these issues can also be caught by linting or optional runtime checks.

The protection has a real cost. Every growth episode abandons its old buffers: a container that doubles its way to N bytes leaves about N more bytes of garbage behind, and nothing can reuse that memory until the next heap_collect. Code that grows containers all the time — a serializer, an audio voice pool — pays this on every run.

4.3.3. Opting out per container: scratch

A trusted container whose interior is never aliased across a grow does not need the protection. Two unsafe primitives turn it off for exactly that container, leaving the rest of the context very safe:

set_scratch(container, true) marks an array or a table once. From then on its growth frees the old buffer eagerly, exactly as in a normal context. The mark survives table rehash, move, and clear; delete resets it. Use it on containers user code can never reach — internal pools and queues — set at the place that constructs them. is_scratch(container) reads the mark back.

var pool : array<Voice>
unsafe(pool |> set_scratch(true))   // internal pool: nothing aliases its interior

scratch_reserve / scratch_ensure_capacity / scratch_resize grow through the eager path once, without marking anything. Use them at a trusted growth site when the container itself is later handed to user code — the handed-off array carries no special state. scratch_reserve reserves the exact size (no power-of-two rounding); scratch_ensure_capacity doubles like ensure_capacity; scratch_resize reserves exactly and then resizes, so a shrink never reallocates.

unsafe(data |> scratch_ensure_capacity(pos + size))   // eager grow of a write buffer
data |> resize_no_init(pos + size)                    // within capacity - never grows

Both primitives require unsafe on purpose: in a sandbox that withholds unsafe from user code, only trusted library and host code can opt a container out.

4.3.4. Iteration and container moves

A related hazard occurs when a container is moved while it is being iterated:

var a <- [1, 2, 3, 4]
var b : array<int>
for (i, x in count(), a) {
    if (i == 0) {
        b <- a
    }
    x++
}

This example throws a runtime error, but only at the exit of the loop. Detecting it earlier would require expensive per-iteration or per-move checks. In the meantime, the loop body operates on a shadow copy of the data.