Memory Management

How Strada handles memory allocation, references, and cleanup.

Key Concept Strada uses automatic reference counting for most values, with a key optimization: small integers are encoded directly in pointers (tagged integers) with zero heap allocation. You rarely need to think about memory, but understanding the model helps write efficient code.

Tagged Integers

Integers that fit in 63 bits are stored directly inside the pointer itself, not on the heap. The low bit of the pointer is set to indicate a tagged integer. This means integer operations — arithmetic, comparisons, loop counters, array indexing — require no memory allocation and no reference counting. This optimization is completely transparent: all runtime functions like strada_to_int() handle both tagged and heap-allocated integers automatically.

Automatic Reference Counting

Non-integer values (strings, arrays, hashes, references) use reference counting. When a value is assigned to a variable or passed to a function, the count increases. When a variable goes out of scope, the count decreases. When the count reaches zero, the memory is freed.

func example() void {
    my str $s = "hello";   # refcount = 1
    my str $t = $s;         # refcount = 2 (shared)

    # When function returns:
    # $t goes out of scope: refcount = 1
    # $s goes out of scope: refcount = 0 -> freed
}

Inspecting Reference Counts

Use refcount() to see how many references exist to a value:

my array @arr = [1, 2, 3];
say(refcount(\@arr));  # 1

my scalar $ref = \@arr;
say(refcount(\@arr));  # 2

Scope and Lifetime

Variables are scoped to the block where they're declared:

func main() int {
    my str $outer = "outer";

    if (1) {
        my str $inner = "inner";
        say($outer);  # OK: outer is visible
        say($inner);  # OK: inner is visible
    }
    # $inner is freed here

    say($outer);  # OK
    # say($inner);  # Error: $inner not in scope

    return 0;
}

Loop Variable Scope

for (my int $i = 0; $i < 10; $i++) {
    # $i exists here
}
# $i is freed here

foreach my str $item (@items) {
    # $item exists here
}
# $item is freed here

References

References allow multiple variables to point to the same data. Creating a reference increases the refcount:

my array @data = [1, 2, 3];
my scalar $ref = \@data;    # Reference to @data

# Both point to the same array
push(@data, 4);
say($ref->[3]);  # 4

# Array freed only when BOTH go out of scope

Anonymous References

Anonymous arrays and hashes are created with refcount 1:

my scalar $arr = [1, 2, 3];       # Anonymous array ref
my scalar $hash = { "a" => 1 };  # Anonymous hash ref

# Freed when $arr and $hash go out of scope

Object Destruction (DESTROY)

When an object's refcount reaches zero, Strada automatically calls its DESTROY method if one exists:

package Connection;

func new(str $host) scalar {
    my hash %self = {
        "host" => $host,
        "socket" => core::socket_client($host, 80)
    };
    return bless(\%self, "Connection");
}

func DESTROY(scalar $self) void {
    # Called automatically when object is freed
    say("Closing connection to " . $self->{"host"});
    core::close_fd($self->{"socket"});
}

func main() int {
    {
        my scalar $conn = Connection::new("example.com");
        # Use connection...
    }
    # DESTROY called here automatically
    return 0;
}
DESTROY Order With inheritance, DESTROY methods are called from child to parent. Use $self->SUPER::DESTROY() to call parent destructors.

Manual Memory Control

Sometimes you need explicit control over when memory is freed:

undef() - Clear a Variable

my str $big_string = slurp("huge_file.txt");
# Process the string...

undef($big_string);  # Free memory now, don't wait for scope end

release() - Free via Reference

my scalar $ref = [1, 2, 3];
release($ref);  # Decrements refcount, $ref becomes undef

Performance Optimization

Pre-allocating Arrays

If you know an array's size in advance, pre-allocate to avoid reallocations:

# Method 1: Declaration with capacity
my array @data[10000];  # Pre-allocate for 10,000 elements

# Method 2: Reserve after creation
my array @items = [];
core::array_reserve(@items, 5000);

# Now push() won't need to reallocate
for (my int $i = 0; $i < 5000; $i++) {
    push(@items, $i);
}

Pre-allocating Hashes

# Declaration with capacity
my hash %cache[1000];  # Pre-allocate buckets for ~1000 keys

# Or set default for all new hashes
core::hash_default_capacity(500);

StringBuilder for String Building

String concatenation creates new strings each time. For building large strings, use StringBuilder:

# Inefficient: O(n²) - creates many intermediate strings
my str $result = "";
for (my int $i = 0; $i < 10000; $i++) {
    $result = $result . "line " . $i . "\n";
}

# Efficient: O(n) - appends to buffer
my scalar $sb = sb::new();
for (my int $i = 0; $i < 10000; $i++) {
    sb::append($sb, "line " . $i . "\n");
}
my str $result = sb::to_string($sb);

Array Capacity Functions

Function Description
core::array_capacity(@arr)Get current allocated capacity
core::array_reserve(@arr, $n)Ensure capacity for at least $n elements
core::array_shrink(@arr)Shrink capacity to match current length

Memory and Closures

Closures capture variables by reference. The captured variables stay alive as long as the closure exists:

func make_counter() scalar {
    my int $count = 0;

    return func () {
        $count++;  # Captures $count by reference
        return $count;
    };
}

my scalar $counter = make_counter();
# $count from make_counter() is kept alive by the closure

say($counter->());  # 1
say($counter->());  # 2

undef($counter);
# Now $count is finally freed

Closures Without Return Statements

Closures (anonymous functions) that don't have an explicit return statement automatically return undef. This is safe and efficient:

# This closure has no return - it's used as a callback
my scalar $callback = func () {
    say("Processing...");
    do_something();
    # No return statement - automatically returns undef
};

# Safe to call - result is undef
$callback->();

# Common pattern: pass closure to methods
$db->transaction(func () {
    $db->execute("UPDATE ...");
    $db->execute("INSERT ...");
});
Watch for Cycles If a closure captures a variable that references the closure itself, you create a cycle that won't be freed automatically. Break cycles manually with undef().

Circular References

Reference counting cannot automatically free circular references:

# This creates a cycle that won't be freed!
my hash %a = ();
my hash %b = ();
$a{"other"} = \%b;
$b{"other"} = \%a;
# Both have refcount 2, won't reach 0

# Fix: Break the cycle before scope exit
delete(%a, "other");
# Now %b refcount = 1, %a refcount = 1
# Both freed when scope exits

Weak References

A weak reference does not keep its target alive. When the target's last strong reference is dropped, the target is freed and all weak references to it become undef. Use core::weaken() to break circular references.

my scalar $parent = { "name" => "parent" };
my scalar $child = { "name" => "child" };
$parent->{"child"} = $child;
$child->{"parent"} = $parent;

core::weaken($child->{"parent"});  # Break the cycle
say(core::isweak($child->{"parent"}));  # 1
say($child->{"parent"}->{"name"});    # "parent" (still accessible)
FunctionDescription
core::weaken($ref)Make $ref a weak reference
core::isweak($ref)Returns 1 if weak, 0 otherwise
Weak Reference Details
  • Works on hash entry values: core::weaken($hash->{"key"})
  • Idempotent: calling core::weaken() on an already-weak ref is a safe no-op
  • Multiple weak references to the same target are supported
  • When the target is freed, dereferencing the weak ref returns undef

Memory Profiling

Enable memory profiling to track allocations:

core::memprof_enable();

# ... your code ...

core::memprof_report();  # Print allocation stats
core::memprof_reset();   # Clear counters
core::memprof_disable();

C Interop Memory

When working with C code, you may need manual memory management:

# Allocate C memory
my scalar $ptr = c::alloc(1024);

# Use the memory...
c::write_int32($ptr, 42);

# Free when done
c::free($ptr);

See C Interoperability for more details on working with C memory.

Best Practices

Memory Tips
  • Let automatic cleanup handle most cases - don't over-optimize
  • Use undef() for large objects you're done with early
  • Pre-allocate arrays/hashes when you know the size
  • Use StringBuilder for building large strings
  • Implement DESTROY for objects that hold external resources
  • Avoid circular references, or break them explicitly
  • Use memory profiling to find leaks in long-running programs