Skip to content

The API

Vex has three packages. @mark1russell7/vex has the kernel and the builder, and it has no runtime dependencies. @mark1russell7/vex-domains has the domains of the site. @mark1russell7/vex-testkit has the property-test tools.

import { space, vex } from "@mark1russell7/vex";
import { NumDomain, Vec2Domain } from "@mark1russell7/vex-domains";
const root = vex(Vec2Domain, NumDomain).over(space.record({ A, B, C }));
const nearest = root.from("position").others((e) => e._.subtract("position")._.length()).min();
nearest.at("A"); // : Optional<number>
nearest.result("A"); // : Result<number>
nearest.explain("A"); // : Trace
nearest.all(); // : Traversal<"A" | "B" | "C", number>
nearest.program; // : Expr
nearest.at("A"); // => some(5)

vex(...domains) gives the entry point for a list of domains. The interpreter finds the op of a value in the first domain that accepts the value. vex(...).withOptions({ fns, extensions }) adds free functions and extension handlers. A chain applies a free function with call:

const withHalf = vex(NumDomain).withOptions({ fns: { half: (n: number) => n / 2 } }).over(space.record({ A }));
withHalf.from("weight").call("half").at("A"); // => some(1)
withHalf.from("weight").call("half").at("A"); // : Optional<number>
Member What it gives
from(field) A chain whose value is the field at the focus. The types accept only the fields of the records.
start(value) A chain whose value is value. A string is a field reference.
of(key, field) An argument: the field of the record at key
ofPath<T>(key, path) An argument: a dotted path at key, with the type T that you give
field<T>(path) An argument: a dotted path at the focus
lit(value) An argument: a literal. Use it for a string or an array, because a bare string is a field reference.
rec(fields) An argument: a record of argument values
ext<T>(kind, data) An argument: the node ext(kind, data). The extension handler of kind gives its value.
sheet() A sheet over the space: named columns of formulas. Refer to the next section.
space The space of the root
Member What it does
._.op(...args) Applies an op of the domain of the current value. The types list only the declared ops, with their parameter types.
from(field) Starts a new value: the field at the current address
to(key), index(i), offset(...d) Moves the address of the later field references
other() Moves to the other key of a pair. The types offer it only for a space with two keys.
origin() Moves back to the origin
parent() Moves to the parent of the focus, in a tree space
others(body, { where }) The others axis. The body starts with the current value, and its field names read at each target.
neighbors(n, body, { where }) The neighbors axis of a grid. n is 4 or 8.
children, ancestors, descendants, siblings (each with (body, { where })) The axes of a tree space
each(body, { where }) The all axis inside the expression
with(binds, body) Binds names. The body gets a typed argument for each name.
fork(branches) Evaluates each branch with the current value, and gives a record
ifError(fallback) Gives fallback when the chain gives an error
call(name, ...args) Applies a free function of withOptions to the current value. The types accept only the functions whose first parameter accepts the value.
at(key), value(key), result(key), explain(key), all() Evaluates the chain
program The expression of the chain

An axis gives a list chain. A list chain has the reductions count, sum, mean, min, max, any, all, none, values, first and reduce(op). Each reduction takes { strict: true }. By default, a reduction skips error items.

all() evaluates the chain at each key, and gives a traversal.

Member What it gives
items The key and the result of each item
get(key) The result at one key, as an Optional
values(), keys(), errors() The values, the keys and the errors of the items
count(), sum(), mean(), min(), max() A number, as an Optional
any(), all(), none() A boolean, as an Optional
reduce(op) The left fold with a domain op
map(f) A new traversal. If f throws, the item gets the error #CALC!.
fold(monoid, f) The fold of the values with a monoid
strict() A traversal whose reductions give the first error

root.sheet() gives an empty sheet. A sheet is immutable: each call of column gives a new sheet.

Member What it does
column(name, formula) Adds a column. The formula gets a root with cell, and gives a chain.
declare<T>() Gives the types of columns before their formulas. A formula can then read the column itself or a later column with its type.
r.cell(column, at) In a formula: an argument whose value is the column at the focus, at the key at, or at the address at
result(k, column), at(k, column), explain(k, column) Evaluates one cell
table() Evaluates each cell in key order, and gives one row for each key
columns The program of each column, as JSON data

A cell on a cycle of cell references gives #CYCLE!. Refer to sheets and cycles.

space.record({ A: a, B: b }); // keys "A" and "B"
space.array([a, b, c]); // keys "0", "1" and "2"
space.grid([[a, b], [c, d]]); // keys "0,0", "0,1", "1,0" and "1,1"
space.tree({ r: a, x: b, y: c }, { x: "r", y: "r" }); // keys "r", "x" and "y": x and y are the children of r

A space copies its input. Nothing changes a space after it is made.

import { defineDomain, type Domain, type OpTable } from "@mark1russell7/vex";
export const MoneyDomain: Domain<"Money", Money, OpTable<Money, "add" | "times", "add">> = defineDomain({
name: "Money",
is: (u: unknown): u is Money => u instanceof Money,
fromScalar: (n: number): Money => new Money(n),
valid: (m: Money): boolean => Number.isFinite(m.cents),
ops: {
add: { laws: ["commutative", "associative"], identity: () => Money.zero, liftScalar: true, params: ["domain"] },
times: { params: ["number"] },
},
});
Field Meaning
name The name of the domain
is The runtime test for the values. It must not accept a value of another kind.
ops The ops. Each op can declare laws, identity, liftScalar, params and fn.
methods With "all", each method of a value is an op. The default is "declared".
fromScalar The lift from a number to a value
valid A check of the values that ops give. A value that fails it gives #NUM!.
show A short text for a value
encode, decode The JSON form of a value, for literals in a serialized program

An op without fn calls the method of the value with the same name. An op with fn calls the function: use it for values without methods, for example numbers.

Function What it gives
evaluate(expr, { space, origin, domains, fns, extensions, vars }) A Result. It does not throw.
compile(expr, { domains, fns, extensions }) A program with run({ space, origin, vars }) and explain. One program runs at each origin of each space. evaluate uses it too.
explain(expr, options) A trace: one event for each node, with the focus, the reads and the result
deps(expr) Each field read of the expression, with its address and the axes around it
serialize(expr, domains), parse(text, domains) The JSON text of an expression, and the expression of a JSON text

lit, ref, app, let_, v, rec, each and ext make expressions. key, index, other, offset, origin and parent make moves. axes.all, axes.others, axes.other, axes.neighbors(n), axes.children, axes.ancestors, axes.descendants, axes.siblings and axes.where(axis, test) make axes. Refer to the IR.

Result<T> is { ok: true, value } or { ok: false, error }. Optional<T> is { tag: "some", value } or { tag: "none" }. It is the Optional of the family, from @mark1russell7/optional, so the values of render and Vex are the same type. The helpers are ok, fail, some, none, isSome, isNone, toOptional, getOr, mapResult and chainResult. An error has a code, a kind, a message and the path of its node. Refer to the error codes.

Function What it does
checkLaws(domain, { arb, eq, numRuns }) Tests each declared law with fast-check, and gives one result for each law
assertLaws(domain, options) Throws an error that lists each failed law
arbSpace(value), arbExpr(options), arbOrigin(space) Arbitraries for random spaces and programs
referenceEvaluate(expr, options) A short reference interpreter, for differential tests
fromCore, fromReference Comparable forms of the two outcomes

A domain author checks the laws of a new domain in one line:

expect(checkLaws(MoneyDomain, { arb: fc.integer().map((n) => new Money(n)) }).filter((r) => !r.ok)).toEqual([]);