Skip to main content

Variables & References

Variables hold data temporarily in the runtime, outside the document itself, while it's being read or transformed. Declare them with let or constconst just means the binding can't be reassigned. A typed declaration casts on assignment, the same way a typed field does:

declare.stof
start: "Hello, "
end: "!!"

#[main]
fn main() {
  let start = self.start;
  const name: str = "John";
  pln(start + name + self.end);
}
Output

Scopes

A new brace-delimited block creates a new scope, the same as most other languages — an inner scope can read outer variables, but disappears (along with anything it declared) once the block ends:

scopes.stof
#[main]
fn main() {
  let outer = 10;

  {
      let inner = 5;
      pln(outer + inner);
  }

  pln(outer);
}
Output

Value Types

Booleans, numbers, strings, versions, and promises are value types — copied automatically whenever they're read into a variable:

value-copy.stof
value: 42

#[main]
fn main() {
  let val = self.value;
  val += 50;
  pln(val, self.value);
}
Output

val and self.value are independent after that copy — changing one doesn't touch the other. The & operator opts a value type into reference semantics instead:

value-ref.stof
value: 42

#[main]
fn main() {
  let val = &self.value;
  val += 50;
  pln(val, self.value);
}
Output

Same code, one character different — but now val is self.value, not a copy of it, so changing one changes both.

Pass by Value vs Reference

The same distinction applies at a function call. Passing a value type as a plain argument hands the function a copy; passing it with & hands it the original:

by-value.stof
fn push_name(name_str: str, name: str) {
  name_str.push(name);
}

#[main]
fn main() {
  let s = "Hello, ";
  self.push_name(s, "John");
  pln(s);
}
Output
by-ref.stof
fn push_name(name_str: str, name: str) {
  name_str.push(name);
}

#[main]
fn main() {
  let s = "Hello, ";
  self.push_name(&s, "John");
  pln(s);
}
Output

Identical function, identical call site — the only difference is the & at the call, and it's the difference between s staying "Hello, " and coming back "Hello, John".

Reference Types

Tuples, maps, sets, lists, and blobs are reference types — copied by reference automatically, with no & needed:

list-ref.stof
value: [1, 2]

#[main]
fn main() {
  self.value.push_back(3);
  self.value.push_back(4);
  pln(self.value);
}
Output

Nodes, data pointers, and function pointers are technically value types, but behave like reference types in practice — they just point into the document's graph rather than holding a copy of it, so passing one around is always cheap. This is the same flat-pointer structure from The Document Graph.

Call by Reference

Indexing supports references too — &list[i] is shorthand for &list.at(i):

index-ref.stof
#[main]
fn main() {
  const list = [1, 1, 1, 1];

  let val = &list[1];
  val += 99;

  let last = &list.back();
  last += 99;

  pln(list);
}
Output

const on list only blocks reassigning the list binding itself — mutating what it points to, through a reference, is unaffected.

Loop by Reference

for...in can bind by reference too, since it's calling at internally on each iteration:

loop-ref.stof
#[main]
fn main() {
  const list = [1, 2, 3];
  for (let val in &list) val += 5;
  pln(list);
}
Output