Skip to main content

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:

drop.stofPlayground ↗
#[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());
}
Output

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:

remove.stofPlayground ↗
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());
}
Output

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:

arena.stofPlayground ↗
#[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());
}
Output

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.