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:
int count: 42
float ratio: 3.14
bool active: true
str label: "v1"
blob greeting: |104, 101, 108, 108, 111|
#[main]
fn main() {
let hex = 0xFf;
pln(self.count, self.ratio, self.active, self.label, hex as float);
pln(self.greeting.utf8());
}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:
#[type]
Server: {
port: 8080
}
// 'Server' type instead of 'obj' will cast
Server instance: {}
#[main]
fn main() {
pln(typeof self, typename self, typeof self.main);
pln(typeof self.instance, typename self.instance, self.instance.port);
}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:
list scores: [90, 85, 100]
map ages: map(("Ada", 30), ("Grace", 34))
set tags: set('core', 'stof', 'core')
(str, int, bool) row: ("alpha", 1, true)
#[main]
fn main() {
assert_eq(self.scores.len(), 3);
assert_eq(self.ages.get('Ada'), 30);
assert_eq(self.tags.len(), 2);
assert_eq(self.row[0], 'alpha');
pln('passing');
}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:
m height: 135cm + 5ft + 123in
#[main]
fn main() {
pln(self.height, 'meters');
pln((self.height as km).round(6), 'km');
pln((self.height as ft).round(2), 'ft');
}The same system covers time, memory, temperature, mass, and angles:
#[main]
fn main() {
const one_day: hours = 1day;
const half_gig: MB = 512MiB;
pln(one_day, typename one_day);
pln(half_gig, typename half_gig);
}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:
#[main]
fn main() {
const val = 12km + 34s;
pln(val, typeof val, val.has_units());
}Semantic Versions
ver is a built-in type for semver values, comparable and queryable directly:
ver release: 1.2.3-beta+build
#[main]
fn main() {
pln(self.release, self.release.patch());
pln(self.release > 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:
fn identify(v: unknown) -> 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.
greeting: "hello"
second: {
third: {}
}
#[main]
fn main() {
const handle: data = Data.field('self.greeting');
assert(handle.exists());
// Any data component can be instanced across the graph (multiple nodes at once)
handle.attach(self.second);
handle.attach(self.second.third);
handle.drop_from(self);
for (const prnt in handle.objs()) pln(prnt.path());
}This is also the type behind rich, library-backed values — a loaded PDF or image comes back as Data<Pdf> or Data<Image> rather than a plain scalar. Those are a Libraries-tab topic, not this page's, but it's the same underlying mechanism.