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 const — const just means the binding can't be reassigned. A typed declaration casts on assignment, the same way a typed field does:

declare.stofPlayground ↗
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.stofPlayground ↗
#[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.stofPlayground ↗
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.stofPlayground ↗
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.stofPlayground ↗
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.stofPlayground ↗
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.stofPlayground ↗
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.stofPlayground ↗
#[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.stofPlayground ↗
#[main]
fn main() {
  const list = [1, 2, 3];
  for (let val in &list) val += 5;
  pln(list);
}
Output