Garbage Collector
StarLang uses a mark-and-sweep garbage collector for automatic memory management.
Managed Types
The GC tracks heap-allocated objects:
| Type | Allocation |
|---|---|
| Array | StarArray struct + items buffer |
| Dict | StarDict struct + keys + values buffers |
| Struct | StarStruct struct + fields buffer |
| Channel | StarChannel struct + ring buffer + pending send values |
| Result | StarResult struct + inner value |
Strings from the constant pool are not GC-managed — they live in the bytecode buffer.
Object Tracking
Every heap object has a header:
typedef struct StarObj {
struct StarObj *next; // linked list
StarObjType type; // OBJ_ARRAY, OBJ_DICT, OBJ_STRUCT, OBJ_CHANNEL, or OBJ_RESULT
bool marked; // GC mark bit
} StarObj;
All objects are linked into a single list (vm->objects). New objects are prepended to the head.
Mark Phase
The collector uses an iterative worklist algorithm to mark reachable objects. This avoids C stack overflow on deeply nested structures — critical for ESP32 where the call stack is small.
Worklist capacity is 256 entries (stack-allocated, no heap cost). The algorithm:
- Scan all GC roots, enqueue unmarked arrays/dicts into the worklist
- Pop objects from the worklist, scan their contents, enqueue any unmarked children
- Repeat until the worklist is empty
If the worklist overflows (>256 objects in flight), the collector sets an overflow flag and runs a follow-up pass: it walks the full object list, re-scanning marked objects for unmarked children. This repeats until no new objects are discovered. Correctness is guaranteed regardless of nesting depth — overflow only costs an extra linear scan.
GC roots:
- Stack —
stack[0..sp-1] - Task stacks — all task stacks
tasks[0..task_count-1].stack[0..sp-1](when multitasking is active) - Globals —
globals[0..global_count-1] - Constants —
constants[0..const_count-1]
Channel objects get special treatment during marking: the collector scans the ring buffer (buffer[0..len-1]) and the pending send values (send_values[0..send_waiter_count-1]) for reachable objects.
Result objects are scanned for their .value field, which may reference a heap-allocated object (string, array, etc.).
Sweep Phase
The collector walks the object list. Unmarked objects are freed. Marked objects have their mark bit reset for the next cycle.
Stop-the-World
GC runs stop-the-world — all execution pauses during collection. On PC this is negligible. On ESP32, use gc.collect() before time-critical sections to control when the pause happens:
Trigger
- GC runs automatically when
bytes_allocated >= next_gc - After each collection:
next_gc = bytes_allocated * grow_factor - On platforms with limited RAM,
max_gccaps the threshold to prevent runaway growth
Platform Defaults
| Platform | Initial Threshold | Grow Factor | Max Threshold |
|---|---|---|---|
| PC | 1 MB | 2.0x | — (no cap) |
| ESP32 | 32 KB | 1.5x | 128 KB |
Without max_gc, the threshold grows unbounded:
With max_gc = 128 KB, GC triggers more frequently instead of running out of memory.
Configuration
The HAL sets platform-appropriate values at startup:
// PC defaults (set by star_vm_init)
vm->next_gc = 1024 * 1024; // 1 MB
vm->gc_grow_factor = 2.0;
vm->max_gc = 0; // no cap
// ESP32 (set by HAL)
star_vm_configure_gc(vm, 32 * 1024, 1.5, 128 * 1024);
Manual Collection
Use gc.collect() to trigger a collection manually:
Currently returns void. A future version may return the number of bytes freed.
Shutdown
star_vm_free() frees all remaining objects when the VM shuts down, regardless of reachability.