Skip to content

Concepts

A Vex program is an expression. It evaluates at one key of a space of records, and it gives a value or an error value. Seven words describe the model. This page shows each word with a live program over the four boxes of the site.

A field at the focusorigin
root.from("position")._.add("size")
step 3 of 3

Result at A: (7, 6)

originABCD
result(7, 6)(10, 9)(16, 9)(11, 13)

The program reads position and size at the focus, and adds them. The result is the far corner of the box. Select another origin, or drag a box, and the program evaluates again.

Term Meaning In the examples
space An immutable collection of records with keys the boxes A, B, C and D
origin The key where an evaluation starts the box that you select
focus The key where a reference without an address reads the origin, until an address or an axis moves it
address A list of moves from the focus root.of("B", "position") has the move key("B")
axis A relation between the focus and a list of target keys others relates A to B, C and D
domain A set of values and the ops on them Vec2Domain, NumDomain and BoolDomain
result A value, or an error value with a code (7, 6), or #REF!

A space holds the records. It copies its input, and nothing changes it after that. Vex has three kinds of space:

import { space } from "@mark1russell7/vex";
space.record({ A: boxA, B: boxB }); // keys "A" and "B"
space.array([cell0, cell1, cell2]); // keys "0", "1" and "2"
space.grid([[a, b], [c, d]]); // keys "0,0", "0,1", "1,0" and "1,1"

The kind of space sets the valid moves and axes. An offset needs an array or a grid, and the axis neighbors needs a grid. A fourth kind, the tree space, has a parent for each key that is not a root. Refer to trees.

The origin is the key where an evaluation starts. The focus is the key where a bare field name reads. At the start, the focus is the origin.

In the first example, from("position") reads the position of the box at the focus. add("size") reads the size of the same box, because a bare string argument is a field at the focus. A spreadsheet formula reads relative cells in the same way.

An address is a list of moves from the focus. A reference with an address reads at another key. root.of("B", "position") reads the position of B from each origin, like the absolute reference $B$2 in a spreadsheet.

An absolute reference to Borigin
root.from("position")._.add("size")._.subtract(root.of("B", "position"))
step 5 of 5

Result at A: (1, 1)

originABCD
result(1, 1)(4, 4)(10, 4)(5, 8)

At the origin B, the result is the size of B: the far corner of B minus the position of B. These are the moves:

Move Builder Goes to Gives #REF! when
key(k) .to(k), root.of(k, field) the key k the space has no key k
index(i) .index(i) the key at position i in the key order i is outside the keys
other .other() the other key of a pair the space does not have two keys
offset(d) .offset(...d) the focus position plus d the target is outside the space, or the space is a record space
origin .origin() the origin of the evaluation not applicable

The moves of an address apply in order. If one move fails, the address fails. Vex does not simplify an address, because a simplification can hide a failure.

An axis is a relation between the focus and a list of target keys. The body of an axis evaluates once at each target. Inside the body, the focus is the target, but the origin stays the same.

An axis: the nearest other boxorigin
root.from("position")
  .others((e) => e._.subtract("position")._.length())
  .min()
step 16 of 16

Result at A: 5

originABCD
result557.286.32

At the origin A, the axis others has B, C and D as its targets. The body starts with the position of A. In the body, the field name "position" reads the position of each target. min() reduces the list of distances to one number. Refer to axes as relations for each axis.

A domain tells Vex which values belong to it and which ops they have. A chain finds the domain of its current value, and ._ lists the ops of that domain. After length(), the value is a number, so ._ lists the ops of NumDomain.

import { space, vex } from "@mark1russell7/vex";
import { BoolDomain, NumDomain, Vec2Domain } from "@mark1russell7/vex-domains";
const root = vex(Vec2Domain, NumDomain, BoolDomain).over(space.record(boxes));
root.from("position")._.add("size"); // a Vec2 op
root.from("position")._.length()._.gt(3); // a Num op after length()

An op can declare laws, for example commutative. @mark1russell7/vex-testkit checks each law with property tests. Refer to the domains for the ops of each domain.

The interpreter does not throw. Each evaluation gives a value or an error value with a code, like a cell of a spreadsheet. The builder gives three views of one evaluation:

Method Gives Use it for
at(k) an Optional: some(value) or none the value only
result(k) a Result: the value, or the error with its code the reason for none
explain(k) a trace, with one event for each node each step of the evaluation

Refer to error values for the codes.