Memory Management

How Strada handles memory allocation, references, and cleanup.

Key Concept Strada uses automatic reference counting. Memory is freed when no references remain. You rarely need to think about memory, but understanding the model helps write efficient code.

Automatic Reference Counting

Every value in Strada has a reference count. 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" => sys::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"});
    sys::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 = [];
sys::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
sys::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
sys::array_capacity(@arr)Get current allocated capacity
sys::array_reserve(@arr, $n)Ensure capacity for at least $n elements
sys::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
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 (Pattern)

For parent-child relationships, have children reference parents weakly:

package TreeNode;

func new(str $value) scalar {
    my hash %self = {
        "value" => $value,
        "children" => [],
        "parent" => undef  # Don't store ref to parent
    };
    return bless(\%self, "TreeNode");
}

func add_child(scalar $self, scalar $child) void {
    push($self->{"children"}, $child);
    # Don't: $child->{"parent"} = $self;  # Creates cycle!
}

Memory Profiling

Enable memory profiling to track allocations:

sys::memprof_enable();

# ... your code ...

sys::memprof_report();  # Print allocation stats
sys::memprof_reset();   # Clear counters
sys::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