Document Memory Management
Stof doesn't garbage-collect nodes automatically. Every object created programmatically — inside a function, not written directly into the document — sticks around until something explicitly removes it.
drop() and #[dropped]
The global drop(target) function removes an object from the graph — and if that object's prototype has a #[dropped] function (from Prototypes & Schemas), it runs on the way out:
#[type]
Tracked: {
#[dropped]
fn on_drop() {
pln('cleaning up:', self.name());
}
}
#[main]
fn main() {
const temp = new Tracked {};
pln(temp.exists());
drop(temp);
pln(temp.exists());
}Obj.remove() and the shallow Option
self.remove('key') detaches a field, but by default it's shallow — the field is gone, but if its value was an object, that node isn't necessarily cleaned up along with it. shallow = false removes the field and drops the underlying subtree:
holder: {
a: { value: 1 }
b: { value: 2 }
}
#[main]
fn main() {
pln(self.holder.children().len());
self.holder.remove('a', shallow = false);
pln(self.holder.children().len());
}The Arena Pattern
A common problem: objects get created deep inside nested function calls, often from a #[static] prototype function — so where should they live, if you want to clean them all up together later? The answer is to pass a shared object around as a designated parent, and drop that one object when you're done. new X { } on target is what places a created object under a specific parent instead of wherever it was created:
#[type]
Prompt: {
str! text: ''
list! blocks: []
#[static]
fn new(text: str!, blocks: list = [], arena?: obj) -> Prompt {
new Prompt { text, blocks } on arena
}
fn push(prompt: Prompt) {
self.blocks.push_back(prompt);
}
fn out(level: int = 0) -> str {
const out = self.text;
const indent = '';
for (let _ in level + 1) indent.push('\t');
for (const prompt: Prompt in self.blocks) out.push(`\n${indent}${prompt.out(level + 1)}`);
out
}
}
#[main]
fn main() {
const arena = new {};
const top = <Prompt>.new('Title', arena = arena);
const mid = <Prompt>.new('Middle', [
<Prompt>.new('First', arena = arena),
<Prompt>.new('Second', arena = arena),
], arena);
top.push(mid);
const bot = <Prompt>.new('Bottom', [
<Prompt>.new('First', arena = arena),
], arena);
top.push(bot);
pln(top.out());
pln('objects in arena before drop:', arena.children().len());
drop(arena);
pln('arena still exists after drop:', arena.exists());
}Every Prompt created anywhere in this — top-level, nested inside mid's block list, however deep — ends up parented under arena, regardless of which function created it. One drop(arena) at the end is enough to clean up all of them at once.
When This Actually Matters
For a document that gets parsed, run once, and discarded, none of this is usually worth thinking about — the whole graph goes away regardless. It starts to matter once a document sticks around: a long-running process, or one that gets exported as a whole later (the bstf binary format, for instance, captures every object still in the graph). Temporary objects created along the way are exactly what accumulate if nothing's cleaning them up.