Every form, operator and builtin
The exhaustive companion to the tour. The builtin tables below are generated by running the compiler. A script compiles a probe program for each entry through all three backends and records what each one did. As of 2026-10-18 there are 267 builtins; 266 work in all three (the interpreter, the C backend and the wasm backend) and 1 in some of them.
Lexical structure
- Numbers
-
A number is a 64-bit float.
10lexes as10.0, scientific notation works (1e-10), and a 64-bit integer is anInt, declared with an annotation (n: Int).%and//follow Python's rules for floats (see operators);mod(a, b)is C's fmod, whose result takes the sign ofa. - No implicit multiplication
-
A name cannot follow a number directly:
2xis an error that says to write2 * x. Theiof an imaginary literal (2.5i) and theeof an exponent (1e-10) are part of the number. - Names
-
A name starts with a letter or
_and continues with letters, digits and_. Letters are Unicode letters, soΔxandσare names. Names that start withviv__are used by the parser for the hidden variables ofa[i] += xand the longerforforms. - The dot rule
-
A
.is part of a number only with digits on both sides. So1.5is a float,x.yis field access, and1.is neither. - Statements end at the newline
- No semicolons. Inside parentheses or brackets the lexer suppresses newlines, so long expressions wrap freely.
- Comments
-
#to end of line, dropped by the lexer. The annotation markers —#|what,#||how,#|||why,#~tags,#:variable glossary,#[/#]regions — are comments the compiler ignores andviv docextracts. Above adefthey form its structured doc. - Strings
-
"text", with the escapes\n\t\r\0\\\"\'and\u{1F600}(any other backslash is an error), and f-strings,f"x = {x:.3f}". A string is a value: see Strings. - Docstrings
- A bare string as the first statement of a
defdocuments it and compiles to nothing.
Statements
| Form | Notes |
|---|---|
x = expr | Assignment. At top level it creates a global; inside a def, a local hoisted to function scope. |
x += e -= *= /= | Compound assignment; desugars to the plain form, x = x + e. |
a[i, j] += e -= *= /= | Compound assignment on an array element: a[i, j] = a[i, j] + e, with each index evaluated once. An index that is not a number or a variable is computed into a hidden variable first, so c[floor(10 * rand())] += 1 draws one random number, not two. |
a[i, j] = expr | Indexed store, up to rank 4. The base may be any array-valued expression, including a record field. |
a[lo:hi] = v A[:, j] = v | A write into a slice: v is a number, which every selected element takes, or an array of exactly the slice's shape. The value is copied before the write, so s[1:5] = s[0:4] shifts. a[lo:hi] += v and the other compound forms read the slice, combine and write it back, with each bound evaluated once. |
return expr | A def that ends without a return gives nothing: it is called as a statement, and the compiler refuses a use of its value (show f(x), f(x) + 1.0). |
return a, b | Several results. Every return of the def gives the same number of values, each with one type (a number, a boolean, a complex number, an array, a record); return g(x) passes on the values of a def g that returns as many. The caller unpacks them, a, b = f(x), on a line of its own, or calls f(x) alone and drops them. f(x) + 1, show f(x) and f as a function value are errors that name f. The def returns a record the compiler declares for it (Records), so every engine runs it. |
a, b = e | Unpacking. e is a call of a def that returns as many values, a list of values (a, b = b, a swaps: every value on the right is computed first, then the targets are assigned left to right), or an array with exactly one element per name, such as B, C, H, W = shape(X); an array of another length stops the run, located at the line. A target is a name, an element v[i] or a slice v[lo:hi], and its indices are computed after the right side, as in Python: v[i], v[j] = v[j], v[i] swaps two elements. loss, g = value_and_grad(...) is this form too. |
if c { } else { } | With else if chains. |
while c { } | With break and continue. |
for i to n { } | i runs 0 … n−1. |
for i from a to b { } | i runs a, a+1, … while it is below b. As in for i to n, to names the end and the end is left out. |
for i from a to b by s { } | i runs a, a+s, a+2s, … and stops before reaching b: below b for a positive s, above b for a negative one (for i from 10 to 0 by -3 is 10, 7, 4, 1). The loop makes ceil((b − a) / s) passes, none when that is 0 or less, and pass k has i = a + k·s. a, b and s are evaluated once, in that order. A step of 0 is an error: at compile time when it is a number, at run time otherwise. |
for i to n reversed { } | The same values in the opposite order: for t to T reversed is T−1, …, 0 and for i from 1 to n reversed is n−1, …, 1. reversed goes last, after any from and by. These longer forms are rewritten by the parser into for k to count over a hidden counter, with i assigned at the top of each pass, so every engine runs them. |
parallel for i to n { } | Sequential in the interpreter; the C backend emits an OpenMP pragma. Only this form: no from, by or reversed. Each pass may write only the elements of a shared array indexed by i (a[i], m[i, j], m[:, i] = col, setslab(W, i, mine), or a def given i as the index it writes at), and may read a shared array it writes only at those elements; a write two passes could share, a write of a global through a def, a break or a return is refused before the run, in every engine. Arrays a pass makes are its own, and are freed when the pass ends. |
show expr | Prints the value. A variable, or fmt of one, is labeled with its name (x: 2); any other expression prints bare, as print would. |
show a, b, c | Several labeled values on one line, two spaces apart: a: 1.5 b: 0.3 v: [1, 2]. A variable, or fmt of one, is labeled with its name, so show fmt(x, 17) prints x: 0.30000000000000004. Arrays print on one line here. It is print("a: ", a, " b: ", b, ..., sep=""); see Output. |
print(...) | Text and values on one line, a space between them; see Output. |
for x in e { } | Each element of e in order: a string's characters, a list's strings, a vector's numbers. for i, x in enumerate(e) gives the index too, and enumerate(e, 1) (or start=1) counts from 1. for a, b in zip(u, v) walks several sequences together and stops at the end of the shortest. for row in rows(M) gives the rows of a matrix, M[k, :] (an array's rank is known only when the program runs, so for x in M reads numbers and stops on a matrix). e is evaluated once; the parser writes the loop as for k to len(e) with x = e[k] at the top of each pass. |
stop("text") | Ends the run with error: text and the line it is on, exit status 1, in every engine: the error a failed check should give. The argument is one string, a literal or a value (stop(f"no atom {name}")). Output printed before it stays printed (the wasm engine reports the error alone, as for every run-time error). |
narrate "text" | A progress line. A non-string argument is commentary and compiles to nothing. |
test "name" { } | Runs the body and reports under the name: one PASS name or FAIL name line, however many asserts are inside. A failure makes the process exit nonzero. |
def f(a, b, c = 1.0) { } | A parameter with a default comes after those without and is passed by name only: f(1, 2, c=0.5); a default may read the parameters before it (scale = 1.0 / sqrt(rows)) and is computed at each call that leaves it out. The front end fills every call out, so it costs nothing at run time; a call through a function value passes its arguments in order. Optional annotations: def f(a: Num, v: Arr) -> Num. Recursion works to a depth of 2500 nested calls, the same in every engine; a call past that stops the run with recursion deeper than 2500 nested calls, located at the def. A def and a top-level variable cannot share a name, even when the def comes from a #! use module: the compiled program has one namespace for both, so every engine refuses it and names the two lines. |
step name "text" { } | A named phase of a def (or of the top level), with optional text. A step is a block, not a scope: its variables are the def's. The engines record entering and leaving it, and the explorer, the story and the intent and debug lenses address it as def/name, so its name is unique in the def. Not inside a parallel for. |
step name: statement | A step of one statement, on that statement's line: step attn_residual: X1 = X + Y is step attn_residual { X1 = X + Y } to every engine and tool, hashes included. The statement starts after the : on the same line and ends where a statement ends, so step bump: for i to n { runs to the loop's closing brace, and an else on the next line continues an if. ; is a line break, so step a: x = 1; y = 2 holds only x = 1. With text: step name "text": statement. |
### … ### | A fenced block of text: everything between two lines that are exactly ### is skipped, so prose needs no # in front of it. A post keeps its title and its notes in one. |
rung "title" { } | One step of a ladder: defs, tests and statements that build on the rungs before it. A def in a later rung replaces an earlier one of the same name. viv run runs the top rung; viv ladder runs every rung and checks the ladder's rules. |
record P { x: Num, v: Arr } | Record type (a user-defined struct), in every engine. Build one with every field named, p = P { x: 1, v: zeros(3) }; read a field with p.x; assign one with p.x = 2; write into an array field with p.v[i] = e. A record is a value: q = p gives q its own, and q.x = 3 leaves p as it was (an array inside is shared until it is written into, as arrays are). A write is into a variable of the def it is in: a def does not change a record it was given, or a global. |
record M { blocks: [B] } | An array of records: [b1, b2], append(bs, b), bs[i], len(bs), bs[i] = b, and a place through it, m.blocks[l].mlp.up = W or m.blocks[l].mlp.up[:] = W. A value, as each record in it is. |
a + b, zeros_like(r), for_leaves(f, r1, r2) | Over a record of arrays (every field an array, a record of arrays, or an array of them): + - * / with another of its type or a number, -r, sqrt(r) and the other elementwise functions, act on each array; zeros_like(r) is one of the same shape; sum(r) adds its arrays' sums; for_leaves(f, r1, ..., x) calls f on each array of the records at once, with x after them, and f may write them in place. loss, g = value_and_grad(f, model, ...) differentiates a record of arrays and gives g of its type. A number field is refused, with its name: keep sizes in a record of their own. |
fn(x) -> expr | A lambda, and a value: assign it, pass it, call it through the parameter. |
Output with print and fmt
print writes its arguments on one line with a space between them, as Python's
does, then a newline. sep=", " puts other text between them and
sep="" none; sep is a string literal or a variable. There is no
end=: every print ends its line. An argument is a string (written as
is), fmt(x, ...) (a value in a format), or any other value (as show
writes it: six significant digits, the way C's %g does). Every value is computed
before anything is written. The interpreter, C and wasm print the same bytes, checked over
106 doubles (tools/print-fmt-sweep/).
| Form | Writes | Python |
|---|---|---|
print("loss", loss) | loss 0.693147 | print(f"loss {loss:g}") |
print("loss", fmt(loss)) | loss 0.6931471805599453: the shortest digits that read back as the same double | print("loss", loss) (repr) |
print(a, b, c, sep=", ") | 1, 2, 3 | the same |
print("[", x, "]", sep="") | [0.5] | the same |
fmt(x, 17) | 17 significant digits, C's %.17g; any count 1 to 17 | {x:.17g} |
fmt(x, ".6g"), ".3e", ".2f" | C's %.6g, %.3e, %.2f | the same spec |
fmt(x, "10.4f") | at least 10 characters, padded on the left | the same spec |
fmt(n, "d"), "5d" | a whole number, no decimal point (%.0f) | {n}, {n:5d} for an int |
print(v), fmt(v, ".2f") | an array on one line, [1, 2.5], [[1, 2], [3, 4]], each element in the format | — |
print(flag, z) | true 1+2i: booleans and complex numbers, bare only | — |
print(x) | one number: the one-argument print, unchanged | — |
print() | an empty line | print() |
A format is [width][.precision] then e, f, g
or d, or empty for repr; precision and width are at most 40 and 100.
The rest of Python's spec (fill, alignment, +, ,, %, a
precision with no type) is refused with its location and what to write instead. The digit count
and the format are written in the source, not computed. nan prints as
nan whatever its sign, and fmt keeps the sign of a negative zero
(-0.0, -0) where a bare value writes 0. A program's own
def fmt or def print replaces the builtin, and no separator is added to its calls.
Until 2026-09-30 print put nothing between its arguments, and programs wrote
print(a, " ", b). tools/migrate_print_sep.py rewrote every program
in the repository to the new form, checked call by call to reach the engines unchanged, so
each prints the bytes it printed before.
Operator precedence
| Level | Operators | Notes |
|---|---|---|
| 1 | x ^ y | power, the same as pow(x, y) (so an array base works elementwise). Right-associative, and tighter than a minus on its left: -x ^ 2 is -(x ^ 2), 2 ^ 3 ^ 2 is 512, 2 ^ -1 is 0.5 |
| 2 | -x not x | unary |
| 3 | * / @ % // | * on two arrays is elementwise; @ is always the matrix product (NumPy's matmul: a stack of matrices multiplies matrix by matrix, under Axes). a % b is the floored remainder and a // b the floor of the quotient, Python's rules for floats: the remainder takes the sign of b, so -7 % 3 is 2 and (0 - 1) // 2 is -1, and a == (a // b) * b + a % b. They take numbers; a zero divisor stops the program with division by zero in |
| 4 | + - | elementwise on arrays; all four arithmetic ops broadcast between an array and a scalar, either side, and between two (not complex) arrays by NumPy's rule (below) |
| 5 | < <= > >= == != in not in | x in a: a number in a real array of any rank (some element == x, so a NaN is in nothing, as in NumPy), a key in a map, a substring in a string, a string in a list of strings. On numbers, the comparisons give a Bool. With a real array on either side they compare elementwise and give an array of 1.0 where the comparison holds and 0.0 where it does not, so dY * (Y > 0.0) is ReLU's adjoint on any rank. all, any and where read such a mask; if a == b on arrays is refused, with a note that says to write all(a == b) |
| 6 | and or | short-circuit in every engine: the right side runs only when the left does not decide, so j > 0 and a[j - 1] > v never reads a[-1] |
| 7 | a if c else b | a when c is true, else b; only the one chosen is computed. Loosest of all, as in Python: a if c else b if d else e is a if c else (b if d else e) |
Measured: 2.0 + 3.0 * 4.0 is 14, -7 % 3 is 2 and 7 // -2
is -4. Parentheses override everything.
Comprehensions, if expressions and keys
A comprehension builds an array, or a list of strings, from loops, as Python's does. Its
for takes any head a for statement takes, and its if
keeps the elements it holds for:
xs = [3, 1, 4, 1, 5, 9]
evens = [x for x in xs if x % 2 == 0] # [4]
squares = [i * i for i to 5] # [0, 1, 4, 9, 16]
table = [[i * j for j from 1 to 4] for i from 1 to 4]
words = [upper(w) for w in split("a bb ccc")] # ['A', 'BB', 'CCC']
print(" ".join(f"{x:.2f}" for x in xs[:3])) # 3.00 1.00 4.00
print(all(x > 0 for x in xs), max(words, key=len))
sign = "neg" if xs[-1] < 0 else "pos"
print(sorted(words, key=len, reverse=true)) | Form | Gives |
|---|---|
[e for x in xs], [e for i to n if c] | a vector of numbers, a matrix when every e is a vector of one length (a row each), or a list of strings. More fors nest, the first outermost; xs may be an array, a string, a list of strings, enumerate(...), zip(...) or rows(M). An empty result is [] |
{k: v for x in xs} | a map, Python's dict comprehension: stoi = {c: i for i, c in enumerate(vocab)}. A key seen again keeps its first place and takes the later value. The same heads and filters as a list's. A brace in an expression is only this: a map literal is map() and stores, and a set ({x, y}) is refused with a pointer to unique |
f(e for x in xs) | a generator as the only argument of a call: f([e for x in xs]), so sum, max and " ".join take one |
all(c for x in xs), any(...) | a Bool, and the loop stops at the first element that decides it, as Python's does |
a if c else b | a when c holds, else b; only the one chosen is computed |
max(xs, key=f), min(xs, key=f) | the first element whose key is largest (smallest), as Python's; a NaN key is never chosen after the first element; an empty xs stops with max() arg is an empty sequence |
sorted(xs, key=f, reverse=r), argsort(xs, key=f, reverse=r) | the elements (or their positions) in the order of their keys, numbers or strings: stable, so equal keys keep their order, with reverse=true too, as Python's; a NaN key goes after every number |
A key is a def's name, a builtin's (len, abs), a variable holding a
function, or a lambda fn(w) -> ..., which may read the program's variables. A
comprehension's variables are its own: x outside it is neither read nor changed,
as in Python 3. The parser writes each of these forms as the loop it stands for, computed
just before the statement that uses it, so every engine runs code it already runs; what a
statement computes before it (a call of a def, a print) still runs first, and
in a while condition it is computed again each time. For that reason one cannot
be the body of a fn(...) -> ... (except a key's): write a def.
Slices
A range inside the brackets selects a block of an array, and the result is a copy,
never a view: b = a[1:3] is a new array, and writing b leaves
a alone. (Assignment and parameter passing alias; slices do not.)
| Form | Selects |
|---|---|
a[lo:hi] | a[lo], a[lo+1], …, a[hi−1]: hi − lo elements, none when lo = hi |
a[:hi] a[lo:] a[:] | a missing lo is 0 and a missing hi is the length of the axis |
a[lo:hi:s] a[::s] | every s-th element from lo, stopping before hi; s is 1 or more |
A[i, :] A[:, j] | row i and column j, as vectors: a plain index drops its axis |
A[r0:r1, c0:c1] | a block, still a matrix; any rank up to 4 mixes ranges and indices the same way |
X[lo:hi] on a matrix | rows lo … hi−1: axes left out at the end are taken whole, as in NumPy |
Bounds are read as an index is (a fraction is cut toward zero), and a negative index or bound
counts from the end, as in Python: a[-1] is the last element,
a[-3:] the last three, a[:-1] all but the last, A[-1, :]
the last row. Nothing is clamped: after that a range needs 0 ≤ lo ≤ hi ≤ n, and
anything else stops the run with its line in every engine, for example
slice end 4 is past the end of dimension 0 (size 3), where NumPy would clamp. A
step of 0 or less is an error, so a[::-1] is not a reversal of an array (it is of
a string).
Four builtins go with slices. copy(a) is a new array with a's shape and elements.
concat(a, b, ...) joins vectors end to end. vcat(A, B, ...) stacks
rows and hcat(A, B, ...) puts matrices side by side; to vcat a vector
is one row and to hcat it is one column, so hcat(u, v) is
transpose(vcat(u, v)) and hcat(t) turns a vector into an n × 1
matrix. All of them work on complex arrays, and grad differentiates through
slices, slice writes and all four.
The parser writes a slice as a call, a[lo:hi, j] as
viv__slice_get(a, [1, lo, hi, 1, 0, j, 0, 1]), four numbers per index, so the
interpreter, C and wasm share one set of rules (lang/viv/src/interp/slices.rs).
Views that share storage would need their own rules for the arena and for aliasing, and are
a separate design.
Growable arrays
[] is an empty vector, and a = append(a, x) is a with
x on the end, so an output whose length is found while scanning needs neither a
count pass nor a buffer sized by hand:
kept = []
for i to len(s) {
if s[i] > 0.0 {
kept = append(kept, s[i])
}
} | Form | Gives |
|---|---|
a = append(a, x), x a number | vector a with x after its last element |
a = append(a, r), r a vector | matrix a with r as a new last row; an empty [] takes a first row and becomes 1 × len(r) |
a = extend(a, b) | vector a followed by vector b (b may be a) |
An append gives back a new array, and it is written rebinding the name it grows, with the
same name on both sides; b = append(a, x), an append inside an expression and
one in a parallel for are refused before the program runs. An alias taken
before the append keeps the array as it was: after b = a and
a = append(a, 9.0), b has the old length, and a write into
a does not reach it. Each engine grows the storage in place, doubling its room,
when no other name, value or argument holds the array, and copies otherwise, so a loop of n appends costs O(n)
and nothing a program prints depends on the room. grad differentiates through
both. There is no pop: a = a[:-1] drops the last element (a
copy). Adding a number to a matrix, a vector to a non-empty vector, or a row of the wrong
length stops the run with its line, for example `append`: the row has 2 numbers, and
the matrix has 3 columns.
Why rebinding, not push(a, x): an array handle is copied by value in C, so an
in-place push would leave an alias with the old length, or pointing at freed memory after
a reallocation.
Axes, broadcasting, and agreement with NumPy
The array builtins take NumPy's names and give NumPy's results: reshapes and transposes,
eye, diag, arange, reductions along an axis, gathers
and masks, cumsum, argsort, einsum, and solvers that
return what they compute. Their tables are under Builtins; the rules
they share are here.
X = reshape(arange(12), 3, 4)
Xc = X - mean(X, 0) # the column means, broadcast down the rows
C = transpose(Xc) @ Xc / 3.0
vals, vecs = eig_sym(C)
w = lin_solve(C + eye(4), ones(4))
top = argsort(vals)
picked = take(X, [2.0, 0.0], 0) # X[[2, 0], :]
big = extract(X > 5.0, X) # X[X > 5] - Broadcasting
-
+ - * /, the comparisons and two-arraymaxandminbetween two arrays of different shapes (not complex ones) line up their last axes; each pair of lengths is equal, or one of them is 1 and repeats. So a row vector against a matrix repeats down its rows,reshape(v, n, 1)is a column that repeats across, and shapes that do not fit stop with the words they always did:array shape mismatch: [2, 3] vs [3, 2]. Complex arrays do not broadcast.gradsums a broadcast operand's gradient back to its shape. - Matrix products
-
A @ Bis NumPy'smatmul. Two matrices give their product; a vector on the left is a row and on the right a column, and the axis it adds is dropped. An array of rank 3 or 4 is a stack of matrices in its last two axes, and the axes before them broadcast, soX @ WwithXof shape (4, 8, 16) andW(16, 8) multiplies each of the four matrices byW. Two vectors are refused (dot(u, v)is their product). Each entry adds its terms in order of the inner index from 0.0, skipping a term whose left factor is zero, with a separate multiply and add (no fused multiply-add); the loop is blocked for the cache without changing that order, so every engine gives the same bits (lang/viv/src/interp/matmul.rs). NumPy calls BLAS, which sums in its own order and agrees to rounding. - Axes
-
An axis is 0 to rank − 1, or −rank to −1 counted from the end, as in NumPy and as an
index is. A reduction along an axis (
axis=) drops it; withkeepdims=trueit keeps it with length 1, as NumPy's does, which is what letsA - mean(A, axis=1, keepdims=true)subtract each row's mean. A builtin's options are always given by name, with NumPy's names (axis=,keepdims=,k=fortriu/tril/diag,offset=fortrace,endpoint=,axes=); written by position they are refused. A reduction along the only axis of a vector would give a number, and is refused with the whole-array form to write instead (mean(v)). - Orders of summation
-
sum,mean,varandstd, along an axis or of a whole array, andtraceadd in NumPy's order: from 0.0, a contiguous run in blocks of 8192 entries, each block by NumPy's pairwise sum; along an axis that is not the last, the slabs in order. They agree with NumPy to the bit (tools/py2viv/probe_numpy_order.pystates the order), somean(a)issum(a) / numel(a)to the bit. The order binds the exact engines; fast mode may add in another.prod,cumsumandcumprodgo left to right, as NumPy does.dotandnormgo left to right; NumPy calls BLAS, which agrees to rounding.einsumloops over the output entries in row-major order and over the summed indices in their order of first appearance, adding each product to 0.0; NumPy's einsum has its own loops, so the two agree to rounding. - NaN and signed zeros
-
amax,amin,argmaxandargminare NumPy's: a NaN wins, andamaxputs 0.0 above −0.0. (max(a)of one array is older and skips NaN, as C'sfmaxdoes.)argsortputs NaN last and keeps −0.0 and 0.0 in their order.clipkeeps a NaN entry and treats a NaN bound as no bound. - Masks and indices
-
A mask is an array of 1.0 and 0.0, as a comparison gives, so an array cannot say whether it
holds truth values or indices. NumPy's
a[mask]isextract(mask, a),a[mask] = visputmask(a, mask, v),a[idx]along an axis istake(a, idx, axis=k), andnp.flatnonzeroisflatnonzero. The indicestakeandputread are not negative:a[-1]counts from the end,take(a, [-1.0], 0)stops. - Solvers that return
-
x = lin_solve(A, b)(a matrixBsolves for every column),inv(A),L = cholesky(A),vals, vecs = eig_sym(A),w, V = eig_herm(A)andU, s, Vt = svd(A)run the same code as the forms that fill arrays you pass, which still work, and give the same bits. The compiler writes an unpacking ofeig_sym,eig_hermorsvdas that call with arrays it allocates.svdis one-sided Jacobi (lib/svd.viv's algorithm); its vectors can have other signs than LAPACK's.
tools/arrays/numpy_cases.py writes 4,812 cases whose results NumPy computes in an
order Viv follows, and 89 that agree with NumPy within a tolerance (einsum, the solvers,
dot); cargo test --test arrays runs them in the interpreter, C and
wasm, which print NumPy's results byte for byte.
Strings
A string is an immutable UTF-8 value. Length, indices and slices count code points, as
Python's do (len("héllo") is 5), negative indices count from the end, and slice
bounds are clamped (s[:80] of a short line is the line). Every function below is
Python's, character for character: its whitespace, its Unicode tables (CPython 3.11's), its
error messages. a.f(b) is f(a, b) for any call, so text code reads as
it would in Python:
line = "O 0.000 0.757 -0.469"
parts = line.split()
z = float(parts[3])
label = f"{parts[0]}: z = {z:.3f}"
words = []
for w in "a b c".split() {
words = append(words, w.upper())
}
print(join("-", words), label) | Form | Gives |
|---|---|
a + b, s * n, == < …, sub in s | concatenation, repetition, order by code point, substring (for a list of strings, membership) |
len(s), s[i], s[a:b:k], for c in s | code points; one character; a slice (any step but 0); each character |
str(x), repr(x), fmt(x[, spec]) | a number as Python's repr (str(3.0) is "3.0"); a string in quotes; a value in a format, anywhere (not only in print). A string's spec is Python's [[fill]align][width][.precision] |
f"a {x} {y:.2f} {s!r} {x=}" | str(x), fmt(y, ".2f"), repr(s), "x=" + repr(x) joined; a field may hold string literals |
float(s), int(s[, base]) | CPython's parsing (whitespace, _ between digits, inf, nan, Unicode digits), correctly rounded; int gives the double nearest the integer |
upper lower strip lstrip rstrip split rsplit splitlines join | Python's, with full Unicode case mapping (upper("ß") is "SS") |
find rfind index rindex count replace startswith endswith contains | Python's, positions in code points; index stops with substring not found |
ljust rjust center zfill chr ord isspace isalpha isalnum isdigit isdecimal isnumeric isupper islower isascii isprintable | Python's |
sorted(s), unique(s), unique(words) | Python's sorted(s): a string's characters as a list, in code point order; and sorted(set(s)): each character once, or each string of a list once, in that order. Viv has no set type: a Python set of strings iterates in an order that changes from run to run, and the sorted list is the form a program can print and test. sort is for arrays of numbers, in place, and refuses a string or a list |
read_text(p), read_lines(p), write_text(p, s) | a UTF-8 text file (newlines as Python reads them); in a browser, the files attached to the program |
A list of strings ([Str]) comes from split, a literal
["a", "b"], or [] grown by a = append(a, s); it has
len, indices, slices, +, in, sorted,
unique, join and for. Strings and lists are never changed in place:
s[0] = "x" and words[0] = "x" are refused, so no alias can see a
change. A string can be a record field (sym: Str), a parameter, a result, one of
several results, a figure title or a say caption. An index out of range, a text
float cannot read and the other Python errors stop the run with CPython's words
and the line.
Maps
map() is an empty map. Its keys are numbers or strings, and so are its values:
one kind of key and one kind of value in each map, set by what the program stores.
m[k] = v stores,
m[k] reads, and the keys stay in the order they were first stored, so what a
program prints never depends on how the keys hash:
counts = map()
for i to len(xs) {
counts[xs[i]] = get(counts, xs[i], 0) + 1
}
ks = keys(counts)
for i to len(ks) {
print(fmt(ks[i], "d"), fmt(counts[ks[i]], "d"))
}
words = map()
for w in split("the cat and the hat") {
words[w] = get(words, w, 0) + 1
}
print(words) # {'the': 2, 'cat': 1, 'and': 1, 'hat': 1} | Form | Gives |
|---|---|
m[k] = v | v under k: a key already there changes its value and keeps its place; a new key goes last |
{k: v for x in xs if c} | a new map, stored into in the loop's order: {c: i for i, c in enumerate(unique(text))} |
m[k], m[k] += v | the value under k; a key that is not there stops the run |
has(m, k) | true when k is a key of m |
get(m, k, d) | m[k] when k is there, else d |
remove(m, k), remove(m, k, d) | takes k out and gives its value; without d, a key that is not there stops the run |
keys(m), values(m) | a new vector of the keys, or the values, in insertion order (a list of strings for strings) |
len(m), copy(m), clear(m) | the number of keys; a new map with the same pairs in the same order; every key taken out |
print(m), show m | {3: 2, 1: 2}, each number as show writes it; a string key or value in its repr, as Python prints a dict: {'a': 1} |
print(fmt(m, "d", "")) | the keys in the first format and the values in the second, here Python's repr of a dict with int keys and float values: {1: 2.5, 3: 4.0} |
A map is one handle, changed in place: after b = m, a key stored through
b is in m, and a def that stores into a map parameter stores into
the caller's map, as a record field holding a map does. copy(m) is a second map.
Keys compare with ==, so 0.0 and -0.0 are one key (the
first stored is kept, as in Python); a NaN key stops the run (a map key cannot be
NaN), and a lookup with a NaN finds nothing. A loop over a map is a loop over a
snapshot of its keys, ks = keys(m), so the loop may change the map. Every
operation is O(1) on average; 106 stores and 106 reads take about
0.07 s compiled to C, and with string keys about 0.3 s beyond making the strings. A string
key hashes and compares by its UTF-8 bytes, so "é" and "e\u301" are
two keys. A map owns a copy of each string stored in it, and a read gives a new string. A map
that stores a number key and a string key (or both kinds of value), an array, a record or a
map as a value, a map as an array element, a map inside grad or a
parallel for, and two keys (m[i, j]) are refused before the run.
The layout is CPython's compact dict.
Types
- Num
-
f64, and the default: a literal is a Num. The other scalars are declared:
Int(a 64-bit integer,n: Int),Float32(IEEE binary32) andCplx(below). A double holds every integer up to 253 exactly, so counters, indices andn == 1.0are exact. - Bool
-
trueorfalse: what a comparison yields and whatifandwhiletake. It can be stored, passed and returned (def f(x: Num) -> Bool), combined withand,or,not, and compared with==and!=. It is not a number:true + 1.0andif count {are refused, identically, by the interpreter and the compiler.showprints it astrueorfalse. Arrays hold numbers, so a mask is still an array of 1.0 and 0.0: a comparison with an array on either side makes one, andall(m)andany(m)turn one back into a Bool. See booleans. - Str
-
An immutable UTF-8 string, indexed by code point (Strings). A list of
them is
[Str]. - Arr
-
Rank 1 to 4, row-major, f64 elements. Arrays pass into functions and return from them, and
scalars broadcast against them from either side, and two arrays (not complex ones) broadcast as NumPy's
do (Axes, broadcasting, NumPy).
Assignment and parameter passing alias:
b = ashares storage, and a function that writes into an array parameter writes into the caller's array. A real copy iscopy(a), and a slice is a copy too — see scope & mutation. Indices count from 0, and an index outside its dimension stops the run in the interpreter and in C with the same message,index 2 out of bounds for dimension 0 (size 2), and the line it happened on. A negative index counts from the end, as in Python:a[-1]is the last element, anda[-3]of two elements stops the run. A fractional index is cut toward zero, soa[1.7]isa[1]. - Map
-
Numbers under number keys, in the order the keys were first stored, made by
map(). A handle: assignment, parameters and record fields share one map. A parameter may be annotatedm: Map. See Maps. - Cplx
-
A first-class complex scalar: the
isuffix on a numeric literal (2.0 + 3.0i), full arithmetic with real promotion, and complex overloads ofabs/exp/log/sqrt/sin/cos/tan. Compiled to C99double complex. Every engine computes these the same way, to the bit, with Viv's own formulas rather than C's operators or<complex.h>:- A real operand is lifted to
(x, 0.0)first, so2.0 * zis the complex product with its zero terms. +and-work part by part.a * bis(ar br − ai bi) + i (ar bi + ai br), each product rounded on its own (no fused multiply-add).a / bis Smith's algorithm (1962): divide through by the larger part ofb. If|br| ≥ |bi|, then withr = bi/brandd = br + bi rthe quotient is((ar + ai r)/d, (ai − ar r)/d); otherwiser = br/bi,d = bi + br r, and the quotient is((ar r + ai)/d, (ai r − ar)/d). It never squaresb, so it overflows only where the quotient does, not once|b|passes 1e154 as the textbooka conj(b) / |b|²does. A real divisor divides each part once, so(1 + 2i) / 3is exactly1/3 + (2/3)i. Dividing by zero givesnan + nan i.abs(z)ishypot(re, im).expise^re (cos im + i sin im).logisln|z| + i atan2(im, re).sqrtis√|z|at half the argument.sinandcosuse thecosh/sinhformulas, andtanissin z / cos zby the division above. All of them use Viv's libm.
- A real operand is lifted to
- CArr
-
An array of complex numbers, rank 1 to 4:
czeros(n), or a literal with a complex element in it ([1.0 + 2.0i, 3.0]— the reals promote). Indexing gives aCplx; storing a real promotes it. It is packed: interleaved (re, im) doubles, which is exactly how C99 lays outdouble complex, so both engines walk the same memory andfftagrees between them to the byte. A complex and a real array are different types and do not mix silently —sin(a)on a complex array is an error. Annotate a parameter asCArrwhen you want to say so; otherwise it is inferred from the call site. - Records
-
User-defined structs with typed fields, copied by value: the C backend compiles them to C
structs, the wasm backend to a block of memory, and the interpreter holds the fields. Array
fields refer to the original array, so bundling a mesh into one value copies no array data.
A def that takes or returns one annotates it (
def f(p: P) -> P). Records have no operators and noshow: compare and print their fields. The several results ofreturn a, bare a record the compiler declares for the def, with one field per value and the field types inferred from the returns; the program never names it, because the caller unpacks it. - Function values
-
A
defname or a lambda is a value. The compiled backends implement higher-order calls by monomorphization —run_it(sq, x)specializes torun_it__sq— withviv_fnfunction pointers for the cases where a function is held in a variable. Arrays of functions work in the interpreter and are a compile error in C.
A note on contracts
integrate and find_root are not builtins. They are
contracts: functions named for the mathematical problem rather than the
method, backed by several methods from different families of algorithms. By default they run
every applicable method and check that the answers agree, and a disagreement stops the
program with an error. viv run --release runs only the
one method the contract selects (Gauss quadrature for integrate, a safeguarded
secant for find_root).
The methods are ordinary Viv defs in
lang/viv/std/contracts.viv, brought in with
viv --use lang/viv/std/contracts.viv program.viv. The methods a contract chooses
among are Viv code, not code in the compiler.
Interpreter only, today. A contract picks its method out of an array of functions, and the C backend implements higher-order calls by monomorphization — it specializes the callee per call site — which cannot express a callee chosen at runtime. So the contract library runs in the interpreter, and the C backend stops with a compile error naming the feature instead of producing wrong code.
Derivatives with deriv, grad and value_and_grad
deriv(fn(x) -> expr, x0) is forward mode: the derivative of one scalar
expression at a point. grad and value_and_grad are reverse mode: the
gradient of a whole def, loops, branches, calls and arrays included, with respect
to its first argument, in one forward and one backward pass.
def loss(theta: Arr, X: Arr, y: Arr) {
h = tanh(X @ theta)
return sum((h - y) * (h - y)) / len(y)
}
g = grad(loss, theta, X, y) # the shape of theta
l, g = value_and_grad(loss, theta, X, y) # the loss too
The first argument names a def; the others are passed to it unchanged and are
constants for the derivative. The differentiated argument is a number or a real array, the
gradient has its shape, and the def returns a number. value_and_grad gives two
results, written a, b = value_and_grad(...) on its own line. A program that defines
its own grad keeps it.
The compiler rewrites the program before any engine sees it (lang/viv/src/grad.rs),
so the interpreter, C and wasm run the same generated code and print the same bytes. It adds
one def per differentiated def: the def's statements, pushing every value an assignment
overwrites onto a tape, then the adjoint statements in reverse order. A def called with a
differentiated argument is inlined and differentiated with the caller; a call with constant
arguments stays a call. Branches go back the way they came, and loops run backward over the
recorded count. A def that returns several values (return a, b) or a record is
inlined wherever it is called in differentiated code, and the record it builds becomes one
variable per field, so y, dy = helper(w, x) inside a loss is differentiated like
any other assignment; a record that only comes in (an argument, such as a model's
configuration) is a constant.
| Operation | Adjoint |
|---|---|
+ - * / %, a number with an array | the usual rules; an array's gradient is summed back onto a number it was combined with, and onto an array that was broadcast, down to its shape |
A @ B | dA = g @ transpose(B), dB = transpose(A) @ g (an outer product for a vector operand); for stacks of matrices the last two axes are swapped, and an operand broadcast along the batch axes gets its gradient summed back to its shape |
sum mean var std prod amax amin, along an axis or of the whole array | the gradient spread back along the reduced axis (with or without keepdims): mean divides it by the axis's length, var and std multiply by the deviation from the mean, prod by the product of the other entries (right where an entry is zero); amax and amin share it evenly among the entries equal to the result, as torch.amax |
reshape, ravel, flatten, transpose(a, axes=p), swapaxes, take(a, idx, axis=k) | the gradient reshaped or permuted back; take along an axis adds each slice back where it was read |
dot, norm | g b and g a; g a / norm(a) |
a[i, j], take, slab, a[lo:hi] | added back into the entries read; an index taken twice collects twice |
a[i] = e, setslab, put, a[lo:hi] = e | the overwritten value passes nothing back; e gets the slice's adjoint, summed when e is a number |
copy, concat, vcat, hcat | unchanged; each argument gets its own piece |
exp log sqrt sin cos tan tanh sinh cosh asin acos atan erf pow atan2 | the derivative, from the recorded values |
abs | g sign(x), 0 at 0 |
max(a, b), min(a, b) | to the larger (smaller) side; half each on a tie, as torch.maximum. So max(x, 0.0) passes g / 2 at exactly 0, where torch.relu passes 0 |
max(a), min(a), sum, transpose, where | shared among tied entries (torch.amax); spread over a; transposed; by the mask, summed back to the shape of an argument that was broadcast |
comparisons, floor ceil round //, len numel shape zeros | constant: nothing passes back |
Anything else is a compile error at its line, never a silent zero: a builtin without an adjoint
(gamma, eig_sym, lin_solve, fft,
sort, complex numbers, records) given a differentiated value; break
and continue; return inside a loop; parallel for;
recursion; a read of a variable before its first assignment on some path; b = a for
an array that is written later through either name while both are in use (write
b = a + 0.0); a write into a parameter or global array the def also reads; a call
with a differentiated argument inside an if or while condition; and
grad inside a def being differentiated. Clamp a value with two ifs to
get torch.clamp's gradient, which passes at both ends of the interval.
The tape holds every value the forward pass overwrites, with no checkpointing. The six
models in study/idioms that wrote their backward passes by hand each have a copy
that uses value_and_grad, and the generated gradients match the hand-written ones
to 1.1e-15 of the largest entry or better. lib/nn's RMSNorm, softmax and
cross-entropy are whole-array expressions (reductions along the last axis with keepdims),
and lib/gpt differentiates a whole batch of sequences in one
value_and_grad. Derivatives through eig_sym and
lin_solve, Hessians and checkpointing are not built yet.
Phrases
A phrase is an expression written the way a person says it. It starts with take
followed by a word or a number, or with one of the verbs integrate, solve, differentiate, sum
followed by a word or a number. The parser reads it as the call in the second column below, so
the interpreter, C and wasm run only calls they already run, and a phrase prints what its call
prints in all three.
c = 2.0
M = [[4.0, 1.0, 2.0], [1.5, 5.0, 3.0], [2.0, 3.5, 6.0]]
area = take the integral of x^2 from 1 to 2 # q_gauss(fn(x) -> x^2, 1, 2)
r = solve x^2 = c starting at 1 using newton # r_newton(...), f' by forward-mode AD
s = solve x^2 = c between 0 and 2 using bisection # r_bisect(fn(x) -> x^2 - c, 0, 2)
off = take the off-diagonal row sums of M # sum(M - diag(diag(M)), 1)
x = solve M @ x = [1.0, 2.0, 3.0] # lin_solve(M, [1.0, 2.0, 3.0]) The rules:
-
A phrase runs to the end of its line, or to a comma, a closing bracket,
{,:,iforelseoutside brackets. Sotake the norm of u / nis the norm ofu / n; write(take the norm of u) / nfor the other reading. - The parser tries every pattern and every way to split the words among its holes, and keeps the splits whose holes parse as whole expressions. One reading is the phrase. None is an error that names the closest patterns; two or more is an error that lists them. It does not choose between readings.
-
In an integral, a root or a derivative, the expression is a function of its one free
variable: the one name in it that the program does not assign at that point. With none or
several, the phrase is refused until you name it:
... with respect to tor... for t. An equationl = ris solved asl - r = 0. - A part the call reads more than once (the median reads its argument four times) is computed once, into a hidden variable.
-
take, the verbs and every builtin keep working as names: a phrase needs a word or a number right after its first word, which no other expression has. -
A phrase has the content address of its call, so rewording one (
meanforaverage) does not make a def's intent stale.viv phrases file.vivprints each phrase of a program with the call it is. -
The quadratures and root finders are the defs of
lang/viv/std/contracts.vivandlang/viv/std/phrases.viv. A program that uses one of them gets the def added; one that defines its own def of that name and uses the phrase is refused.
A program can declare its own phrase at the top level. A word of the phrase that is a name the body reads is a hole; the other words are the ones a use must contain:
phrase "the Rayleigh quotient of A at x" -> dot(x, A @ x) / dot(x, x)
q = take the Rayleigh quotient of M at e
The table has 142 patterns, 49 of them ported from Knot. Each example
below is a test in lang/viv/tests/phrases/phrases.viv that checks the phrase
against the value NumPy or a closed form gives; 148 of 148 passed in
the browser compiler when this page was built, and the same file passes in the interpreter,
as C and as wasm.
Aggregates
| Phrase | Is the call | Example | ✓ |
|---|---|---|---|
take the sum of V | sum(V) | take the sum of v | ✓ |
take the product of V | prod(V) | take the product of v | ✓ |
take the mean of V | mean(V) | take the mean of v | ✓ |
take the average of V | mean(V) | take the average of v | ✓ |
take the median of V | 0.5 * (take(V, argsort(V), axis=0)[(len(V) - 1) // 2] + take(V, argsort(V), axis=0)[len(V) // 2]) | take the median of v | ✓ |
take the max of V | amax(V) | take the max of v | ✓ |
take the maximum of V | amax(V) | take the maximum of v | ✓ |
take the min of V | amin(V) | take the min of v | ✓ |
take the minimum of V | amin(V) | take the minimum of v | ✓ |
take the variance of V | var(V) | take the variance of v | ✓ |
take the standard deviation of V | std(V) | take the standard deviation of v | ✓ |
take the cumulative sum of V | cumsum(V) | take the cumulative sum of v | ✓ |
take the cumulative product of V | cumprod(V) | take the cumulative product of w | ✓ |
Matrices
| Phrase | Is the call | Example | ✓ |
|---|---|---|---|
take the transpose of M | transpose(M) | take the transpose of M | ✓ |
take the trace of M | trace(M) | take the trace of M | ✓ |
take the diagonal of M | diag(M) | take the diagonal of M | ✓ |
take the diagonal part of M | diag(diag(M)) | take the diagonal part of M | ✓ |
take the off-diagonal part of M | M - diag(diag(M)) | take the off-diagonal part of M | ✓ |
take the sum of the off-diagonal entries of M | sum(M - diag(diag(M))) | take the sum of the off-diagonal entries of M | ✓ |
take the off-diagonal row sums of M | sum(M - diag(diag(M)), axis=1) | take the off-diagonal row sums of M | ✓ |
take the off-diagonal column sums of M | sum(M - diag(diag(M)), axis=0) | take the off-diagonal column sums of M | ✓ |
take the upper triangle of M | triu(M) | take the upper triangle of M | ✓ |
take the lower triangle of M | tril(M) | take the lower triangle of M | ✓ |
take the strict upper triangle of M | triu(M, k=1) | take the strict upper triangle of M | ✓ |
take the strict lower triangle of M | tril(M, k=-1) | take the strict lower triangle of M | ✓ |
take the row sums of M | sum(M, axis=1) | take the row sums of M | ✓ |
take the column sums of M | sum(M, axis=0) | take the column sums of M | ✓ |
take the sum|mean of each row of M | sum|mean(M, axis=1) | take the sum of each row of M | ✓ |
take the sum|mean of each column of M | sum|mean(M, axis=0) | take the sum of each column of M | ✓ |
take the max|maximum of each row of M | amax(M, axis=1) | take the max of each row of M | ✓ |
take the max|maximum of each column of M | amax(M, axis=0) | take the maximum of each column of M | ✓ |
take the min|minimum of each row of M | amin(M, axis=1) | take the min of each row of M | ✓ |
take the min|minimum of each column of M | amin(M, axis=0) | take the minimum of each column of M | ✓ |
take the inverse of M | inv(M) | take the inverse of M | ✓ |
sum the off-diagonal entries of M | sum(M - diag(diag(M))) | sum the off-diagonal entries of M | ✓ |
sum the off-diagonal entries of each row of M | sum(M - diag(diag(M)), axis=1) | sum the off-diagonal entries of each row of M | ✓ |
sum the off-diagonal entries of each column of M | sum(M - diag(diag(M)), axis=0) | sum the off-diagonal entries of each column of M | ✓ |
sum each row of M | sum(M, axis=1) | sum each row of M | ✓ |
sum each column of M | sum(M, axis=0) | sum each column of M | ✓ |
Vectors
| Phrase | Is the call | Example | ✓ |
|---|---|---|---|
take the L2 norm of V | norm(V) | take the L2 norm of v | ✓ |
take the Frobenius norm of M | norm(M) | take the Frobenius norm of M | ✓ |
take the norm of V | norm(V) | take the norm of v | ✓ |
take the magnitude of V | norm(V) | take the magnitude of v | ✓ |
take the dot product of U and V | dot(U, V) | take the dot product of u and v | ✓ |
take the inner product of U and V | dot(U, V) | take the inner product of u and v | ✓ |
take the outer product of U and V | outer(U, V) | take the outer product of u and w | ✓ |
Sizes and elements
| Phrase | Is the call | Example | ✓ |
|---|---|---|---|
take the size of V | len(V) | take the size of v | ✓ |
take the length of V | len(V) | take the length of v | ✓ |
take the number of rows of M | shape(M)[0] | take the number of rows of B | ✓ |
take the number of columns of M | shape(M)[1] | take the number of columns of B | ✓ |
take the number of elements of A | numel(A) | take the number of elements of M | ✓ |
take the shape of A | shape(A) | take the shape of M | ✓ |
take the first element of V | V[0] | take the first element of v | ✓ |
take the last element of V | V[len(V) - 1] | take the last element of v | ✓ |
take element I of V | V[I] | take element 2 of v | ✓ |
take row I of M | M[I, :] | take row 1 of M | ✓ |
take column J of M | M[:, J] | take column 2 of M | ✓ |
take V sorted | take(V, argsort(V), axis=0) | take v sorted | ✓ |
take the reverse of V | take(V, arange(len(V) - 1, -1, -1), axis=0) | take the reverse of v | ✓ |
take the first row of M | M[0, :] | take the first row of M | ✓ |
take the last row of M | M[len(M) - 1, :] | take the last row of M | ✓ |
take the last N elements of V | V[max(0, len(V) - N):len(V)] | take the last 3 elements of v | ✓ |
Elementary functions
| Phrase | Is the call | Example | ✓ |
|---|---|---|---|
take the sine of X | sin(X) | take the sine of x | ✓ |
take the cosine of X | cos(X) | take the cosine of x | ✓ |
take the tangent of X | tan(X) | take the tangent of x | ✓ |
take the arcsine of X | asin(X) | take the arcsine of x | ✓ |
take the arccosine of X | acos(X) | take the arccosine of x | ✓ |
take the arctangent of X | atan(X) | take the arctangent of x | ✓ |
take the exponential of X | exp(X) | take the exponential of x | ✓ |
take the natural log of X | log(X) | take the natural log of x | ✓ |
take the logarithm of X | log(X) | take the logarithm of c | ✓ |
take the square root of X | sqrt(X) | take the square root of c | ✓ |
take the absolute value of X | abs(X) | take the absolute value of -x | ✓ |
take the floor of X | floor(X) | take the floor of 2.5 | ✓ |
take the ceiling of X | ceil(X) | take the ceiling of 2.5 | ✓ |
take the nearest integer to X | round(X) | take the nearest integer to 2.7 | ✓ |
Geometry and linear algebra
| Phrase | Is the call | Example | ✓ |
|---|---|---|---|
take the matrix product of A and B | A @ B | take the matrix product of M and B | ✓ |
take the projection of U onto V | dot(U, V) / dot(V, V) * V | take the projection of u onto v | ✓ |
take the component of U along V | dot(U, V) / dot(V, V) * V | take the component of u along v | ✓ |
take the unit vector along V | V / norm(V) | take the unit vector along v | ✓ |
take the unit vector in the direction of V | V / norm(V) | take the unit vector in the direction of v | ✓ |
take the angle between U and V | acos(dot(U, V) / (norm(U) * norm(V))) | take the angle between u and v | ✓ |
take the distance from U to V | norm(U - V) | take the distance from u to v | ✓ |
take the distance between U and V | norm(U - V) | take the distance between u and v | ✓ |
take the solution of A @ X = B | lin_solve(A, B) | take the solution of M @ y = w[0:3] | ✓ |
solve A @ X = B | lin_solve(A, B) | solve M @ y = w[0:3] | ✓ |
take the residual of A @ X = B | A @ X - B | take the residual of M @ u[0:3] = w[0:3] | ✓ |
take the residual norm of A @ X = B | norm(A @ X - B) | take the residual norm of M @ u[0:3] = w[0:3] | ✓ |
take the largest residual of A @ X = B | amax(abs(A @ X - B)) | take the largest residual of M @ u[0:3] = w[0:3] | ✓ |
take the largest difference between U and V | amax(abs(U - V)) | take the largest difference between u and v | ✓ |
Construction
| Phrase | Is the call | Example | ✓ |
|---|---|---|---|
take a vector of zeros of length N | zeros(N) | take a vector of zeros of length n | ✓ |
take a vector of ones of length N | ones(N) | take a vector of ones of length 3 | ✓ |
take a matrix of zeros with M rows and N columns | zeros(M, N) | take a matrix of zeros with 2 rows and 3 columns | ✓ |
take the identity matrix of size N | eye(N) | take the identity matrix of size 3 | ✓ |
take N evenly spaced points from A to B | linspace(A, B, N) | take 5 evenly spaced points from 0 to 1 | ✓ |
take the range from A to B | arange(A, B) | take the range from 2 to 6 | ✓ |
Probability and learning
| Phrase | Is the call | Example | ✓ |
|---|---|---|---|
take the softmax of V | softmax(V) | take the softmax of v | ✓ |
take the softmax of each row of M | softmax_rows(M) | take the softmax of each row of M | ✓ |
take the cross-entropy of L against Y | cross_entropy(L, Y) | take the cross-entropy of M against tgt | ✓ |
take the causal mask of size N | triu(ones(N, N), k=1) | take the causal mask of size n | ✓ |
take the gradient of F with respect to X | grad(F) | take the gradient of ph_quad(q, M) with respect to q | ✓ |
take the value and gradient of F with respect to X | value_and_grad(F) | take the value and gradient of ph_quad(q, M) with respect to q | ✓ |
Integrals, roots and derivatives
| Phrase | Is the call | Example | ✓ |
|---|---|---|---|
take the integral of F from A to B with N intervals | simpson(F, A, B, N) | take the integral of exp(-t * t) from 0 to 1 with 60 intervals | ✓ |
take the integral of F from A to B | q_gauss(F, A, B) | take the integral of s^2 from 1 to 2 | ✓ |
take the integral of F from A to B using every method | viv__every_integrate(F, A, B) | take the integral of exp(-t * t) from 0 to 1 using every method | ✓ |
take the integral of F from A to B using simpson with N intervals | simpson(F, A, B, N) | take the integral of exp(-t * t) from 0 to 1 using simpson with 60 intervals | ✓ |
take the integral of F from A to B using the trapezoid rule with N intervals | trapezoid(F, A, B, N) | take the integral of exp(-t * t) from 0 to 1 using the trapezoid rule with 60 intervals | ✓ |
take the integral of F from A to B using gauss-legendre | q_gauss(F, A, B) | take the integral of exp(-t * t) from 0 to 1 using gauss-legendre | ✓ |
take the integral of F from A to B using adaptive simpson | q_adaptive(F, A, B) | take the integral of exp(-t * t) from 0 to 1 using adaptive simpson | ✓ |
take the integral of F from A to B using romberg | q_romberg(F, A, B) | take the integral of exp(-t * t) from 0 to 1 using romberg | ✓ |
take the root|solution of F starting at X0 | r_newton(F, F', X0) | take the root of z^2 - c starting at 1 | ✓ |
take the root|solution of F starting at X0 using newton | r_newton(F, F', X0) | take the root of z^2 = c starting at 1 using newton | ✓ |
take the root|solution of F using newton starting at X0 | r_newton(F, F', X0) | take the solution of z^2 = c using newton starting at 1 | ✓ |
take the root|solution of F between A and B | r_safe(F, A, B) | take the root of z^2 - c between 0 and 2 | ✓ |
take the root|solution of F between A and B using every method | viv__every_find_root(F, A, B) | take the root of z^2 - c between 0 and 2 using every method | ✓ |
take the root|solution of F between A and B using bisection | r_bisect(F, A, B) | take the root of z^2 - c between 0 and 2 using bisection | ✓ |
take the root|solution of F between A and B using illinois | r_illinois(F, A, B) | take the solution of cos(z) = z between 0 and 1 using illinois | ✓ |
take the root|solution of F between A and B using regula falsi | r_illinois(F, A, B) | take the root of cos(z) - z between 0 and 1 using regula falsi | ✓ |
take the root|solution of F between A and B using false position | r_illinois(F, A, B) | take the root of cos(z) - z between 0 and 1 using false position | ✓ |
take the root|solution of F between A and B using safeguarded secant | r_safe(F, A, B) | take the root of cos(z) - z between 0 and 1 using safeguarded secant | ✓ |
take the derivative of F at X | deriv(F, X) | take the derivative of sin(z) * z at x | ✓ |
take the derivative of F at X using the symbolic derivative | cas_deriv_at({f#text}, {f#var}, X, {f#names}, {f#vals}) | take the derivative of atan(z) * z at x using the symbolic derivative | ✓ |
integrate F from A to B | q_gauss(F, A, B) | integrate t^3 from 0 to 2 | ✓ |
integrate F from A to B using every method | viv__every_integrate(F, A, B) | integrate t^3 from 0 to 2 using every method | ✓ |
integrate F from A to B with N intervals | simpson(F, A, B, N) | integrate exp(-t * t) from 0 to 1 with 60 intervals | ✓ |
integrate F from A to B using simpson with N intervals | simpson(F, A, B, N) | integrate exp(-t * t) from 0 to 1 using simpson with 60 intervals | ✓ |
integrate F from A to B using the trapezoid rule with N intervals | trapezoid(F, A, B, N) | integrate exp(-t * t) from 0 to 1 using the trapezoid rule with 60 intervals | ✓ |
integrate F from A to B using gauss-legendre | q_gauss(F, A, B) | integrate exp(-t * t) from 0 to 1 using gauss-legendre | ✓ |
integrate F from A to B using adaptive simpson | q_adaptive(F, A, B) | integrate exp(-t * t) from 0 to 1 using adaptive simpson | ✓ |
integrate F from A to B using romberg | q_romberg(F, A, B) | integrate exp(-t * t) from 0 to 1 using romberg | ✓ |
solve F starting at X0 | r_newton(F, F', X0) | solve z^3 = c starting at 1 | ✓ |
solve F starting at X0 using newton | r_newton(F, F', X0) | solve z^3 = c starting at 1 using newton | ✓ |
solve F using newton starting at X0 | r_newton(F, F', X0) | solve z^3 = c using newton starting at 1 | ✓ |
solve F between A and B | r_safe(F, A, B) | solve z^3 = c between 0 and 2 | ✓ |
solve F between A and B using every method | viv__every_find_root(F, A, B) | solve z^3 = c between 0 and 2 using every method | ✓ |
solve F between A and B using bisection | r_bisect(F, A, B) | solve z^3 = c between 0 and 2 using bisection | ✓ |
solve F between A and B using illinois | r_illinois(F, A, B) | solve z^3 = c between 0 and 2 using illinois | ✓ |
solve F between A and B using regula falsi | r_illinois(F, A, B) | solve z^3 = c between 0 and 2 using regula falsi | ✓ |
solve F between A and B using false position | r_illinois(F, A, B) | solve z^3 = c between 0 and 2 using false position | ✓ |
solve F between A and B using safeguarded secant | r_safe(F, A, B) | solve z^3 = c between 0 and 2 using safeguarded secant | ✓ |
differentiate F at X | deriv(F, X) | differentiate exp(z) * z at x | ✓ |
differentiate F at X using the symbolic derivative | cas_deriv_at({f#text}, {f#var}, X, {f#names}, {f#vals}) | differentiate erf(z) * c at x using the symbolic derivative | ✓ |
Builtins
Columns: I the interpreter, C the C99 backend, W the direct wasm backend. Each mark is a probe program that compiled (and, for I, ran) when this page was built.
A builtin's name is not reserved. A program that writes its own def band(lo, hi),
def where(...) or def sin(a, b, c) calls its def everywhere it
writes that name, in every engine, with the def's own arguments; the builtin is out of reach
in that program. So a new builtin never changes what an existing program computes.
Real math
Every one-argument function here also maps elementwise over an array, so y = sin(x) needs no loop.
| Signature | I | C | W | |
|---|---|---|---|---|
exp(x) | e^x | ✓ | ✓ | ✓ |
log(x) | natural log | ✓ | ✓ | ✓ |
ln(x) | alias for log | ✓ | ✓ | ✓ |
sqrt(x) | square root | ✓ | ✓ | ✓ |
pow(x, y) | x to the y | ✓ | ✓ | ✓ |
abs(x) | magnitude | ✓ | ✓ | ✓ |
fabs(x) | alias for abs | ✓ | ✓ | ✓ |
sin(x) | ✓ | ✓ | ✓ | |
cos(x) | ✓ | ✓ | ✓ | |
tan(x) | ✓ | ✓ | ✓ | |
asin(x) | ✓ | ✓ | ✓ | |
acos(x) | ✓ | ✓ | ✓ | |
atan(x) | ✓ | ✓ | ✓ | |
atan2(y, x) | quadrant-correct angle | ✓ | ✓ | ✓ |
sinh(x) | ✓ | ✓ | ✓ | |
cosh(x) | ✓ | ✓ | ✓ | |
tanh(x) | ✓ | ✓ | ✓ | |
erf(x) | error function | ✓ | ✓ | ✓ |
gamma(x) | Γ, C’s tgamma | ✓ | ✓ | ✓ |
floor(x) | ✓ | ✓ | ✓ | |
ceil(x) | ✓ | ✓ | ✓ | |
round(x) | ✓ | ✓ | ✓ | |
min(a, b) · min(a, b, c, ...) | the smallest; three or more arguments are min(min(a, b), c) | ✓ | ✓ | ✓ |
max(a, b) · max(a, b, c, ...) | the largest, as min | ✓ | ✓ | ✓ |
hypot(x, y) | sqrt(x^2 + y^2) with no overflow on the way (musl's, as abs of a complex number) | ✓ | ✓ | ✓ |
copysign(x, y) | |x| with the sign of y; -0.0 is negative | ✓ | ✓ | ✓ |
fma(a, b, c) | a*b + c rounded once (IEEE 754 fusedMultiplyAdd), the same bits in every engine and every build | ✓ | ✓ | ✓ |
fsum(xs) | the correctly rounded sum, as Python's math.fsum (Shewchuk's partials) | ✓ | ✓ | ✓ |
mod(a, b) | C’s fmod: the result takes the sign of a. The operator a % b is the floored remainder, with the sign of b | ✓ | ✓ | ✓ |
Bits
These take a number (a double), truncate it toward zero, wrap it into [0, 2^32) and hand back the result as an unsigned 32-bit value, so they compose without a cast. A shift of 32 or more gives 0, except that sar of a negative value gives all 32 bits set (4294967295, which i32 reads as -1). These are the operations a logic gate, an adder or a RISC-V instruction needs.
| Signature | I | C | W | |
|---|---|---|---|---|
band(a, b) | bitwise and | ✓ | ✓ | ✓ |
bor(a, b) | bitwise or | ✓ | ✓ | ✓ |
bxor(a, b) | bitwise exclusive or | ✓ | ✓ | ✓ |
bnot(a) | all 32 bits flipped | ✓ | ✓ | ✓ |
shl(a, k) | shift left by k | ✓ | ✓ | ✓ |
shr(a, k) | shift right by k, zeros in | ✓ | ✓ | ✓ |
sar(a, k) | shift right by k, sign in | ✓ | ✓ | ✓ |
u32(x) | the low 32 bits, unsigned | ✓ | ✓ | ✓ |
i32(x) | the same bits, signed | ✓ | ✓ | ✓ |
Ints
Int is a 64-bit integer. Overflow, // and % are checked and stop the run with the same words in every engine; these builtins are the unchecked forms an algorithm on 64-bit patterns needs (a random generator, a hash), with the pattern read as unsigned where the name says so.
| Signature | I | C | W | |
|---|---|---|---|---|
int_zeros(n) | an [Int] array of n zeros | ✓ | ✓ | ✓ |
wrap_add(a, b) · wrap_sub(a, b) · wrap_mul(a, b) | the result mod 2^64, as a pattern: no overflow check | ✓ | ✓ | ✓ |
umulhi(a, b) | the high 64 bits of the 128-bit unsigned product of the two patterns | ✓ | ✓ | ✓ |
ult(a, b) | a < b with both read as unsigned | ✓ | ✓ | ✓ |
ustr(a) | the pattern as an unsigned decimal string | ✓ | ✓ | ✓ |
Binary data
A file's bytes are an [Int], one element per byte, 0 to 255. The readers take that array and an offset and decode the bytes there, little-endian, as Python's struct with < does; an offset past the end, or an element that is not a byte, stops the run. In the browser read_bytes reads the files the page attaches to the run.
| Signature | I | C | W | |
|---|---|---|---|---|
read_bytes(path) · read_bytes(path, offset, count) | a file's bytes as an [Int]; with offset and count, only those (count left out: to the end) | ✓ | ✓ | ✓ |
le_u8(b, i) · le_i8(b, i) · le_u16(b, i) · le_i16(b, i) · le_u32(b, i) · le_i32(b, i) | the Int that the 1, 2 or 4 bytes at offset i encode, unsigned or two's complement | ✓ | ✓ | ✓ |
le_u64(b, i) · le_i64(b, i) | the Int that the 8 bytes at i encode; le_u64 stops when the value is above 2^63 - 1 | ✓ | ✓ | ✓ |
le_f16(b, i) · le_bf16(b, i) · le_f32(b, i) · le_f64(b, i) | the number an IEEE half, a bfloat16, a single or a double at i holds, exactly (subnormals, infinities and NaN included) | ✓ | ✓ | ✓ |
fround(x) | x rounded to the nearest binary32 (ties to even), as a number: JavaScript's Math.fround, NumPy's float64(float32(x)); a format that computes in single precision rounds each step with it | ✓ | ✓ | ✓ |
Arrays
| Signature | I | C | W | |
|---|---|---|---|---|
zeros(d1 … d4) | allocate, rank 1 to 4 | ✓ | ✓ | ✓ |
len(a) | the leading dimension | ✓ | ✓ | ✓ |
numel(a) | the number of elements, every axis multiplied | ✓ | ✓ | ✓ |
shape(a) | the lengths of the axes, as a vector | ✓ | ✓ | ✓ |
linspace(lo, hi, n) | n evenly spaced points | ✓ | ✓ | ✓ |
fill(a, v) | set every element in place | ✓ | ✓ | ✓ |
sort(a) | in place, ascending, stable; -0.0 before 0.0, NaN last; no value (copy first for a sorted copy) | ✓ | ✓ | ✓ |
transpose(A) | ✓ | ✓ | ✓ | |
sum(a) | the sum of every element of a real array, in NumPy's order in every exact engine: the entries in row-major order, from 0.0, in blocks of 8192, each block by NumPy's pairwise sum (below 8 entries left to right; to 128, eight running sums combined as a tree; above, two halves, the first a multiple of 8 long). Fast mode is not bound by this order | ✓ | ✓ | ✓ |
histogram(a, lo, hi, nbins) | counts of a's entries in nbins equal bins over [lo, hi], the last bin closed on the right, entries outside not counted. Draw it with bars() | ✓ | ✓ | ✓ |
roll(a, k) · roll(A, k, axis) | circular shift: element i moves to i + k, wrapping. A periodic stencil in one line: roll(u, 1) - 2.0 * u + roll(u, -1) | ✓ | ✓ | ✓ |
tridiag_solve(sub, diag, super, rhs) | the Thomas algorithm: solves a tridiagonal system in O(n), for the diagonally dominant systems that implicit PDE schemes produce. sub[0] and super[n-1] are unused | ✓ | ✓ | ✓ |
max(a, s) · max(a, b) | elementwise, against a scalar (either side) or another array; max(rho, 1.0e-30) floors a density | ✓ | ✓ | ✓ |
min(a, s) · min(a, b) | elementwise, as max | ✓ | ✓ | ✓ |
max(a) · min(a) | with ONE array: its largest / smallest entry. max(abs(u - exact)) is the max-norm error of a solve | ✓ | ✓ | ✓ |
max(xs, key=f) · min(xs, key=f) | the first element whose key is largest (smallest), as Python's; xs an array, a list of strings, a string or rows(M); f a def, a builtin or fn(x) -> ... | ✓ | ✓ | ✓ |
sorted(xs, key=f, reverse=r) | a new array or list in the order of the keys (numbers or strings), stable, as Python's; a NaN key last | ✓ | ✓ | ✓ |
argsort(xs, key=f, reverse=r) | the positions of xs in the order sorted(xs, key=f) puts them | ✓ | ✓ | ✓ |
all(m) | true when every entry of a real array is nonzero, as in the mask a == b gives; true for an empty array | ✓ | ✓ | ✓ |
any(m) | true when some entry is nonzero; false for an empty array | ✓ | ✓ | ✓ |
where(m, a, b) | a where m is nonzero, b where it is 0. a and b are each an array or a number, and the three broadcast to one shape, as NumPy's where: where(x > 0.0, x, 0.0) | ✓ | ✓ | ✓ |
pow(a, s) | every entry of a real array to the same scalar power: pow(rho, 1.0 / 3.0) | ✓ | ✓ | ✓ |
slab(A, k) | a COPY of the k-th slab along the leading axis: row k of a matrix, plane k of a rank-3 array. Real or complex | ✓ | ✓ | ✓ |
setslab(A, k, S) | overwrite that slab with S, in place | ✓ | ✓ | ✓ |
take(A, idx) | gather: A viewed flat (row-major), idx a real array of flat indices; the result has idx's shape | ✓ | ✓ | ✓ |
put(A, idx, V) | scatter, in place: the inverse of take | ✓ | ✓ | ✓ |
a[lo:hi:s] · A[i, :] · A[:, j] | a COPY of a block: lo to hi - 1 in steps of s, any part left out; a plain index drops its axis. Real or complex. See Slices above | ✓ | ✓ | ✓ |
a[lo:hi] = v · A[:, j] = v | write a number, or an array of the slice's shape, into the block, in place | ✓ | ✓ | ✓ |
copy(a) | a new array with a's shape and elements (assignment aliases; this does not) | ✓ | ✓ | ✓ |
concat(a, b, ...) | vectors joined end to end | ✓ | ✓ | ✓ |
vcat(A, B, ...) | rows stacked; a vector is one row | ✓ | ✓ | ✓ |
hcat(A, B, ...) | matrices side by side; a vector is one column | ✓ | ✓ | ✓ |
[] | an empty vector, the start of an array grown by append | ✓ | ✓ | ✓ |
a = append(a, x) | a with x on the end: a number onto a vector, or a vector as a new last row of a matrix ([] takes a first row). Written rebinding a; amortized O(1). See Growable arrays above | ✓ | ✓ | ✓ |
a = extend(a, b) | vector a followed by vector b, grown like an append | ✓ | ✓ | ✓ |
Shapes and constructors
NumPy's names and results (interp/arrays.rs; each case checked against NumPy in tools/arrays/numpy_cases.py). Every result is a new array. An axis may be negative, as in NumPy: -1 is the last.
| Signature | I | C | W | |
|---|---|---|---|---|
reshape(a, d1 … d4) | the same entries in row-major order under a new shape; one dimension may be -1 and is worked out | ✓ | ✓ | ✓ |
ravel(a) · flatten(a) | every entry as a vector, row-major | ✓ | ✓ | ✓ |
transpose(a) · transpose(a, axes=axes) | the axes reversed (a vector is copied); with axes, axis d of the result is axis axes[d] of a | ✓ | ✓ | ✓ |
swapaxes(a, i, j) | two axes exchanged | ✓ | ✓ | ✓ |
eye(n) · eye(n, m) · eye(n, m, k) | ones on diagonal k (k > 0 above the main one) | ✓ | ✓ | ✓ |
diag(v) · diag(v, k=k) · diag(A) · diag(A, k=k) | a vector onto diagonal k of a square matrix, or diagonal k of a matrix as a vector | ✓ | ✓ | ✓ |
ones(d1 … d4) · zeros_like(a) · ones_like(a) | filled arrays | ✓ | ✓ | ✓ |
arange(stop) · arange(start, stop) · arange(start, stop, step) | NumPy's arange: ceil((stop - start) / step) entries, entry i start + i * ((start + step) - start) | ✓ | ✓ | ✓ |
linspace(lo, hi, n, endpoint=endpoint) | NumPy's linspace: i * step + lo, with the end point included or left out; n may be 0 or 1 | ✓ | ✓ | ✓ |
rand(d1 … d4) · randn() · randn(d1 … d4) | draws from rand()'s generator in row-major order; randn is Box-Muller from two draws, lib/random's formula | ✓ | ✓ | ✓ |
Reductions, products and broadcasting
Arithmetic and comparisons between two arrays (not complex ones) broadcast as NumPy's do: the last axes line up, and each pair of lengths is equal or one of them is 1, so A - mean(A, axis=0) subtracts the column means and A * transpose([w]) scales rows. sum, mean, var, std along an axis and trace add in NumPy's order (pairwise, in blocks of 8192), so they agree with NumPy to the bit; so does sum(a) of a whole array. keepdims=true keeps the reduced axis with length 1 (NumPy's keepdims).
| Signature | I | C | W | |
|---|---|---|---|---|
sum(a, ordered=true) | the sum of every element left to right from 0.0 (row-major), the order of a Fortran or C loop, in every engine; for a port that must match a reference's last digits. ordered=false is sum(a). dot and prod always go left to right and take ordered=true to say so | ✓ | ✓ | ✓ |
sum(a, axis=axis) · sum(a, axis=axis, keepdims=keepdims) | along one axis, in NumPy's order | ✓ | ✓ | ✓ |
mean(a) · mean(a, axis=axis) · mean(a, axis=axis, keepdims=keepdims) | NumPy's pairwise sum divided by the count | ✓ | ✓ | ✓ |
var(a) · std(a) · var(a, axis=axis) · std(a, axis=axis) | population variance (divides by n) and its square root, NumPy's order | ✓ | ✓ | ✓ |
prod(a) · prod(a, axis=axis) | left to right | ✓ | ✓ | ✓ |
amax(a) · amin(a) · amax(a, axis=axis) · amin(a, axis=axis) | NumPy's max and min: a NaN wins, -0.0 is below 0.0 (max(a) of one array skips NaN, as C's fmax) | ✓ | ✓ | ✓ |
argmax(a) · argmin(a) · argmax(a, axis=axis) · argmin(a, axis=axis) | the index of the first largest (smallest) entry, flat or along an axis; the first NaN if there is one | ✓ | ✓ | ✓ |
any(a, axis=axis) · all(a, axis=axis) | masks of 1.0 and 0.0 along an axis | ✓ | ✓ | ✓ |
count_nonzero(a) | how many entries are not 0 | ✓ | ✓ | ✓ |
outer(u, v) | u[i] * v[j], each argument flattened | ✓ | ✓ | ✓ |
dot(u, v) · dot(u, v, ordered=true) | two vectors of one length, summed left to right (A @ B is the matrix product); ordered=true states that order and changes nothing | ✓ | ✓ | ✓ |
norm(a) | sqrt(dot(a, a)) over every entry (the Frobenius norm of a matrix) | ✓ | ✓ | ✓ |
trace(A) · trace(A, offset=k) | the sum of diagonal k, NumPy's order | ✓ | ✓ | ✓ |
triu(A) · triu(A, k=k) · tril(A) · tril(A, k=k) | the entries on and above (below) diagonal k; the rest 0 | ✓ | ✓ | ✓ |
einsum("ij,jk->ik", A, B) | a contraction of one to three arrays written in NumPy's subscripts (a string literal; an implicit output is the letters written once); a number when the output has no index. Summed left to right over the indices in their order of first appearance, so it agrees with NumPy to rounding | ✓ | ✓ | ✓ |
Gathers, masks, sorting and scans
A mask is an array of 1.0 and 0.0, as a comparison gives, so an array cannot say whether it is a mask or a list of indices: selecting by a mask is extract, and by indices take.
| Signature | I | C | W | |
|---|---|---|---|---|
take(A, idx, axis=axis) | NumPy's take along an axis: the result has A's axes before it, idx's shape, then A's axes after it. take(A, idx) with two arguments is flat | ✓ | ✓ | ✓ |
extract(mask, a) | the entries of a where mask is nonzero, as a vector (NumPy's a[mask]) | ✓ | ✓ | ✓ |
putmask(a, mask, v) | a[mask] = v in place: v a number, or an array of a's shape whose entries go where mask is nonzero | ✓ | ✓ | ✓ |
flatnonzero(a) | the flat indices of the nonzero entries | ✓ | ✓ | ✓ |
argsort(a) | indices that sort each row (the last axis), stable, NaN last, -0.0 equal to 0.0: NumPy's argsort(kind="stable") | ✓ | ✓ | ✓ |
cumsum(a) · cumsum(a, axis=axis) · cumprod(a) · cumprod(a, axis=axis) | running sums and products, left to right; without an axis over every entry, flat | ✓ | ✓ | ✓ |
diff(a) · diff(a, n=n) · diff(a, n=n, axis=axis) | a[i + 1] - a[i] along the last axis (or axis), n times | ✓ | ✓ | ✓ |
clip(x, lo, hi) | a number or an array held to [lo, hi]; a NaN entry stays NaN and a NaN bound is no bound, as NumPy's clip | ✓ | ✓ | ✓ |
Strings
Python's semantics, by code point; a.f(b) is f(a, b). See Strings above.
| Signature | I | C | W | |
|---|---|---|---|---|
len(s) | code points (a list: its strings) | ✓ | ✓ | ✓ |
str(x) | a number as Python's repr, a boolean as true/false | ✓ | ✓ | ✓ |
repr(x) | a string in quotes, as Python writes it | ✓ | ✓ | ✓ |
float(s) | CPython's float(str), correctly rounded | ✓ | ✓ | ✓ |
int(s[, base]) | CPython's int(str, base), to the nearest double | ✓ | ✓ | ✓ |
chr(n) | the character with code point n | ✓ | ✓ | ✓ |
ord(c) | the code point of one character | ✓ | ✓ | ✓ |
upper(s) | full Unicode mapping (ß is SS) | ✓ | ✓ | ✓ |
lower(s) | full mapping, final sigma | ✓ | ✓ | ✓ |
strip(s[, chars]) | whitespace (or chars) off both ends; lstrip, rstrip one end | ✓ | ✓ | ✓ |
lstrip(s[, chars]) | ✓ | ✓ | ✓ | |
rstrip(s[, chars]) | ✓ | ✓ | ✓ | |
split(s[, sep[, maxsplit]]) | a list of strings; no sep: runs of whitespace | ✓ | ✓ | ✓ |
rsplit(s[, sep[, maxsplit]]) | from the right | ✓ | ✓ | ✓ |
splitlines(s) | Python's line boundaries | ✓ | ✓ | ✓ |
join(sep, words) | Python's sep.join(words) | ✓ | ✓ | ✓ |
find(s, sub[, start[, end]]) | first position, or -1; rfind the last | ✓ | ✓ | ✓ |
rfind(s, sub[, start[, end]]) | ✓ | ✓ | ✓ | |
index(s, sub) | find, but a miss stops; also a list's index | ✓ | ✓ | ✓ |
rindex(s, sub) | ✓ | ✓ | ✓ | |
count(s, sub) | non-overlapping occurrences | ✓ | ✓ | ✓ |
replace(s, old, new[, count]) | ✓ | ✓ | ✓ | |
startswith(s, p) | ✓ | ✓ | ✓ | |
endswith(s, p) | ✓ | ✓ | ✓ | |
contains(s, sub) | the in operator: sub in s | ✓ | ✓ | ✓ |
ljust(s, width[, fill]) | rjust and center likewise | ✓ | ✓ | ✓ |
rjust(s, width[, fill]) | ✓ | ✓ | ✓ | |
center(s, width[, fill]) | ✓ | ✓ | ✓ | |
zfill(s, width) | zeros after a sign | ✓ | ✓ | ✓ |
isdigit(s) | also isspace isalpha isalnum isdecimal isnumeric isupper islower isascii isprintable | ✓ | ✓ | ✓ |
isalpha(s) · isalnum(s) · isdecimal(s) · isnumeric(s) | Python's character classes, by CPython 3.11's Unicode tables: true when s is not empty and every character is in the class | ✓ | ✓ | ✓ |
isspace(s) · isupper(s) · islower(s) · isascii(s) · isprintable(s) | Python's, character for character (isascii and isprintable are true for an empty string) | ✓ | ✓ | ✓ |
unique(s) · unique(words) · unique(xs) | each character of a string, or each string of a list, once, in code point order: Python's sorted(set(s)); of numbers, the distinct values ascending, as np.unique | ✓ | ✓ | ✓ |
sorted(words) · sorted(xs) | a new list, by code point; of numbers, a new array, ascending and stable, NaN last | ✓ | ✓ | ✓ |
reversed(x) | x[::-1]: an array, a string or a list of strings, back to front | ✓ | ✓ | ✓ |
pop(xs) · pop(xs, i) | a statement: y = pop(xs, i) takes element i (the last when left out) out of the list xs, as Python's | ✓ | ✓ | ✓ |
read_text(path) · read_lines(path) · write_text(path, s) | a UTF-8 file; read_lines(path) its lines; write_text(path, s) | ✓ | ✓ | ✓ |
Databases (SQLite)
SQLite (third_party/sqlite, public domain) is built into viv and compiled beside a C program that uses these (viv export --with-sqlite); the browser does not have them yet. A database is a number, its handle. Parameters are ? in the SQL, bound from a list of strings (as text) or an array of numbers (NaN as NULL), never pasted into it. db_exec runs inside a savepoint: SQL that fails changes nothing. A query is one statement giving one column. NULL reads as NaN in db_col and is an error in db_col_int and db_col_str (select COALESCE(x, k)). SQLite's errors are VivError; a misuse (a bad handle, a wrong count of parameters, a NULL in an Int column) is ValueError.
| Signature | I | C | W | |
|---|---|---|---|---|
db_open(path) · db_close(db) | open or create the database file; its handle. db_close closes it | ✓ | ✓ | ✓ |
db_exec(db, sql) · db_exec(db, sql, params) | run SQL (several statements, or one with ? parameters) inside a savepoint; the rows it changed | ✓ | ✓ | ✓ |
db_col(db, sql, params) · db_col_int(db, sql, params) · db_col_str(db, sql, params) | the query's one column as an array of numbers (NULL is NaN), an [Int], or a list of strings; params may be left out | ✓ | ✓ | ✓ |
db_stage(db, name, column) · db_insert(db, table) | set a column ([Num], [Int] or [Str]) aside, then write every staged column as rows in one transaction (NaN as NULL); the rows written | ✓ | ✓ | ✓ |
Maps
A map holds numbers or strings under number keys or string keys (one kind of each per map), in the order the keys were first stored. It is one handle: b = m, a def's parameter and a record field all change the same map. See Maps above.
| Signature | I | C | W | |
|---|---|---|---|---|
map() | a new empty map | ✓ | ✓ | ✓ |
m[k] = v · m[k] | store v under k (a key already there keeps its place), or read it; a key that is not there stops the run | ✓ | ✓ | ✓ |
m["key"] = v | a string key: printed in its repr, {'the': 2}; keys(m) is then a list of strings | ✓ | ✓ | ✓ |
has(m, k) | true when k is a key of m | ✓ | ✓ | ✓ |
get(m, k, d) | m[k] when k is there, else d | ✓ | ✓ | ✓ |
remove(m, k) · remove(m, k, d) | take k out and give its value; without d a key that is not there stops the run | ✓ | ✓ | ✓ |
keys(m) · values(m) | a new vector of the keys, or the values, in insertion order: loop over a map with ks = keys(m) then for i to len(ks) | ✓ | ✓ | ✓ |
copy(m) | a new map with the same keys, values and order | ✓ | ✓ | ✓ |
clear(m) | take every key out | ✓ | ✓ | ✓ |
len(m) | the number of keys | ✓ | ✓ | ✓ |
Group-by on Int keys
Written in Viv (lang/viv/std/groupby.viv) and added to a program that calls them, so every engine runs the same code. The keys stay Ints (no float conversion), hashed into an open-addressing table whose slot holds the key beside its group id. A group-by is factorize, then a dense loop: s[ids[i]] += v[i].
| Signature | I | C | W | |
|---|---|---|---|---|
uniq, ids = factorize(keys) | the distinct Ints of an [Int] column in first-appearance order, and each row's position in them (uniq[ids[i]] == keys[i]), as pandas.factorize(sort=False); any Int is a key | ✓ | ✓ | ✓ |
ka, kb, ids = factorize2(a, b) | factorize on two key columns at once: the distinct pairs in first-appearance order, as two columns, and each row's pair id; three columns: factorize2 its ids with the third | ✓ | ✓ | ✓ |
match_rows(build, probe) | a hash join's probe: for each Int of probe, the first row of build with that key, or -1 (pandas.Index.get_indexer) | ✓ | ✓ | ✓ |
lo, hi = key_range(keys) | the smallest and largest Int of a column, in one pass (0, 0 for no keys) | ✓ | ✓ | ✓ |
uniq, s, c = group_sum(keys, v) | per-group sum and row count of an [Int] column in one pass, the groups in first-appearance order as factorize's; keys spanning few values (found inside the call) are slotted by key - lo, others hashed with the sums beside the table; group_sum_num sums a number column | ✓ | ✓ | ✓ |
ka, kb, s, c = group_sum2(a, b, v) | group_sum keyed by two [Int] columns: one pass over pair slots when both span few values, else factorize2 and a loop | ✓ | ✓ | ✓ |
Linear algebra
In debug mode each solve checks its own result (a residual, a pivot check or a trace invariant) and stops with an error instead of returning a wrong answer.
| Signature | I | C | W | |
|---|---|---|---|---|
eig_sym(A, vals, vecs) | symmetric spectrum by cyclic Jacobi; values ascending, vectors in columns | ✓ | ✓ | ✓ |
spectrum_sym(A, vals) | the same eigenvalues eig_sym computes; in debug mode it runs every available method and asserts they agree | ✓ | ✓ | ✓ |
lin_solve(A, b, x) | LU with partial pivoting; checks the residual in debug mode | ✓ | ✓ | ✓ |
lin_solve_multi(A, B, X) | factor once, many right-hand sides | ✓ | ✓ | ✓ |
cholesky(A, L) | factors a symmetric positive-definite matrix; checks every pivot | ✓ | ✓ | ✓ |
x = lin_solve(A, b) · X = lin_solve(A, B) · inv(A) | the same LU as lin_solve and lin_solve_multi, into a new array: the same bits | ✓ | ✓ | ✓ |
L = cholesky(A) | the same factorization, into a new array | ✓ | ✓ | ✓ |
vals, vecs = eig_sym(A) · w, V = eig_herm(A) | the eigensolvers, unpacked into new arrays (written as the call that fills them) | ✓ | ✓ | ✓ |
U, s, Vt = svd(A) · svd(A, U, s, Vt) | A = U diag(s) Vt with k = min(m, n) singular values, descending: one-sided Jacobi, lib/svd.viv's algorithm. The signs of the vectors can differ from LAPACK's | ✓ | ✓ | ✓ |
eig_general(A, re, im) | balancing + Hessenberg + Francis double-shift QR; complex pairs split across re/im | ✓ | ✓ | ✓ |
eig_near(A, sigma, k, re, im) | k eigenvalues near a real shift by shift-invert Arnoldi — one LU instead of a full QR | ✓ | ✓ | ✓ |
Higher-order and contracts
A contract is named for what it computes, not for the method. In debug mode it runs every method that applies and asserts they agree; viv run --release runs the auto-selected one. The methods are ordinary Viv code, in lang/viv/std/contracts.viv.
| Signature | I | C | W | |
|---|---|---|---|---|
simpson(f, a, b, n) | composite Simpson | ✓ | ✓ | ✓ |
trapezoid(f, a, b, n) | ✓ | ✓ | ✓ | |
deriv(f, x0) | the exact derivative, computed by forward-mode autodiff when the program is compiled | ✓ | ✓ | ✓ |
apply(f, args…) | call a function held in a variable or an array | ✓ | ✓ | ✓ |
release_mode() | true under --release, else false, so a program can choose what to run in each mode | ✓ | ✓ | ✓ |
clock() | wall time in seconds from a monotonic clock; only the difference of two readings means anything, and only a program with a #! timing line may call it (every engine refuses it elsewhere; three-engines lists a timing program and does not compare it) | · | ✓ | ✓ |
Derivatives and records
grad and value_and_grad are reverse mode over a whole def (lang/viv/src/grad.rs): the compiler writes the gradient def before any engine runs, so the three engines run the same generated code.
| Signature | I | C | W | |
|---|---|---|---|---|
grad(f, x, args…) | the gradient of the number def f returns with respect to its first argument x (a number, an array, or a record of arrays), with the shape of x; the other arguments are constants | ✓ | ✓ | ✓ |
l, g = value_and_grad(f, x, args…) | the value and the gradient in one forward and one backward pass, unpacked on a line of its own | ✓ | ✓ | ✓ |
for_leaves(f, r1, r2, …, x) | call def f on each array of one or more records of the same type at once (the records' arrays first, then x), in field order; f may write the arrays in place: an optimizer step over a model | ✓ | ✓ | ✓ |
Complex scalars
The i suffix works only on a number, so a loop variable named i is still a variable. For arrays of them, see the next group.
| Signature | I | C | W | |
|---|---|---|---|---|
complex(re, im) | ✓ | ✓ | ✓ | |
re(z) | real part | ✓ | ✓ | ✓ |
im(z) | imaginary part | ✓ | ✓ | ✓ |
conj(z) | ✓ | ✓ | ✓ |
Complex arrays and the Fourier transform
A complex array is packed — interleaved (re, im) doubles, which is exactly how C99 lays out double complex, so both engines walk the same memory and agree to the byte. fft is radix-2 at a power of two, mixed radix (with dedicated radix-3 and radix-5 butterflies) when the primes of the length are all ≤ 7, and Bluestein everywhere else, so no length is a special case and 2·3·5-smooth ones are fast. The twiddle factors and factorization for each length are computed once and reused. Whole-array arithmetic works on complex arrays: a + b, a * b, a real array or a real or complex scalar on either side of *, / by a scalar, and @. Convention: fft is unscaled with exp(−2πikn/N); ifft carries the 1/N — the same as numpy and FFTW.
| Signature | I | C | W | |
|---|---|---|---|---|
czeros(d1 … d4) | a complex array of zeros, rank 1 to 4 | ✓ | ✓ | ✓ |
fft(a) | forward transform, in place, any length | ✓ | ✓ | ✓ |
ifft(a) | inverse transform, in place; ifft(fft(x)) returns x to rounding error | ✓ | ✓ | ✓ |
re(a) | real parts, as a real array of the same shape | ✓ | ✓ | ✓ |
im(a) | imaginary parts, as a real array | ✓ | ✓ | ✓ |
abs(a) | magnitudes — what you plot as a spectrum | ✓ | ✓ | ✓ |
conj(a) | the conjugate, as another complex array | ✓ | ✓ | ✓ |
complex(re, im) | a complex array from two real ones of the same shape | ✓ | ✓ | ✓ |
fft3(a) | forward transform of a rank-3 complex array along all three axes, in place; all-zero lines are skipped | ✓ | ✓ | ✓ |
ifft3(a) | its inverse, carrying 1/N | ✓ | ✓ | ✓ |
eig_herm(A, w, V) | the Hermitian eigenproblem: ascending eigenvalues into w, eigenvectors into the COLUMNS of V. Householder to real tridiagonal, then implicit QL | ✓ | ✓ | ✓ |
transpose(A) | plain transpose, NO conjugation: the adjoint is conj(transpose(A)), spelled out where it is meant | ✓ | ✓ | ✓ |
slab(A, k) · setslab · take · put | slab, setslab, take and put work on complex arrays too; source and target must both be real or both complex | ✓ | ✓ | ✓ |
Randomness
Park–Miller MINSTD in pure f64, so the stream is bit-identical between the interpreter and compiled C — a seeded Monte Carlo gives the same answer either way. #! use lib/random adds randn, randn_matrix, randint, shuffle, randperm, choice, arange and pi, written in Viv over rand(), each documented as the formula it computes so Python can replay it (tools/random_replay.py); #! use lib/stats adds mean and std.
| Signature | I | C | W | |
|---|---|---|---|---|
srand(seed) | seed the stream | ✓ | ✓ | ✓ |
rand() | uniform on [0, 1) | ✓ | ✓ | ✓ |
Figures and geometry
Each of these prints a structured record on stdout, and the page draws the chart from it. Compiled C prints the same record, so a figure is part of the program's output, not a call into a plotting library.
| Signature | I | C | W | |
|---|---|---|---|---|
plot(x, y, "label") | a line series; consecutive calls sharing x overlay | ✓ | ✓ | ✓ |
scatter(x, y, "label") | points | ✓ | ✓ | ✓ |
bars(x, y, "label") | a bar chart: a histogram's counts at its bin centers. A plot() on the same x afterwards draws a curve over the bars | ✓ | ✓ | ✓ |
plot2d(Z, "label") | a rank-2 array as a viridis heatmap with a colorbar | ✓ | ✓ | ✓ |
plot3d(x, y, z, "label") | a curve in 3-space, rotatable | ✓ | ✓ | ✓ |
quiver(x, y, u, v, "label") | a vector field | ✓ | ✓ | ✓ |
scene("title") | open a 3D scene; wire/verts output belongs to it | ✓ | ✓ | ✓ |
wire(x, y, z, "group") | a 3D polyline, optionally colormapped by a 4th array | ✓ | ✓ | ✓ |
verts(x, y, z, "group") | point markers — joints, atoms, nodes | ✓ | ✓ | ✓ |
figure(title, xlabel, ylabel[, xlo, xhi, ylo, yhi[, style]]) | REQUIRED before the first plot. The program sets the axis labels and limits; the page adds none of its own. On a plot2d the window is the picture's physical extent and gives it real axes; style is "contour" (overlay level lines) or "pixels" (no smoothing: the cells are categories or counts) | ✓ | ✓ | ✓ |
xtick("text", x) | inside a figure: a tick labeled with the text at x (a high-symmetry point, a category). A chart with any xtick shows only those x ticks. | ✓ | ✓ | ✓ |
xscale("log") · yscale("log") | a logarithmic x or y axis for the open figure: plot the values, not their logs. Ticks at powers of ten; a value of zero or less is a gap, and the chart says how many it left out. "linear" is the default | ✓ | ✓ | ✓ |
aspect("equal") | one unit of x as long on the screen as one unit of y, for an orbit or a shape in a plane: the plot takes the height that makes a circle round, and widens a range only if that height would be too tall or too flat. "auto" is the default | ✓ | ✓ | ✓ |
fill_between("label", x, lo, hi) | a band shaded from lo to hi over x: a confidence band, a gap, a tolerance. Grouped like a plot; takes its label's color, so a band labeled like its curve is drawn in the curve's color | ✓ | ✓ | ✓ |
rangebars("label", x, lo, hi) | a capped bar from lo to hi at each x: error bars, spreads, ranges. It takes the ends, not a center and an error, so mean ± err is rangebars("m", x, m - err, m + err) and an asymmetric interval is no harder; the center is a plot or scatter of its own | ✓ | ✓ | ✓ |
grid("title", rows, cols) · share("x") | the next rows * cols figures become the panels of one picture, row by row, each drawn as it would be alone. share("x") gives each column one x range and labels it under the lowest panel; share("y") does the same along each row; share("xy") both | ✓ | ✓ | ✓ |
yright("series label", "axis label") | the series with that label read against a second, linear y axis on the right, with its own ticks and label: two quantities in different units over one x. The legend marks the entry "(right axis)" | ✓ | ✓ | ✓ |
style("label", "dash s2 ring thick") | how the open figure draws the series with that label: a line (solid, dash, dot, noline), a palette slot (s1 to s6), a marker (circle, ring, square, cross, nomarker) and a width (thin, thick), at most one of each. Any other word is a compile error. With no style, series take the palette in order, solid | ✓ | ✓ | ✓ |
ytick("text", y) | inside a figure: a y tick labeled with the text; a chart with any shows only those y ticks | ✓ | ✓ | ✓ |
origin("lower") · colormap("diverging") | heatmap settings for the open figure: row 0 at the bottom instead of the top; a blue-white-red map centered on zero for a signed field | ✓ | ✓ | ✓ |
clim(lo, hi) · levels(values) | heatmap color limits (beyond them, the end color), and contour lines at exactly these values | ✓ | ✓ | ✓ |
box("id", "style", x0, y0, x1, y1) | in a figure with the style "diagram": a box by two corners. Style words, at most one of each: a color (s1 to s6, ink, muted), a fill (plain, tint, clear), a line (solid, dash, dot, noline), a width (thin, thick), corners (round, square) | ✓ | ✓ | ✓ |
arrow("id", "style", xs, ys) | in a diagram: a line through the points with a head at the last. Words: a color, a line (solid, dash, dot), a width, head or nohead, straight or curved | ✓ | ✓ | ✓ |
note("id", "text", "style", x, y) | in a diagram: text at a point. Words: a color, left, center or right, top, middle or bottom (which part of the text is at the point), small, normal or large (11, 13, 15 units), bold, mono | ✓ | ✓ | ✓ |
checkpoint() | save the figures so far as one frame; a sequence of frames plays as an animation | ✓ | ✓ | ✓ |
Animation
An animation is a Viv program. Objects are created hidden and named by a string id; transitions reveal, morph and move them. Each call prints one record, and the page plays the records with a scrubber.
| Signature | I | C | W | |
|---|---|---|---|---|
stage("title", xlo, xhi, ylo, yhi) | opens an animation; the window is its coordinate system | ✓ | ✓ | ✓ |
axes("x label", "y label") | ticks and a grid on the stage | ✓ | ✓ | ✓ |
curve("id", x, y) | a named polyline, created hidden. The same id again replaces its data | ✓ | ✓ | ✓ |
dots("id", x, y) · dots("id", x, y, r) | named point markers; with a radius, discs of that size in data units (atoms) | ✓ | ✓ | ✓ |
circle("id", x, y, r) | a disc | ✓ | ✓ | ✓ |
rect("id", x, y, w, h) · rect("id", x, y, w, h, angle) | a rectangle centered at (x, y), turned by angle radians | ✓ | ✓ | ✓ |
segment("id", x1, y1, x2, y2) | a line between two points: a pole, a rod, a spring | ✓ | ✓ | ✓ |
poly("id", xs, ys) | a filled polygon | ✓ | ✓ | ✓ |
frame(seconds) | a KEYFRAME: advance the clock; an object set again later moves there over the time between. In a keyframed animation objects are visible from creation | ✓ | ✓ | ✓ |
label("id", "text", x, y) | text at a point; $...$ is TeX, with \\ for a backslash | ✓ | ✓ | ✓ |
hline("id", y) · vline("id", x) | a dashed reference line across the stage | ✓ | ✓ | ✓ |
draw("id", seconds) | a curve is drawn on, start to end; anything else fades in | ✓ | ✓ | ✓ |
fade_in("id", seconds) · fade_out("id", seconds) | reveal or hide any object | ✓ | ✓ | ✓ |
morph("id", x, y, seconds) | move a curve's or dots' points smoothly to new data over the given seconds; the point count may change | ✓ | ✓ | ✓ |
move_to("id", x, y, seconds) | slide a label | ✓ | ✓ | ✓ |
say("caption") · say("caption", value) | the caption until the next say; a value is appended, which is how a caption reports a computed number | ✓ | ✓ | ✓ |
wait(seconds) | hold | ✓ | ✓ | ✓ |
together() … apart() | the transitions between them start at the same time | ✓ | ✓ | ✓ |
Output and testing
A failed assert prints what failed (for assert_near, the two values, their difference and the tolerance), keeps running, and makes the process exit nonzero at the end, so a build can stop on it while you still see every failure in one run.
| Signature | I | C | W | |
|---|---|---|---|---|
print(x) | the bare value | ✓ | ✓ | ✓ |
print("text", x, ..., sep=" ") | text and values on one line, a space (or sep) between them, as Python's print | ✓ | ✓ | ✓ |
fmt(x) | a print argument: the shortest digits that read back as x, as Python's repr | ✓ | ✓ | ✓ |
fmt(x, 17) | a print argument: 1 to 17 significant digits, as %.17g | ✓ | ✓ | ✓ |
fmt(x, ".3f") | a print argument: a Python format, [[fill]align][sign][0][width][,][.precision] then e, f, g, d, % (or E, F, G) | ✓ | ✓ | ✓ |
narrate "text" | prints a progress line while the program runs, separate from data output | ✓ | ✓ | ✓ |
text(codes) | writes an array of Unicode code points as one line of text (the counterpart of chars) | ✓ | ✓ | ✓ |
assert_near(a, b, tol) | ✓ | ✓ | ✓ | |
assert_true(cond) | ✓ | ✓ | ✓ | |
assert_gt(a, b) | ✓ | ✓ | ✓ | |
assert_lt(a, b) | ✓ | ✓ | ✓ | |
assert_allclose(a, b, tol) | elementwise, over arrays | ✓ | ✓ | ✓ |
What a test prints
A test block is the unit that passes or fails, so it prints exactly one line
under the name you gave it — not one line per assertion:
test "simpson integrates exp correctly" {
assert_near(simpson(fn(x) -> exp(x), 0.0, 1.0, 60), exp(1.0) - 1.0, 1.0e-9)
}
$ viv run integrate.viv
PASS simpson integrates exp correctly Several asserts in one block still give one line. A failure names the test and carries the first failing assertion's detail, and a block with no assertions says so instead of printing PASS:
PASS two asserts, one verdict FAIL a failing one -- assert_near: |1 - 2| = 1 > 1e-09 PASS nothing asserted (no assertions) assert_near PASS 1 assertion FAILED
That last assert_near PASS is an assertion at top level, outside any
test — there is no name to report under, so it prints its own line as before.
The trailing count is the summary, and it is what makes the process exit nonzero, so a build
script can fail on the exit code. It goes to stderr, after everything the program printed, in
the interpreter and in a compiled program alike, so stdout holds only the program's own output.
The backend matrix
The interpreter and the two compilers share one parser. Which features each one runs:
| Feature | Interpreter | C backend | Direct wasm |
|---|---|---|---|
Scalars, control flow, def, recursion | ✓ | ✓ | ✓ |
| Arrays | ✓ | ✓ | ✓ |
Lambdas, simpson, deriv | ✓ | ✓ | ✓ |
grad, value_and_grad (rewritten before any engine) | ✓ | ✓ | ✓ |
| Records | ✓ | ✓ | ✓ |
return a, b, a, b = e (rewritten before any engine) | ✓ | ✓ | ✓ |
| Complex scalars | ✓ | C99 double complex | two f64s, the interpreter's formulas |
| Arrays of functions (for example, a list of methods to try) | ✓ | ✓ | a table, call_indirect |
parallel for | sequential | OpenMP pragma | compiles, sequential |
narrate | narrate: line | narrate: line | narrate: line |
Figures (plot … scene) | drawn | printed as records | printed as records |
Call graph (__graph__/__trace__) | ✓ | — | — |
| Runaway-loop guard | step budget | — | — |
| Nonzero exit on a failed assert | ✓ | ✓ (atexit) | ✓ (the host's) |
Every feature goes into both the interpreter and the C backend, and the example suite checks
that it does. The direct wasm backend reaches wasm without C (a sum-of-squares program is a
348-byte module) and is checked beside them as a third engine
(tools/three-engines.sh).
The CLI
| Command | What it does |
|---|---|
viv f.viv -o f.c | Compile to portable C99. Then cc f.c -O2 -lm. |
viv f.viv -o f.wasm | Compile straight to a wasm module, no C step. |
viv f.viv --emit tokens|ast|ir | Dump an intermediate representation. The internals page is these dumps. |
viv run [--release] f.viv | Interpret. The compiler's checks run first, so viv run refuses what the compile refuses, with the same messages: a call to an unknown function in a branch the run never takes, or a variable that holds a number in one place and an array in another. --release runs only the method each contract selects instead of all of them. |
viv run --quiet f.viv | Drop the __graph__/__trace__/__steps__/__story__ lines. |
viv run --json f.vivviv f.viv --json | Write the errors to stderr as one line of JSON instead of text: {"diagnostics":[...]}, each with severity, message, file, line, col, end_line, end_col, label, source (the line) and notes. A run with no errors writes an empty list. For tools and models that read the errors and try again. |
viv --use lang/viv/std/contracts.viv f.viv | Put another file (a prelude) in front of this one before parsing. A program names the modules it uses itself, with import lines. |
import nn · import hf.scf as scf · from nn import rmsnorm, softmax_rows | Python's forms with Python's bindings: nn.rmsnorm(X), scf.run(...), rmsnorm(X); import a.b binds a; a from list may be parenthesized over several lines. Module a.b is a/b.viv beside the file, then lib/a/b.viv, then a/b.viv under the repository root; two different files that match are an error. A module is spliced in once, its tests left out, and its names are its own: a program sees what its import lines bind and not what its imports import, and a name two modules define is two names. Its phrases come with it. import * and relative imports are refused, and so is #! use, with the import to write. Resolved before anything parses: no run-time cost. |
private def f · private N = 3 | Keep a top-level def, record, enum or constant in its module: importing it, or reading it from another module, is a compile error. |
import post "slug" as name | A post used as a module. |
#! if-used basis.sto3g … #! end-if | Keep the lines between only when the program imports that module, anywhere in its imports. A lookup can serve whichever of several modules a program loads: basis.named finds a basis set by name among the tables the program imports. |
#! if-none-used basis.a basis.b … #! end-if | Keep the lines between, imports included, when the program's other imports reach none of the modules named: a default the program overrides by importing one. basis.named loads all eight basis tables for a program that imports none. |
viv expand f.viv · viv imports --write f.viv · viv fix --keywords --write f.viv | The program as the engines read it, imports spliced and names resolved; rewrite a file's #! use lines as imports; name the builtin options a file writes by position. |
viv doc f.viv [fn] | Structured annotations as JSON, or one function's doc card. |
viv whatis f.viv name | One name explained: kind, type, where it came from. |
viv lsfunc f.viv | Every def with its parameters. |
viv lsvar f.viv [fn] | Every binding, with its glossary line if it has one. |
viv src f.viv fn | One function's source, annotations stripped. |
viv deps f.viv | The static call graph. |
viv canon f.viv | Canonical dependency order. Idempotent. |
viv hash f.viv | Per-definition content addresses: node hash and body hash. |
viv features [name]viv features --usage [paths] | What Viv can do, by area, each entry with what it does, when to use it, an example and its output; --usage counts each feature's uses in a corpus and lists code that does by hand what a feature does. See the feature catalog. |
viv diff a.viv b.viv | Structural diff — edits, renames, unchanged. |
An error at run time reads like one at compile time: the message, the file, line and column, the line itself, and a caret under the part that failed. A compiled program prints the same lines, byte for byte, because the compiler writes each indexing site's location into the C.
$ viv run last.viv
2
error: index -3 out of bounds for dimension 0 (size 2)
--> last.viv:3:6
3 | show a[-3]
^^^^^ here
The notes answer habits from Python, ** (write x ^ y) and
for i in range(n) (write for i to n), and one from Julia,
2x (write 2 * x).
Every one of these commands also runs in the browser, on this site, at
/cli — the wasm module exports the same library functions
main.rs wraps.
The feature catalog
viv features lists what Viv has: 182 features written up in
9 areas, and 592 entries generated from the compiler's
own tables (builtins263, phrases9, cli verbs40, lens kinds11, keywords55, modules214).
Each written-up entry has a minimal example, and cargo test --test features
runs every example in the interpreter, as C and as wasm and checks the output it records. The
tables below are that catalog; viv features <name> prints one entry with
its example and output, and viv features --search <words> finds entries by
words. Agents writing Viv consult it before writing code, and run
viv features --usage on what they wrote: it lists loops, sorts, roundings and
copies written out where a feature does the work.
Syntax
| Feature | What it does | Source |
|---|---|---|
list-comprehension | [e for x in xs if c] builds a vector, a matrix (when every e is a vector of one length) or a list of strings, as Python's does. More fors nest, the first outermost, and the for takes any head a for statement takes. The parser writes it as the loop it stands for, so every engine runs it. | lang/viv/src/surface.rs; www/src/pages/reference.astro |
map-comprehension | {k: v for x in xs if c} builds a map, Python's dict comprehension, with the same heads and filters as a list comprehension. A key seen again keeps its first place and takes the later value. A brace in an expression is only this form: a set literal {x, y} is refused with a pointer to unique. | lang/viv/src/surface.rs |
generator-argument | A comprehension without brackets as the only argument of a call, f(e for x in xs), is f([e for x in xs]), so sum, max and join take one. all(c for x in xs) and any(...) give a Bool and stop at the first element that decides it. | lang/viv/src/surface.rs; www/src/pages/reference.astro |
if-expression | a if c else b is a when c holds and b otherwise; only the chosen side is computed. It binds loosest of all operators, as in Python, so a if c else b if d else e chains. | lang/viv/src/surface.rs; www/src/pages/reference.astro |
negative-index | A negative index or slice bound counts from the end, as in Python: a[-1] is the last element, a[-3:] the last three, a[:-1] all but the last, A[-1, :] the last row. An index still has to land inside the axis: a[-3] of two elements stops the run with its line. | lang/viv/src/interp/slices.rs; www/src/pages/reference.astro |
slices | a[lo:hi:s], A[i, :], A[:, j] and A[r0:r1, c0:c1] select a block of an array of rank up to 4. The result is a copy, never a view; a plain index drops its axis. Bounds are not clamped: a range past the end stops the run with its line, where NumPy would clamp. A negative step walks back, as Python's: a[::-1] is a back to front, a[4:1:-1] is a[4], a[3], a[2], A[:, ::-1] reverses the columns. | lang/viv/src/interp/slices.rs; www/src/pages/reference.astro |
slice-write | a[lo:hi] = v and A[:, j] = v write a number into every selected element, or an array of exactly the slice's shape into the block, in place. The value is copied before the write, so s[1:5] = s[0:4] shifts. a[lo:hi] += v and the other compound forms read the slice, combine and write it back. | lang/viv/src/interp/slices.rs; www/src/pages/reference.astro |
in-operator | x in a tests membership: a number in a real array of any rank (some element == x), a key in a map, a substring in a string, a string in a list of strings. not in is its negation. Both give a Bool. | www/src/pages/reference.astro |
key-argument | max(xs, key=f) and min(xs, key=f) give the first element whose key is largest (smallest); sorted(xs, key=f, reverse=r) and argsort(xs, key=f, reverse=r) give the elements (or their positions) in the order of their keys, stable, as Python's. xs is an array, a list of strings, a string or rows(M); the key is a def, a builtin such as len or abs, or a lambda. | lang/viv/src/surface.rs; www/src/pages/reference.astro |
for-in-loops | for x in e walks a vector's numbers, a string's characters or a list's strings. for i, x in enumerate(e) gives the index too, for a, b in zip(u, v) walks several sequences together and stops at the shortest, and for row in rows(M) gives the rows of a matrix. e is evaluated once; the parser writes the loop as for k to len(e). | www/src/pages/reference.astro; lang/viv/src/parser.rs |
enumerate-start | enumerate(e, 1) or enumerate(e, start=1) counts from 1 (or any start) instead of 0, in a loop head or a comprehension. In a loop head with an integer literal start the index is an Int, as Python's is. | www/src/pages/reference.astro |
for-from-by | for i to n runs 0 to n - 1. for i from a to b starts at a, by s steps by s (negative steps count down), and a trailing reversed runs the same values in the opposite order. The end is left out, as in Python's range. A step of 0 is an error at compile time when it is a literal, at run time otherwise. | www/src/pages/reference.astro; lang/viv/src/parser.rs |
bare-return-and-trailing-commas | return with no value leaves a def early; such a def gives 0, as one that reaches its end does. A list literal may end with a comma, [a,\n b,\n], as a call's arguments and a record literal's fields may. | |
python-math | pi, e, tau and inf are Python's math constants wherever a program does not bind the name itself. from math import pi, sqrt and import math (then math.sqrt(x)) are accepted: the names are Viv's builtins, and an import of one Viv does not have says so. del xs[i] removes an element, as pop(xs, i) does without giving it back. | |
python-literals | .5, 2.e5, 1_000_000, 0xff, 0o17 and 0b101 are numbers as Python writes them (a base-16, 8 or 2 literal is an integer literal, an Int where one is wanted). A string may be in single quotes, 'it', with the escapes of a double-quoted one and \xHH. lo <= x < hi is lo <= x and x < hi, the middle computed once. | lang/viv/src/lexer.rs |
while-loops | while cond { ... } runs its body while cond is true; the condition must be a Bool, not a number. break leaves the innermost while or for loop and continue starts its next pass. | www/src/pages/reference.astro; lang/viv/src/parser.rs |
compound-assignment | x += e, -=, *=, /= and //= on a variable, an array element (a[i, j] += e) or a slice. On an element each index is evaluated once: an index that is not a number or a variable is computed into a hidden variable first, so c[floor(10 * rand())] += 1 draws one random number. | www/src/pages/reference.astro |
operators | x ^ y is pow(x, y), right-associative and tighter than a unary minus (-2 ^ 2 is -4). a % b is the floored remainder and a // b the floored quotient, Python's rules. @ is the matrix product (NumPy's matmul). and and or short-circuit in every engine, so j > 0 and a[j - 1] > v never reads a[-1]. + of two arrays adds them element by element; it does not join them as Python's lists, and [1.0] + row (a one-element literal beside an array, or literals of different lengths) is refused with a note naming concat. | www/src/pages/reference.astro |
multiple-returns | return a, b gives several values; every return of the def gives the same number, each with one type. The caller unpacks them, lo, hi = f(x), on a line of its own. The compiler declares a record for the results, so every engine runs it. | lang/viv/src/tuples.rs |
unpacking | a, b = e assigns from a def that returns as many values, from a list of values (a, b = b, a computes the right side first, then assigns left to right), or from an array with one element per name (n, m = shape(A)). A target may be a name, an element or a slice, so v[i], v[j] = v[j], v[i] swaps two elements. | www/src/pages/reference.astro |
steps | step name "text" { ... } names a phase of a def or of the top level. A step is a block, not a scope: its variables are the def's. The engines record entering and leaving it, and the explorer, the story and the intent and debug lenses address it as def/name. | |
one-line-steps | step name: statement (or step name "text": statement) is a step of one statement on that statement's line, the same to every engine and tool as the braced form, hashes included. A statement that opens a block runs to its closing brace. | |
phrases | A phrase starts with take and a word or number (take the trace of M), or with one of the verbs integrate, solve, differentiate, sum. The parser matches it against the phrase table and reads it as an ordinary call, so all three engines run only calls they already run. Integrals, roots and derivatives take their one free variable as the variable; the quadratures and root finders come from lang/viv/std. viv phrases lists the table; viv phrases file.viv prints what each phrase of a program is. | lang/viv/src/phrases.rs |
user-phrases | phrase "<words>" -> <expr> at the top level declares a phrase. A word of the phrase that is a name the body reads is a hole; the other words are the ones a use must contain. A use may come before the declaration, and a module's phrases reach the programs that use it. | |
lambdas | fn(x) -> expr is a function value with a single-expression body, which may read the program's variables. Assign it, pass it, call it through a parameter. The body may hold a comprehension, a map comprehension, a generator argument or an if-expression (encode = fn(text) -> [stoi[c] for c in text]). Inside a def it captures the locals it reads, by value (see closures). | www/src/pages/reference.astro |
closures | A lambda written inside a def reads the def's locals and parameters, and holds the values they had when it was made: a later assignment does not reach it. Bind it to a name and call it, or pass it to a def. It cannot be returned, stored in an array, record or map, or assign a name it captured. The compiled engines lift it to a def whose trailing parameters are the captured values, and specialize the def it is passed to, so no closure is allocated and every call is direct. | lang/viv/src/defun.rs; lang/viv/src/surface.rs |
method-call-syntax | a.f(b) is f(a, b) for any call, so line.split(), w.upper() and v.norm() read as they would in Python. A name that is a record field is a field access instead. | www/src/pages/reference.astro |
f-strings | f"x = {x:.3f}" formats each field as fmt does: {x} is str(x), {x:spec} a Python format, {s!r} the repr, {x=} the name and its repr. The result is a string value usable anywhere. | |
format-specs | A spec is Python's: [[fill]align][sign][0][width][,|_][.precision][type], the type one of e E f F g G d % or none (the repr). {x:06.2f} is -01.50, {x:+.3e} shows the sign, {x:*^9.2f} centers with *, {n:,} groups thousands, {p:.1%} is a percent, {x:G} is upper case; an Int takes the same flags. A width or precision can be a value, as in Python: {name:>{w}}, {x:.{p}f}, fmt(x, f".{p}g"). fmt(x, spec) reads the same specs. #, z, n and the integer bases are refused with a message. | |
print-and-fmt | print(a, b, ...) writes its arguments on one line with a space between them, as Python's, or sep= between them. A number prints with six significant digits, and a whole number below 10^16 as its digits (1059490), as str and f-strings write it; fmt(x) is Python's repr, the shortest digits that read back as x (3.0), fmt(x, 17) gives 17 significant digits, and fmt(x, ".3e") a Python format. The interpreter, C and wasm print the same bytes. | |
show-and-narrate | show a, b prints each value labeled with what was written (x: 0.3), two spaces apart; a variable or fmt of one is labeled with its name, any other expression is printed bare. narrate "text" prints a progress line (narrate: text); a non-string argument is commentary and compiles to nothing. | www/src/pages/reference.astro |
stop | stop("text") ends the run with error: text and the line it is on, exit status 1, in every engine. The argument is one string, a literal or a value such as an f-string. | www/src/pages/reference.astro |
docstrings-and-annotations | A bare string as the first statement of a def documents it and compiles to nothing. The comment markers #| (what), #|| (how), #||| (why), #~ (tags), #: (a variable's glossary entry) and #[/#] (regions) are comments to the compiler that viv doc, viv whatis and viv lsvar read. | www/src/pages/reference.astro section Lexical structure |
text-fences | Everything between two lines that are exactly ### is skipped, so prose needs no # in front of each line. A post keeps its title and notes in one. | www/src/pages/reference.astro |
imports | Python's three forms with Python's bindings: import nn (then nn.rmsnorm(X)), import hf.scf as scf, and from nn import rmsnorm, softmax_rows (a parenthesized list may run over several lines). import a.b binds a. A module a.b is a/b.viv beside the importing file, then lib/a/b.viv, then a/b.viv under the repository root; two different files that match are an error. A module's top-level names are its own: a program sees only what its import lines bind, not the modules its imports import, and a name two modules define is two names. A module's phrases and its .intent, .debug and .memory files come with any import of it. Resolved before anything parses, so a run pays nothing. import * and relative imports are refused; #! use is refused with the import that replaces it. | lang/viv/src/imports.rs |
private-names | private before a top-level def, record, enum or constant keeps it in its module: private SH_L = 1, private def es_pair(i, j). An import of it, or a read of it from another module, is a compile error naming the module. Names are public by default, as in Python. | lang/viv/src/imports.rs |
keyword-arguments | def init(rows, cols, scale = 1.0 / sqrt(rows)): a parameter with a default comes after those without, and a call passes it by name only, init(16, 8, scale=0.5) (init(16, 8, 0.5) is refused). A parameter without a default may be passed by position or by name. A default may read the parameters before it and the program's top-level names; it is computed at each call that leaves it out, and an argument it reads is computed once. The front end fills every call out, so the engines see positional calls and a run pays nothing. A call through a function value passes its arguments in order. | lang/viv/src/kwargs.rs |
set-overrides | viv run --set NAME=value file.viv (and the compile) replaces the top-level assignment NAME = ... with the given value before parsing, in every engine: a number for a number, a double-quoted string for a string (--set 'CLASS="W"'). A --set that names no such assignment is an error. | lang/viv/src/lib.rs |
ladders | rung "title" { ... } is one step of a program that builds itself. The program at rung k is everything outside the rungs plus rungs 1 to k, and a def in a later rung replaces an earlier one of the same name; the earlier rungs' tests still run against it. viv run runs the top rung; viv ladder runs every rung and reports, as JSON, what each added, edited and broke. | |
placeholder-defs | placeholder def name(params) declares a def that has intent but no body yet. Calling it stops the run with an error that names it; viv lsfunc --placeholders lists them. | |
growable-arrays | [] is an empty vector. a = append(a, x) adds a number to a vector, or a vector as a new last row of a matrix; a = extend(a, b) joins two vectors. The append is written rebinding the name it grows, and each engine grows the storage in place when nothing else holds the array, so n appends cost O(n). There is no pop: a = a[:-1]. | |
recursion | A def may call itself, directly or through other defs, to a depth of 2500 nested calls, the same in every engine. A call past that stops the run with recursion deeper than 2500 nested calls, located at the def. | lang/viv/src/recursion.rs |
Types
| Feature | What it does | Source |
|---|---|---|
num-and-bool | Num is a 64-bit float, the default for every number; a double holds every integer up to 2^53 exactly. Bool is true or false: what a comparison gives and what if and while take. A Bool is not a number: true + 1.0 and if count { are refused by every engine. | www/src/pages/reference.astro |
int-type | Int is a signed 64-bit integer. An Int comes from an annotation (n: Int, a parameter, a return, a field), from arithmetic on Ints or from an Int array's element. + - * // % ^ of Ints give Ints, and a result outside 64 bits stops the run with the same message in every engine; / gives a number. A number where an Int is bound is a compile error naming int(...). An integer literal is a number unless it stands where an Int is expected; 2 ^ 62 of two literals is a number, so start from an Int variable. | |
ints-and-numbers | An Int goes wherever a number does, converted exactly (a magnitude past 2^53 stops the run): sqrt(n), chr(97 + i), append(xs, n) on a real array, a map key or value, a def's parameter written without a type that is also passed numbers. k = int(x) makes k an Int when every assignment to k is an Int (int(...), a literal, Int arithmetic); k = k / 2 keeps it a number. enumerate(xs, start=1) counts in Ints. A comprehension of Int values is a real array unless an [Int] is wanted. A number is never made an Int without int, round, floor or ceil. | |
int-arrays | [Int] is a packed rank-1 array of Ints: int_zeros(n), a literal in an Int position (idx: [Int] = [2, 0, 1], ps: [Int] = []), indexing, len, printing, passing and returning. It grows with ps = append(ps, k) (amortized, like a real array's), and a comprehension in an [Int] position builds one (sq: [Int] = [k * k for k to n]). An Int indexes an array directly. | |
float32 | Float32 is an IEEE binary32 number. x: Float32 = 0.1 holds the float32 nearest 0.1; float32(x) rounds a number or an Int to one and float(x) widens it exactly. A literal beside a Float32 is a Float32 (x * 3.0 + 1); a Float32 and a number variable do not mix: the compiler names float32(y) and float(x). + - * /, sqrt, abs, min, max are correctly rounded and evaluated in float32 in every engine (no fused multiply-add, no excess precision); exp, log, sin, cos, tanh are Viv's double functions rounded once, checked on all 2^32 inputs. bfloat16(x) and float16(x) round a Float32 to those precisions, to nearest even. fmt(x) writes the shortest digits that read back as the same float32, as NumPy's repr(np.float32(x)) does. | |
packed-arrays | Rank-1 arrays stored at their width: float32_zeros(n), bfloat16_zeros(n), float16_zeros(n), int8_zeros(n), int16_zeros(n), int32_zeros(n), uint8_zeros(n), or a conversion named after the type: float32(a) of an Arr, bfloat16(w) of a [Float32], int8(q) of an [Int]. A float element reads as a Float32 and a store rounds to nearest even; an integer element reads as an Int and a store out of range stops the run (128 does not fit in an Int8 (-128 to 127)). float(a) widens a float array to an Arr, int64(a) an integer one to an [Int]. sum of a [Float32] takes NumPy's float32 order and gives numpy.sum's bits; dot(a, b) adds a[i]*b[i] in float32 with i ascending. | |
binary-data | read_bytes(path) gives a file's bytes as an [Int], one element per byte; read_bytes(path, offset, count) gives only that window, so a large file is read in the pieces a program needs. le_u8, le_i8, le_u16, le_i16, le_u32, le_i32, le_u64, le_i64 read the Int at an offset, and le_f16, le_bf16, le_f32, le_f64 the number, little-endian and exact (Python's struct.unpack_from("<e", b, i) and the rest). In the browser a program reads the files attached to its run. | |
enums-and-match | enum Name { A, B(T1, T2) } declares a sum type; a value is Name.A or Name.B(x, y). match e { B(x, _) => { ... }, A | C => ..., _ => ... } takes it apart. A match must cover every variant or end with _: a missing variant is a compile error that names it, and so is an arm that can never run. Payloads are Num, Int, Bool, Str, an enum (the enum itself included) or an optional. Enum values are immutable. | |
optionals | T? is an optional T, Some(x) or None, taken apart by match with Some(v) and None arms. None adapts to an optional position. | |
records | record P { x: Num, v: Arr } declares a struct with typed fields, in every engine. Build one with every field named, P { x: 1.0, v: zeros(3) }; read a field with p.x. A def that takes or returns one annotates it (def f(p: P) -> P). Records nest, and hold Num, Int, Bool, Str, arrays, maps, other records and arrays of records. | |
record-defaults | record GptConfig { vocab: Int, context: Int = 8, width: Int = 16 } gives a field a default; a literal that leaves the field out gets it, GptConfig { vocab: 9 }. A field with no default must be given. A default is a constant: a literal, a top-level constant or arithmetic of them (a value that calls a def or draws belongs in a constructor def). A record update R { f: e, ..base } takes what it leaves out from base. The front end writes the defaults into each literal, so the engines see complete literals. | lang/viv/src/ctor.rs |
constructor-defs | def Head(c: GptConfig) -> Head { ... } is a def named after its record; Head(c) calls it and Head { ... } is the literal, so it reads as Python's Head(cfg). It must declare the record as its return type, and a record has at most one. A literal's values run in the order written; values that draw or call defs must be written in the record's declaration order (the order the engines compute them), or the literal is refused. [Block(c) for l to c.layers] builds an array of records in order. | lang/viv/src/ctor.rs |
record-update | T { f1: e1, ..base } is the record base with the fields written replaced, Rust's struct-update syntax. Any field may be written, or none (T { ..base } is a copy). | |
field-assignment | p.x = e assigns a field, and a place may go through records and arrays of records: m.blocks[l].attn.Wq = W, m.last.W[1] = 5.0. Records are values: after q = m, m.first.scale = 2.0 leaves q as it was. A write is into a variable of the def it is in; a def does not change a record it was given. | |
arrays-of-records | [T] is an array of records of one type: [a, b], append(rs, r), rs[i], len(rs), rs[i] = r, for r in rs, and places through it (mol.atoms[1].z = 2.0). It is a value, as each record in it is: mol = Molecule { atoms: atoms } and a write through mol leaves atoms as it was. | |
record-arithmetic | For a record whose every field is an array (or a record or array of them), + - * / with another of its type or a number, -r, sqrt(r) and the other elementwise functions act on each array; zeros_like(r) is one of the same shape, and sum(r) adds its arrays' sums. A number field is refused, with its name. | www/src/pages/reference.astro |
strings | Str is an immutable UTF-8 value. Length, indices and slices count code points, as Python's do, negative indices count from the end, and slice bounds are clamped. The functions are Python's, character for character (CPython 3.11's Unicode tables and error messages): split, join, strip, find, replace, upper, startswith, float, int and the rest. Strings are never changed in place. Literals side by side join ("ab" "cd", across lines inside parentheses), and """...""" holds newlines. | |
string-lists | [Str] comes from split, a literal ["a", "b"], a comprehension, or [] grown by append. It has len, indices, slices, +, in, sorted, unique, join and for. unique gives each string once, sorted, which is the form of a Python set a program can print and test. | |
maps | map() is an empty map with number or string keys and values (one kind of each per map). m[k] = v stores, m[k] reads, and the keys stay in the order first stored, so printing never depends on hashing. has, get(m, k, d), remove, keys, values, len, copy and clear complete it. A map is one handle: after b = m, a store through b is in m. | |
complex-numbers | The i suffix makes an imaginary literal (3.0 + 4.0i); complex scalars have full arithmetic with real promotion, re, im, conj, abs and complex exp, log, sqrt, sin, cos, tan. czeros(n) or a literal with a complex element makes a complex array (CArr), packed as interleaved doubles. Every engine computes them with Viv's own formulas (Smith's division, no fused multiply-add), so the bits agree. Complex arrays do not broadcast and are not printed whole. | www/src/pages/reference.astro |
function-values | A def name or a lambda is a value: pass it to a def, hold it in a variable, call it through the parameter. The compiled engines specialize a higher-order def per function passed (run_it(sq, x) becomes run_it__sq). A builtin name such as sqrt is not a function value; wrap it in a def or a lambda. | lang/viv/src/defun.rs |
type-annotations | A parameter (v: Arr), a return (-> Num) and a variable (label: Str = "x") may be annotated with Num, Int, Bool, Str, Arr, CArr, [Int], [Str], Map, a record name, [T] or T?. Unannotated types are inferred from the first binding; a record parameter or a record result needs its annotation. A variable's type cannot change. | |
aliasing-and-copies | Assignment and parameter passing alias arrays: b = a shares storage, and a def that writes into an array parameter writes into the caller's array. copy(a) and every slice are new arrays. Records are values (q = p gives q its own fields), but an array field is shared like any array, so an element write through q.v is seen through p.v. Maps are handles. Strings and lists of strings are never changed in place. | www/src/pages/reference.astro |
Arrays
| Feature | What it does | Source |
|---|---|---|
numpy-vocabulary | The array builtins take NumPy's names and give NumPy's results: reshape, ravel, flatten, transpose of any rank and with axes, swapaxes, eye, diag both ways, ones, zeros_like, ones_like, arange, linspace(..., endpoint), outer, dot, norm, trace, triu, tril. Every result is a new array, in every engine. An axis may be negative, as in NumPy. | www/src/pages/reference.astro |
elementwise-math | Every one-argument math builtin (sin, exp, sqrt, abs, tanh, ...) maps over a real array, and + - * / and ^ work element by element between an array and a number (either side) or two arrays. pow(a, s), max(a, s) and min(a, s) do the same with a scalar. y = sin(x) * exp(-x) needs no loop. | www/src/pages/reference.astro section Builtins |
broadcasting | Arithmetic, the comparisons, where and two-array max and min between two arrays of numbers (not complex ones) line up their last axes; each pair of lengths is equal, or one is 1 and repeats. A - mean(A, 0) subtracts the column means, and reshape(v, n, 1) makes a column that repeats across. Shapes that do not fit stop the run with both shapes named. grad sums a broadcast operand's gradient back to its shape. Complex arrays do not broadcast. | www/src/pages/reference.astro section arrays |
builtin-options | A builtin's array operands are positional and its options are named, with NumPy's names: axis= and keepdims= (the reductions, argmax, cumsum, diff, take), n= (diff), k= (triu, tril, diag), offset= (trace), endpoint= (linspace) and axes= (transpose). An option written by position is refused with its name; viv fix --keywords rewrites a file. The front end writes the options into the positional call the engines read. | lang/viv/src/kwargs.rs |
axis-reductions | sum, mean, var, std, prod, amax, amin, any and all take axis=, and argmax/argmin take one or reduce the flat array. keepdims=true keeps the reduced axis with length 1 (NumPy's keepdims), so A - mean(A, axis=1, keepdims=true) subtracts each row's mean. A reduction along the only axis of a vector is refused with the whole-array form to write instead. | www/src/pages/reference.astro section arrays |
batched-matmul | A @ B multiplies two matrices; a vector on the left is a row and on the right a column. An operand of rank 3 or 4 is a stack of matrices in its last two axes, and the axes before them broadcast, so X @ W with X of shape (B, T, d) and W (d, k) multiplies every matrix of the stack. Two vectors are refused (dot(u, v) is their product). grad differentiates it, summing over the axes an operand was broadcast along. | lang/viv/src/interp/matmul.rs |
masks-and-where | A comparison with an array on either side gives a mask of 1.0 and 0.0. where(m, a, b) picks from a or b and broadcasts all three; extract(m, a) is NumPy's a[mask], putmask(a, m, v) is a[mask] = v in place, flatnonzero(m) gives the positions, and count_nonzero, any and all read a mask. if a == b on arrays is refused with a note to write all(a == b). | www/src/pages/reference.astro section arrays |
gathers | take(A, idx, axis) is NumPy's A[idx] along an axis (a row gather is take(X, rows, 0)); take(A, idx) reads the flat array, row-major, and the result has the shape of idx. put(A, idx, V) is the scatter back, in place. The indices are not negative: take(a, [-1.0], 0) stops the run. | www/src/pages/reference.astro section arrays |
sorting | sort(a) sorts an array of numbers in place, ascending and stable, with NaN last. argsort(a) gives the positions in sorted order. sorted(xs, key=f, reverse=r) and argsort(xs, key=f, reverse=r) order by a key, stably, as Python's do, and take arrays, lists of strings and rows(M). sorted of an array of numbers needs a key= or a reverse= (sorted(v) alone is the string form and refuses an array): copy and sort for a plain ascending copy. | www/src/pages/reference.astro section comprehensions |
scans | cumsum and cumprod give the running sum and product, left to right as NumPy does; diff the differences of neighbors; clip(a, lo, hi) bounds every entry, keeping a NaN entry and reading a NaN bound as no bound. | www/src/pages/reference.astro section arrays |
einsum | einsum("ij,jk->ik", A, B) contracts one to three operands by their index letters, as NumPy's does; with no -> the output indices are the ones that appear once, in alphabetical order. It loops over the output entries in row-major order and over the summed indices in order of first appearance, so it agrees with NumPy to rounding. | www/src/pages/reference.astro section arrays |
solvers-returning-tuples | x = lin_solve(A, b) (a matrix b solves for every column), inv(A), L = cholesky(A), vals, vecs = eig_sym(A), w, V = eig_herm(A) and U, s, Vt = svd(A) return what they compute. The older forms that fill arrays passed in still work and give the same bits. svd is one-sided Jacobi, so its vectors may differ in sign from LAPACK's. | www/src/pages/reference.astro section arrays |
fft | fft(z) and ifft(z) transform a complex vector in place, and fft3/ifft3 a rank-3 complex array; ifft divides by the length, so ifft(fft(z)) gives z back to rounding. The arrays are packed (re, im) doubles, so the interpreter, C and wasm walk the same memory and agree to the byte. | www/src/pages/reference.astro section Builtins |
random-draws | srand(seed) seeds one generator; rand() and randn() draw a number, and rand(d1, ...), randn(d1, ...) draw an array of up to four axes in row-major order. randn is Box-Muller from two uniform draws. The same seed gives the same numbers in every engine. | www/src/pages/reference.astro section Builtins |
numpy-agreement | sum, mean, var and std, of a whole array or along an axis, and trace add in NumPy's pairwise order (blocks of 8192), so they print what NumPy prints, and mean(a) is sum(a) / numel(a) to the bit. amax/amin/argmax/argmin follow NumPy on NaN and signed zeros. tools/arrays/numpy_cases.py checks 4,812 cases byte for byte in three engines. | tools/arrays/numpy_cases.py; www/src/pages/reference.astro section arrays |
ordered-sum | sum(a, ordered=true) adds every element left to right from 0.0, the order of a Fortran or C loop, where sum(a) adds in NumPy's pairwise order. The two can differ in the last digits (or more, when large terms cancel). dot and prod always go left to right; dot(u, v, ordered=true) says so in the code and changes nothing. The value is true or false as written, and the option takes no axis=. | lang/viv/src/interp/arrays.rs; lang/viv/src/kwargs.rs |
complex-arrays | czeros(n, ...) makes a complex array of up to four axes, and a vector literal with a complex element ([1.0 + 2.0i, 3.0]) is one, the reals promoted; a matrix literal with a complex element is refused, so build a complex matrix with czeros and stores. Indexing gives a Cplx; storing a real promotes it; re, im and abs of a complex array give arrays of numbers. A complex and a real array do not mix silently, complex arrays do not broadcast, and print/show refuse a whole complex array (print re(z) and im(z)). | www/src/pages/reference.astro section types |
Autodiff
| Feature | What it does | Source |
|---|---|---|
deriv | deriv(fn(x) -> expr, x0) is the exact derivative of a scalar expression at x0. The compiler emits the chain rule inline (a value and a derivative per node), so there is no runtime dual type. The body is one expression of builtins it has rules for; a call of a program's own def inside it is refused. | |
grad | g = grad(f, x, a, b) is the gradient of the number f(x, a, b) with respect to its first argument, with x's shape; the other arguments are constants. Loops, branches, calls, slices and the array vocabulary are differentiated. The compiler rewrites the program before any engine sees it, so the interpreter, C and wasm run the same generated def and print the same bytes. | |
value-and-grad | l, g = value_and_grad(f, x, a, b) gives f(x, a, b) and its gradient with respect to x from one forward and one backward pass. It is an unpacking, written on a line of its own. | |
value-and-grad-over-records | When the differentiated argument is a record whose every field is an array, a record of arrays or an array of them (Gpt { tok, pos, blocks: [Block], unembed }), the gradient is a record of the same type: each array of g is the derivative with respect to the array at the same place. The first parameter's annotation (m: Model) marks it. A number, boolean or string field is refused with its name: keep sizes in a record of their own. | |
reduction-adjoints | grad differentiates sum, mean, var, std, prod, amax and amin, whole or along an axis with or without keepdims; reshape, ravel, flatten, transpose with axes, swapaxes; take along an axis; dot, norm; where; and @ on stacks of matrices. A broadcast operand's gradient is summed back to its shape, and amax/amin share it among tied entries as torch.amax does. | study/idioms/autodiff-probes/axes.viv |
for-leaves | for_leaves(f, r1, r2, ..., x) calls the def f once per array of the records, passing the arrays at the same place in each record, then any extra arguments; f may write the first in place. zeros_like(r) is a record of the same shape filled with zeros, and sum(r) adds the sums of its arrays. | www/src/pages/reference.astro section Statements |
autodiff-limits | Anything grad cannot differentiate is a compile error at its line, never a silent zero: a builtin with no adjoint given a differentiated value (eig_sym, lin_solve, gamma, fft, sort, complex numbers), break and continue, return inside a loop, parallel for, recursion, a write into a parameter or global array the def also reads, and grad inside a differentiated def. The tape keeps every overwritten value, with no checkpointing. The error's note lists the differentiable builtins. |
Correctness
| Feature | What it does | Source |
|---|---|---|
test-blocks | test "name" { ... } runs its body and prints one line, PASS name or FAIL name -- <the first failure> (and N more), however many asserts are inside. A failure does not stop the run: the program keeps going, and the process exits nonzero at the end with N assertions FAILED on stderr. The same lines come out of the interpreter, C and wasm. | www/src/pages/reference.astro (What a test prints); tools/three-engines.sh |
asserts | assert_near(a, b, tol) (|a - b| <= tol), assert_allclose(A, B, tol) (elementwise over arrays), assert_gt(a, b), assert_lt(a, b) and assert_true(cond). A failure prints the values, their difference and the tolerance, and the run goes on. Inside a test block they report under the test's name; outside one each prints its own line (assert_near PASS). | www/src/pages/reference.astro (Output and testing) |
holds | A hold in a debug lens (<module>.debug, or an inline debug { } block) is an assertion with a sentence, checked at a step (hold at update ...) or after every pass of a def's loops. a == b within tol compares with a tolerance, over every element for arrays. A hold may read the def's variables and call pure builtins. A failed hold goes into the story and makes the run exit nonzero (1 hold FAILED); the printed output does not change. viv run --release and -DVIV_RELEASE skip them. | |
contracts | integrate(f, a, b) and find_root(f, a, b) are named for the problem, not a method. Each is a contract with three methods from different families (Gauss-Legendre, adaptive Simpson and Romberg; a safeguarded secant, bisection and Illinois). A call runs the default (Gauss, the secant); using q_romberg picks another; using every method solves by all three, stops if one is farther than relative 1e-8 from their median, and returns the median. find_root first asserts that f changes sign on [a, b]. They are in lang/viv/std/contracts.viv, brought in with #! use lang/viv/std/contracts. | lang/viv/std/contracts.viv |
contract-declarations | contract name(params) -> out: Type, ... { ... } names a problem: intent "..." says what it computes, methods a, b lists the defs that solve it (or a block of label: a, b lines, groups that using every <label> method runs; the first, or the default, is the one a call runs), and agree out within relative T, within absolute T or exactly says what two methods must agree on, T one number or one name (relative: |a - b| <= T max(1, |reference|)). A method's parameters beyond the contract's are its settings, with defaults in its own signature; a top-level settings b(tol = 1e-12) gives them wherever b runs. A program chooses once at the top level: using name = b, using name = sel (a variable, such as a program parameter), or using name = every method, which runs them all and stops at the first disagreement; following calls run that choice. name(...) using b is a one-off for one call; viv run --using name=b sets the start. The compiler checks every method's parameters, types, settings and number of results against the contract. It compiles to an enum, a selection variable and a def, so all three engines run it. | lib/eigen/lowest.viv |
method-registries | A registry is an array of function values, [q_gauss, q_romberg], returned from a def or held in a variable. sweep(methods, f, a, b) (std/contracts) runs every method in it, asserts that the worst deviation from the median is below 1e-8 (1 + |median|), and returns the median; sweep_spread(methods, f, a, b, out) writes each result into out and returns the worst deviation, without asserting. Adding a method is adding its name to the array. The compiled engines call through a function pointer. spectrum_sym(A, vals) is the builtin form: it checks that A is symmetric and computes the spectrum by cyclic Jacobi and by Householder plus QL, and stops on dissent; release mode runs QL only. | lang/viv/std/contracts.viv |
using-every-method | A root phrase ends with using <method>: bisection, illinois, newton, or every method, which calls the find_root contract and so solves by all three bracketing methods and checks that they agree. The parser adds the std defs the phrase needs to the program. | |
check-phrases-certificates | viv check --phrases <file> finds every phrase with a closed form (integrals, derivatives, polynomial roots of degree 1 to 3, sums, norms and traces of written-out arrays), computes its exact value with lib/cas, and compares it with the number the program computes, within a tolerance set by the method. It prints the exact value, how it was found (an antiderivative whose derivative is checked against the integrand; the quadratic formula), the numeric value, and the math as TeX. A phrase it cannot handle (a hole that reads a value computed at run time, a polynomial of degree 4) is reported as refused, never passed. A phrase that disagrees makes it exit 1. --json prints the records; --emit out.viv writes the check program it runs. | |
three-engine-identity | Every program runs in the interpreter (viv run), as C (viv f.viv -o f.c) and as wasm (viv f.viv -o f.wasm), and the three print the same bytes: they share Viv's own libm (lang/viv/src/vmath and its C twin), NumPy's summation orders, Smith's complex division, and one formatter. tools/three-engines.sh runs a directory through all three and fails on any byte that differs. | tools/three-engines.sh |
strictness-rules | The front end stops with a located error, in every engine, where another language would pick a meaning: 2x (write 2 * x); if a == b on arrays (a comparison of arrays is a mask; write all(a == b)); a number or an array as a condition; true + 1.0; a call with the wrong argument count; an unknown name; a def and a top-level variable with one name; a format outside [width][.precision]e|f|g|d; a map that stores a number key and a string key. At run time an index or slice outside its array stops the run with its line (index 5 out of bounds for dimension 0 (size 3)); nothing is clamped or wrapped. | www/src/pages/reference.astro (Lexical structure, Types); lang/viv/tests/semantics.rs |
viv-check | viv check <file> runs the compiler's front end (what the C and wasm builds share) and the interpreter's own checks, and nothing else. It is silent with exit 0 when the program would run, and prints located diagnostics with exit 1 when not. --json prints them as one JSON object; --use and --set work as for viv run; a Python file is translated by py2viv first. --phrases is a different check (check-phrases-certificates). | lang/viv/src/main.rs |
problem-posts | A post with kind: problem in its ### header (older posts: #! kind problem) is a program whose tests are meant to fail: the defs are stubs and the tests say what a solution must compute. tools/three-engines.sh requires such a file to fail and the three engines to agree; the feed lists its failing tests. A solution post (kind: solution, solves: <slug>) keeps every test word for word and passes them. | posts/README.md; www/scripts/feed-build.mjs |
Program knowledge (lenses)
| Feature | What it does | Source |
|---|---|---|
intent-lens | <module>.intent holds one block per def, headed by the def's name and its body hash: what, how and why lines, a glossary line per variable (x the current estimate), and the def's steps as an outline with the control words repeat until ..., if ..., for each .... The same entries can be written inline as #|, #||, #||| and #: comments above the def; each anchor has one home, and viv lens move --to-file|--to-inline moves them. viv lens init writes a skeleton with every def, step and hash; viv lens check fails on a stale entry, a public def with no what, a step with no text, an outline whose nesting differs from the code, or a number in the text with no [test "..."] citation. | |
intent-levels | An intent entry explains a def (what, how, why, the glossary), each of its steps (one line of text, and why and how lines under it), and single statements: = x <text> under a step explains the first statement of that step that assigns x, = f() the first that calls f, #k the k-th, and > adds a paragraph. An anchor that names no statement fails viv lens check. | |
intent-stamps | Each .intent and .debug entry carries @<body hash>, the hash viv hash prints for the def's body (it ignores formatting and the def's name). When the body changes, the entry is stale: viv lens check reports it with the structural diff and fails. viv lens stamp <file> <def> records that the entry was re-read and is right for the new body. Inline entries are stamped by a #@ <hash> line. | |
lens-coverage | viv lens coverage [files or dirs] counts, per module, the defs with a what, the steps with text, the glossary lines against the variables, and the stale entries, dead defs and serves of nothing. --check compares the repository with lens_coverage.json and fails when a module's coverage drops; --update records the current numbers. viv lens needed lists dead defs: defs reached from no program, test or entry point. | |
debt | viv debt [files or dirs] lists every def without a what, every step without text, every undocumented variable, every stale or unstamped entry, dead defs, and the entries no person has signed, with each def's number of callers, the most central first. --limit n shortens the list; --run <program> runs it once and puts the defs it calls first, the most called first. | |
debug-lens | <module>.debug (or an inline debug { } block) says what to look at while a def runs. watch at <step> x and watch per iteration x, r = <expr> record values; an item name = expr computes a value the code does not hold, in a debug build only. hold checks a claim (see holds). trace f, g records every call of those defs. All of it goes into the story, never to stdout, so the program prints the same with and without the lens, and a release build skips it. Entries are stamped with the def's body hash. | |
story | viv story <file> runs the program in debug mode and prints its calls, steps and loop passes as a tree, each def and step with its intent text, each pass with the values the debug lens watches, and each failed hold where it happened, then a count of holds checked. A def appears when it has steps or is traced. --depth k limits the call depth, --expand shows the steps inside each pass, --engine c takes the events from the compiled program, --json feeds the explorer. | |
digits | viv digits <file> runs the program in the interpreter once as written, then again with its floating-point evaluation perturbed, and gives for every printed number the leading significant digits that stay the same, with a | where they stop. The perturbations, each switchable: the order of every built-in reduction (sum, dot, the sums of @: pairwise, left to right, reversed, random), FMA contraction of a*b + c, and Monte Carlo arithmetic as Verificarlo does it (noise of half an ulp on each + - * /, digits -log10(sigma/|mu|) over the samples). It then perturbs the most-executed lines one at a time and says which printed numbers each one moves. A whole result with no rounding error (an index, a size) is left exact. --json and -o write the report the explorer shows; vipyr.digits runs it from Python. | |
memory-lens | viv view <file> --lens memory prints, for each def, what the compiler infers: which parameters it writes in place (the caller sees the change), which it only reads, what it returns, and what a loop pass keeps or frees. Nothing in it is written by hand. A <module>.memory file holds the one fact the compiler takes on trust: a disjoint pin saying a parallel for writes different elements on every pass, which checked builds verify while they run. | |
arch-lens | A .arch file names a system's modules in layers (library, program, verification) with an order, private names and their owners, budgets (how many modules and defs a program may load, how deep its calls go, a def's fan-in), and multi-paths (a stage with several implementations, its registry and the test that checks they agree). viv arch check reports upward and verification dependencies, private reads, broken budgets and stages that do not match the code; --check compares with arch_ratchet.json. viv arch report <program> measures one program. | |
cert | viv check --phrases --cert <file> writes <file>.cert: one entry per phrase, stamped with a hash of what its check read, with the phrase as written, the math in TeX, the exact value, how the CAS found it, the numeric value and its method, and the outcome. On the next --cert run an entry whose phrase changed or is gone is reported as stale. | |
pseudocode-view | viv view <file> --lens pseudocode prints each def as an algorithm: its what, its steps numbered with their intent text, the control words of the outline, and each statement as x ← expr, tagged [phrase] when it is written as a phrase and [code] otherwise, with a count of each. --def d shows one def; --words-only leaves out the code. | lang/viv/src/pseudo.rs |
explain | viv explain <file> prints the program's calls from the top level down, each def with its what and its steps with their text, as the intent lens gives them. --from <def> starts at a def, --depth k limits the depth, --json is for tools. It reads the code and the lenses and runs nothing. | |
view-lenses | viv view <file> --lens <l> shows the program as code (without annotations), intent (the default), pseudocode, debug, story (runs it), memory, or all: the code with its intent entries written back in as #| comments and step text. --json prints it for tools. It never changes a file. |
Tools
| Feature | What it does | Source |
|---|---|---|
run | viv run f.viv runs the program in the tree-walking interpreter, the same one the browser workbench runs. -q drops the __graph__, __trace__, __steps__ and __story__ lines the debug lens prints; --release makes release_mode() true, so a contract runs one method instead of sweeping; --json writes the errors and the failed-assert summary to stderr as one JSON object; --use p.viv puts a prelude before the program; --set NAME=value replaces a top-level NAME = <number> line before the run. A failed assert or hold makes the exit status 1. A .py file, or one that starts with #! lang python, is translated by py2viv first. The interpreter stops a run past 2e9 steps, its guard against a hung loop; VIV_STEP_BUDGET=2e10 viv run f.viv raises the limit for a long run that is not a hang. | www/src/pages/reference.astro |
compile | viv f.viv -o f.c writes one C99 file that holds the program and the runtime it needs; viv f.viv -o f.wasm writes a WebAssembly module from the direct wasm backend, which tools/run-wasm.mjs runs in node. --emit tokens, --emit ast and --emit ir print the lexer's tokens, the syntax tree or the IR (each node with its content hash) and stop; the IR shows the call a form becomes, such as 2.0 ^ 3, which is a call of pow. The C is built with any C compiler (cc -O2 f.c -lm), and prints what the interpreter prints, byte for byte. | |
explorer | viv graph f.viv prints the program as a graph of defs, the steps inside them and the loops that hold them, with each step's inputs and outputs. --json is the graph for tools, --bundle adds the program as run, its line table, the intent and the debug lens (what the /explore page loads), --run runs the program as the explorer does, and --svg [-o out.svg] draws the whole program with lib/graphdraw. The explorer page runs the program in your browser and places each moment of the run in this graph, step by step. | |
atlas | viv atlas f.viv runs the program once and writes f.atlas.html: every def the top level reaches, by module, with its intent, steps and loops, the calls between defs and how many times each ran, and the whole-program picture of viv graph --svg. It is one file with the data and the viewer inside (the site's code map); it opens from file:// with no network. -o names the file, --text prints the atlas in the terminal (modules, defs with their intent lines, steps, loops, call counts, the call tree from the top level), --json prints the explorer bundle with the counts and the map for tools, and --no-run skips the run and shows the calls the text makes. | lang/viv/src/atlas.rs; www/scripts/atlas-viewer-build.mjs |
deps | viv deps f.viv lists every def with its parameter count, the defs it calls and the callers it has (◂), then what the top level calls. --json gives the same as data. It reads the code; nothing runs. | lang/viv/src/lib.rs |
whatis | viv whatis f.viv name prints what the #: glossary line says a variable is, and the def (and lines) it belongs to. The glossary is the comment #: name meaning above a def. | www/src/pages/reference.astro section Lexical structure |
lsfunc | viv lsfunc f.viv lists every def with its line range, its parameters and the first line of its #| doc. --placeholders lists only the placeholder defs, the defs whose intent is written and whose code is not. | www/src/pages/reference.astro section Lexical structure |
lsvar | viv lsvar f.viv [def] prints every #: glossary entry, grouped by the def it belongs to, or only those of one def. | www/src/pages/reference.astro section Lexical structure |
src | viv src f.viv name prints one def with its line numbers, with the annotation comments above it. | lang/viv/src/lib.rs |
expand | viv expand f.viv prints the program with its imports spliced in and its names resolved: a qualified name (nn.rmsnorm) written as the name it means, and a name two modules define written <module path>__<name> in the module that defines it. | lang/viv/src/imports.rs |
imports-migration | viv imports [--write] f.viv ... replaces each #! use m line with from m import <the names the file reads from m> (or import m when it reads none), followed by a from line for each module m brought in that the file reads from, so no name comes from a module the file does not import. Every file is read before any is written. | lang/viv/src/imports.rs |
fix-keywords | viv fix --keywords [--write] f.viv ... rewrites sum(X, -1, true) as sum(X, axis=-1, keepdims=true), and every other builtin option written by position (take(..., axis=), triu(..., k=), linspace(..., endpoint=)), keeping a file only when its definitions hash as before: the filled call is the call the engines read either way. | lang/viv/src/kwargs.rs |
doc | viv doc f.viv writes the file's annotations as JSON: each def's #| what, #|| how, #||| why, #~ tags, its #: glossary and its line notes, with the code. viv doc f.viv name prints one def's entry as text, with the defs it calls. The annotations are comments to the compiler; this verb is how tools read them. | www/src/pages/reference.astro section Lexical structure |
lint | viv lint f.viv (or a directory) reports an integer literal other than 0, 1 and 2 used as an array size (zeros(37)), a loop bound (for i to 37) or a bound on a variable used as an index. Literals in a NAME = value line, in array literals, in a test's expected values and in literal indices are allowed. --count prints a count per path. The exit status is 1 when it finds any. | lang/viv/src/lint.rs |
lint-round | viv lint --round f.viv lists each round(...) whose value is used as an index, a slice bound, a loop bound or an array size, directly or through the variable it is assigned to. With --count it prints how many of the file's round calls are such uses. An index that is a number must already be a whole number, so these calls either do nothing or hide a table of integers stored as numbers. | lang/viv/src/lint.rs |
formatter | This repository has no viv fmt. What exists: viv canon puts defs in dependency order; tools/steps/one_line.py --fix rewrites a step whose body is one short statement into the one-line form step name: statement, keeping a file only when its hashes and syntax tree are unchanged. The line-breaking formatter, npm run vivfmt, is kept outside this repository (scripts/viv-lint.mjs) for now. | tools/steps/one_line.py |
hash-diff-canon | viv hash f.viv prints, for every def, global and the top level, a node hash (name included) and a body hash (the code alone, from the IR): reformatting moves neither, renaming moves only the node hash. viv diff old.viv new.viv compares two versions by those hashes and reports each def as unchanged, edited, renamed (same body, new name), added or removed. viv canon f.viv rewrites the file with defs in dependency order, callees first, and is idempotent. | www/src/pages/reference.astro section The CLI |
claims | viv claims lists the claims in claims/ (--json: one object; --only <slug>: some), each a statement with a Viv program and its vipyr twin, and the status recorded in claims/results.json. viv claims run runs the fast ones (--all, --heavy or --only <slug> for others) through tools/claims/run.py; --check fails when a program fails, the two disagree on a value both print, or a number of the statement is not established; --write records the results. | tools/claims/run.py |
facts | viv facts --json f.viv prints one JSON object: each def with its span, parameters, signature, three hashes (ast_hash, the code as parsed with comments, step headings and the def's own name left out; closure_hash, which also covers every def it calls; ir_hash, the body hash viv hash prints), its intent state, steps, #| notes and calls; the tests and the top level with their calls; the imports; the features used. | |
plot | viv plot [out.txt | -] [-o out.svg] [--width N] [--chart K] reads the figure records a run printed (from figure, plot, scatter, bars and the rest) and writes SVG, the same bytes every time: every chart one under another, or the K-th. With no file, or -, it reads stdin, so viv run -q f.viv | viv plot -o f.svg draws a run. | |
plot-math | A figure label's $...$ parts are TeX, set as math by viv plot and by the site's charts alike: Greek letters, ^ and _, \frac, \sqrt, accents, \mathrm and \text, operator names such as \log set upright, \sum and \int, \left( \right), the spacing commands and the common relations and operators. Titles, axis labels, legend entries, ticks and label marks all take it; text outside the dollars stays as it is, and \$ is a dollar sign. An unknown command makes viv plot fail with the record's line and the column in the label. In a Viv string a backslash is written \\. | lang/viv/src/mathtext.rs |
export | viv export p.export.toml reads a manifest that names a program's files, its decks, its parameters and its results, and writes p.c (the program and its runtime, which cc -shared builds into a library), p.h, p.py (a ctypes wrapper with a typed run), a README and a test, by default into target/export/p. A run of the library gives the bits a run of the program gives; an error in it raises a Python exception with Viv's message and line, and the next run works. A manifest with kind = "kernels" and [kernels.<name>] tables exports several defs instead, each a C entry point and a Python function with the def's signature (complex numbers and several results included), after a module initialization that runs once. | |
cuda | viv cuda p.viv -o p.cu writes one CUDA C file: Viv's libm marked for the device, a __global__ function for each kernel def and for each parallel for whose body can run on a GPU, and an extern "C" launcher for each that computes the grid as the emulator does (the default, or the kernel's schedule line), copies the arrays to the device, launches and copies the written ones back. A parallel for that cannot be a kernel is listed at the end of the file with the reason. The file is meant for nvcc with -fmad=false -prec-div=true -prec-sqrt=true -ftz=false. --set NAME=value sets a global constant first, as in a run, so one source gives a file per block shape or tile size. | |
cas-derivs | viv cas derivs prints lib/cas/derivs.viv, the derivative rules of the computer algebra system, written from lib/cas/rules/derivatives.toml, the file grad.rs reads too. So the CAS and autodiff differentiate a builtin by one table. | lib/cas/rules/derivatives.toml |
py2viv | py2viv (tools/py2viv/py2viv.py) translates numeric Python with NumPy into Viv. viv run prog.py, viv check prog.py and viv prog.py -o prog.c translate first, as does a file that starts with #! lang python. A translated program behaves as CPython with NumPy runs it, or py2viv refuses it, listing every construct outside the subset at its Python line and column with what to write instead. def test_x(): becomes a test block, and a run-time error is reported at the Python line it came from. | tools/py2viv/SUBSET.md; tools/py2viv/RULES.md |
compare-harness | study/compare holds the same small programs in Python and in Viv, topic by topic (basics, strings, maps, records, autodiff, ml, numerics, plotting and more), each pair split into matching # @@ regions. study/compare/check.sh lints them, runs both sides and requires the same printed text, checks each pair's .gen.json (which the /compare pages are built from) is current, and runs the three engines; measure.py keeps each measured pair's line and lookup counts current in its notes. | study/compare/check.sh; study/compare/measure.py |
algo-harness | python3 tools/algo/check.py checks lib/algo's specification (lib/algo/algo.spec: each algorithm's postconditions, stability and complexity bound) and the test programs generated from it; --run runs each implementation on generated cases, recomputes every property in Python, compares with Python and NumPy, counts comparisons against the bound and runs the three engines. It is part of www's npm run check. | tools/algo/check.py |
memcheck | tools/memcheck/memcheck.sh compiles every program of the named directories to C and builds it twice: with AddressSanitizer and UndefinedBehaviorSanitizer, and plainly with a memory report (what the array arena holds at exit, peak resident set, leaks). It writes one row per program. tools/memcheck/ci.sh is the gate: every program under the sanitizers, the leak probes (scale.sh, adversarial.sh: no row may grow with the loop length), the parallel for probes and the runtime fuzzers; --quick takes a few minutes. | tools/memcheck/ci.sh |
three-engines | tools/three-engines.sh DIR [names...] runs every program in the interpreter, as C built natively, and as wasm from the direct backend in node, and requires every test to pass in each and the three outputs to agree byte for byte. A problem post's tests are meant to fail; for it only the agreement is required. tools/both-engines.sh is the same check without wasm, and NO_C=1 skips C. | tools/three-engines.sh; tools/both-engines.sh |
hub-golden | tools/hub-golden.sh record DIR [paths] writes, for every program of the corpus, its def hashes, AST, IR, C, the wasm module's sha256, and the output and exit status of the interpreter, the native build and the wasm build. tools/hub-golden.sh compare A B lists every difference between two snapshots and exits 1 if there is any. | tools/hub-golden.sh |
race-engines | tools/race-engines.sh prog.viv ... times a program four ways (the interpreter compiled to wasm, the direct wasm backend, C through emcc to wasm, and native C), each around the run alone, best of RUNS, and checks that all four print the same. tools/bounds-cost.sh measures what the array bounds checks cost by building with and without -DVIV_NO_BOUNDS. | tools/race-engines.sh; tools/bounds-cost.sh |
benchmarks | study/benchmarks/viv holds Viv versions of programs from published benchmark sets: the Julia micro benchmarks (micro/), the Benchmarks Game (bg/), the NAS Parallel Benchmarks (nas/), PolyBench (polybench/) and numpy-benchmarks translated by py2viv (npb/). | study/benchmarks/viv/nas/cg.viv |
jupyter-kernel | tools/jupyter holds a Jupyter kernel for Viv and a converter between Viv notebooks and .ipynb. A Viv notebook is a .viv file whose ### blocks are prose cells; running cell N runs cells 1 to N as one program and shows what cell N printed, so the file still runs with viv run. | tools/jupyter/README.md |
features | viv features prints what Viv has, by area: syntax, types, arrays, autodiff, correctness, the lenses, tools, performance, libraries, and the builtins, phrases, CLI verbs, lenses and modules read from the compiler's own tables. viv features NAME prints one feature (what it does, when to use it, an example and its output, the design doc, related features) or one area; --search WORDS ranks features by the words; --json prints the whole catalog as data. --usage [paths] counts how often each feature is used in the corpus, and lists the places where code does by hand what a feature provides. | lang/viv/features |
Performance
| Feature | What it does | Source |
|---|---|---|
release-mode | A default (checked) run verifies: a contracted solve such as spectrum_sym(A, vals) runs two independent methods and stops on dissent, array accesses are bounds-checked, holds and disjoint pins are tested, and steps are recorded. viv run --release and a C build with -DVIV_RELEASE run the selected method alone and drop those checks. release_mode() says which build is running. | |
fast-mode | viv build --fast compiles the same C with fused multiply-add (-ffp-contract=fast), the platform's libm for exp, log, pow, sin and the rest, and on macOS Accelerate for @ on large finite operands and for lin_solve; viv run --fast builds and runs it. Its numbers differ from the exact engines' in the last bits. viv check --fast runs the interpreter and the exact C build (and the direct wasm with --wasm), requires them to agree byte for byte, runs the fast build, and compares the outputs number by number: text, integers and test results must be the same, and every other number within a stated tolerance (an ulp budget, widened by the condition number of the worst solve). The report names each number that differs, by how many ulps and what share of its tolerance; --without contract,libm,blas tells which freedom moves it. Exact stays the default everywhere, and the browser runs exact. | |
blocked-matmul | A @ B runs over blocks of b that stay in the cache and keeps tiles of four rows by eight columns of the result in registers. For each entry the terms are still added in the order of the inner index, with separate multiplies and adds, so the result is the same bit for bit as the plain i-p-j loop and the same in every engine. NumPy calls BLAS, which agrees to rounding. | lang/viv/src/interp/matmul.rs |
gpu-kernels | kernel def declares a GPU kernel. One whose body is a single return of an elementwise expression (return a * x + exp(z)) is one thread per element, the tree fused. An explicit kernel uses thread_id(), block_id(), block_size(), grid_size(), s = shared(N) for a block's shared array and barrier(). Both are called like defs, y = k(x, y), and the call returns the array parameters the kernel writes. The grid is 256 threads to a block and the blocks that cover the first array parameter, unless a top-level line schedule k(blocks = 2, threads = 8) gives it (an integer expression of literals, the kernel's Int parameters, len of its arrays and global constants; one per axis for a 2-D grid, gpu-2d-grids). Every engine runs a kernel as a loop over blocks and, between barriers, over threads (MCUDA), and stops with the block, thread, element and line on an index out of range, a shared element read before it is written, two threads touching one element with no barrier between, or two blocks touching one element. A barrier under a per-thread condition, a per-thread value kept across a barrier, allocation, strings, I/O and calls of defs are refused before the run. viv cuda writes the CUDA C. | lang/viv/src/cuda.rs |
gpu-2d-grids | A kernel's grid can have two axes. thread_id(1), block_id(1), block_size(1) and grid_size(1) are the y axis (CUDA's threadIdx.y and the rest), thread_id(0) and thread_id() the x axis. A schedule key takes one value per axis, threads = [32, 8] and blocks = [bx, by], and cover = [w, h] gives the index space the threads must cover, from which the blocks are derived ((w + 32 - 1) // 32 along x); cover and blocks exclude each other. A thread of a stencil or a matrix product then has its column and row without dividing a flat index. The emulator numbers threads and blocks x first, as CUDA does, so every check (races, indices, shared memory) works as in 1-D; viv cuda launches with dim3. | examples/cuda/heat2d.viv; examples/cuda/matmul.viv |
device-arrays | on device u, v { ... } keeps the named arrays on the device while the block runs: each is copied there once on entry and back once on exit, and kernel calls inside read and write them in place, so a time loop of launches makes no copies. u, v = v, u swaps two device buffers. Inside the block a named array may only be a kernel's array argument, a kernel's result (v = k(u, v), the array the kernel writes), part of a swap, or len(u); reading it on the host, a call of a def, return, and break out of the block are refused before the run, so no stale copy can be read. The arrays the names held before the block are not changed by it. device_transfers() is [to device, to host, launches] so far, the same in every engine; viv cuda writes the block as one host function with its copies at the two ends. | lang/viv/src/cuda.rs; examples/cuda/heat2d.viv |
parallel-for | parallel for i to n { } runs its passes on several threads in a C build compiled with OpenMP, and in order everywhere else. Each pass may write a shared array only at elements indexed by i in one fixed position (a[i], m[i, j], setslab(W, i, s), a def given i as the index it writes), or by i plus a constant (a[i + 1], or a[j] after j = i + 1 at the top of the pass), and may read a shared array it writes only there. A write two passes could share, a global written through a def, break and return are refused before the run in every engine, with the write named and a fix suggested. A scalar total is a reduction. | lang/viv/src/parallel.rs |
disjoint-pins | When the passes write different elements for a reason the compiler cannot see (a permutation, a list of unique pairs), a line disjoint parallel for i: out "why" under the def in the module's .memory file lets the loop compile. Every checked build records each element a pass writes and stops the run naming both passes if two write the same one; a release build trusts the pin. | lang/viv/src/memory_lens.rs; lib/hf/md_eri.memory |
loop-arena | In C and wasm, when a loop's body can allocate, the end of each pass frees everything allocated since the loop began except what the variables the body assigns hold now. A def returning a record keeps the record's arrays and frees the rest. The interpreter counts references, so all three engines run a loop of temporaries in bounded memory. | |
native-builds | viv prog.viv -o prog.c writes portable C99 and cc -std=c99 -O2 prog.c -lm builds it; add -fopenmp (on macOS -Xpreprocessor -fopenmp with libomp) for parallel for, and -DVIV_RELEASE for a release build. viv prog.viv -o prog.wasm writes a wasm module directly. All three engines compute sin, exp, pow with Viv's own libm, so the outputs agree byte for byte. | lang/viv/examples/README.md |
separate-compilation | viv build writes the C as one translation unit per imported module (a large module in parts) and one for the program (viv prog.viv --split <dir>), compiles each unit once per distinct text, flags and compiler, and keeps the object under its SHA-256 in ~/.cache/viv (or $VIV_CACHE_DIR); the next program that loads the module links the kept object. The exact build is cc -O3. The output is the one-file build's, byte for byte. | |
monomorphization | A def or a lambda passed to a def is a value in the interpreter. For C and wasm the compiler specializes the callee per function argument (apply_twice(triple, x) becomes apply_twice__triple(x)), and lifts lambdas to defs, so the compiled code has only direct calls. A lambda passed to a def may read only its parameters, top-level names and defs, not the caller's locals. | lang/viv/src/defun.rs |
Libraries
| Feature | What it does | Source |
|---|---|---|
lib-nn | The forward pass of each piece a transformer is made of, as whole-array expressions over the last axis, so each works on a sequence (T by D) or a batch (B by T by D): rmsnorm, relu2 (ReLU squared), softmax_rows, causal attention (returning the output and the weights), cross_entropy, embedding. The backward passes come from value_and_grad over these defs; the tests hold the generated gradient of each piece to central differences. The hand-written adjoints are kept in lib/nn_adjoints as references. | lib/nn.viv |
lib-optim | SGD, SGD with momentum, Adam and Muon. For a model held as records of matrices, adam_update and muon_update update one matrix and its memories in place with the settings in an AdamSettings or MuonSettings record, and for_leaves applies them to every matrix of the model, as opt.step() does in PyTorch. The older flat form keeps the parameters in one vector with a lib/params layout and steps with optim_step; a test holds the two forms to the same bits. | lib/optim.viv |
lib-gpt | A GPT in the shape of nanoGPT and the modded-nanogpt speedrun: token and position embeddings, blocks of RMSNorm, multi-head causal attention and a ReLU-squared MLP with residuals, and an output map. The model is a Gpt record holding an array of Block records; train runs the batch at once, takes the gradient with value_and_grad with respect to the whole model, and updates it with Muon on the blocks and Adam on the rest through for_leaves. A GptConfig record holds the sizes. lib/gpt/story_gpt trains one on a sentence and continues a prompt. | lib/gpt/gpt.viv |
lib-params | Keeps all of a model's matrices in one flat vector with a layout table beside it (the offset, rows and columns of each matrix): param_table, param_declare, param_zeros, param_get, param_set, param_add. It exists because grad once took only one flat array. | lib/params.viv |
lib-hf | Restricted, unrestricted and restricted-open Hartree-Fock on Gaussian basis sets, held against PySCF. molecule("O 0 0 0; H ...", "6-31g") (lib/hf/mole) builds a Molecule from text and a basis name, as PySCF's gto.M does; rhf(mol) (lib/hf/scf) runs the SCF and gives a record with the energy. lib/hf/hf runs the same pipeline with every integral stage cross-checked by its registry of methods (McMurchie-Davidson, Head-Gordon-Pople, Rys, Obara-Saika, Gauss-Hermite). It also has direct J/K, DIIS, stability analysis and Casida TDHF. | lib/hf/hf.viv; lib/hf/mole.viv |
lib-xyz-basis | parse_xyz(text) reads an XYZ file, or atoms written as PySCF's atom= string, into a Geometry record: the symbols as a [Str], the atomic numbers and an n by 3 array of positions. lib/basis holds the basis sets STO-3G, 6-31G, 6-31G*, 6-31G**, cc-pVDZ, cc-pVTZ, def2-SVP and def2-TZVP as Basis records, looked up by PySCF's names through lib/basis/named. | lib/xyz.viv; lib/basis/basis.viv |
lib-cas | A computer algebra system written in Viv: cas("x^2 - 1") parses text into a hash-consed expression store with exact rationals; cas_diff, cas_expand, cas_definite (a definite integral with a certificate verdict), cas_text (Viv source) and cas_tex (LaTeX). The compiler carries it, so #! use lib/cas/cas works from any directory, and viv check --phrases uses it to hold phrases to exact values. | lib/cas/cas.viv |
lib-minpack | A port of netlib MINPACK's Levenberg-Marquardt solver with a trust region: lmder1 and lmder, with lmpar, qrfac, qrsolv and enorm, keeping the Fortran's arithmetic in its order so the iterates match a gfortran build of the original to the last bit (checked on the Moré-Garbow-Hillstrom problems by lib/minpack/mgh_check.viv). The residual function is a def fcn(data, x, fvec, fjac, iflag) that fills fvec or fjac. Every trial step writes a row of hist. | lib/minpack/lmder.viv; lib/minpack/README.md |
lib-randla | Sketching operators generated on the fly from Philox4x32-10 keyed by (seed, stream), so a sketch is the same in every engine (Gaussian, Rademacher, sparse sign SASO, subsampled randomized Hadamard), and the drivers built on them: the randomized range finder, QB and SVD with power iterations, Nystrom for PSD matrices, randomly pivoted Cholesky, sketch-and-precondition least squares (LSQR on A R^-1, to full accuracy), and the Hutchinson and Hutch++ trace estimators over an operator passed as a def. | lib/randla/README.md |
lib-random | Draws that are plain Viv over the one generator rand() (Park-Miller MINSTD, seeded by srand), so every engine draws the same stream: randn (Box-Muller), randn_matrix, randint(lo, hi), shuffle (Fisher-Yates in place), randperm, choice(p). Each is documented as the formula it computes, so tools/random_replay.py replays it in Python. | lib/random.viv |
lib-nprandom | NumPy's generators, word for word: PCG64 behind default_rng(seed) with its SeedSequence (lib/nprandom/pcg64, seedseq, generator), and the legacy Mersenne Twister behind np.random.seed (lib/nprandom/mt19937, legacy), with uniform, integer, normal (ziggurat), shuffle and choice draws. lib/nprandom/selftest compares every value with NumPy 1.24.4's draws exactly. | lib/nprandom/generator.viv; lib/nprandom/selftest.viv |
lib-linalg | lib/schur: the real Schur form A = Q T Q^T by Householder reduction and Francis's double-shift QR, with a Sylvester solver for its blocks. lib/svd: the SVD by one-sided Jacobi (the algorithm of the svd builtin). lib/polar: Newton-Schulz orthogonalization, which Muon uses. lib/lobpcg: the lowest eigenpairs of an operator given as a def that applies it. lib/linalg: an inverse and a reproducible test matrix. Each has residual checks (schur_residual, orthogonality, svd_residual). | lib/schur.viv; lib/svd.viv; lib/polar.viv; lib/lobpcg.viv |
lib-control | lib/are solves the algebraic Riccati equation by the Schur method; lib/lqr is its front end for one input (lqr_riccati, lqr_gains, lqr_residual); lib/lyap solves the Lyapunov equation by Bartels and Stewart and gives controllability and observability Gramians and the H2 norm; lib/balance does balanced truncation. lib/cartpole and lib/chain are the plants: a cart-pole and a planar chain of rigid links (recursive Newton-Euler). | lib/lqr.viv; lib/are.viv; lib/lyap.viv; lib/balance.viv |
lib-ode | ode_solve(rhs, jac, events, t0, t1, y0, p, cfg, t_eval, dirs, term) integrates y' = rhs(t, y, p) by Dormand-Prince 5(4) (DOPRI5), Radau IIA of order 5 (RADAU), variable-order BDF (BDF), or AUTO (Dormand-Prince until Hairer's stiffness test fires, then Radau). Step-size control on rtol and atol as SciPy's solve_ivp does it (the same steps), dense output, events located on the dense output with direction and terminal flags, and ode_batch for many parameter sets on one grid. The rhs, its Jacobian and the events are defs passed as function values; an exact Jacobian is a def of grad rows (lib/ode/problems.viv), or JAC_FD takes forward differences. Python calls it as vp.ode.solve and vp.ode.batch. | lib/ode/solve.viv; lib/ode/radau.viv; lib/ode/bdf.viv; lib/ode/dopri5.viv; lib/ode/ode.docs.toml |
lib-graphdraw | lib/graphlayout lays out a directed graph in layers: cycle breaking, longest-path ranks, crossing reduction by weighted medians, placement without overlaps. lib/graphdraw draws a whole program as a graph with it, from the ProgramGraph record (lib/proggraph); viv graph f.viv --svg runs it in the interpreter and writes the SVG. | lib/graphlayout.viv |
lib-gates | The outlines of AND, OR, XOR and NOT gates and orthogonal wires between them, as point lists for the figure builtins, for drawing a schematic of a circuit built from the bit builtins. | lib/gates.viv |
lib-stats | mean(a) and std(a) (population, divides by n) written with sum, which adds in NumPy's pairwise order (left to right below 8 entries, so tools/random_replay.py reproduces its six-sample test in Python). A program that uses it replaces the builtins mean and std, which add in the same order. | lib/stats.viv |
lib-algo | lib/algo is the planned counterpart of C++'s <algorithm>: sorting, selection, binary search, set operations, permutations, gcd, then heaps, union-find and graph searches. What exists is the specification (lib/algo/algo.spec: each algorithm's postconditions, stability and complexity bound), the property checks (lib/algo/props.viv), the case generator and the corpus's hand-written versions for testing (lib/algo/legacy); there is no module of algorithms to #! use yet. | lib/algo/algo.spec |
std-contracts | lang/viv/std/contracts.viv holds integrate(f, a, b) and find_root(f, a, b): each runs every method in its registry (three quadratures: Gauss-Legendre, adaptive Simpson, Romberg; three bracketing root finders: bisection, Illinois, a safeguarded secant) and stops the run when two disagree, while viv run --release runs one. The methods are ordinary defs, a registry is an array of them ([r_bisect, r_illinois, r_safe]), and sweep(methods, f, a, b) cross-checks any registry. Bring it in with #! use lang/viv/std/contracts or --use lang/viv/std/contracts.viv; a phrase that needs one of these defs gets it added. | lang/viv/std/contracts.viv |
std-phrases | lang/viv/std/phrases.viv holds the defs a phrase can turn into that are neither builtins nor contracts: r_newton (Newton's method with the exact forward-mode derivative), softmax, softmax_rows, cross_entropy. The compiler adds the ones a program's phrases need before any engine sees it; a program with no phrase is not changed. | lang/viv/std/phrases.viv |
The C runtime
Compiled programs carry their runtime with them. One struct,
typedef struct { int rank; int dims[8]; int size; double* data; } viv_arr;
plus a set of small viv_* functions — array ops, the eigensolvers, the LU and
Cholesky factorizations, quadrature, the Park–Miller generator, and the show/plot/narrate
printers — emitted only on demand. A program that never touches an array gets
none of them; the nine-line example on the internals page pulls in
exactly one. Building it takes only cc program.c -O2 -lm.
Every array read and write in the C is checked: the number of indices against the array's
rank, then each index against its dimension. A wrong count or an index out of bounds stops a
compiled program with the interpreter's error and location instead of reading memory the
array does not own. The checks are on by default. Build with
-DVIV_RELEASE to drop them along with the other debug-build checks, or with
-DVIV_NO_BOUNDS to drop only them. What they cost, measured with
tools/bounds-cost.sh (CPU time, best of 7 runs, cc -O2, Apple M2 Pro):
| Program | Checked | Unchecked | Cost |
|---|---|---|---|
bench/hf_md_eri_x100.viv (water 6-31G ERIs, built 100 times) | 0.385 s | 0.275 s | +40% |
bench/matmul_loops.viv (400×400, all indexing) | 0.105 s | 0.082 s | +29% |
scf/meta_integrals.viv | 0.503 s | 0.396 s | +27% |
scf/meta_scf.viv | 0.660 s | 0.368 s | +79% |
scf/lock_water.viv | 0.200 s | 0.115 s | +73% |
scf/coverage.viv | 0.260 s | 0.145 s | +80% |
bench/hf_scf_bench.viv (water cc-pVDZ RHF) | 0.042 s | 0.023 s | +84% |
That is 27% to 84% on these programs, so a build you time for speed should pass
-DVIV_RELEASE; a build you run to check a program should keep them. The
WebAssembly modules this site serves (dft/wasm/, engines/wasm/) are
built with -DVIV_RELEASE; the same programs run with the checks on in
dft/check.sh and in the interpreter and in C. In those modules an index out of range is not
caught: a write can land in another array of the same program (it cannot leave the module's
memory). The interpreter and the module the compiler in your browser makes when you press Run
(the direct wasm backend) check every index and stop with the same error as the C. -DVIV_RELEASE also trusts the
disjoint pins of a memory lens instead of
testing them; a checked build records which pass of a pinned parallel for
writes each element and stops if two passes write the same one.
The location in a run-time error names the program as you gave it when the path is
relative (lib/hf/scf.viv:12:5) and by its file name when the path is
absolute (scf.viv:12:5), in the interpreter and in C. The C carries those locations, so a
program compiled by absolute path gives the same C on every machine and in every checkout.
The builtin tables were probed at build time (2026-10-18).