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.