Skip to main content

How Stof Works

A Stof document is a graph, not a tree of dead values. Understanding that graph is most of what you need to reason about how Stof behaves.

The Mental Model: A Graph of Nodes

A document is a set of named nodes (objects), connected in a DAG. Each node points to any number of data components — fields, functions, or richer data like images and PDFs. Internally, this is a flat list of nodes and a flat list of data with pointers between them, so moving part of the graph around never means copying it.

Inside a function, three keywords navigate that graph:

server: {
port: 8080
address: "localhost"

fn url() -> str {
`https://${self.address}:${self.port}`
}
}
  • self — the node the function lives on
  • super — its parent node
  • root — the document root

self.address and self.port above are fields on the same node as url(). Nothing is passed in explicitly — the function already knows where it lives.

Data and Logic, Same Document

Because Stof is a strict superset of JSON, this is a valid Stof document as-is:

{
"name": "Stof",
"age": 30,
"active": true
}

Add optional types, drop the punctuation you don't need, and add a function — still the same document, now with behavior inside:

name: "Stof"
age: 30
bool active: true

fn greet() -> str {
`Hello, ${self.name}!`
}

Nothing was migrated. The data didn't move to a new file and the logic didn't move to a new codebase — a function just joined the same document as another data component, addressable the same way a field is.

Sandboxed By Default

A Stof document can only see and manipulate itself. There's no ambient filesystem access, no network access, no reaching outside the graph — unless you explicitly hand it a library that provides that capability. That's what makes it safe to accept a document, and the logic inside it, from somewhere you don't fully trust: an API request, a config pulled from storage, another service entirely.

Optional libraries extend what a document can touch, and code can check for them before relying on one:

// explicit lib checks
if (lib("Http")) {
const res = lib("Http", "fetch") ? await Http.fetch("https://myurl.com/resource") : null;
}

// return null when not present with '?'
const res = await ?Http.fetch("https://myurl.com/resource");

If Http isn't available at the time, the document degrades instead of failing — the same document behaves correctly on a minimal embedded runtime and a fully-loaded server process.

Documents Can Transform Themselves

A running document can parse new fields, types, or functions into itself. This is the mechanism behind everything from live config updates to a document extending its own API at runtime:

index.ts
import { StofDoc } from '@formata/stof';

const doc = await StofDoc.parse({ points: [{ x: '42m', y: '42cm' }, { x: '50ft', y: '60cm' }] });

// simulate a fetch API or database call
doc.lib('Geo', 'types', async (): Promise<string> => `
#[type]
Point: {
float x: 0,
float y: 0,

fn length() -> meters {
Num.sqrt(self.x.pow(2) + self.y.pow(2))
}
}`);

doc.parse(`
fn sum() -> meters {
const arena = new {};
let total: meters = 0;

parse(await ?Geo.types() ?? '', arena);
for (const p: Point in self.points) total += p.length();

drop(arena); // remove types from doc
drop(this); // remove this func from doc
total.round(2)
}`);

let res = await doc.call('sum');
console.log(res, 'meters'); // 70.46 meters

try { res = await doc.call('sum'); }
catch (e) { console.log(e); } // no longer exists

sum parses an additional Stof API into the document for the duration of the call, then removes it and itself from the document before returning.

Where Logic Runs

The same document and the same functions run unmodified across targets: natively via the Rust crate, in a browser or edge function via WebAssembly, or embedded through the Python or TypeScript bindings. There's no host-specific dialect to write around — the sandboxing model is what makes that portability safe rather than just convenient.

Stof also has a native type ver for Semantic Versioning:

import { stofAsync } from '@formata/stof';

const doc = await stofAsync`
#[version(0.1.0)]
fn hello() -> str {
'Hello, World!'
}

fn main() -> bool {
let v: ver = ?self.attributes('hello').get('version') ?? 0.0.1;
v > 0.0.5 ? true : false
}`;

console.log(await doc.call('main')); // true

Round-Trips Back Out

A document built from JSON, extended with types and functions, stays exportable as JSON — or YAML, TOML, or Stof's own binary format — at any point:

console.log(doc.record()); // back out as a plain JS object
const yaml = doc.stringify('yaml'); // back out as YAML (lose funcs)

Add logic, run it, and hand the result to something that has never heard of Stof. Every format added to the runtime provides I/O for every object.