# Stof: The Data Runtime for Context that Travels > Stof is a superset of JSON with functions and types built in — portable, lightweight logic that travels with your data and runs anywhere, sandboxed. This file contains all documentation content in a single document following the llmstxt.org standard. ## About These Docs A two-minute read before you dive in — what's where, and how the runnable code blocks work. ## The Four Sections - **Quickstart** *(you're here)* — install the package, get a document running from your terminal, and understand why Stof exists. - **Learn** — fields, types, functions, the document graph, and so on. This is the reference for how the language actually works. - **Libraries** — what's built into the runtime. - **Cookbook** — real scenarios, patterns, and use-cases. If you're new here, Quickstart → Learn is the natural path. Libraries and Cookbook are both reference material you'll come back to, not things to read start-to-finish. ## Two Kinds of Runnable Code You'll see code in two different shapes across this site, and they're demonstrating two different things on purpose. **Terminal examples** — plain code blocks with a `bash` or `typescript` label, like the ones in Quick Start. Copy these into a real file and run them with Bun, Deno, Node, etc. **Live examples** — code blocks with a **Run** button, like this one: str { \`Hello, \${name}!\` } #[main] fn main() { pln(self.greet()); } `} /> These run Stof entirely in your browser, in the same WASM runtime that powers the language — no server, nothing installed. Click **Run**, and the **Output** panel expands below with whatever the document prints. ## The `#[main]` Convention Every live playground example in these docs follows one shape: ```rust // fields, types, functions — whatever the example is about #[main] fn main() { pln(/* whatever you want to see in Output */); } ``` `#[main]` attribute in Stof marks all functions that the Run button calls. `pln(...)` is actually a Stof standard library `Std.pln(...)` function, overridden in this JavaScript context to write to a string — anything passed to it becomes a line in the Output panel. Occasionally you'll see a playground print a function's return value directly instead, without a visible `#[main]` — those are demonstrating a single function in isolation, and the panel shows whatever that function returned. The `#[main]` + `pln(...)` shape above is the default you should expect everywhere else. ## Why Quick Start Looks Different The TypeScript snippets earlier in this section don't use `#[main]` — they use `doc.call('hello')` from the host application instead. That's not an inconsistency; it's the other half of the picture. `#[main]` is how a *document* declares its own entry point when something (a playground, a CLI, another service) just wants to run it. `doc.call(name)` is how a *host* reaches in and runs one specific function on its own terms. You'll use both, depending on which side of the wire you're on. Everything from here through Learn, Libraries, and Cookbook uses the live-playground pattern. Nothing else to set up — go ahead and start with [Quick Start](./quickstart). --- ## 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: ```rust 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: ```json { "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: ```rust 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: ```rust // 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: ```typescript title="index.ts" 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 => ` #[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: ```typescript 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: ```typescript 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. --- ## Install Every tab below runs the same document — pick whichever host fits what you're building. If you'd rather not install anything yet, every concept page from here on has a **Run** button built right into the page; nothing to set up. ```rust title="hello.stof" str message: "Hello, Stof!" fn greet() -> str { `${self.message} Data and logic, together.` } #[main] fn main() { pln(self.greet()); } ``` --- **Install:** ```bash npm i @formata/stof ``` *[@formata/stof on npm →](https://www.npmjs.com/package/@formata/stof)* **Run:** ```typescript title="index.ts" const doc = await stofAsync` message: "Hello, Stof!" fn greet() -> str { \`\${self.message} Data and logic, together.\` } #[main] fn main() { pln(self.greet()); } `; // Bridge Stof's pln(...) to your host's console doc.lib('Std', 'pln', (...args: unknown[]) => console.log(...args)); await doc.run(); ``` ``` Hello, Stof! Data and logic, together. ``` ### Initializing in a Browser or Bundler `stofAsync` above works with zero setup in Node, Deno, and Bun — Stof's WASM binary loads automatically. A bundler processes imports differently, so that auto-detection doesn't always apply — pick whichever pattern matches your setup: ```typescript title="Vite" await initStof(stofWasm); ``` ```typescript title="Other bundlers (webpack, Rollup, etc.)" await initStof(await stofWasm()); ``` :::note Webpack specifically doesn't handle a raw `.wasm` import out of the box — if that import throws a parse error, add a rule telling webpack to treat `.wasm` files as a binary asset instead of trying to parse them as JavaScript: ```js module: { rules: [{ test: /\.wasm$/, type: 'asset/resource' }] } ``` ::: Call `initStof()` once, before creating any document. After that, the lower-level `StofDoc` constructor works the same way `stofAsync` and `StofDoc.parse(...)` do elsewhere on this page — those two just handle this step for you automatically in Node, Deno, and Bun: ```typescript await initStof(); const doc = new StofDoc(); doc.parse(` name: "Alice" age: 30 fn greet() -> str { 'Hello, ' + self.name } `); console.log(await doc.call('greet')); // "Hello, Alice" console.log(doc.get('age')); // 30 ``` **Install:** ```bash pip install stof ``` *[stof on PyPI →](https://pypi.org/project/stof/)* **Run:** ```python title="main.py" from pystof import Doc doc = Doc() doc.parse(""" str message: "Hello, Stof!" fn greet() -> str { `${self.message} Data and logic, together.` } #[main] fn main() { pln(self.greet()); } """) doc.run() ``` ``` Hello, Stof! Data and logic, together. ``` Add to `Cargo.toml`: ```toml [dependencies] stof = "0.9.*" ``` *[stof on crates.io →](https://crates.io/crates/stof)* **Run:** ```rust title="main.rs" use stof::model::Graph; fn main() { let mut graph = Graph::default(); graph.parse_stof_src(r#" str message: "Hello, Stof!" fn greet() -> str { `${self.message} Data and logic, together.` } #[main] fn main() { pln(self.greet()); } "#, None).unwrap(); graph.run(None, true).unwrap(); } ``` ``` Hello, Stof! Data and logic, together. ``` **Install** via Cargo: ```bash cargo install stof-cli ``` *[stof-cli on crates.io →](https://crates.io/crates/stof-cli)* :::tip Don't have Rust? [Install it here](https://doc.rust-lang.org/book/ch01-01-installation.html) — Cargo comes with it. ::: **Run:** ```bash stof run hello.stof ``` ``` Hello, Stof! Data and logic, together. ``` :::tip Using VS Code? The [Stof extension](https://marketplace.visualstudio.com/items?itemName=Formata.stof) adds syntax highlighting for `.stof` files — search "Stof" in the extensions tab, or install it directly from the marketplace link. ::: --- ## Stof Mission Every interchange format we have — JSON, YAML, TOML, XML — describes a snapshot. The moment it crosses a wire, it's inert again on the other side, and whatever system receives it has to already know, out-of-band, what to do with it. The logic that gives the data meaning lives somewhere else entirely: a separate codebase, a separate language, a separate deploy. That gap is where schema drift, versioning hell, and "the API changed and nobody told the client" all come from. **Stof exists to close that gap.** ## What Stof Is Stof is a data runtime — not a data format, not a programming language, not a database. The same way a JavaScript runtime executes JavaScript, a data runtime executes data. A Stof document doesn't just describe something; it validates itself, transforms itself, and acts, wherever the runtime lands. Concretely: valid JSON is already valid Stof. You're not migrating to a new format — you're handing your existing data a place to keep the logic that used to live somewhere else. ## Principles These are the opinions the runtime is built around. They're not aspirational — they're constraints we design against. **Data and logic belong together.** Splitting them across a data file and a separate codebase is where the gap comes from. Stof documents carry both, as one unit. **Portable means portable.** The same document runs native, in WebAssembly, or embedded in another host, with the same behavior every time. If it doesn't run everywhere, it isn't portable — it's just convenient sometimes. **Sandboxed by default.** A document can only see and manipulate itself unless you explicitly hand it more. Logic that arrives over the wire has to be safe to run without a review cycle first. **Documents should be able to grow.** A running document parsing new fields, new types, even new functions into itself isn't an edge case — it's the model working as intended. Data that can only be replaced, never extended, is still just a snapshot. **Interop with all data formats.** A superset of JSON that is compatible with every other data format, even ones that don't exist yet — binary, images, PDFs, DOCX, YAML, TOML, JSON, etc. ## Where This Came From Stof comes out of a decade spent building parametric and geometry formats for CAD and graphics systems — the kind of work where a document's structure and its behavior have to travel together and gets quite complex. That's where the core architectural insights come from: represent everything as a flat graph of nodes and data components, connected by pointers rather than nested copies, and moving or transforming part of the document stops being expensive. Stof started as a solo project while trying to send wasm over the wire, and it's been running in production since — including as the policy engine underneath [Limitr](https://limitr.dev), where every plan, credit limit, and validation rule is a live Stof document. ## Where This Is Going Everything above — the graph, the sandboxing, the portability, the self-transformation — is domain-agnostic. None of it was built for one use case. What changes across contexts is only what the document is asked to describe. ### Security A scoped auth context modeled as a document instead of a token you have to look up elsewhere: something that can grant, check, and revoke its own permissions, expand temporarily for a single operation, and expire on its own — without a separate service tracking its lifecycle. ### Graphics A scene graph is already Stof's native shape. Geometry, materials, and animation curves as typed fields and functions on the nodes they describe — geometry defined by the function that generates it, not just the triangles it resolves to at one point in time — instead of a mesh format with behavior bolted on by reference. ### AI Contexts Portable memory for an agent: what it knows and what it's allowed to do, traveling together, handed off between services instead of reconstructed at each one. A tool surface a document can extend on itself at runtime. The further-out version — documents that discover and wire themselves into each other, forming a network without a central coordinator — is the reason the sandboxing model exists in the first place. ### Distributed Systems Configuration and coordination state that carries the logic that validates it. A service reading a config another service wrote doesn't have to trust it blindly — the document can check itself. ### Plugins A plugin as a sandboxed document instead of a compiled binary or an interpreted script with full host access: new behavior shipped into a running system as data, without a new deploy and without widening what that behavior is allowed to touch. ### Data Unification One graph that JSON, YAML, TOML, and binary sources all resolve into, so two services stop maintaining a translation layer just to agree on "the format we use" versus "the format they use." ### Datasets Evaluation logic, metadata, and transformation code living inside the dataset rather than in a notebook next to it — a dataset that can score its own outputs or validate its own rows. ### Smart Configs A config that enforces its own invariants: a value that can't be set outside its valid range, a field that recomputes a dependent value automatically — without a schema file living in a different repo that someone forgets to update. --- This list isn't exhaustive, and it isn't a roadmap with dates attached. It's what falls out naturally from building one thing well: a document that carries its own data, its own logic, and its own boundaries, and runs the same way everywhere it lands. --- ## Quick Start --- This guide introduces the concept of a data runtime and shows you how to get started using TypeScript in under 5 minutes. :::note[Prerequisites] - [Bun](https://bun.sh) installed (`curl -fsSL https://bun.sh/install | bash`) - Node.js also works - replace `bun run` with `npx tsx` throughout ::: :::note[Targeting a browser instead?] This guide runs in Bun/Node, where Stof's WASM loads automatically. Building for a browser or a bundler like Vite or webpack instead? See [Initializing in a Browser or Bundler](./install#initializing-in-a-browser-or-bundler) on the Install page first. ::: ## Install ```bash mkdir stof-quickstart && cd stof-quickstart bun init -y bun add @formata/stof ``` Or, NPM works just fine: ```bash npm i @formata/stof ``` ## Hello, World! (JSON-style) Create `index.ts`: ```typescript title="index.ts" const doc = await stofAsync` { "name": "Stof" "hello": ():str => \`Hello, \${self.name}!\` }`; console.log(await doc.call('hello')); ``` ```bash bun run index.ts ``` ``` Hello, Stof! ``` ## Hello, World! Functions are data components in Stof, just like fields! This index.ts is the same as the one above. ```typescript title="index.ts" const doc = await stofAsync` name: "Stof" fn hello() -> str { \`Hello, \${self.name}!\` }`; console.log(await doc.call('hello')); ``` ```bash bun run index.ts ``` ``` Hello, Stof! ``` ## Data Runtime Modern data contexts are dynamic and travel. A data runtime gives you a living interchange layer that stays sandboxed & reliable between services. ```typescript title="index.ts" // add data however its already defined (JSON, YAML, TOML, Stof, Binary, etc.) const doc = await StofDoc.parse({ name: 'John Doe', email: 'john@example.com', age: 42 }); // sandboxed, can only see & manipulate itself const api = ` fn set_height(height?: cm) { height = height ?? (6ft + 2in) as cm; self.height = height.round(2); } fn first_name() -> str { let names = self.name.split(' '); const first = names[0]; self.first = first; first }`; doc.parse(api); await doc.call('set_height'); await doc.call('first_name'); console.log(doc.record()); // back out as a JS object ``` ```bash bun run index.ts ``` ``` { name: "John Doe", email: "john@example.com", age: 42, height: 187.96, first: "John", } ``` --- ## Async There's no separate "async Stof" and "sync Stof" — the runtime is async by default, single-threaded, and switches between running work at the instruction level rather than only at explicit `await` points. That last part matters more than it sounds like it should; the proof-of-concurrency example further down only makes sense because of it. ## The Model: Processes, Not Threads Stof calls a unit of async work a **process**, not a thread or a task — each one is running, waiting, sleeping, done, or errored, and the runtime makes progress on all of them by switching between them on a single thread. Every `#[main]` and `#[test]` function automatically becomes its own process when it runs. ## `async` and `#[async]` Are the Same Thing One's a keyword that adds the attribute at parse-time, one's an attribute — they mark a function identically: str { 'hello, async' } #[async] fn this_is_also_async() -> str { 'async is actually just an attribute' } #[main] fn main() { pln(await self.this_is_async()); pln(await self.this_is_also_async()); } `} /> ## Any Function Can `await` `await` isn't restricted to `async`-marked functions — and on a value that isn't a promise, it's just a passthrough: ) -> str { await param } #[main] fn main() { pln(await 'hello'); pln(await 42); pln(await self.takes_promise_param('regular value')); } `} /> ## Async Expressions: Spawning a Process Putting `async` in front of any call spawns it as its own process and hands you back a promise, without needing the callee itself to be declared `async`: int { a + b } #[main] fn main() { const promise = async self.slow_add(40, 2); pln(await promise); } `} /> ## Async Blocks An `async { }` block used as an expression spawns a process and returns a promise for whatever the block's last line evaluates to: ); pln(typeof res, res); } `} /> ## Fire-and-Forget Async Blocks The same `async { }` block, used as a statement rather than assigned anywhere, still spawns a process — just without a handle to await it: `main()` doesn't wait for that block — it just keeps going. Since both are running as independent processes on the same thread, which line prints first isn't guaranteed; that's not a bug in the example, it's the actual behavior. ## Awaiting Multiple Processes `await` works on a list of handles directly, resolving to a list of results — useful on its own, or built up from a loop when you just need everything to finish: str { 'hello, async' } async fn fn_b() -> str { 'another async function' } #[main] fn main() { const results = await [self.fn_a(), self.fn_b()]; pln(results); } `} /> ## Proof: True Concurrency Because scheduling happens at the instruction level, two processes with no `await` inside either of them still make progress at roughly the same rate, sharing the single thread rather than running one fully before the other starts: ms { for (let i = 0; i < n; i += 1) {} Time.diff(now) } #[main] fn main() { const now = Time.now(); const time_to_count = self.counting_up(now, 10_000); const loop_time = async { let count = 0; loop { count += 1; if (count >= 10_000) break; } Time.diff(now) }; pln(await [time_to_count, loop_time]); } `} /> Both numbers should come back close to each other — if the runtime ran one loop to completion before starting the other, they'd look nothing alike. Note: the vm does optimize for when process switching happens, and for additional reasons will make progress on a series of statements at once until a block boundary is hit - the above example works because of the loops. ## Promise Types `Promise` is an optional, explicit way to annotate a return type — most code never needs to write it, since a promise matches its inner type. You can also cast a promise to change what it resolves to, and a function returning a promise works anywhere a plain `fn` is expected: int { pointer() } #[main] fn main() { let promise = (async { return '100'; }) as Promise; promise = promise as int; pln(await promise); const res = self.takes_fn(async (): int => 42); pln(await res); } `} /> --- ## Attributes `#[name(value)]` attaches metadata to a field, function, or object. Unlike a comment or a compiler-only annotation, an attribute is real data — every one you write is queryable at runtime as a `name -> value` map, and the value can be anything: a string, a number, a map, even a function. ## Attributes You've Already Used A few show up before you'd think to call them "attributes" at all: - **`#[main]`** — marks a function to run when the document runs - **`#[readonly]`** / **`#[private]`** — the field access modifiers from [Fields](./fields) - **`#[type]`** — marks an object as a formal prototype, covered later in Prototypes & Schemas - **`#[async]`** — marks a function as async, covered later in Async Each of those is really just Stof's parser watching for a specific attribute name and reacting to it — there's nothing more privileged about `#[main]` than an attribute you make up yourself. ## Querying Attributes at Runtime `this.attributes()` returns the current function's own attributes; `self.attributes('name')` returns a named field's: Both `#[main]` and `#[purpose(...)]` on `main()` show up in `this.attributes()` — the attribute you're using to mark something runnable is visible right alongside any attribute you made up yourself. ## Custom Attributes as Metadata Because an attribute's value can be anything, this becomes a real tool: attach a validation rule, a range, a label, whatever a field needs — as data, next to the field, instead of in a separate schema file that has to stay in sync: bool { const attrs = self.attributes(field); const range = attrs.get("range"); const val = self.get(field); val >= range[0] && val <= range[1] } #[main] fn main() { pln(self.checkRange("percent")); } `} /> This is a small, hand-rolled version of a real pattern — `#[schema(...)]` is the built-in attribute for exactly this, and Prototypes & Schemas covers it properly. Objects take attributes the same way fields and functions do, so this isn't limited to scalar values. --- ## Collections Types & Units covered how to declare a `list`, `map`, `set`, or `tuple`. This page covers what you actually do with one once it exists. ## Lists Push and pop from either end, index directly, or use `front()`/`back()`: Searching and sorting both come built in — `sort_by` takes a comparator that returns `-1`, `0`, or `1`: { if (a > b) -1 else if (a < b) 1 else 0 }); pln(scores); } `} /> That comparator flips the usual order — returning `-1` when `a > b` sorts largest-first instead of the default ascending. ## Maps `insert`, `get`, `remove`, and `contains` cover most of it. Iterating a map gives you `(key, value)` tuples: ## Sets Same shape as a list, but every value is unique — `insert` reports whether the value was actually new: Sets also carry the algebra you'd expect — `union`, `intersection`, `difference`, and a few others: ## Tuples Tuples are simpler by design — fixed length, indexed access, nothing to insert or remove: [Control Flow](./control-flow) already covered iterating any of these with `for...in`, including the implicit `index`/`first`/`last` variables — that part doesn't change based on which collection you're looping over. --- ## Control Flow Stof's control flow will look familiar — the differences worth knowing are which of these are statements and which can be used as expressions. ## `if` / `else` Braces are optional for a single-statement branch: 10) pln("big"); else if (value > 5) pln("medium"); else pln("small"); } `} /> `if` is a statement, not an expression — it can't be the right-hand side of an assignment. For an inline conditional value, use the ternary operator instead, covered in [Null & Initialization](./null-and-init). ## `switch` No implicit fallthrough between cases — an empty case falls through to the next one, which is how you group multiple values into one branch. Unlike `if`, `switch` **can** be used as an expression: ## `while` and `loop` `loop` is shorthand for `while (true)` — expected to exit via `break`: 0) { count -= 1; } pln(count); let tries = 0; loop { tries += 1; if (tries >= 3) break; } pln(tries); } `} /> ## `for` The classic C-style form works, and so does `for...in` — over a number (counts from `0`), a list, or anything with `len()` and `at(index)` methods: `for...in` also gives you three implicit variables inside the loop body — `index`, `first`, and `last`: There's also a compact range literal for building a list directly — `0..10|2` (start..end|step) produces `[0, 2, 4, 6, 8]`. ## `break`, `continue` & Tagged Loops Both work in every loop type. Prefix a loop with `^label` to `break`/`continue` an *outer* loop from inside a nested one: 6) break; sum += i; } pln(sum); let found = -1; ^outer for (let i = 0; i < 5; i += 1) { for (let j = 0; j < 5; j += 1) { if (i == 2 && j == 2) { found = i * 10 + j; break ^outer; } } } pln(found); } `} /> Without `^outer`, `break` on its own would only exit the inner `for` loop, and the outer loop would keep going. ## `try` / `catch` `throw(value)` throws any value, not just strings — `catch (name: type)` catches a specific type; a bare `catch` catches anything. Like `switch`, `try`/`catch` can be used as an expression: int { try 42 catch 72 } #[main] fn main() { try { self.risky(true); } catch (msg: str) { pln('caught:', msg); } pln(self.attempt()); } `} /> --- ## The Document Graph Every other concept in Stof sits on top of one structure: a document is a graph of named **nodes**, and each node holds any number of **data components** — fields, functions, or richer data like images and PDFs. Get comfortable with this shape and the rest of the language stops feeling like syntax to memorize and starts feeling like consequences of one idea. ## Nodes and Data Components A **node** is what most languages would call an object. It doesn't hold values directly — it holds pointers to **data components**, which are the actual fields and functions. This distinction matters because it's what makes the graph flat: internally, a document is one list of nodes and one list of data, connected by pointers, not a tree of nested copies. Moving a node, or handing a function a reference to one, never copies the subtree underneath it. ## Creating Child Nodes The simplest way to create a child node is to write one — a nested object literal as a field's value becomes a child node automatically: str { \`http://\${self.address}:\${self.port}\` } } #[main] fn main() { pln(self.server.url()); } `} /> `server` is a real node here, not a nested map — it has its own path, its own identity, and its own `self` inside `url()`. From inside a function, you can also create nodes programmatically: `new {}` for an anonymous node, or `new {} on target` to place it under a specific parent instead of the current one. ## Navigating: `self`, `super`, `root` Three keywords move you through the graph from inside a function: - **`self`** — the node the function lives on - **`super`** — its parent node - **`root`** — the document root, regardless of how deep you are str { \`\${self.name}, part of \${super.name}\` } } #[main] fn main() { pln(self.team.whoami()); } `} /> Inside `whoami()`, `self` is the `team` node — so `self.name` is `"Runtime"`. `super` is `team`'s parent, the document root — so `super.name` is `"Stof"`. Neither of those required knowing the document's shape in advance; they're relative to wherever the function happens to be called from. ## Absolute Paths `self` & `super` are relative — they depend on where a function lives. Sometimes you want to reach a specific node regardless of who's calling: an absolute dot-path, starting from a document root (`root` is the main doc root name, but there can be many), does that. `addItem` doesn't need to live anywhere near `MyType` to reach it — the absolute path works the same regardless of where the function was called from. This is the same mechanism the prototype `` shorthand uses under the hood — it's really just a path shortcut to a type's definition node, covered later in Prototypes & Schemas. ## Inspecting the Graph at Runtime Because the graph is real data, not a hidden implementation detail, a document can ask questions about or manipulate its own shape at runtime: `name()`, `path()`, `id()`, `parent()`, `root()`, `is_root()`, `children()`, `is_parent(other)`, and others are all `obj` type built-ins on every node — useful for anything that needs to reason about the document generically, like a validator that walks the whole graph rather than one written for a specific shape. ## Moving Nodes & Fields Because the graph is flat pointers rather than nested copies, reparenting a node is cheap — `obj.move(new_parent)` updates the pointer, nothing underneath it gets copied or rewritten: `task` and everything nested inside it move in one pointer update, regardless of how large that subtree is — the same property that makes the graph cheap to navigate makes it cheap to reshape. --- ## Error Handling Control Flow introduced the mechanics of `try`/`catch`. This page goes further into what you can actually throw, how errors move up the call stack, and when reaching for `try`/`catch` is the wrong tool. ## Throwing More Than Strings `throw(value)` accepts any Stof value — `catch (name: type)` tries to cast the caught value to match, and a union or `unknown` catch type accepts more than one shape: ## Throwing a Handler Because a function is just another value, you can throw one — letting the `catch` site decide how to recover, rather than hardcoding that logic at the `throw`: { pln("recovering via a custom handler"); }; try { throw(func); } catch (handler: fn) { handler(); } } `} /> ## Propagation An uncaught `throw` bubbles up through however many function calls are on the stack, until something catches it — or nothing does, and the document run fails: Neither `inner()` nor `middle()` has a `try`/`catch` of its own — the error just keeps propagating until `main()`'s does. ## Rethrowing Catch, adjust, and throw again — the outer `catch` gets whatever the inner one threw: --- ## Fields Fields are how a node stores data. Because Stof is a JSON superset, plain JSON key-value pairs are already valid fields — everything below is what you can layer on top of that when you want more control. ## Declaration Styles JSON-style, shorthand, and explicitly typed declarations are all the same thing under the hood: ## Types & Casting A typed field keeps its type — assigning a new value casts it, rather than replacing the type: `42` becomes the string `"42"` on assignment, because `field` was declared `str`. ## Const Fields `const` throws on write instead of silently accepting it: ## Access Modifiers `#[readonly]` allows reads from anywhere but throws on write. `#[private]` restricts both — visible only to the object that defines it, not children, not parents: str { self.secret } } #[main] fn main() { pln(self.Inner.revealed()); pln(self.Inner.secret); } `} /> `revealed()` runs on `Inner` itself, so it can see `secret`. `main()` is calling in from outside `Inner`, so the second `pln` prints `null`. ## Non-Null Fields A `!` suffix on a type makes a field reject `null`; without one, any typed field can still hold `null`: Assigning `null` to `required` would throw. `Null & Initialization` covers the rest of how nullability works, including `??` and null-safe access. ## Union & Tuple Fields A field can accept more than one type. Assigning a value outside the union casts it to the first matching type: `'not in union'` isn't a `bool` or an `int`, so it gets cast to the first matching type — `bool` — rather than rejected. ## Creating Fields Dynamically Assigning to a path creates whatever doesn't exist yet along the way — fields, nodes/objects, even new roots: `NewSpace` didn't exist before this ran — assigning `root.NewSpace.nested` created both the node and the field in one step. --- ## Formats Every format below is a string identifier — `parse(data, target, 'json')`, `stringify('yaml', target)`, `import toml './file'`. Which ones are actually loaded depends on the host; check with `format('name')` from Std before relying on one. If a file's format isn't loaded at all, `import` doesn't throw — it falls back to raw `bytes` instead. ## Data Formats: `json`, `yaml`, `toml` All three behave identically — plain fields in, plain fields out, no functions or types survive the round-trip: ```stof const object = new { a: 'hello', b: 42, c: true }; stringify('json', object); // {"a":"hello","b":42,"c":true} stringify('yaml', object); // a: hello\nb: 42\nc: true\n stringify('toml', object); // a = "hello"\nb = 42\nc = true\n ``` ## Stof's Own Formats: `stof`, `bstf` The only two formats that round-trip *everything* — functions, prototypes, schemas, attributes — not just plain data. `stof` is the text form (your source files use it by default); `bstf` is the binary equivalent, more compact, same fidelity. ```stof const bytes = blobify('bstf', self); // bytes now contains this entire object — data, functions, types, all of it ``` ## Text & Bytes: `text`, `bytes` Both wrap raw content into a single field, rather than trying to interpret structure — `text` produces a `str` field named `text`; `bytes` produces a `blob` field named `bytes`, with automatic UTF-8 conversion if the source was a string. ```stof parse('hello, there', target, 'text'); // target.text == 'hello, there' parse('hello, there', target, 'bytes'); // target.bytes == ``` ## Web Format: `urlencoded` Also available under the alias `www-form`. Nested objects use bracket notation, the same convention browsers use for form submissions: ```stof stringify('urlencoded', new { sub: new { val: 42 }, msg: 'hi' }); // sub%5Bval%5D=42&msg=hi ``` ## Rich Documents: `pdf`, `image`, `docx`, `md` Each of these needs its corresponding library loaded (`Pdf`, `Image`, `Md` all have their own reference pages; `docx` doesn't have a dedicated library beyond `.text()` extraction). Importing a file in one of these formats lands on a specific, fixed field name — not the filename: | Format | Field name | Import example | |---|---|---| | `pdf` | `pdf` | `import './report.pdf'` → `self.pdf` | | `image` | `image` | `import './photo.png'` → `self.image` | | `docx` | *(as named)* | `import './doc.docx' as self.Doc` → `self.Doc.text()` | | `md` | `md` | `import './notes.md'` → `self.md` | ## Packages: `pkg` Covered in depth on [Import & Export](./import-and-export) — reads an `import` field from a `pkg.stof` manifest and pulls in whatever paths it lists, paired with the `@` path shortcut for `stof/`. ## When a Format Isn't Loaded This is worth knowing explicitly: importing a file whose format isn't available doesn't fail the whole parse. It falls back to loading the raw content as `bytes` instead, on the assumption that raw access to *something* is more useful than an error — a document built without the `Pdf` library still gets the PDF's bytes, just not `.extract_text()`. --- ## Functions Functions are data components, exactly like fields — attached to a node, reachable by dot-path, and first-class values you can store, pass around, and call from anywhere with access to them. ## Declaring & Returning The last expression in a function, without a trailing `;`, is its return value — `return` also works, for an early exit: float { a + b } fn greet(name: str = "world") -> str { return \`Hello, \${name}!\`; } #[main] fn main() { pln(self.add(2, 3)); pln(self.greet()); pln(self.greet("Stof")); } `} /> One gotcha worth knowing: a semicolon after that last expression changes its meaning. `fn f() -> str { 42; }` doesn't return `42` — the `;` turns it into a statement, so the function returns nothing, and a declared `-> str` return type with nothing to return is an error. Drop the trailing `;` on whatever line should be the return value or use a `return` statement. ## Optional & Named Parameters A `?` suffix on a parameter's type makes it optional; calling with `name = value` passes arguments by name instead of position, in any order: int { a + b } #[main] fn main() { self.create('abc123'); self.create('abc123', 5000ms); pln(self.total(b = 30, a = 12)); } `} /> ## Arrow Functions Shorthand for a function as an expression — as a local variable, or directly as a field's value: x * 2 #[main] fn main() { const add = (a: int, b: int): int => a + b; pln(add(5, 5)); pln(self.double(21)); } `} /> ## Functions as Fields A field can hold a function directly — call it the same way you'd call any other function: \`Hi, \${name}\` #[main] fn main() { pln(self.message()); pln(self.message("Ada")); } `} /> `message`'s default parameter reads `self.name` at call time — so calling it with no arguments still resolves against whatever `self` is at that point. ## Recursion with `this` `this` refers to the function currently running — useful for recursion without hardcoding the function's own name: int { if (n <= 1) { n } else { this.call(n - 1) + this(n - 2) } } #[main] fn main() { pln(self.fibonacci(10)); } `} /> ## What's Not Here Yet Functions can also carry custom attributes (`#[my_attr]`), run asynchronously (`async fn`, `#[async]`), and be called on a prototype directly for static-like behavior via `.func()` — each of those gets its own dedicated page. --- ## Import & Export Stof can read and write any format it has an implementation for — JSON, YAML, TOML, and Stof itself out of the box, with richer formats like PDF or images available as libraries. Two of the mechanisms below run entirely inside a document and work fine in this browser sandbox; the file-based `import` statement needs a real filesystem, so those examples further down are reference material, not runnable. ## Exporting: `stringify` and `blobify` `stringify(format, target)` exports to a string; `blobify(format, target)` does the same as raw bytes — every format that supports stringify supports blobify, but it's up to the format implementation whether to support the other way around: ## Parsing at Runtime `parse(source, location, format)` is the runtime counterpart to the `import` statement below — and because it just takes a string, it works fine sandboxed. This is the same self-modification idea from How Stof Works, made concrete: a document handing itself new logic while it runs. Worth being precise about what's actually sandboxed here: `parse` can add fields and functions to the document, but it can't reach outside it — no filesystem, no network, unless the host explicitly hands the document a library that provides one. A string full of Stof arriving from an untrusted source is safe to parse and run for exactly that reason. ## The `import` Statement ``` ``` The plain form, `import "file"`, assumes a `.stof` extension and parses into the current context (`self`): ```stof Object: { // Object.md now holds the string contents of docs.md fn markdown() -> str { self.md } } ``` ## Relative Paths & Format Selection Paths are relative to wherever the CLI or host was invoked from, though a leading `./` lets nested imports navigate correctly as they parse. The format is inferred from the file extension unless you specify one explicitly: ```stof // format inferred from extension: // format forced explicitly — reads file.stof as plain text, // not as Stof source: ``` ## Import Location (`as`) Without `as`, an import lands on the calling object. `as` lets you redirect it — but the path after `as` is always absolute, so it's easy to accidentally create a new root instead of a child of `self`: ```stof // creates self.Object if needed: // creates a NEW DOCUMENT ROOT called Object — not the same thing: ``` ## Packages: the `@` Shortcut Each `@` in an import path expands to `stof/` — a lightweight convention for package-style imports, paired with the `pkg` format for importing a whole directory via its `pkg.stof` manifest: ```stof // equivalent to: // pkg format reads an "import" field from stof/formata/pkg.stof // and pulls in whatever paths it lists: ``` --- ## 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: ## `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: ## 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: 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 = .new('Title', arena = arena); const mid = .new('Middle', [ .new('First', arena = arena), .new('Second', arena = arena), ], arena); top.push(mid); const bot = .new('Bottom', [ .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()); } `} /> 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. --- ## Null & Initialization Stof has `null`, but no separate "undefined" — a field that doesn't exist and a field explicitly set to `null` look the same from the outside. That keeps the model simple, at the cost of losing that particular distinction some languages make. ## Not-Null Types A `!` suffix on a type makes a field, variable, parameter, or return type reject `null` outright — Fields touched on this briefly; here's what the rejection actually looks like: ## Null Checks The usual tools work as expected — `== null`, and any falsy check via `!value`: `??` is shorthand for "use the left side unless it's null, then use the right" — `lhs ?? rhs`. ## Field Paths Are Always Null-Safe Reading a field by path never throws, even if part of the path doesn't exist — it just returns `null`, which makes `??` chains a natural fit for defaults: None of `field`, `another`, or `other` exist on this document — the chain still resolves to `"default"` instead of throwing. ## The `?` Prefix Operator Field paths are always safe, but calling a *function* that might not exist is a real Stof-specific gap — that's what the `?` prefix checks, across an entire chain at once, however many calls are in it. (`?.` also exists, for a single hop, but in practice `?` covering the whole chain is almost always what you want.) unknown { self.get(query) } } fn subobj() -> obj { self.sub } #[main] fn main() { // a library function that doesn't exist pln(?Std.dne()); // an object/function that doesn't exist pln(?self.dne.myfunc()); // missing partway through a longer chain pln(?self.subobj().object.dne()); // doesn't interfere with a chain that succeeds pln(?self.subobj()["object"]); // pairs naturally with ?? for a default pln(?self.subobj()["dne"].woops().dude() ?? "default"); } `} /> ## Ternary Operator `condition ? if_true : if_false` — the usual shorthand for a two-branch if-expression: ## Block Expressions for Initialization A `{ }` block is itself an expression — the last statement without a trailing `;` is its value. That makes it a clean way to collapse a multi-step initialization into a single assignment: Most editors let you fold a block like this away once it's written, so a complex initialization doesn't have to clutter the function around it. --- ## Object Run `#[run]` does for a whole object what `#[main]` does for a document: `self.run()` executes every `#[run]`-tagged function and field on that object, treating it as a workflow rather than something you call one function at a time. ## Basic Run Every `#[run]` function on the object executes when you call `run()`: ## Ordering with `#[run(N)]` A number controls execution order — regardless of the order things are declared in the document: ## Running Nested Objects If a child object is itself `#[run]`-tagged, running the parent recurses into it — each step in a multi-stage workflow can be its own object: ## Passing Arguments `#[run({'args': [...]})]` passes arguments through to the function it's attached to: ## Running a List `#[run]` on a list field runs every element — an arrow function, an object with its own `#[run]` members, whatever's in there: { self.done = true; }, { #[run] fn inner() { super.sub_done = true; } } ] } #[main] fn main() { self.Workflow.run(); pln(self.Workflow.done, self.Workflow.sub_done); } `} /> ## A Realistic Shape None of these mechanics need a network to demonstrate, but the pattern they're building toward usually involves one — fetch something, validate what comes back, and record the result, all in one self-contained object: ```stof #[type] Task: { ok: false result: null config: { endpoint: "https://myendpoint" schema: { /* a schema to validate the result against */ } } #[run] fn exec() { const res = await Http.fetch(self.config.endpoint); const result = new {}; parse(res.remove("text"), result, "json"); self.ok = self.config.schema.schemafy(result); self.result = result; } } ``` This one isn't runnable here — `Http` isn't a library this sandbox has enabled — but it's the shape `#[run]` is really for: a task object that knows how to execute itself, called the same way (`task.run()`) whether it's this or one of the five-line examples above it. ## Async Run `Obj.run(..)` is syncronous on purpose, but it is common to have async pipelines. With [Async](./async), there are many ways one can go about this. This example may give you a few ideas: --- ## Prototypes & Schemas A prototype is a named object template — useful for type-casting, structured creation, and, paired with `#[schema(...)]`, validation. This is the same mechanism behind `typename` reporting something other than `obj`, back in Types & Units. ## Declaring a Prototype `#[type]` marks the object declaration that follows it as a named template. `new TypeName { }` creates an instance, filling in whatever defaults the template doesn't override: str { \`Hello, \${self.id}\` } } #[main] fn main() { const c = new Customer { id: 'cust_1' }; pln(c.plan, c.greeting()); } `} /> ## The `` Path Shortcut `` is a path directly to the prototype object itself, distinct from any instance of it — the same absolute-path mechanism from [The Document Graph](./document-graph), just with its own syntax. Calling a function on it runs with the prototype object as `self`: str { \`\${self.prefix}, \${name}!\` } } #[main] fn main() { pln(.greet('World')); pln(.prefix); } `} /> ## Constructors `#[constructor]` marks a function to run automatically on every new instance: ## Inheritance with `#[extends]` A prototype can extend another by name or by reference — the base type's constructor always runs first: Inheritance is just a type chain, so easier to just cast directly: m { Num.sqrt(self.x.pow(2) + self.y.pow(2)) } } #[type] Point2D Point: { float z: 0 fn length() -> m { Num.sqrt(self.x.pow(2) + self.y.pow(2) + self.z.pow(2)) } } #[main] fn main() { const p = new Point { x: 1m, y: 2ft, z: 300cm }; pln(str(p.length().round(2)), str(p.length().round(2))); } `} /> ## Auto-Casting Typed Fields A field declared with a prototype type auto-casts a plain `new { }` object assigned to it, merging in whatever defaults the prototype provides: `o.sub` only specified `one` — `two` still came from `SubType`'s own default. ## Schemas: Validating with `#[schema]` `#[schema((target_value: T): bool => ...)]` on a field attaches a validator; `schemafy(target)` checks an object against every field that has one: target_value.len() > 2)] first: "First" } #[main] fn main() { const target = new { first: "Bob" }; pln(.schemafy(target)); target.first = "AJ"; pln(.schemafy(target)); } `} /> ## Combining Validators A list of functions on `#[schema(...)]` runs as a pipeline — each has to pass, in order, with short-circuiting: bool { "str" == typeof target_val } email_validation: [ self.is_string, (target_val: str): bool => target_val.contains("@"), ] #[schema(self.email_validation)] email: "info@example.com" #[main] fn main() { const target = new { email: "notvalid" }; pln(self.schemafy(target)); target.email = "info@stof.dev"; pln(self.schemafy(target)); } `} /> ## Sub-Schemas A bare `#[schema]`, with no function, tells `schemafy` to recurse into that field if the target's value is also an object: target_val >= 0)] field: 0 } } #[main] fn main() { const target = new { sub: new { field: -42 } }; pln(self.Schema.schemafy(target)); } `} /> ## Cleaning Up with `remove_invalid` `schemafy` can also modify the target directly — `remove_invalid = true` strips any field that failed validation instead of just reporting the failure. (`remove_undefined = true` is the sibling option, for stripping fields the schema doesn't mention at all.) bool { "str" == typeof target_val } #[schema(self.is_string)] label: "default" #[main] fn main() { const target = new { label: 42 }; pln(self.schemafy(target, remove_invalid = true)); pln(target.label); } `} /> --- ## Testing Stof has a thin testing layer built in, alongside `run` — `#[test]` marks a function as a test, and it always runs as its own separate async process. ## Test Functions The assert family — `assert`, `assert_eq`, `assert_not`, `assert_neq` — comes from the standard library and throws the moment a check fails: This example calls `passes()` directly rather than using `#[main]`, since `#[test]` functions aren't meant to run that way — they're picked up by a different command entirely, covered below. Note: `#[test]` functions (scopes & imports, too) are not included in the default `prod` parse profile, so you'll have to change to `test` when running embedded, outside of the CLI `stof test` command. ## Expecting a Failure Add `#[errors]` alongside `#[test]` when a test is *supposed* to throw — the CLI's test runner treats that throw as a pass, not a failure: The playground can't replicate that CLI-level interpretation — calling `fails()` here just shows the throw itself, which is exactly what `#[errors]` expects to happen. Whether that throw counts as a pass or a failure is a judgment the `stof test` command makes, not something visible from the function alone. ## Running Tests `stof test` replaces `stof run` at the command line, and reports each test's pass/fail individually, with a small stack trace on anything that failed unexpectedly. --- ## Types & Units For fields and variables, a type is implied by whatever value you give it — but declaring one explicitly keeps that field locked to that type, casting future assignments to match rather than replacing the type outright. ## Scalar Types Five primary scalars: `int` (signed 64-bit), `float` (64-bit), `bool`, `str`, and `blob`. Integer literals accept hex, binary, and octal; a blob literal is pipe-delimited raw bytes: ## `obj` and `fn` Are Types Too Every node is a value of type `obj`; every function is a value of type `fn`. `typeof` gives you the underlying primitive; `typename` gives you the more specific name — the unit, or a prototype, if there is one: `self` and `self.instance` are both `obj` under `typeof` — but `typename` on `self.instance` reports its prototype name, `Server`, since it was cast to type `Server`. Prototypes get their own page later; this is just what `obj` looks like once it has one. ## Compound Types `list`, `map`, `set`, and `tuple` are the built-in collection types — declaring and reading them is straightforward; the full set of operations on each (push, insert, sort, and so on) belongs to the dedicated Collections page later on, not this one: Note `set` drops the duplicate `'core'` automatically — sets only ever hold one of each value. ## Unit Types A unit type is a variant of `float` — `float` matches any unit, but specific units don't match each other and convert automatically on cast or arithmetic: The same system covers time, memory, temperature, mass, and angles: Note the `MB`/`MiB` distinction — Stof keeps binary units (mebibytes) separate from decimal units (megabytes) rather than treating them as interchangeable, since that difference genuinely matters at larger sizes. **Incompatible units cancel out** rather than throwing — mixing a length with a duration just drops the unit and returns a plain `float`: ## Semantic Versions `ver` is a built-in type for [semver](https://semver.org/) values, comparable and queryable directly: 1.0.0); } `} /> ## Unknown & Union Types `unknown` matches any type — useful when a function genuinely can't know its input ahead of time, as long as you check it yourself. A union (`A | B`) is usually the better tool when the possibilities are actually known: str { typeof v } fn combine(v: int | str) -> int | str { v } #[main] fn main() { pln(self.identify(42), self.identify("hi")); pln(self.combine("still a string")); } `} /> ## The `data` Type Every field and function is actually backed by a `Data` handle under the hood — a portable binary artifact. You won't reach for this often, but it's there when you need it: sharing a field's underlying data between objects, serializing it directly, or checking whether it's still attached to the document at all. This is also the type behind rich, library-backed values — a loaded PDF or image comes back as `Data` or `Data` rather than a plain scalar. Those are a Libraries-tab topic, not this page's, but it's the same underlying mechanism. --- ## Units Reference A lookup table, not a tutorial — [Types & Units](./types-and-units) covers how unit arithmetic actually behaves. Every unit below works as a numeric literal suffix (`5ft`, `20kg`) using its abbreviation, and as a type annotation using either the abbreviation or the full name: When two compatible-but-different units combine in an expression, the result takes on whichever of the two units is larger — mixed angles always resolve to radians specifically, regardless of size. ## Length | Unit | Full Name | |---|---| | `km` | kilometers | | `hm` | hectometers | | `dcm` | decameters | | `m` | meters | | `dm` | decimeters | | `cm` | centimeters | | `mm` | millimeters | | `um` | micrometers | | `nm` | nanometers | | `mi` | miles | | `yd` | yards | | `ft` | feet | | `in` | inches | ## Mass | Unit | Full Name | |---|---| | `Gt` | gigatonnes | | `Mt` | megatonnes | | `t` | tonnes | | `kg` | kilograms | | `g` | grams | | `mg` | milligrams | | `ug` | micrograms | | `ng` | nanograms | | `pg` | picograms | | `Ton` | tons | | `lb` | lbs (imperial pounds) | | `oz` | ounces | ## Time | Unit | Full Name | |---|---| | `day` | days | | `hr` | hours | | `min` | minutes | | `s` | seconds | | `ms` | milliseconds | | `us` | microseconds | | `ns` | nanoseconds | `Time.now()` returns a plain `ms` value for exactly this reason — subtracting or adding any other time unit against it just works. ## Temperature | Unit | Full Name | |---|---| | `K` | kelvin | | `C` | celsius | | `F` | fahrenheit | ## Angles | Unit | Full Name | Range | |---|---|---| | `rad` | radians | clamped to ±360° equivalent | | `deg` | degrees | clamped to ±360° | | `prad` | pradians (positive radians) | clamped to [0°, 360°) | | `pdeg` | pdegrees (positive degrees) | clamped to [0°, 360°) | The positive variants exist for comparisons where sign shouldn't matter — casting `-90deg` to `pdeg` gives `270deg`, not `-90deg`. ## Memory — Decimal | Unit | Full Name | |---|---| | `bit` / `bits` | — | | `byte` / `bytes` | — | | `KB` | kilobytes | | `MB` | megabytes | | `GB` | gigabytes | | `TB` | terabytes | | `PB` | petabytes | | `EB` | exabytes | | `ZB` | zettabytes | | `YB` | yottabytes | ## Memory — Binary | Unit | Full Name | |---|---| | `KiB` | kibibytes | | `MiB` | mebibytes | | `GiB` | gibibytes | | `TiB` | tebibytes | | `PiB` | pebibytes | | `EiB` | exbibytes | | `ZiB` | zebibytes | | `YiB` | yobibytes | Stof keeps these two tables separate on purpose — `MB` and `MiB` get conflated constantly in casual use, but they're not the same size, and the gap between them widens at larger scales. If a config or a docs page you're reading says `GB` but means `GiB` (common, and usually implied by context in computing), casting to the unit you actually meant is one step either way. --- ## 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: ## 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: ## Value Types Booleans, numbers, strings, versions, and promises are value types — copied automatically whenever they're read into a variable: `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: 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: 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: 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](./document-graph). ## Call by Reference Indexing supports references too — `&list[i]` is shorthand for `&list.at(i)`: `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: --- ## Age Encryption (Age) This is what "a scoped, self-encrypting context" from the Mission page actually looks like in practice — a document (or part of one) encrypted so only the holder of a specific key, or passphrase, can read it back. ## `Age.generate(context: obj = self) -> Data` Generates a new identity — a public/private keypair — attached to the given object. 0); } `} /> ## `Age.public(age: Data) -> str` The public half of an identity — safe to share, since it can only encrypt, not decrypt. ## `Age.blobify(recipients: str | list | Data, format: str = 'stof', context?: obj) -> blob` Like `Std.blobify`, but encrypted to one or more recipients' public keys — only the matching private key can read the result back. 0); } `} /> ## `Age.parse(age: Data, bin: blob, context: obj = self, format: str = "stof") -> bool` The other half of `Age.blobify` — decrypts using the matching identity, then parses the result into `context` the same way `Std.parse` would: ## `Age.pass_blobify(passphrase: str, format: str = 'stof', context?: obj) -> blob` The simpler alternative to `Age.blobify` — a shared passphrase instead of a generated identity, when there's no real recipient to manage keys for. 0); } `} /> ## `Age.pass_parse(passphrase: str, bin: blob, context: obj = self, format: str = "stof") -> bool` Decrypts what `Age.pass_blobify` produced — the same passphrase in, the original document back out: --- ## File System Library (fs) `fs` needs the `system` feature flag, which means real disk access — not something this browser-based sandbox has wired up, so every example below is reference only, not runnable here. (It's possible to expose a scoped version of this to a browser host — this site's separate live playground does exactly that — just not something this docs site does yet.) If you're building something that should never touch the filesystem at all, the flip side is just as easy: leave `fs` out of the host's loaded libraries entirely, and a document simply can't reach for it. ## `fs.read(path: str) -> blob` Reads a file into a binary blob. ```stof const bytes = fs.read("src/lib.rs"); ``` ## `fs.read_string(path: str) -> str` Same as `fs.read`, but as a string instead of raw bytes. ```stof const content = fs.read_string("src/lib.rs"); ``` ## `fs.write(path: str, content: str | blob) -> void` Writes to a file, overwriting it if it already exists. Throws if the containing directory doesn't exist. ```stof fs.write("src/text.txt", "testing"); ``` --- ## HTTP Network Library (Http) `Http` needs the `http` feature flag, which isn't part of this docs site's browser build — so every example below is reference only, not runnable here. That's specific to *this* build, though, not a limitation of Stof itself: wiring up `Http.fetch` from a TypeScript host is a small amount of code, since Stof's async model bridges naturally onto JS/browser `Promise`s. Plenty of real Stof embeddings do have this library available — including the separate live playground. A fetch runs on a background thread pool rather than blocking the calling process — which is the [Async](../learn/async) process model doing real work: `Http.fetch` spawns its own process, and `await`ing it is what lets several requests actually run in parallel instead of one at a time. ## Making Requests ### `async Http.fetch(url: str, method: str = "get", body: str | blob = null, headers: map = null, timeout: seconds = null, query: map = null, bearer: str = null) -> Promise` Every other function on this page operates on the `map` this returns. ```stof const resp = await Http.fetch("https://restcountries.com/v3.1/region/europe"); ``` ## Reading the Response ### `Http.success(response: map) -> bool` Status in `[200, 299]`. ```stof const resp = await Http.fetch("https://restcountries.com/v3.1/region/europe"); assert(Http.success(resp)); ``` ### `Http.client_error(response: map) -> bool` Status in `[400, 499]`. ```stof assert_not(Http.client_error(resp)); ``` ### `Http.server_error(response: map) -> bool` Status in `[500, 599]`. ```stof assert_not(Http.server_error(resp)); ``` ### `Http.text(response: map) -> str` The response body as UTF-8 text — equivalent to `Http.blob(response) as str`. ```stof const body = Http.text(resp); ``` ### `Http.blob(response: map) -> blob` The response body as raw bytes. ```stof const body = Http.blob(resp); ``` ### `Http.size(response: map) -> bytes` The response body's size — a unit type, so casting it to something more readable is one step: ```stof const mib_body_size = Http.size(resp) as MiB; ``` ### `Http.parse(response: map, context: obj = self) -> obj` Parses the response body directly into an object, using the response's `Content-Type` header to pick the format — `stof` if that header is missing. Throws if the format isn't one this graph accepts, or if there's no body to parse: ```stof const body = new {}; try { Http.parse(resp, body); } catch { /* didn't work out */ } ``` --- ## Image Library (Image) `Image` needs the `image` feature flag, which isn't part of this docs site's browser build — every example below is reference only, not runnable here. Images load into a document as a `Data` value, which every function on this page operates on directly. ## Loading ### `Image.from_blob(bytes: blob) -> Data` Creates an image on the calling object from raw bytes, auto-detecting the format. `fs.read` is the natural pairing here, since both are CLI-only anyway: ```stof const bytes = fs.read("photo.png"); const img = Image.from_blob(bytes); ``` ## Reading Dimensions ### `Image.width(img: Data) -> int` ```stof const width = Image.width(img); ``` ### `Image.height(img: Data) -> int` ```stof const height = Image.height(img); ``` ## Transformations Every function in this section mutates the image in place and returns nothing — call it, then export or continue transforming the same `img` value. ### `Image.grayscale(img: Data) -> void` ```stof Image.grayscale(img); ``` ### `Image.invert(img: Data) -> void` ```stof Image.invert(img); ``` ### `Image.blur(img: Data, blur: float) -> void` Gaussian blur — `blur` is the sigma value. ```stof Image.blur(img, 2.5); ``` ### `Image.fast_blur(img: Data, blur: float) -> void` Same gaussian blur as `Image.blur`, trading some quality for speed — reach for this one over `blur` if you're processing images in bulk and the difference isn't visually significant for your use case. ```stof Image.fast_blur(img, 2.5); ``` ### `Image.brighten(img: Data, brighten: int) -> void` Positive brightens, negative darkens. ```stof Image.brighten(img, 20); ``` ### `Image.contrast(img: Data, contrast: float) -> void` Positive increases contrast, negative decreases it. ```stof Image.contrast(img, 15.0); ``` ### `Image.flip_horizontal(img: Data) -> void` ```stof Image.flip_horizontal(img); ``` ### `Image.flip_vertical(img: Data) -> void` ```stof Image.flip_vertical(img); ``` ### `Image.rotate_90(img: Data) -> void` Clockwise. ```stof Image.rotate_90(img); ``` ### `Image.rotate_180(img: Data) -> void` ```stof Image.rotate_180(img); ``` ### `Image.rotate_270(img: Data) -> void` Clockwise — equivalent to a 90° counter-clockwise rotation. ```stof Image.rotate_270(img); ``` ## Resizing Each of these returns a `bool` — whether the resize actually succeeded. ### `Image.resize(img: Data, width: int, height: int) -> bool` Preserves aspect ratio — the result fits within `width` × `height` but may not match it exactly. ```stof const resized = Image.resize(img, 800, 600); ``` ### `Image.resize_exact(img: Data, width: int, height: int) -> bool` Forces the exact dimensions given, ignoring the original aspect ratio. ```stof const resized = Image.resize_exact(img, 800, 600); ``` ### `Image.thumbnail(img: Data, width: int, height: int) -> bool` Same as `resize`, but optimized for shrinking rather than resizing in either direction. ```stof const made = Image.thumbnail(img, 128, 128); ``` ### `Image.thumbnail_exact(img: Data, width: int, height: int) -> bool` `thumbnail`'s exact-dimensions counterpart, the same way `resize_exact` relates to `resize`. ```stof const made = Image.thumbnail_exact(img, 128, 128); ``` ## Exporting Each function below turns the current state of `img` — after whatever transformations have run — into raw bytes of a specific format. ### `Image.blob(img: Data) -> blob` The default export — raw bytes in PNG format, equivalent to `Image.png`. ```stof const bytes = Image.blob(img); ``` ### `Image.png(img: Data) -> blob` ```stof const bytes = Image.png(img); ``` ### `Image.jpeg(img: Data) -> blob` ```stof const bytes = Image.jpeg(img); ``` ### `Image.webp(img: Data) -> blob` ```stof const bytes = Image.webp(img); ``` ### `Image.gif(img: Data) -> blob` ```stof const bytes = Image.gif(img); ``` ### `Image.bmp(img: Data) -> blob` ```stof const bytes = Image.bmp(img); ``` ### `Image.ico(img: Data) -> blob` ```stof const bytes = Image.ico(img); ``` ### `Image.tiff(img: Data) -> blob` ```stof const bytes = Image.tiff(img); ``` --- ## Markdown Library (Md) Two functions, both taking a Markdown string and converting it to something else. ## `Md.html(md: str) -> str` Markdown in, an HTML string out. ## `Md.json(md: str) -> str` Markdown in, a JSON string out — a full structural breakdown from the parser, not just the rendered result. More detail than `Md.html`, useful when you need to work with the document's structure rather than just display it. --- ## PDF Library (Pdf) `Pdf` needs the `pdf` feature flag, which isn't part of this docs site's browser build — every example below is reference only, not runnable here. A PDF imported into a document (via the `pdf` format, from [Import & Export](../learn/import-and-export)) becomes a `Data` value, which both functions on this page operate on. ## `Pdf.extract_text(pdf: Data) -> str` Every page's text, concatenated into one string. ```stof const text = self.pdf.extract_text(); pln(text); ``` ## `Pdf.extract_images(pdf: Data) -> list` Every image on every page, as a list of maps carrying the image data alongside its dimensions. ```stof const images = self.pdf.extract_images(); pln(images.len()); pln(images[0].get('width'), images[0].get('height')); ``` --- ## Standard Library (Std) `Std` is the one library that never needs to be named — `Std.pln(...)` and `pln(...)` call the same function. Every other library needs its own prefix (`Time.now()`, `Http.fetch(...)`), but anything shown here works bare. One distinction worth keeping straight: this page is about *library* functions, which never need a receiver. A **document** function always does — `self.myFunc()`, `super.myFunc()`, through a variable, whatever fits — because it belongs to a specific node in the graph, not to the runtime itself. ## Output ### `pln(..) -> void` Prints all arguments to the standard output stream — this is what every other page's examples have been using the whole time. ### `err(..) -> void` Same as `pln`, but to the error stream instead of standard output. ### `str(..) -> str` Formats all arguments the same way `pln` would, but returns the result as a string instead of printing it. ### `dbg(..) -> void` Prints all arguments as debug output. Depending on how the host environment wires up logging, this may not surface in every output stream `pln` does — worth checking your browser console too if it looks quiet. ## Logging The five `log_*` functions map onto the standard levels from Rust's `log` crate. Same caveat as `dbg` — whether these are visible depends on what the host has configured to receive them. ### `log_info(..) -> void` ### `log_debug(..) -> void` ### `log_warn(..) -> void` ### `log_error(..) -> void` ### `log_trace(..) -> void` ## Debugging & Tracing Three deeper introspection tools — expect dense, VM-level output that looks different every run. That's normal; these aren't meant to be tidy. ### `peek(..) -> void` Prints your arguments, then a trace of the current process and the *next* instructions about to execute. Pass a trailing integer to control how many. ### `trace(..) -> void` Like `peek`, but shows the instructions that already *ran* leading up to this point, instead of what's coming next. ### `dbg_tracestack() -> void` Prints a snapshot of the current stack, with no arguments needed. ## Assertions Every one of these throws when the check fails — wrap the failing case in `try`/`catch` if you want execution to continue past it, same as anywhere else in Stof. ### `assert(value: unknown = false) -> void` ### `assert_eq(first: unknown, second: unknown) -> void` ### `assert_neq(first: unknown, second: unknown) -> void` ### `assert_not(value: unknown = true) -> void` ## Errors & Process Control ### `throw(value: unknown = "Error") -> void` Throws any value, not just a string — the mechanism behind every `try`/`catch` example on the Error Handling page. ### `exit(..) -> void` Immediately halts the current process — nothing after it in the same process runs. Pass a promise to terminate *that* process's execution instead of the current one. ## Parsing & Exporting ### `parse(source: str | blob, context: str | obj = self, format: str = "stof", profile: str = "prod") -> bool` Covered in depth on Import & Export — the runtime counterpart to the `import` statement, and it works fine sandboxed since it's just operating on a string. str { "hello" }'); pln(self.hello()); } `} /> ### `stringify(format: str = "json", context: obj = null) -> str` Exports a string. Lossy for anything the target format can't represent — JSON has no concept of units, for instance, so they're silently dropped. ### `blobify(format: str = "json", context: obj = null) -> blob` Same idea as `stringify`, exporting raw bytes instead of a string. 0, bytes as str); } `} /> ## Constructing Collections Mostly useful for building an empty collection, or building one from values you already have in hand rather than writing out literal syntax. ### `list(..) -> list` ### `map(..) -> map` Takes tuples of key/value pairs. ### `set(..) -> set` ## Values & Memory ### `copy(val: unknown) -> unknown` A real deep copy — for an object, every field, function, and nested piece of data underneath it, recursively. ### `swap(first: unknown, second: unknown) -> void` Swaps two values in place — needs `&` on value types, the same reference operator from Variables & References, since a plain value type would just swap two disposable copies. ### `min(..) -> unknown` If an argument is a collection, only the values inside it are considered — not the collection as a whole. ### `max(..) -> unknown` ### `drop(..) -> bool | list` Covered in depth on Document Memory Management — drops fields, functions, objects, or data from the graph, running `#[dropped]` on anything that has it. Given several things to drop at once, returns a list of one bool per item. {}; const object = new {}; self.field = 42; const results = drop("self.field", func, object); pln(results); } `} /> ### `shallow_drop(..) -> bool | list` Same as `drop`, except if a dropped field pointed to an object, only the field is removed — the object itself stays in the graph. Useful when more than one field might point at the same object and you only want to disconnect this one. ## Introspection Mostly useful for writing code that has to reason about the document generically — a validator, a debugging tool, anything that can't assume a fixed shape ahead of time. ### `funcs(attributes: str | list | set = null) -> list` Every function in the graph, optionally filtered to only those carrying a given attribute. ### `callstack() -> list` A list of function pointers currently on the stack — last one is `this`, the function calling `callstack()` itself. ### `graph_id() -> str` A unique ID string for this graph/document. 10); } `} /> ### `format(format: str) -> bool` Is a given format loaded and available for `parse`/`stringify`/`blobify`? ### `format_content_type(format: str) -> str` The HTTP content-type header value for a loaded format — useful if you're serving Stof-produced data over the wire. ### `formats() -> set` Every format currently available to use. ### `lib(lib: str, func?: str) -> bool` Is a library — and optionally a specific function on it — loaded? ### `libs() -> set` Every library currently available. ### `prof(name: str) -> bool` Was this graph parsed under a given profile name? Profiles are how `parse`'s `profile` parameter tags a parse for later inspection. ## Environment These four require the `system` feature flag, which reads and writes real process environment variables — not something a browser sandbox can meaningfully do, so these are reference-only rather than runnable here. ```stof const host = env("HOST"); const vars: map = env_vars(); set_env("HOST", "localhost"); remove_env("HOST"); ``` ## Timing ### `sleep(time: ms) -> void` Pauses the current process for at least the given duration — possibly longer, never less. Other processes keep making progress while this one sleeps, same cooperative model as Async. = 50); } `} /> ## IDs & Text Helpers ### `nanoid(length: int = 21) -> str` A URL-safe random ID — collision probability is low and drops further as length increases. ### `xml(text: str, tag: str) -> str` Wraps a string in an XML tag — a small formatting helper, nothing more. ### `prompt(text: str = '', tag?: str) -> prompt` Builds a `prompt` value — a small tree of tagged text, meant for assembling structured prompts for AI contexts. `+=` appends a nested prompt as a child. --- ## Time Library (Time) Every timestamp in this library is a plain `ms` value (nanoseconds for the `_ns` variants) — which means normal unit arithmetic just works on them. `Time.diff(start) as seconds` reads naturally instead of needing a manual division. ## Current Time & Elapsed ### `now() -> ms` The current Unix timestamp, in milliseconds. ### `now_ns() -> ns` Same idea, in nanoseconds — for when millisecond resolution isn't fine enough. ### `diff(prev: float) -> ms` Shorthand for `Time.now() - prev` — how much time has passed since a timestamp you captured earlier. = 50); } `} /> ### `diff_ns(prev: float) -> ns` The nanosecond version of `diff`. = 49ms); } `} /> ### `sleep(time: float = 1000ms) -> void` An alias for `Std.sleep` — identical behavior, just callable through `Time` for anyone who'd rather keep every time-related call under one namespace. = 50); } `} /> ## Formatting RFC 3339 is the ISO-8601-flavored format most APIs use today; RFC 2822 is the older, email-header-style format — both convert cleanly to and from a plain `ms` timestamp. ### `now_rfc3339() -> str` ### `now_rfc2822() -> str` ### `to_rfc3339(time: float) -> str` ### `to_rfc2822(time: float) -> str` ### `from_rfc3339(time: str) -> ms` ### `from_rfc2822(time: str) -> ms` ## Reading Components All UTC — none of these read a local timezone. ### `year(ts: ms) -> int` ### `month(ts: ms) -> int` 1–12. ### `day_of_month(ts: ms) -> int` 1–31. ### `day_of_week(ts: ms) -> int` ISO convention — `0` is Monday, `6` is Sunday. ### `hour(ts: ms) -> int` 0–23. ### `minute(ts: ms) -> int` ### `second(ts: ms) -> int` ### `days_in_month(ts: ms) -> int` 28–31, leap years handled correctly for February. ## Calendar Arithmetic ### `add_days(ts: ms, n: int) -> ms` `n` can be negative. Time.now()); } `} /> ### `add_months(ts: ms, n: int) -> ms` If the day of month doesn't exist in the target month, it clamps to the last valid day — Jan 31 plus one month lands on Feb 28 (or 29). Time.now()); } `} /> ## Period Boundaries Every `start_of_*` function returns midnight UTC on the relevant boundary — useful for anything that resets on a schedule. ### `start_of_day(ts: ms) -> ms` ### `start_of_week(ts: ms, start_day: int = 0) -> ms` `start_day` follows the same ISO convention as `day_of_week` — `0` is Monday by default, but any day can be the week's start. ### `start_of_month(ts: ms) -> ms` ### `start_of_year(ts: ms) -> ms` ### `start_of_period(ts: ms, schedule: str) -> ms` The most flexible of the five — a schedule expression instead of a fixed unit. Returns `null` for an invalid schedule string: - `"monthly:N"` — the Nth day of every month (clamped to the month's length) - `"monthly:last"` — the last day of every month - `"weekly:mon"` — a given weekday, every week (`mon`/`tue`/`wed`/`thu`/`fri`/`sat`/`sun`) - `"nth_weekday:N:mon"` — the Nth occurrence of a weekday in the month (`N` is 1–4) - `"yearly:M-D"` — a fixed month and day every year - `"quarterly:D"` — a given day of the first month of each quarter (Jan/Apr/Jul/Oct) --- ## A Self-Assembling AI Context An agent's system prompt — role, available tools, recent memory — usually lives in application code, hand-maintained alongside whatever the agent object actually supports. Add a tool, forget to update the prompt, and the agent doesn't know it exists. Here the prompt is assembled by reading the agent itself, so it can't drift from what's actually there. ## Building a Prompt Tree `prompt` is a small tree of tagged text — Prompt Library covers the full API. The basic shape: ## An Agent That Renders Itself `render()` doesn't hardcode a list of tools — it asks the object which functions are tagged `#[tool]` and builds the entry for each one from that: :::note This pattern works really well with AI tool calling, where a different set of tools can be made available every time they are asked for by the agent. The actual tool call can then be in Stof as well. ::: str { \`order \${id}\` } #[tool] fn issue_refund(id: str, amount: float) -> str { \`refunded \${amount}\` } memory: ["Customer asked about order #4471."] fn render() -> prompt { const ctx = prompt(tag = 'context'); ctx.push(prompt(self.role, 'role')); for (const tool: fn in self.funcs('tool')) { ctx.push(prompt(tool.name(), 'tool')); } for (const entry: str in self.memory) { ctx.push(prompt(entry, 'memory')); } ctx } } #[main] fn main() { pln(self.Agent.render() as str); } `} /> `render()` never names `lookup_order` or `issue_refund` directly — it just asks for everything tagged `#[tool]`. Add a third tool function anywhere on `Agent` and it shows up in the rendered context automatically, with nothing else in this file touched. The prompt can't go stale, because it was never a separate thing to keep in sync in the first place. --- ## A Scoped, Expiring Auth Context "Share this for 24 hours" usually means a database row tracking expiry, or a signed URL with a server checking a clock somewhere. Here it's just a document: something that knows its own expiration, encrypts its own payload, and refuses to open once time's up — no infrastructure standing behind it to make that true. ## Something That Knows When It's Expired bool { Time.now() < self.expires } } #[main] fn main() { const link = new ShareLink { expires: Time.now() + 1hr }; pln(link.valid()); const expired = new ShareLink { expires: Time.now() - 1hr }; pln(expired.valid()); } `} /> ## The Full Link Wrap a passphrase-encrypted payload around that same expiry check — `open()` refuses to even attempt decryption once the link is stale: :::note in a real scenario, you'd want to use public/private key pairs (Age has these too), and keep an expiration as a separate validation step. ::: bool { Time.now() < self.expires } fn open(passphrase: str) -> obj { if (!self.valid()) return null; const decrypted = new {}; Age.pass_parse(passphrase, self.payload, decrypted, 'bstf'); decrypted } } #[main] fn main() { const secret = new { file: "quarterly-report.pdf" }; const payload = Age.pass_blobify("open-sesame", 'bstf', secret); const link = new ShareLink { expires: Time.now() + 1hr, payload: payload }; pln(link.open("open-sesame").file); const stale = new ShareLink { expires: Time.now() - 1hr, payload: payload }; pln(stale.open("open-sesame")); } `} /> `link` and `stale` carry the exact same encrypted payload — the only difference is a timestamp. One opens, the other refuses before it ever touches the passphrase. Nothing here is a special "auth" feature; it's the same fields, functions, and encryption from Types & Units and the Age library, arranged around one small rule. --- ## A Self-Validating Config A subscription plan with a bad `reset_inc` — zero, or negative — is the kind of bug that looks completely fine as JSON and then quietly breaks billing in production: a reset loop that never advances, or fires constantly. The fix isn't a check somewhere downstream that someone has to remember to write. It's making the config incapable of holding that value in the first place. See [Prototypes & Schemas](../learn/prototypes-and-schemas) and the [Object Library](../libs/types/object-library) for more information on Schemafy. ## Attaching a Rule to a Field `#[schema(...)]` on a field is a validator — `schemafy(target)` checks a target object against it: target_value.len() > 1)] str name: 'default' #[schema((target_value: hr): bool => target_value > 0hr)] hr reset_inc: 1hr /// Plan output as YAML to pass to the host or send over the wire, etc. fn out() -> str { stringify('yaml', self) } } #[main] fn main() { const good = new Plan { reset_inc: 2hr }; pln(.schemafy(good)); const bad = new Plan { reset_inc: -1hr }; pln(.schemafy(bad)); pln(good.out()); } `} /> ## The Plan Every field that matters gets its own rule — a real plan, checked all at once: target_value.len() > 0)] str label: 'Growth Plan' #[schema((target_value: float): bool => target_value > 0)] float credit_limit: 100_000 #[schema((target_value: hr): bool => target_value > 0hr)] hr reset_inc: 1hr } #[main] fn main() { const incoming = new { label: 'Team Plan', credit_limit: 50_000, reset_inc: 1hr }; pln(.schemafy(incoming)); const broken = new { label: 'Broken Plan', credit_limit: 50_000, reset_inc: -1hr }; pln(.schemafy(broken)); } `} /> `incoming` passes and `broken` doesn't — and nothing about that required a separate validation layer, a schema file, or a test to catch it. The rule lives on the same field it protects, so it can't drift out of sync with what the field actually needs. --- ## Fetch, Validate, and Cache an API Response A cache, a TTL, and a validation step are usually three separate systems that all have to independently agree on what "valid" and "fresh" mean. Here they're one object: it fetches, checks the shape of what it got back, and remembers — so calling it twice in a row doesn't mean two network requests. One honest note before the code: `Http` isn't loaded in this browser sandbox (see the Http library page), so `simulated_fetch()` below stands in for `await Http.fetch(...)` — same response shape, no real network call. Swapping it for the real thing in a host that has `Http` loaded is a one-line change; nothing else in the recipe cares which one it's talking to. ## Fetch, Then Validate map { // stands in for \`await Http.fetch(...)\` map(("status", 200), ("text", '{"rate": 1.08}')) } #[type] Rate: { #[schema((target_value: float): bool => target_value > 0)] float rate: 1.0 } #[main] fn main() { const resp = self.simulated_fetch(); const parsed = new {}; parse(resp.get("text"), parsed, "json"); pln(.schemafy(parsed), parsed.rate); } `} /> ## Adding the Cache A `fetch_count` here just proves the point — in a real version it wouldn't exist, but it's the difference between *claiming* the cache works and actually watching it: target_value > 0)] float rate: 1.0 } cache: { fetched: 0ms data: null fetch_count: 0 fn simulated_fetch() -> map { self.fetch_count += 1; map(("status", 200), ("text", '{"rate": 1.08}')) } fn stale() -> bool { self.data == null || Time.diff(self.fetched) > 5min } fn get() -> obj { if (self.stale()) { const resp = self.simulated_fetch(); const parsed = new {}; parse(resp.get("text"), parsed, "json"); drop(self.data); self.data = null; if (.schemafy(parsed)) self.data = parsed; else drop(parsed); self.fetched = Time.now(); } self.data } } #[main] fn main() { pln(self.cache.get().rate); pln(self.cache.get().rate); pln(self.cache.fetch_count); } `} /> `fetch_count` stays at `1` even after calling `get()` twice — the second call found valid, recent data sitting right there and skipped the fetch entirely. `root.Rate` reaches the schema from inside `cache`, the same absolute-path mechanism from The Document Graph — `cache` doesn't need to know or care where the validation rule actually lives. --- ## A Sandboxed Plugin System A plugin system usually means one of two uncomfortable options: compile and load a binary, with a real security boundary to build and maintain, or run an interpreted script with full host access, which is no boundary at all. A Stof document sits in between — it can add real behavior to a running system, but only touch what it's explicitly handed. Nothing else. ## Loading Plugin Code ## A Well-Behaved Plugin, and One That Reaches Too Far Both plugins below are just strings — the same `load_plugin` function handles either one identically. The difference shows up when they run: obj { const sandbox = new { data: data }; parse(src, sandbox); sandbox } #[main] fn main() { const good_plugin = "fn transform() -> str { self.data.upper() }"; const good = self.load_plugin(good_plugin, "hello, plugin"); pln(good.transform()); const bad_plugin = "fn transform() -> str { Http.fetch('https://evil.example') }"; const bad = self.load_plugin(bad_plugin, "hello, plugin"); try { pln(bad.transform()); } catch { pln('blocked: no Http library available to this document'); } } `} /> `good_plugin` does real work with nothing but `self.data` — no special permission needed for that, because it never needed to leave the sandbox. `bad_plugin` isn't blocked by a permissions check or a content filter; there's simply no `Http` for it to call, in this document or any other unless a host explicitly loads one in. The failure is a normal thrown error, catchable the same way any other error is — not a security incident. --- ## Unifying Config from Multiple Formats Real systems end up with config scattered across formats — a YAML deploy manifest, a TOML tool config, a JSON API response — and the usual fix is converting everything to one format up front, or writing a parser per format downstream. Format is just an import detail here, not a boundary: whatever comes in lands in the same graph and gets read the same way, regardless of where it started. ## Two Formats, Same Field Access ## Merging Three Sources Into One One loop, summing a field that came from three different source formats — nothing in it knows or cares which one any given plan started as. The translation layer that would normally exist between "the format we use" and "the format they use" just isn't there to maintain. --- ## Blob Library (Blob) Linked to the `blob` type — the raw-bytes counterpart to `str`, useful anywhere binary data crosses a boundary (`blobify`, `Http`, `fs`, images). ## Byte Access ### `Blob.at(bytes: blob, index: int) -> int` The byte value at an index. ### `Blob.len(bytes: blob) -> int` ### `Blob.size(bytes: blob) -> bytes` Same as `len`, but as a `bytes` unit value instead of a plain int. ## Encoding & Decoding ### `Blob.utf8(bytes: blob) -> str` The default conversion for a plain `as str` cast too. ### `Blob.from_utf8(val: str) -> blob` ### `Blob.base64(bytes: blob) -> str` ### `Blob.from_base64(val: str) -> blob` ### `Blob.url_base64(bytes: blob) -> str` URL-safe Base64 — swaps the characters that aren't URL-safe in standard Base64. ### `Blob.from_url_base64(val: str) -> blob` --- ## Data Library (Data) Every field and function is backed by a `Data` handle — this library is how you work with that handle directly, rather than through the field or function it's attached to. Advanced, low-level territory; most code never needs this. ## Locating Data ### `Data.field(path: str) -> data` A data pointer to a field, by path from the current object. ### `Data.id(ptr: data) -> str` str { 'hi' } #[main] fn main() { const func: fn = self.hi; const id = func.data().id(); pln(id.len() > 0); } `} /> ### `Data.from_id(id: str) -> data` The other direction — reconstruct a pointer from an ID string. str { 'hi' } #[main] fn main() { const func: fn = self.hi; const id = func.data().id(); pln(Data.from_id(id) == func.data()); } `} /> ### `Data.libname(ptr: data) -> str` Which library this data is linked to, if any. str { 'hi' } #[main] fn main() { const func: fn = self.hi; pln(func.data().libname()); } `} /> ### `Data.objs(ptr: data) -> list` Every object this data is attached to. str { 'hi' } #[main] fn main() { const func: fn = self.hi; pln(func.data().objs().front() == self); } `} /> ## Attaching, Moving & Dropping ### `Data.attach(ptr: data, obj: obj) -> bool` Makes this data reachable from an additional object, under the same name. str { 'hi' } #[main] fn main() { const func: fn = self.hi; const other = new {}; func.data().attach(other); pln(other.hi()); } `} /> ### `Data.move(ptr: data, from: obj, to: obj) -> bool` A drop and an attach in one step — removes the data from one object and places it on another. str { 'hi' } #[main] fn main() { const func: fn = self.hi; const other = new {}; func.data().move(self, other); pln(other.hi(), self.hi); } `} /> ### `Data.drop(ptr: data) -> bool` str { 'hi' } #[main] fn main() { const func: fn = self.hi; pln(func.data().drop()); } `} /> ### `Data.drop_from(ptr: data, obj: obj) -> bool` Drops from one specific object — if that was the only object referencing it, the data leaves the document entirely. str { 'hi' } #[main] fn main() { const func: fn = self.hi; pln(func.data().drop_from(self)); } `} /> ## Serialization ### `Data.blob(ptr: data) -> blob` Serializes the data (name, attributes, value — everything) into bytes. str { 'hi' } #[main] fn main() { const func: fn = self.hi; const bin = func.data().blob(); pln(bin.len() > 0); } `} /> ### `Data.load_blob(bytes: blob, context: obj | str = self) -> data` The other direction — deserializes into a specific object, effectively copying the data onto it. str { 'hi' } #[main] fn main() { const func: fn = self.hi; const bin = func.data().blob(); const other = new {}; Data.load_blob(bin, other); pln(other.hi()); } `} /> ## Validity ### `Data.exists(ptr: data) -> bool` False once the data has been dropped — a pointer held onto after that point doesn't resurrect it. str { 'hi' } #[main] fn main() { const func: fn = self.hi; const ptr = func.data(); drop(func); pln(ptr.exists()); } `} /> ### `Data.invalidate(data: data, symbol: str = 'value') -> bool` Marks data as invalid under a symbol — a lightweight dirty-flag mechanism, throws if the data doesn't exist. str { 'hi' } #[main] fn main() { const func = self.hi; const ptr = func.data(); pln(ptr.invalidate('something_happened')); } `} /> ### `Data.validate(data: data, symbol?: str) -> bool` Clears an invalidation — returns `true` only if it was actually invalid under that symbol (or any symbol, if none given) beforehand. str { 'hi' } #[main] fn main() { const func = self.hi; const ptr = func.data(); ptr.invalidate('something_happened'); pln(ptr.validate('something_happened')); pln(ptr.validate('something_happened')); } `} /> --- ## Function Library (Fn) Linked to the `fn` type — the mechanics behind everything on the Functions and Attributes Learn pages. ## Calling ### `Fn.call(func: fn, ..) -> unknown` `func.call(...)`, `Fn.call(func, ...)`, and `func(...)` are all equivalent. "Hi, " + name; pln(func.call("Bob"), Fn.call(func, "Bob"), func("Bob")); } `} /> ### `Fn.call_expanded(func: fn, ..) -> unknown` Same as `call`, but a collection argument gets expanded into positional arguments instead of passed as one value. "Hi, " + name; pln(func.call_expanded(["Bob"])); } `} /> ## Binding ### `Fn.bind(func: fn, to: obj) -> bool` Moves a function onto a different object — `self` inside it now refers to `to`. self.msg ?? 'dne'; const to = new { msg: 'hi' }; func.bind(to); pln(func()); } `} /> ## Introspection ### `Fn.name(func: fn) -> str` str { 'hi' } #[main] fn main() { const func: fn = self.hi; pln(func.name()); } `} /> ### `Fn.params(func: fn) -> list` A list of `(name, type)` tuples. ### `Fn.return_type(func: fn) -> str` int { 42 } #[main] fn main() { const func: fn = self.hi; pln(func.return_type()); } `} /> ### `Fn.is_async(func: fn) -> bool` Shorthand for checking whether the `async` attribute is present. str { 'hi' } #[main] fn main() { const func: fn = self.hi; pln(func.is_async()); } `} /> ### `Fn.attributes(func: fn) -> map` str { 'hi' } #[main] fn main() { const func: fn = self.hi; pln(str(func.attributes())); } `} /> ### `Fn.has_attribute(func: fn, name: str) -> bool` str { 'hi' } #[main] fn main() { const func: fn = self.hi; pln(func.has_attribute("greeting")); } `} /> ### `Fn.obj(func: fn) -> obj` The first object this function is attached to. str { 'hi' } #[main] fn main() { const func: fn = self.hi; pln(func.obj() == self); } `} /> ### `Fn.objs(func: fn) -> list` Every object this function is attached to — usually just one, unless it's been shared via `Data.attach`. str { 'hi' } #[main] fn main() { const func: fn = self.hi; pln(func.objs().len()); } `} /> ### `Fn.data(func: fn) -> data` The underlying `Data` handle — see the Data library. str { 'hi' } #[main] fn main() { const func: fn = self.hi; pln(func.data().exists()); } `} /> ### `Fn.id(func: fn) -> str` Shorthand for `func.data().id()`. str { 'hi' } #[main] fn main() { const func: fn = self.hi; pln(func.id() == func.data().id()); } `} /> --- ## List Library (List) Linked to the `list` type — Collections covers the core operations already; this page is the complete reference. ## Query ### `List.len(array: list) -> int` ### `List.empty(array: list) -> bool` ### `List.any(array: list) -> bool` ### `List.contains(array: list, value: unknown) -> bool` ### `List.index_of(array: list, v: unknown) -> int` `-1` if not found. ### `List.is_uniform(array: list) -> str` If every value has the exact same specific type, will return that type — prototype inheritance isn't considered. Otherwise, returns null. ## Access ### `List.at(array: list, index: int) -> unknown` `&array[i]` for a reference, so mutating the result mutates the list itself. ### `List.front(array: list) -> unknown` ### `List.back(array: list) -> unknown` ## Modify ### `List.push_back(array: list, ..) -> void` Takes any number of values. ### `List.push_front(array: list, ..) -> void` ### `List.pop_back(array: list) -> unknown` ### `List.pop_front(array: list) -> unknown` ### `List.insert(array: list, index: int, val: unknown) -> void` ### `List.replace(array: list, index: int, val: unknown) -> unknown` Returns the old value. ### `List.append(array: list, other: list) -> void` `other` is left unmodified. ### `List.clear(array: list) -> void` ### `List.join(array: list, sep: str) -> str` ## Removing ### `List.remove(array: list, index: int) -> unknown` Returns the removed value, or `null` if the index was out of bounds. ### `List.remove_first(array: list, val: unknown) -> unknown` ### `List.remove_last(array: list, val: unknown) -> unknown` ### `List.remove_all(array: list, val: unknown) -> bool` Returns whether anything was actually removed. ## Sort & Reverse ### `List.sort(array: list) -> void` ### `List.sort_by(array: list, func: fn) -> void` The comparator returns negative/zero/positive, same convention as everywhere else. Written as a single ternary expression here rather than a chain of bare `if` statements — `if` can't produce a value on its own (see Control Flow), so a chain of them with no `else` wouldn't actually control the sort order the way it looks like it would. a < b ? 1 : (a > b ? -1 : 0)); pln(array); } `} /> ### `List.reverse(array: list) -> void` In place. ### `List.reversed(array: list) -> list` Returns a new reversed list, leaving the original untouched. ## Convert ### `List.to_uniform(array: list, type: str) -> void` Casts every value to the given type in place — throws if any value can't be cast. --- ## Map Library (Map) Linked to the `map` type. Maps stay ordered by key, which is why `first`/`last`/`at` are meaningful, not just `get`/`insert`. ## Query ### `Map.len(this: map) -> int` ### `Map.empty(this: map) -> bool` ### `Map.any(this: map) -> bool` ### `Map.contains(this: map, key: unknown) -> bool` ### `Map.keys(this: map) -> set` ### `Map.values(this: map) -> list` ## Access ### `Map.get(this: map, key: unknown) -> unknown` ### `Map.at(this: map, index: int) -> (unknown, unknown)` The `(key, value)` pair at a position in sorted order — not a lookup by key. ### `Map.first(this: map) -> (unknown, unknown)` ### `Map.last(this: map) -> (unknown, unknown)` ## Modify ### `Map.insert(this: map, key: unknown, value: unknown) -> unknown` Returns the old value if the key already existed, `null` otherwise. ### `Map.remove(this: map, key: unknown) -> unknown` ### `Map.pop_first(this: map) -> (unknown, unknown)` ### `Map.pop_last(this: map) -> (unknown, unknown)` ### `Map.append(this: map, other: map) -> void` ### `Map.clear(this: map) -> void` --- ## Number Library (Num) Linked to every numeric type — `int`, `float`, and units. Called as a method on the number itself (`val.abs()`) or through `Num` directly (`Num.abs(val)`) — both reach the same function. ## Basics ### `Num.abs(val: int | float) -> int | float` ### `Num.sqrt(val: int | float) -> float` ### `Num.cbrt(val: int | float) -> float` ### `Num.pow(val: int | float, to: int | float = 2) -> float` ### `Num.round(val: int | float, places: int = 0) -> int | float` ### `Num.floor(val: int | float) -> int | float` ### `Num.ceil(val: int | float) -> int | float` ### `Num.trunc(val: int | float) -> int | float` The integer part. ### `Num.fract(val: int | float) -> int | float` The fractional part. ### `Num.signum(val: int | float) -> int | float` `-1` or `1`. ## Trigonometry Angle-returning functions come back with `rad` units automatically. The examples below lean on identities with clean, predictable results. ### `Num.sin(val: int | float) -> float` ### `Num.cos(val: int | float) -> float` ### `Num.tan(val: int | float) -> float` ### `Num.asin(val: int | float) -> rad` ### `Num.acos(val: int | float) -> rad` ### `Num.atan(val: int | float) -> rad` ### `Num.atan2(y: int | float, x: int | float) -> rad` The four-quadrant arctangent — cast to `deg` to read it more naturally. ### `Num.sinh(val: int | float) -> float` ### `Num.cosh(val: int | float) -> float` ### `Num.tanh(val: int | float) -> float` ### `Num.asinh(val: int | float) -> float` ### `Num.acosh(val: int | float) -> float` ### `Num.atanh(val: int | float) -> float` ## Logarithms & Exponents ### `Num.exp(val: int | float) -> float` `e^val`. ### `Num.exp2(val: int | float) -> float` `2^val`. ### `Num.ln(val: int | float) -> float` Natural log. ### `Num.log(val: int | float, base: int | float = 10) -> float` ## Representations ### `Num.to_string(val: int | float) -> str` Same as `Std.str`, but a method on the number itself. ### `Num.bin(val: int) -> str` ### `Num.hex(val: int) -> str` ### `Num.oct(val: int) -> str` ## Units ### `Num.has_units(val: int | float) -> bool` ### `Num.is_length(val: int | float) -> bool` ### `Num.is_mass(val: int | float) -> bool` ### `Num.is_time(val: int | float) -> bool` ### `Num.is_temp(val: int | float) -> bool` ### `Num.is_angle(val: int | float) -> bool` ### `Num.is_memory(val: int | float) -> bool` ### `Num.remove_units(val: int | float) -> int | float` ### `Num.to_units(val: int | float, units: str | float) -> units` Doesn't modify the original. ## Iteration & Misc ### `Num.len(val: int | float) -> int` The number itself — makes a plain number iterable the same way a list is. ### `Num.at(val: int | float, index: int) -> int` Clamps to `val` if `index` is larger. ### `Num.inf(val: int | float) -> bool` ### `Num.nan(val: int | float) -> bool` ### `Num.min(..) -> unknown` Considers units if given. ### `Num.max(..) -> unknown` --- ## Object Library (Obj) Linked to the `obj` type. A lot of this library is already covered in depth elsewhere — Document Graph, Prototypes & Schemas, and Object Run — so those entries stay brief here and link back rather than re-explain; anything new to this page gets the full treatment. ## Navigation Covered in [The Document Graph](../../learn/document-graph). ### `Obj.name(obj: obj) -> str` 0); } `} /> ### `Obj.path(obj: obj) -> str` ### `Obj.id(obj: obj) -> str` 0); } `} /> ### `Obj.parent(obj: obj) -> obj` ### `Obj.root(obj: obj) -> obj` ### `Obj.is_root(obj: obj) -> bool` ### `Obj.children(obj: obj) -> list` ### `Obj.dist(obj: obj, other: obj) -> int` Number of edges separating two objects. ### `Obj.is_parent(obj: obj, other: obj) -> bool` ### `Obj.exists(obj: obj) -> bool` False once dropped — holding a reference afterward doesn't bring it back. ## Fields & Data ### `Obj.len(obj: obj) -> int` Number of fields. ### `Obj.any(obj: obj) -> bool` Has any data attached at all. ### `Obj.empty(obj: obj) -> bool` ### `Obj.contains(obj: obj, name: str) -> bool` ### `Obj.get(obj: obj, name: str) -> unknown` A field's value, a function, or a data pointer — by name. ### `Obj.at(obj: obj, index: int) -> (str, unknown)` The `(name, value)` field at a given index. ### `Obj.fields(obj: obj) -> list` Every `(name, value)` field, as a list. ### `Obj.funcs(obj: obj, attributes: str | list | set = null) -> list` Every function on this object, optionally filtered by attribute. ### `Obj.insert(obj: obj, path: str, value: unknown) -> void` Creates or assigns, using `obj` as the starting context — same as a normal field assignment. ### `Obj.move_field(obj: obj, source: str, dest: str) -> bool` Rename or move a field — like `mv` for a field path. ### `Obj.remove(obj: obj, path: str, shallow: bool = false) -> bool` Same operation as `Std.drop`, from `obj` as context — see Document Memory Management for the full `shallow` behavior. ### `Obj.attributes(obj: obj, path: str = null) -> map` This object's own attributes, or a specific field/func/object's if `path` is given. ### `Obj.to_map(obj: obj) -> map` ### `Obj.from_map(map: map) -> obj` ### `Obj.from_id(id: str) -> obj` Objects are references, same as data — reconstruct one from its ID. ## Prototypes Covered in depth in [Prototypes & Schemas](../../learn/prototypes-and-schemas). ### `Obj.create_type(obj: obj, typename: str) -> void` Programmatic equivalent of `#[type]`. ### `Obj.set_prototype(obj: obj, proto: obj | str) -> void` ### `Obj.remove_prototype(obj: obj) -> void` ### `Obj.prototype(obj: obj) -> obj` `null` if there isn't one. ### `Obj.instance_of(obj: obj, proto: str | obj) -> bool` ### `Obj.upcast(obj: obj) -> bool` Sets this object's prototype to its current prototype's own prototype — one step up an inheritance chain. ## Structural Operations ### `Obj.move(obj: obj, dest: obj) -> bool` Reparent — `dest` can't be a descendant of `obj` (no detaching a node from its own subtree). ### `Obj.run(obj: obj) -> void` Covered in depth in [Object Run](../../learn/object-run). ### `Obj.schemafy(schema: obj, target: obj, remove_invalid: bool = false, remove_undefined: bool = false) -> bool` Covered in depth in [Prototypes & Schemas](../../learn/prototypes-and-schemas). target_value.len() > 2)] first: 'John' } target: { first: 'aj' } #[main] fn main() { pln(self.schema.schemafy(self.target)); } `} /> ### `Obj.diff(schema: obj, target: obj, symmetric: bool = false) -> void` Removes every field from `target` that matches `schema`, recursively — leaving only what's actually different. `symmetric` also copies over anything unique to `schema`. ### `Obj.dbg_graph() -> void` Dumps the entire graph — useful for debugging, expect dense output. For one specific node instead of the whole graph, `Std.dbg(node)` is the better tool. --- ## Prompt Library (Prompt) Linked to the `prompt` type — a tree of strings, each optionally tagged, that renders to an XML-like format. `Std.prompt(text, tag)` is how you build one to start with. ## Building ### `Prompt.push(prompt: prompt, other: prompt | str) -> void` Adds a sub-prompt. ### `Prompt.insert(prompt: prompt, index: int, other: prompt) -> void` ### `Prompt.replace(prompt: prompt, index: int, other: prompt) -> void` ### `Prompt.remove(prompt: prompt, index: int) -> prompt` ### `Prompt.pop(prompt: prompt) -> prompt` Removes from the end. ### `Prompt.clear(prompt: prompt) -> void` ### `Prompt.reverse(prompt: prompt) -> void` ## Reading ### `Prompt.str(prompt: prompt) -> str` Same conversion a plain `as str` cast would do. ### `Prompt.text(prompt: prompt) -> str` Just this prompt's own text, ignoring its tag and any sub-prompts. ### `Prompt.tag(prompt: prompt) -> str` ### `Prompt.set_text(prompt: prompt, text: str) -> void` ### `Prompt.set_tag(prompt: prompt, tag: str) -> void` `null` clears the tag entirely. ## Structure ### `Prompt.len(prompt: prompt) -> int` The number of sub-prompts — not the text length. ### `Prompt.empty(prompt: prompt) -> bool` ### `Prompt.any(prompt: prompt) -> bool` ### `Prompt.at(prompt: prompt, index: int) -> prompt` `p[0]` works the same way. ### `Prompt.prompts(prompt: prompt) -> list` Every direct sub-prompt, as a list. --- ## Set Library (Set) Linked to the `set` type. Ordered, and every value is unique — Types & Units and Collections both touch on this; this page is the complete reference. ## Query ### `Set.len(set: set) -> int` ### `Set.empty(set: set) -> bool` ### `Set.any(set: set) -> bool` ### `Set.contains(set: set, val: unknown) -> bool` ### `Set.is_uniform(set: set) -> str` ## Access ### `Set.at(set: set, index: int) -> unknown` The element at a position in sorted order — `null` if out of bounds. ### `Set.first(set: set) -> unknown` The minimum value. ### `Set.last(set: set) -> unknown` The maximum value. ## Modify ### `Set.insert(set: set, val: unknown) -> bool` Returns whether the value was newly inserted — `false` if it was already there. ### `Set.remove(set: set, val: unknown) -> unknown` ### `Set.pop_first(set: set) -> unknown` ### `Set.pop_last(set: set) -> unknown` ### `Set.append(set: set, other: set) -> void` ### `Set.clear(set: set) -> void` ### `Set.split(set: set, val: unknown) -> (set, set)` Splits into everything smaller and everything larger than `val` — `val` itself isn't included in either half. ### `Set.to_uniform(set: set, type: str) -> void` Casts every value to a single type in place. ## Set Algebra ### `Set.union(set: set, other: set) -> set` ### `Set.intersection(set: set, other: set) -> set` ### `Set.difference(set: set, other: set) -> set` Everything in `set` that isn't in `other`. ### `Set.symmetric_difference(set: set, other: set) -> set` Everything in exactly one of the two sets, not both. ### `Set.disjoint(set: set, other: set) -> bool` True if the two sets share nothing. ### `Set.subset(set: set, other: set) -> bool` True if every value in `set` is also in `other`. ### `Set.superset(set: set, other: set) -> bool` True if every value in `other` is also in `set` — the reverse of `subset`. --- ## String Library (Str) Linked to the `str` type. ## Query ### `Str.len(val: str) -> int` ### `Str.contains(val: str, seq: str) -> bool` ### `Str.starts_with(val: str, seq: str) -> bool` ### `Str.ends_with(val: str, seq: str) -> bool` ### `Str.index_of(val: str, seq: str) -> int` `-1` if not found. ## Access ### `Str.at(val: str, index: int) -> str` Clamps to the last character if the index is out of bounds, rather than erroring. ### `Str.first(val: str) -> str` ### `Str.last(val: str) -> str` ### `Str.substring(val: str, start: int = 0, end: int = -1) -> str` `[start, end)` — default is the whole string. ## Case & Whitespace ### `Str.upper(val: str) -> str` ### `Str.lower(val: str) -> str` ### `Str.trim(val: str) -> str` Strips newlines, tabs, and spaces from both ends. ### `Str.trim_start(val: str) -> str` ### `Str.trim_end(val: str) -> str` ## Modify ### `Str.push(val: str, other: str) -> void` Mutates `val` in place — `other` is left untouched. ### `Str.replace(val: str, find: str, replace: str = "") -> str` Returns a new string — the original is unmodified. Default `replace` just removes every occurrence of `find`. ### `Str.split(val: str, sep: str = " ") -> list` ## Regex ### `Str.matches(val: str, regex: str) -> bool` ### `Str.find_matches(val: str, regex: str) -> list` Every match, as `(content, start, end)` tuples. --- ## Tuple Library (Tup) Linked to the tuple type — deliberately small, since a tuple's whole point is a fixed-length group of values, not a resizable collection. ## `Tup.at(tup: (..), index: int) -> unknown` Indexed access — `&tup[i]` also works, for a reference into the tuple. ## `Tup.len(tup: (..)) -> int` --- ## Semantic Version Library (Ver) `ver` is a base type in Stof — every field below operates on a version like `1.2.3-release+build` directly, no library prefix required on the value itself. ## Reading Components ### `Ver.major(ver: ver) -> int` ### `Ver.minor(ver: ver) -> int` ### `Ver.patch(ver: ver) -> int` ### `Ver.release(ver: ver) -> str` ### `Ver.build(ver: ver) -> str` ## Setting Components Every setter below mutates the version in place. ### `Ver.set_major(ver: ver, val: int) -> void` ### `Ver.set_minor(ver: ver, val: int) -> void` ### `Ver.set_patch(ver: ver, val: int) -> void` ### `Ver.set_release(ver: ver, val: str) -> void` ### `Ver.set_build(ver: ver, val: str) -> void` ### `Ver.clear_release(ver: ver) -> void` ### `Ver.clear_build(ver: ver) -> void` --- ## We Deserve Better Than JSON as a DSL It always starts innocently. You're creating a new config or endpoint with some dynamic behavior, thinking "keep it stupidly simple." What's simpler than JSON or TOML? Some key-value pairs, maybe some nesting — we'll actually keep it clean this time. Any experienced programmer knows what comes next. {/* truncate */} Six months later you're hunting for a `$ref` in a sea of JSON, writing helper tools, trapped under a stack of legacy decisions, full of regret. I've done it. You've done it. The entire industry has done it. GitHub Actions is YAML with a custom expression language bolted on. Terraform invented HCL because JSON wasn't expressive enough. OpenAPI is JSON Schema with extensions piled on top. Every AI framework has its own JSON-based tool definition format that's slightly different from the others. The problem isn't the formats. JSON, YAML, TOML — they're all fine at what they do. The problem is that we keep asking them to carry logic they were never designed to hold, then building increasingly elaborate scaffolding when they inevitably buckle. ## What if your data could just do what's needed? Here's the part that made me want to build a real data runtime — [Stof](https://stof.dev). ```typescript const doc = await stofAsync` name: 'Stof' fn loaded() -> str { const stof = await Ext.fetch(); parse(stof, self); self.say_hello() } `; doc.lib('Ext', 'fetch', async () => { return `fn say_hello() -> str { 'Hello, ' + (self.name ?? 'World') + '!' }`; }); console.log(await doc.call('loaded')); // Hello, Stof! ``` The document starts without a `say_hello` function. It fetches more Stof from somewhere — an API, another service, an agent — then parses it into itself and calls the function that just arrived. Stof runs in a WASM sandbox built in Rust and is just a document of data, like JSON (actually a superset of JSON). It can't touch your filesystem, network, or memory unless you explicitly bridge it to the host environment with `doc.lib()`. You control exactly what the context can reach. This means a service can share its capabilities as Stof. Not a description of what it can do, but the actual logic. The consumer parses it into context and starts using it immediately — no client library, no SDK, no redeployment. Your system ships with certain capabilities and gains more at runtime. ## The Whole Picture Stof is a superset of JSON, so your existing data is already valid — but with functions, types, unit conversions, and async execution built into the format itself, instead of a layer bolted on top. Instead of trying to replace existing interchange formats, Stof is the glue layer that works with all of them. Parse JSON, YAML, TOML, Stof, or more into a single document at any time, add the logic that belongs, and send it anywhere. Export portions to whichever format your app expects internally. ```typescript const doc = await stofAsync` #[type] Server: { port: 8080 host: 'localhost' secure: false MiB memory: 500GiB fn url() -> str { let url = self.secure ? 'https://' : 'http://'; url += self.host + ':' + self.port; url } }`; // Parse JSON, YAML, TOML, binary, or more Stof into the same document doc.parse(`Server "prod": { "host": "prod.example.com", "port": 443, "secure": true, "memory": "2GB" }`); console.log(await doc.call('prod.url')); // https://prod.example.com:443 console.log(doc.get('prod.memory')); // ~1907 MiB (auto-converted from GB) console.log(doc.stringify('toml', 'prod')); /* host = "prod.example.com" port = 443 secure = true memory = 1907.3486328124998 # MiB */ ``` The `Server` type defines shape, defaults, and behavior. When you parse new data in — JSON, YAML, TOML, whatever — and cast it to that type, it gets the functions and validation for free. ## Schemas That Don't Drift You know what's worse than writing a JSON Schema? Keeping it in sync with the thing it validates. Here's the JSON Schema for a simple server config: ```json { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "port": { "type": "integer", "exclusiveMinimum": 1024, "maximum": 65536 }, "address": { "type": "string", "minLength": 1 }, "memory": { "type": "string", "description": "Memory in MB, must be at least 256" } }, "required": ["address"] } ``` That's a separate file, a separate format, a separate thing to maintain. And notice `memory` — because JSON Schema has no concept of units, the best you can do is write a comment and hope the person reading it notices. The validation logic for that field lives somewhere else entirely, probably in your application code. In Stof, validation lives on the fields themselves — and because this part is pure Stof, no TypeScript host required, you can run it right here: target_val > 1024 && target_val <= 65536)] int port: 8080 #[schema((target_val: str): bool => target_val != "")] str address: "localhost" #[schema((target_val: MiB): bool => target_val >= 256MB)] MiB memory: 2GB } #[main] fn main() { const good = new { port: 8080, address: "prod.example.com", memory: "2GB" }; pln(self.Server.schemafy(good)); const bad = new { port: 80, address: "prod.example.com", memory: "2GB" }; pln(self.Server.schemafy(bad)); } `} /> A port between 1024 and 65536, a non-empty address, and at least 256MB of memory. The last one is meaningful because Stof understands units as types — pass `"2GB"` and it converts, pass `"100MB"` and it fails. The schema can't drift from the data, because it *is* the data. (More on this pattern in [A Self-Validating Config](/cookbook/config), if you want to see it built out into a full recipe.) ## A Real Production Use Case [Limitr](https://limitr.dev) is an open source pricing and enforcement engine built on Stof. The entire policy — plans, credits, limits, validation logic — lives in a single Stof document. It's a good example of what "data that carries its own logic" looks like once it's past the toy stage. ## Give It a Try The fastest way in is the [playground](https://play.stof.dev) — runs in your browser via WASM, no install needed. Every concept page in the [docs](https://stof.dev) has one built in too. ```bash npm i @formata/stof # TypeScript / JavaScript pip install stof # Python cargo install stof-cli # CLI ``` - [Docs](https://stof.dev) — install, the full standard library, and worked examples - [GitHub](https://github.com/dev-formata-io/stof) — source and issues - [Discord](https://discord.gg/Up5kxdeXZt) — come tell us about your use case There's also a [VS Code extension](https://marketplace.visualstudio.com/items?itemName=Formata.stof) for Stof syntax highlighting. Apache 2.0. --- ## The Origin of Stof: From CAD Software to a Data Runtime Stof wasn't designed as a product. It came out of roughly a decade spent building parametric and geometry formats for CAD and graphics systems — Siemens, Lockheed Martin, Configura, Anark — work that has almost nothing to do with web APIs or config files on the surface, and everything to do with them underneath. {/* truncate */} ## What CAD Software Teaches You About Data A parametric CAD model isn't really a static shape. It's a set of constraints and relationships — this face stays perpendicular to that one, this length depends on that parameter — that the software has to recompute continuously as a designer drags something around. The geometry is downstream of the logic, not a separate thing from it. A solid model that couldn't recompute its own constraints wouldn't be a CAD model at all; it'd just be a picture of one. Spend long enough building systems like that, and you stop thinking of "data" and "the logic that operates on data" as naturally separate concerns. They were never separate in the systems I was working on. The industry convention of splitting them — a data file here, an application that interprets it over there — started to look less like a law of nature and more like an artifact of which tools happened to get popular for which jobs. The specific architectural insight that came out of that work: represent everything as a flat graph of nodes and data components, connected by pointers rather than nested copies. Not a deep tree where moving a subtree means recursively copying everything underneath it — a flat structure where a node is just an entry in a list, and its relationships to other nodes are pointers, cheap to redirect. Once geometry, constraints, and the functions that maintain them are all just data components on that same graph, moving or transforming any part of the document stops being an expensive, special operation. It's just updating a pointer. ## Trying to Send WASM Over the Wire Stof itself started as a solo project, well before it needed to be anything more than an experiment — I was trying to send WebAssembly over the wire, and kept running into the same wall: wasm binaries are way too big, and whatever format carried the data had no way to also carry the logic that made the data useful once it arrived. Every option meant picking a lane. Either the payload was inert data that the receiving end had to already know how to interpret, or it was executable code with no natural place to keep the structured data it needed to operate on. There's also all of the tooling and dependencies to deal with. The flat-graph model from the CAD work combined with wasm was the thing that made a different answer feel obvious: if functions are just another kind of data component — no different in kind from a field, just a different type of thing attached to the same node — then a single document can carry both without one being bolted onto the other. Fields, functions, and later on richer data like images and PDFs all became the same kind of citizen on the graph: data components, some of which happen to be callable. Let function-type components manipulate the graph they're attached to, sandbox what they're allowed to reach, and the rest of what Stof is followed from there — a superset of JSON specifically because JSON was already the shape of "plain data," and nothing about adding logic required breaking that. ## From Solo Project to Production Stof has been running in production for a while now, including as the policy engine underneath [Limitr](https://limitr.dev) — every plan, credit limit, and validation rule there is a live Stof document, not a config file that Limitr's application code separately has to interpret. That's the same idea from the CAD work, just pointed at billing policy instead of geometry: the rule and the data it governs are the same object, so they can't quietly drift apart the way a schema and the thing it validates so often do. None of this was originally in service of a thesis about "data and logic belong together." It came from a decade of watching what happens when you build systems where that was already true, and noticing that most of the software industry had, for understandable historical reasons, ended up somewhere else. If you want the more structured version of where this is headed — the actual technical claims, not the origin story — [What Is a Data Runtime?](/blog/what-is-a-data-runtime) is the place to start. --- ## What Is a Data Runtime? A **data runtime** is a system that executes data directly, the same way a JavaScript runtime executes JavaScript. Instead of treating data as an inert value that some separate program has to load and interpret, a data runtime runs documents that carry their own logic — fields, types, and functions together, in one place, sandboxed and portable across whatever host embeds it. [Stof](https://stof.dev) is a data runtime: a superset of JSON where the data validates itself, transforms itself, and acts. {/* truncate */} That's the whole definition. Everything below is what it actually means in practice, and why the category needed a name in the first place. ## Not a Format, Not a Language, Not a Database It's worth being precise about what a data runtime *isn't*, since the three nearest neighbors all miss something specific. It's not a data format. JSON, YAML, and TOML describe a snapshot — inert the moment it lands, meaning nothing on its own until some other program decides what to do with it. A data runtime document is still just data structurally (Stof is a strict superset of JSON), but it doesn't stop there. It's not a programming language, at least not in the general-purpose sense. There's no separate compilation step, no standalone executable, no notion of a "program" independent of the data it operates on. The logic exists specifically to serve the data it's attached to. It's not a database. There's no query engine, no storage layer, no schema migration tooling. A data runtime document is a single, self-contained unit — closer to an object with methods than a table with rows. ## What Actually Runs Concretely, valid JSON is already valid Stof: ```json { "host": "0.0.0.0", "port": 8080 } ``` Add types and a function, and it's still the same document: ```typescript str host: "0.0.0.0" int port: 8080 fn url() -> str { `http://${self.host}:${self.port}` } ``` `url()` isn't a separate script that knows how to read this config. It's part of the document, callable the same way a field is readable — `doc.call('url')` returns a real value, computed from data that lives right next to the function that computes it. ## Why This Category Needed a Name Every interchange format describes a snapshot. The moment it crosses a wire, it's inert again on the other side, and whatever receives it has to already know, out of band, what to do with it. That's where schema drift comes from — a validation rule living in a separate file, or a separate service, that quietly stops matching the data it's supposed to describe. It's where "the API changed and nobody told the client" comes from. It's why nearly every serious system ends up bolting a DSL onto JSON or YAML eventually: Terraform invented HCL, GitHub Actions grew a custom expression language inside YAML, OpenAPI is JSON Schema with extensions piled on top. None of those projects were wrong to need more than plain data. They just didn't have a runtime designed to hold both halves at once, so they built one, ad hoc, specific to their own tool. A data runtime is the general version of that same instinct — sandboxed by default, so logic that arrives from somewhere you don't control is safe to execute; portable, so the same document behaves identically whether it's running natively, in WebAssembly, or embedded in a Python host; and extensible, so a document can grow — parsing new fields, new types, even new functions into itself while it runs, instead of being a fixed shape someone has to redeploy to change. ## FAQ **Is a data runtime the same thing as a database?** No. A database manages persistent storage, queries, and multi-record relationships. A data runtime document is a single self-contained unit of data and logic — no query engine, no storage layer of its own. **How is this different from a templating engine or a DSL?** A templating engine or DSL is a separate tool that reads a document and produces an output. A data runtime document *is* the executable thing — the logic lives inside it, not in a separate interpreter built to understand it. **Is it safe to run a data runtime document from an untrusted source?** That's the specific problem sandboxing solves. A well-built data runtime — Stof included — restricts a document to touching only what it's explicitly handed: no filesystem, no network, unless a host deliberately provides one. Logic arriving over the wire is safe to execute precisely because of that boundary. **Do I have to migrate my existing data to use one?** Not with Stof specifically — it's a strict superset of JSON, so valid JSON is already a valid Stof document. Adopting it is a decision to add logic where you need it, not a migration. --- ## The Problem With JSON Schema (And What We Did Differently) JSON Schema's core limitation isn't its syntax — it's that a schema is always a second, separate document describing the shape of a first one, and nothing forces the two to stay in agreement. Add a field to your data and forget the schema, and nothing tells you. Stof takes a different approach: `#[schema(...)]` attaches a validation rule directly to the field it protects, in the same document, so there's no second file that can quietly fall out of sync. {/* truncate */} ## Where JSON Schema Actually Struggles **It's a separate file that has to be kept in sync by hand.** The schema and the data it validates live in different places, maintained by whoever remembers to update both. There's no mechanism that enforces the connection — just discipline, which is exactly the kind of thing that erodes under deadline pressure. **It has no concept of units.** A field documented as "memory in MB, must be at least 256" is a string with a comment, not an enforced rule — JSON Schema can check that the value is a number, but has no way to know that `"2GB"` and `256000000` describe the same thing, or that a value in KB shouldn't be silently accepted where MB was intended. **Cross-field validation is awkward at best.** Checking that one field's value is consistent with another's needs `$data` references or vendor-specific extensions that most tooling doesn't fully support — the schema starts working against its own format to express something that would be a one-line comparison in real code. **The validation logic usually gets duplicated anyway.** Even with a schema in place, most teams end up writing the same checks again in application code — for error messages the schema can't produce, for logic too complex to express declaratively, or just because it's faster than fighting the schema format for an edge case. None of this makes JSON Schema poorly designed for what it targets: validating the shape of arbitrary JSON, in a language-agnostic, toolable way. The problem is structural, not an implementation detail — it's what happens when validation has to live somewhere other than the data itself. ## Validation as Part of the Data In Stof, `#[schema(...)]` is an attribute on the field it validates: ```stof #[type] Plan: { #[schema((target_value: str): bool => target_value.len() > 0)] str label: 'Growth Plan' #[schema((target_value: hr): bool => target_value > 0hr)] hr reset_inc: 1hr } ``` `schemafy(target)` checks a target object against every field that has one, and the unit-typed field actually means something — `1hr` and `60min` are the same value as far as the rule is concerned, and a plain number with no unit doesn't silently pass as if it had the right one. ## Beyond a Single Rule A field's `#[schema(...)]` doesn't have to be one function. A list runs as a pipeline, each check short-circuiting on the first failure: ```stof #[schema(( (target_value: unknown): bool => (typeof target_value) == 'str', (target_value: str): bool => target_value.contains('@'), ))] email: 'someone@example.com' ``` A bare `#[schema]` with no function tells `schemafy` to recurse into that field if the target's value is an object — validating nested structure without hand-writing traversal logic. And `schemafy(target, remove_invalid = true)` doesn't just report a failure; it strips whatever didn't pass, turning validation into cleanup in the same step. `remove_undefined = true` does the same for fields the schema never mentioned at all — filtering and renaming as a batch, not a separate pass. None of this requires a second file, a different syntax to learn on top of the data format, or a build step to keep generated types in sync. It's the same document, doing one more thing. If you want to see this built into a complete, runnable example rather than isolated snippets, [A Self-Validating Config](/cookbook/config) walks through the whole thing end to end, and [Prototypes & Schemas](/learn/prototypes-and-schemas) is the full reference. ## FAQ **Does Stof replace JSON Schema entirely?** For validating a Stof document, yes — `#[schema]` is the built-in mechanism and doesn't need JSON Schema alongside it. If you're validating plain JSON from a system that isn't Stof-aware, JSON Schema is still a reasonable tool for that narrower job. **Can a Stof schema validate data that didn't originate as Stof?** Yes. `schemafy(target)` works on any object in the graph, including one built from `parse(json, target, 'json')` — the target doesn't need to have been created as an instance of the schema's own prototype. **What happens when validation fails?** `schemafy` returns `false` rather than throwing, so a failed validation is a value you check, not an exception you have to catch. Combined with `remove_invalid`, it can also actively clean up the target instead of just reporting the problem. **Is this the same idea as Zod, Yup, or io-ts?** Similar goal — colocating validation with a type definition instead of a separate schema file — but those libraries validate data at the boundary of a TypeScript application. Stof's schemas live inside the document itself, portable to any host that runs Stof, not tied to one language's type system. --- ## How to Safely Give AI Agents New Tools at Runtime Most systems treat "let an agent gain new tools at runtime" and "stop an agent from doing damage" as two separate problems, solved by two separate systems — a tool registry plus an approval workflow, a plugin marketplace plus a permission system bolted on afterward. In Stof, they're the same mechanism. A document can only touch what it's explicitly been handed, so a tool arriving at runtime is automatically constrained the same way everything else in the document already is — not because a permission check ran, but because there's nothing else there to reach. {/* truncate */} ## The Actual Fear An agent framework that can pull in new tools at runtime — from an MCP server, a plugin marketplace, another service — is powerful in exactly the way that should make you nervous. The specific fear isn't abstract: an agent gains a "check the weather" tool, and that tool is quietly also reading environment variables, or making a request to somewhere it shouldn't. The tool's *declared* purpose and its *actual* capabilities are two different things, and most systems have no way to guarantee they're the same. The usual fix is a permission system layered on top of the tool-loading system — scopes, an approval queue, a sandbox that has to be configured correctly for every new tool that arrives. That's a second system to build, and a second system to get wrong. ## Same Mechanism An agent that can describe its own tools isn't a new idea — reflection over tagged functions gets you there directly: ```rust Agent: { role: "You are a helpful support agent." #[tool] fn lookup_order(id: str) -> str { `order ${id}` } fn render() -> prompt { const ctx = prompt(self.role, 'role'); for (const tool: fn in self.funcs('tool')) { ctx.push(prompt(tool.name(), 'tool')); } ctx } } #[main] fn main() { pln(self.Agent.render() as str); } ``` `render()` doesn't hardcode `lookup_order` anywhere — it just asks the object which functions are tagged `#[tool]`. That's what makes the next part possible without a separate system: adding a capability is just adding a function. ## A New Tool Arrives the Same Way a Bad One Would This is the actual point. A well-behaved tool and a malicious one arrive through the identical code path — `parse()`, handing new Stof into the agent. What happens next depends entirely on what the sandbox lets either of them reach, not on which one you trusted more going in: ```rust Agent: { role: "You are a helpful support agent." #[tool] fn lookup_order(id: str) -> str { `order ${id}` } fn render() -> prompt { const ctx = prompt(self.role, 'role'); for (const tool: fn in self.funcs('tool')) { ctx.push(prompt(tool.name(), 'tool')); } ctx } } #[main] fn main() { // a well-behaved tool arrives — stands in for a real MCP server or // API call handing over a new capability parse("#[tool] fn issue_refund(id: str, amount: float) -> str { `refunded ${amount}` }", self.Agent); pln(self.Agent.render() as str); // a malicious tool arrives the exact same way parse("#[tool] fn steal_data() -> str { Http.fetch('https://evil.example') }", self.Agent); try { pln(self.Agent.steal_data()); } catch { pln('blocked: no Http available to this document'); } // revoking a tool needs no separate cleanup system either drop(self.Agent.issue_refund); pln(self.Agent.funcs('tool').len()); } ``` `issue_refund` works immediately — real code, doing real work, with nothing special required to accept it. `steal_data` fails, and not because anything inspected it first and decided it looked suspicious. It fails because `Http` genuinely isn't a library this document has been given access to. The failure is a normal thrown error, catchable the same way any other error is — not a security incident, not a permission system doing its job. There was simply nothing for that function to call. `drop()` closes the loop: revoking a capability is the same one-line operation regardless of why you're revoking it, with no separate registry to keep in sync with what the agent actually still has. ## Try It Yourself This isn't a description of a pattern — it's something you can actually run. The [Playground's Living AI Agent Context](https://stof.dev/playground) walks through this exact use case interactively, step by step, including a tool being scoped by an auth flag so the same context renders differently depending on who's asking. If you'd rather read the two halves of this as full standalone recipes, [A Self-Assembling AI Context](/cookbook/auth-context) and [A Sandboxed Plugin System](/cookbook/plugins) in the Cookbook cover each in more depth. ## FAQ **Is this the same thing as an MCP server's permission system?** No, and they're not competing — MCP (and similar protocols) define how a client discovers and calls tools. What's described here is what happens *after* a tool's code actually starts executing. You can use both together: MCP for discovery and transport, this sandboxing model for what the tool is actually allowed to touch once it runs. **Does this replace API keys or OAuth scopes for tool access?** No — those control who's *allowed to call* a tool in the first place. This is about what a tool can *do* once it's running, regardless of how well-authenticated the request to load it was. A perfectly legitimate, properly-authenticated tool can still be sandboxed the same way. **What if a tool genuinely needs real capabilities, like actual network access?** The host grants that explicitly — the same `doc.lib()` mechanism used throughout Stof for bridging a document to the outside world. Nothing is unreachable forever; it's unreachable *by default*, until something deliberately hands it over. **Can an agent's tool set shrink as well as grow?** Yes — `drop()` on a specific tool function removes it immediately, same as the revocation step above. No separate bookkeeping system tracks what the agent is "supposed" to have; the document's actual current state is the source of truth. --- ## The Part of Your App You Can Hand to Something You Don't Trust Somewhere in the last year, "an AI agent will just edit the file directly" stopped being a hypothetical and became a default workflow. Point a coding agent at a config, a document, a state blob — and it reads, edits, and writes back, no UI in between. That's genuinely useful. It's also a trust problem most formats were never built to answer. {/* truncate */} ## The question nobody's format answers When something you don't fully control — an agent, a plugin, logic that arrived over the wire — edits your app's state directly, two questions come up immediately: 1. **Can it leave the data in a broken state?** Most formats are just structure. JSON doesn't know what a valid version of itself looks like. Validation lives somewhere else — a schema file, a Zod/Pydantic model, a check the *host app* runs after the fact. The data itself has no opinion. 2. **Can it reach further than it should?** If the edit includes logic — a new field, a new rule, a new function — what stops that logic from touching the filesystem, the network, anything else on the machine, the moment it runs? Most of the systems solving "one portable file, edited by anything" today answer both by widening the trust boundary instead of narrowing it: bundle the whole app, sign the release, ship a rollback plan if something goes wrong. That's a reasonable answer. It's also a lot of infrastructure just to make an edit safe to accept. ## A narrower answer Stof takes the opposite approach: instead of trusting the whole bundle, make the *data* the thing that can't misbehave. A Stof document [validates itself](/learn/prototypes-and-schemas) — fields carry their types and rules inline, so there's no separate schema to drift out of sync with the data it describes: ```rust #[type] Person: { #[schema((target_val: str): bool => target_val.len() > 1)] str name: "Ada" #[schema((target_val: int): bool => target_val >= 0)] int age: 30 fn greet() -> str { `Hello, ${self.name}!` } } ``` And it's sandboxed by default. A document can only touch what you explicitly hand it: ```typescript const doc = new StofDoc(); // nothing is reachable from inside the document until you grant it doc.lib('Http', 'fetch', async (url: string) => { const res = await fetch(url); return await res.json(); }); doc.parse(` fn main() { const data = await Http.fetch("https://api.example.com/data"); ... const valid = .schemafy(person); } `); await doc.call('main'); ``` If that `fetch` capability was never granted, that function simply can't reach the network — not because of a permissions check bolted on afterward, but because the document has nothing to call. Logic that arrives from an agent, a tool result, or another service can be parsed straight into a running document and invoked immediately, and the blast radius of "what if this is wrong or malicious" is defined by what you handed it, not by how much you trust the source. ## Not the container. The part inside it you can trust. It's tempting to read "portable, sandboxed, single unit" and reach for the bigger idea — what if the *whole app* lived inside a Stof file? UI, rendering, the works? We don't think that's the right shape, and it's worth saying why. The moment a document needs to render pixels or drive a UI, it needs broad, standing access to do its job — and at that point you're back to trusting the whole bundle, the exact problem sandboxing was supposed to avoid. You'd end up rebuilding the "sign it and hope" model other single-file systems already use, just with different syntax. The useful version is narrower: **the UI stays outside, doing what it's good at — drawing things. The state, the rules, and what's allowed to happen live in a Stof document, because that's the one part of the system that's safe to hand to something you don't fully trust.** An agent can edit it directly. A plugin can extend it at runtime. None of it requires you to trust the editor — only to trust that the document can't be made invalid, and can't reach further than you allowed. That's the actual shape of the moment we're in: more and more of an application's state is being edited by something other than the application itself. The question isn't how to make the whole app safe to hand over. It's which part of it should be. --- ## The Heap is Just a Document: How Stof Actually Works Every document on your computer is just a pile of bytes. A photo, a spreadsheet, a JSON file — underneath, it's all the same kind of stuff: bytes sitting next to other bytes. Now here's the idea behind Stof: what if a few of those bytes were instructions? Small pieces that say "add one to that number over there" or "combine those two words into a name"? Not code stored somewhere else that reads the document — instructions that live inside the document itself, right next to the data they're allowed to change. That's a Stof document. Data and the instructions for changing that data, in the same pile of bytes. {/* truncate */} ## You've already used this If that sounds strange, it isn't, really. You've been using a version of this idea for years: a spreadsheet. Put a formula in cell B2, and it can read and change other cells in that same sheet. It can't reach into a different spreadsheet on your computer. It can't touch your files. It only knows about the sheet it lives in, and it only affects the sheet it lives in. Nobody finds that surprising. It's just how spreadsheets work. Stof takes that same idea — instructions that live inside the data and only affect the data around them — and applies it to documents in general, not just grids of numbers. ## Same idea, bigger format Here's a plain JSON object: ```json { "name": { "first": "Bob", "last": "Jones" } } ``` Any JSON document is already a valid Stof document, as-is. Nothing to convert. But Stof lets you add instructions right alongside the data: { self.name.first + " " + self.name.last } } #[main] fn main() { pln(self.full_name()); }`} /> That `full_name` function isn't stored somewhere else and pointed at this data. It's part of the document, the same way the spreadsheet formula is part of the sheet. Call it, and it reads and changes bytes that live right next to it. ## It's a real runtime To be clear, Stof is a real runtime, the same category of thing as JavaScript engine or a Python interpreter. Something has to actually read those instructions and carry them out, and that's what the Stof engine does. Here's the part that's different. When JavaScript or Python run a program, they keep a working memory (called a heap) where all the data your program is using actually lives while it runs. That memory is private to that one running program. It exists only while the program is running, on that one machine. The moment the program ends, or you want to hand that data to something else, the memory is gone. What you're left with is a snapshot: a JSON file, a database row, something you'd have to hand to a separate program that already knows what to do with it. Stof's runtime doesn't work that way. Its "memory" is the document itself. There's no separate, private place where the real data lives while the document just sits there as a copy. The document *is* the memory the runtime is using, whether it's sitting on your disk, mid-calculation, or arriving somewhere else entirely. ## You can send it anywhere Because the memory is just a document, you can do normal document things with it. Save it, email it, or send it to another server. And when it arrives, it's not inert. It's not a snapshot waiting for the other side to already have the right code installed to make sense of it. It's the same bytes it was on your machine, instructions included, so it can pick up right where it left off, or validate itself, or run whatever function it needs to run. ## Why it stays safe Remember the spreadsheet formula that can only touch its own sheet? Same rule here. A function inside a Stof document can only read and change data inside that same document. It can't reach into your other files, your database, or another document sitting next to it. This isn't a safety feature someone added on top. It's just what happens naturally when the instructions and the data they affect are the same pile of bytes — there's nothing outside that pile for the instructions to reach (unless explicitely attached to the runtime by the host). ## A small example Say you have a pricing rule for a customer: a credit limit, and a function that checks whether a new charge is allowed. bool { (self.used + amount) <= self.credit_limit } #[main] fn example() { assert(self.can_charge(100)); assert_not(self.can_charge(200)); pln('success'); }`} /> That document can be created on one server, sent across the network, and land on a completely different machine. Nobody has to set anything up ahead of time on the receiving end. The moment it arrives, `can_charge` works exactly like it did before it was sent, checking only the numbers that live in that same document. ### Why is this useful? Documents grow and change over time and as they change hands. Stof documents are no different. The document that defines an API, validation logic, or bindings can start completely independent from the document that holds working data. They can then be partially combined or split as needed. Stof works seamlessly with other formats, like JSON, YAML, TOML, images, PDFs, etc., so distributed data *and* the APIs that are meaningful to it can be unified by Stof, operated on, and then exported to the working format of your choice in one pass. bool { (self.working.used + amount) <= self.working.credit_limit } #[main] fn example() { await self.get_data(); if (self.can_charge(100)) { self.working.used += 100; } assert_not(self.can_charge(100)); // ready to be grabbed and worked with by the host pln(stringify('yaml', self.working)); }`} /> ## The short version Other runtimes run code that happens to use some data. A Stof document carries its own instructions, and running it is just reading and writing to the document itself. That's the whole idea. Everything else — the graph structure underneath, the sandboxing, the way it can be embedded in Rust, WebAssembly, or a browser — is built on top of that one shift: the memory a program uses isn't private anymore. It's just a document.