Skip to content

The specification

This document is the specification of Vex 1.0. Each normative statement has a requirement ID in square brackets, for example [EVAL.TOTAL]. Each requirement ID has at least one test that names it. The test spec-coverage.test.ts makes CI fail when an ID has no test, or when a test names an ID that this document does not define.

The design and the reasons for it are in docs/REVIEW.md. The v0.9 specification is in docs/archive/spec-v0.9.md.

A Vex program is an expression. It evaluates at one key of a space. The key where it starts is the origin. The key where relative references read is the focus. At the start, the focus is the origin.

Term Meaning
space An immutable collection of records with keys: a record space, an array space or a grid space
origin The key where an evaluation starts
focus The key where a reference without an address reads
address A list of moves from the focus
axis A relation between the focus and a list of target keys
domain A set of values and the ops on them
result A value, or an error value with a code
  • A record space has the property names of an object as keys, in their order.
  • An array space has the keys "0", "1" and so on.
  • A grid space has the key "row,column" for each cell. A row can be shorter than the others.
  • SPACE.TREE6 tests pass A tree space has records by key, and a parent for each key that is not a root. A parent that is not a key, or a cycle of parents, makes the constructor throw. The children of a key are in key order.
  • A space copies its input. A later change to the input does not change the space.

A move changes the key where a reference reads.

  • NAV.KEY1 tests pass The move key(k) goes to the key k. If the space has no key k, the result is the error #REF!.
  • NAV.INDEX1 tests pass The move index(i) goes to the key at position i in the key order. A position outside the keys gives #REF!.
  • NAV.OTHER.PAIR2 tests pass The move other goes to the other key of a space with two keys. In a space with a different number of keys, it gives #REF!.
  • NAV.OFFSET1 tests pass The move offset(d) adds d to the position of the focus: one number in an array space, two numbers in a grid space. A position outside the space, or an offset in a record space, gives #REF!.
  • NAV.ORIGIN1 tests pass The move origin goes back to the origin of the evaluation.
  • NAV.PARENT5 tests pass The move parent goes to the parent of the focus in a tree space. A root gives #REF!, and a space that is not a tree gives #REF!.
  • NAV.SEQUENCE1 tests pass The moves of an address apply in order. If one move fails, the address fails. Vex does not simplify addresses, because a simplification could hide a failure.

An expression is plain JSON data. It has one of these kinds.

Kind Fields Value
lit value the value
ref path, at the field at the path, in the record at the address
app op, args the op applied to the values of the arguments
let bind, body the body, with names bound to the values of the bindings
var name the value of a bound name
rec fields a record of the values of the fields
each axis, body a list: the body at each target of the axis
ext kind, data the value of an extension handler
  • IR.JSON10 tests pass An expression whose literals are JSON data gives the same expression after serialize and parse. A literal that is a domain value needs encode and decode in its domain.
  • EVAL.TOTAL6 tests pass The interpreter does not throw. It gives a value or an error value. A value is not undefined.
  • EVAL.PURE1 tests pass An evaluation does not change any state. Two evaluations of the same expression at the same key give the same result.
  • EVAL.ORIGIN1 tests pass An origin that is not a key of the space gives #REF!.
  • EVAL.LOCATION1 tests pass An error value has the path of the node that made it, and the origin and the focus of the evaluation at that node. An extension handler can give an error without an origin. Then the interpreter adds the path, the origin and the focus of the extension node.
  • EVAL.COMPILE3 tests pass compile(e, options) gives a program. Its run and explain give the same results and the same traces as evaluate and explain. One program evaluates at each origin of each space, and one evaluation does not change a later evaluation.
  • EVAL.REFERENCE107 tests pass The interpreter of @mark1russell7/vex and the reference interpreter of @mark1russell7/vex-testkit give the same value or the same error code.
  • REF.FOCUS1 tests pass A reference without an address reads the field at the focus.
  • REF.PATH1 tests pass A path with more than one segment reads nested fields. The empty path reads the whole record.
  • REF.MISSING2 tests pass A missing field, or a field with the value null, gives #N/A.
  • REF.FORBIDDEN2 tests pass The segments __proto__, constructor and prototype are not fields. They give #N/A.
  • REF.ADDRESS1 tests pass A reference with an address reads at the key where the address points. A failed address gives #REF!.
  • CALL.METHOD1 tests pass For an op without fn, the interpreter calls the method of the receiver with the same name.
  • CALL.FN1 tests pass For an op with fn, the interpreter calls fn with the receiver and the other arguments.
  • CALL.UNKNOWN-OP1 tests pass An op that the domain of the receiver does not declare gives #NAME?. An inherited member, for example toString, is not an op.
  • CALL.RECEIVER1 tests pass A receiver that no domain accepts gives #VALUE!, if no free function has the name of the op.
  • CALL.FREE1 tests pass A free function applies when no domain accepts the receiver.
  • CALL.PARAMS3 tests pass If an op declares the kinds of its parameters, an argument of the wrong kind gives #VALUE!.
  • CALL.LIFT3 tests pass If an op declares liftScalar, the interpreter changes each number argument into a domain value with fromScalar.
  • CALL.THROW1 tests pass An op that throws gives #CALC!. The error keeps the thrown value.
  • CALL.RESULT3 tests pass An op result that is undefined or null gives #CALC!. A number that is not finite, or a domain value that fails the valid check of its domain, gives #NUM!.
  • CALL.ARGS1 tests pass The interpreter evaluates all arguments. One failed argument gives its own error. Two or more failed arguments give #ARGS, with each error as a cause.
  • CALL.STRING-ARGS1 tests pass A string literal is a value. The builder makes a field reference only from a bare string.
  • LET.BIND1 tests pass let binds each name to the value of its binding, for the body.
  • LET.LAZY-ERROR1 tests pass A binding that fails has an effect only where a var reads it.
  • LET.UNBOUND1 tests pass A name without a binding gives #NAME?.
  • LET.VARS1 tests pass The caller of the interpreter can give variables.
  • REC.FIELDS1 tests pass rec makes a record of its fields. Two or more failed fields give #ARGS.
  • AXIS.ALL1 tests pass The axis all has each key of the space as a target.
  • AXIS.OTHERS1 tests pass The axis others has each key except the focus as a target.
  • AXIS.OTHERS.ORIGIN1 tests pass Inside an axis, the focus is the target, and the origin does not change. A value bound outside the axis keeps its value. Thus a base value from the origin and a field of the target can meet in one body.
  • AXIS.OTHER.PAIR1 tests pass The axis other has the other key of a pair as a target. Outside a pair, it gives #REF!.
  • AXIS.NEIGHBORS2 tests pass The axis neighbors(4) or neighbors(8) has the neighbor cells of the focus in a grid as targets. Outside a grid, it gives #REF!.
  • AXIS.TREE8 tests pass In a tree space, the axis children has the children of the focus as targets, in key order. ancestors has the parent first and the root last. descendants has each key under the focus, depth first, with each parent before its children. siblings has the other children of the parent, or the other roots for a root. In a space that is not a tree, each of these axes gives #REF!.
  • AXIS.WHERE2 tests pass The axis where(axis, test) keeps the targets of axis where test gives true. A test that fails, or that does not give a boolean, gives an error item for that target.
  • AXIS.OTHERS-UNION1 tests pass The targets of others and the focus are the targets of all.
  • AXIS.OTHER-TWICE1 tests pass In a pair, the address [other, other] points to the focus.
  • AXIS.EXTEND1 tests pass For an expression that does not read the origin, the item at key k of each(all, e) equals e evaluated at origin k. This is the law of a comonad: extract after extend gives the program.
  • LIST.LENIENT2 tests pass A list op skips error items by default. With { strict: true }, the first error item is the result.
  • LIST.EMPTY3 tests pass min, max, mean and first of an empty list give #N/A. sum gives 0 and count gives 0. reduce gives the identity of the op, or #N/A if the op has no identity.
  • LIST.KINDS3 tests pass sum, mean, min and max need numbers. any, all and none need booleans. A value of another kind gives #VALUE!.
  • LIST.REDUCE1 tests pass reduce folds the values from the left with a domain op. For an associative op, any order of reduction gives the same value.
  • FORM.IF2 tests pass if evaluates only the branch that its condition selects. A condition that is not a boolean gives #VALUE!.
  • FORM.AND-OR1 tests pass and and or stop at the first argument that decides the result.
  • FORM.IFERROR1 tests pass ifError gives its first argument, or its second argument when the first is an error.
  • TRACE.EVENTS9 tests pass explain records one event for each node evaluation, in the order that the nodes finish. A reference event records its reads.
  • TRACE.AXIS1 tests pass Inside an axis, the focus of each event is the target.
  • DEPS.READS1 tests pass deps gives each reference of an expression, with its address and the axes around it.
  • DOMAIN.OWNED3 tests pass A domain owns its op table. defineDomain does not change a class or a prototype, and an import has no side effects.
  • DOMAIN.LAWS5 tests pass Each law that a domain declares holds for the values of the domain. @mark1russell7/vex-testkit checks each law with property tests.
  • BUILD.IMMUTABLE2 tests pass A builder call gives a new chain. An earlier chain does not change.
  • BUILD.CALL3 tests pass call(name, ...args) applies a free function of withOptions: the expression is app(name, value, ...args). A domain op with the same name for the value comes first at run time.
  • TYPE.FROM1 tests pass from(p) accepts only the fields of the record type, and the chain value has the type of the field.
  • TYPE.OPS1 tests pass ._ has only the ops of the domain of the current value, with the parameter types of each op.
  • TYPE.LIFT1 tests pass A number argument for a domain parameter needs liftScalar on the op.
  • TYPE.STATE1 tests pass After an op gives a value that no domain of the chain accepts, the chain has no ._.
  • TYPE.OTHER1 tests pass other() exists only on a chain over a space with two keys, or with keys that the compiler does not know.
  • TYPE.KEYS1 tests pass Evaluation accepts only the keys of the space.
  • TYPE.LIST1 tests pass A number reduction needs a list of numbers.
  • TYPE.NO-ANY1 tests pass No value type of the builder is any.
  • TYPE.CALL1 tests pass call accepts only the names of the free functions whose first parameter accepts the current value. The other arguments have the types of the other parameters, and the chain value has the return type of the function.

A sheet has named columns of formulas over a space. A cell is one column at one key. A formula reads a cell with a cell reference: the extension node ext("vex.cell", { column, at }).

  • SHEET.CELL9 tests pass A cell reference resolves its address from the focus, then gives the result of that cell. An unknown column gives #NAME?, and an address outside the space gives #REF!.
  • SHEET.RECURRENCE2 tests pass A column can read itself at another key. A recurrence over the key order, for example a running total, gives the same result as a loop.
  • SHEET.DECLARE1 tests pass declare<T>() gives the types of columns before their formulas. A formula can read a declared column with its type, also the column itself or a later column. The formula of a declared column must give a value of the declared type. declare has no effect at run time.
  • SHEET.CYCLE2 tests pass Each cell on a cycle of cell references gives #CYCLE!. A cycle can contain one cell, or cells at different keys.
  • SHEET.ORDER2 tests pass The result of a cell does not depend on the order of the evaluations. ifError does not hide a cycle from the cells on that cycle.
  • EXAMPLE.SEPARATION3 tests pass The test is position + size - other.position in a pair of boxes. A component that is not positive shows that the box at the origin ends before the other box starts.
  • EXAMPLE.NEAREST3 tests pass The minimum of the distances from the origin to the others is the distance to the nearest other record.
  • EXAMPLE.OFFSETS3 tests pass The reduction with add of the offsets from the origin to the others is the sum of the offsets.
  • EXAMPLE.TREE2 tests pass In a tree of people, the salary of a person plus the sum over descendants is the cost of the team of that person. The count over ancestors is the depth of the person.