Built-ins

Everything the standard library adds to a vocabulary: string tests, edit distance, building text, a number's value and unit, arithmetic, and absent, read from its registry.

stdlib.Register adds these to a vocabulary. The table is built from the registry itself when the site is built, with each name’s signature and the doc it was registered with.

Name and argumentsWhat it does
absent(value)reports whether the field carried no value at all, which is different from an empty string and from zero; not absent(?x) reads "this field is stated"
coalesce(value, fallback, result)binds the value, or the fallback when the value is absent
list.at(list: list, index: number, value)binds the value at an index from 0, a negative one counting from the end (-1 is the last)
list.length(list: list, length: number)binds how many values the list holds
list.member(list: list, index: number, value)binds each value of the list with its index from 0; given a value, binds where it is
num.add(a: number, b: number, c: number)c = a + b, run from any two of the three; units follow the vocabulary's table
num.div(a: number, b: number, c: number)c = a / b, run from any two of the three; units follow the vocabulary's table
num.mul(a: number, b: number, c: number)c = a * b, run from any two of the three; units follow the vocabulary's table
num.neg(a: number, b: number)b = -a, run from either; the unit carries over
num.sub(a: number, b: number, c: number)c = a - b, run from any two of the three; units follow the vocabulary's table
num.unit(number: number, unit: string)binds the number's unit ("V", "A"), or absent for a number without one; the number must be bound
num.value(number: number, value: number)binds the number without its unit, so 5 V and 5 A both bind 5; the number must be bound
path.edge(path: string, index: number, edge)binds each edge of a path over a relation of three arguments, index from 0
path.edges(path: string, edges: list)binds the path's edges as a list
path.length(path: string, steps: number)binds how many steps the path takes
path.node(path: string, index: number, node)binds each node of the path, index from 0
path.nodes(path: string, nodes: list)binds the path's nodes as a list
path.step(path: string, index: number, from, to)binds each step of the path, index from 0
str.capitalize(string: string, capitalized: string)binds the string with its first character in upper case and the rest as written
str.concat(head: string, tail: string, text: string)text is head followed by tail, run from any two of the three; cutting a head or tail off holds for none when text lacks it
str.contains(string: string, substring: string)reports whether a string contains a substring
str.distance(string: string, other: string, distance: number)binds the edit (Levenshtein) distance between two strings, in characters; both strings must be bound
str.glob(string: string, pattern: string)reports whether the whole string matches a SQLite-style glob (* any run, ? one character, [a-z] or [^a-z] one of a class, [[] a literal [)
str.lower(string: string, lower: string)binds the string in lower case
str.match(string: string, regex: string)reports whether the string matches an unanchored regular expression
str.prefix(string: string, prefix: string)reports whether a string starts with a prefix
str.replace(string: string, old: string, new: string, result: string)binds the string with every occurrence of old replaced by new
str.split(string: string, separator: string, index: number, part: string)binds each part of the string between separators with its index from 0; given an index, binds that part alone, a negative index counting from the end (-1 is the last part)
str.suffix(string: string, suffix: string)reports whether a string ends with a suffix
str.upper(string: string, upper: string)binds the string in upper case

All but str.distance, the text functions, num.value, num.unit and the arithmetic functions are tests: each holds or it doesn’t, and binds nothing. So every argument needs a value from the rest of the goal, pretty much the way a comparison does. jaala’s planner runs a test once its arguments are bound, wherever you wrote it in the goal, and refuses one whose arguments nothing binds:

str.contains(?p, "a") => ?p

query: str.contains needs all arguments bound (a variable must appear in a relation before str.contains tests it)

Facts: the deps dataset

String tests

str.contains, str.prefix and str.suffix compare plain substrings:

package(?p), str.contains(?p, "ac") => ?p
?p
cache
legacy
Facts: the deps dataset
package(?p), str.prefix(?p, "a") => ?p
?p
api
app
auth
Facts: the deps dataset
test(?t, ?p), str.suffix(?t, "_test") => ?t
?t
api_test
auth_test
db_test
util_test
Facts: the deps dataset

str.glob matches the whole string against a SQLite-style glob, where * is any run of characters, ? one character, and [a-c] one of a class:

package(?p), str.glob(?p, "[a-c]*") => ?p
?p
api
app
auth
cache
config
Facts: the deps dataset

str.match takes a regular expression in Go’s syntax, unanchored, so anchor it with ^ and $ to match the whole string:

package(?p), str.match(?p, "^(api|auth)$") => ?p
?p
api
auth
Facts: the deps dataset

A pattern that doesn’t compile is refused before anything runs, with the compiler’s reason:

package(?p), str.match(?p, "(") => ?p

query: invalid regex "(": error parsing regexp: missing closing ): `(`

Facts: the deps dataset
package(?p), str.glob(?p, "[") => ?p

query: invalid glob "[": unclosed [ (write [[] for a literal [)

Facts: the deps dataset

Go code that has to agree with these (a host filtering the same names outside a query, say) can use stdlib.CompileGlob and stdlib.CompilePattern, the compilers the tests use.

Edit distance

str.distance(?a, ?b, ?d) binds ?d to the edit distance between two strings: the fewest characters inserted, deleted or replaced to turn one into the other (Levenshtein’s distance). A character is a Unicode character, so Ω against O is one edit. Both strings need values from the rest of the goal, and ?d can be a variable it binds or a number it tests. Two names a character apart are often one name misspelled, so this finds them:

package(?a), package(?b), ?a < ?b, str.distance(?a, ?b, ?d), ?d <= 1 => ?a, ?b, ?d
?a?b?d
apiapp1
Facts: the deps dataset

Each pair is measured once per binding, so a goal comparing every name with every other does work for every pair. Matching is exact on case: EN and en are two edits apart.

With neither string bound there is nothing to measure, and the goal is refused:

str.distance(?p, ?q, ?d) => ?p, ?q, ?d

query: the query calls str.distance(?p, ?q, ?d), and nothing binds what str.distance needs first: it needs ?p and ?q bound

Facts: the deps dataset

Building text

str.concat, str.replace, str.lower, str.upper, str.capitalize and str.split build text from text, so a name one fact spells can be matched to the name another fact spells differently. Each takes its inputs and then its result, like the arithmetic functions, so a call can sit in a body or in an expression (?f = str.concat(?p, ".go")). The inputs need values from the rest of the goal, and with the result given too a call tests it.

str.concat(?head, ?tail, ?text) puts two strings together:

owner(?p, "data"), str.concat(?p, ".go", ?f) => ?p, ?f
?p?f
cachecache.go
dbdb.go
Facts: the deps dataset

Like num.add it runs from any two of its three values, so given the text and one end it cuts that end off. Text that doesn’t start or end with it gives no row, so this finds each package that has a test named after it, and leaves out auth_fuzz:

test(?t, _), str.concat(?p, "_test", ?t) => ?t, ?p
?t?p
api_testapi
auth_testauth
db_testdb
util_testutil
Facts: the deps dataset

str.replace(?s, ?old, ?new, ?out) replaces every occurrence:

test(?t, "auth"), ?n = str.replace(?t, "_", " ") => ?n
?n
auth fuzz
auth test
Facts: the deps dataset

str.lower and str.upper change the case of every character, and str.capitalize only the first:

owner("auth", ?team), str.upper(?team, ?u), str.capitalize(?team, ?c), str.lower(?u, ?l) => ?team, ?u, ?c, ?l
?team?u?c?l
securitySECURITYSecuritysecurity
Facts: the deps dataset

str.split(?s, ?sep, ?i, ?part) gives one row per part between separators, numbered from 0:

test("auth_fuzz", _), str.split("auth_fuzz", "_", ?i, ?part) => ?i, ?part
?i?part
0auth
1fuzz
Facts: the deps dataset

Given an index it gives that part alone, and a negative index counts from the end: -1 is the last part, which is a qualified name’s own name, and -2 the one before it. An index past either end gives no row:

test(?t, _), str.split(?t, "_", -1, ?kind) => ?t, ?kind
?t?kind
api_testtest
auth_fuzzfuzz
auth_testtest
db_testtest
util_testtest
Facts: the deps dataset

None of them makes text up from nothing, so a call with nothing to read is refused:

str.concat(?a, "_test", ?t) => ?a, ?t

query: the query calls str.concat(?a, "_test", ?t), and nothing binds what str.concat needs first: it needs ?a and "_test" bound, or ?a and ?t bound, or "_test" and ?t bound

Facts: the deps dataset

Numbers and units

A number can carry a unit (5V, 0.5A), which a host’s source sets. num.value(?x, ?n) binds ?n to ?x’s number without its unit, and num.unit(?x, ?u) binds ?u to the unit, or to absent for a number that has none. Both need ?x from the rest of the goal, and bind nothing for a value that isn’t a number:

reading(?r, ?x), num.value(?x, ?n), num.unit(?x, ?u) => ?r, ?n, ?u
?r?n?u
r15V
r20.5A
r37absent
Facts: reading("r1", 5V); reading("r2", 0.5A); reading("r3", 7)

They’re how a query says what it wants when the units in a column differ, which an aggregate won’t guess at (guide).

Arithmetic

num.add, num.sub, num.mul, num.div and num.neg are the functions arithmetic is written in: ?v = ?i * ?r is num.mul(?i, ?r, ?v). Called as relations, each takes its operands and then its result:

loc("app", ?n), num.add(?n, 1, ?a), num.sub(?n, 1, ?s), num.mul(?n, 2, ?m), num.div(?n, 2, ?d), num.neg(?n, ?g) => ?n, ?a, ?s, ?m, ?d, ?g
?n?a?s?m?d?g
300301299600150-300
Facts: the deps dataset

Each runs from any two of its three values (one of two for num.neg), so the same call finds a product or a missing factor. Here the factor that makes app’s 300 lines three times something:

loc("app", ?n), num.mul(?x, 3, ?n) => ?x
?x
100
Facts: the deps dataset

Each carries units through the vocabulary’s table. The guide shows them in expressions.

absent

absent(?x) holds when ?x is a field its source didn’t state. None of deps’s fields are absent, so here it holds for nothing, and the guide has an example over facts that do leave fields out:

package(?p), absent(?p) => ?p

No rows.

Facts: the deps dataset

coalesce

coalesce(?x, ?fallback, ?v) binds ?v to ?x, or to ?fallback when ?x is absent. Both must be bound first, absent counting as a value. An expression can call it, ?v = coalesce(?x, 0), and it nests for more fallbacks: coalesce(?a, coalesce(?b, ?c)). No package has more than 5000 lines, so big’s min is absent and the fallback answers:

big(min(?n)) :- loc(_, ?n), ?n > 5000; big(?m), ?v = coalesce(?m, 0) => ?v
?v
0
Facts: the deps dataset

Paths

A path from graph.paths is text, a JSON list of its nodes, or {"nodes": [...], "edges": [...]} over a relation that names its edges. The path.* functions read it, each needing the path bound. path.length(?p, ?n) is how many steps it takes, and path.node(?p, ?i, ?node) gives each node with its index from 0, so not path.node(?p, _, "config") keeps the paths that avoid config:

graph.paths("imports", "db", "util", "acyclic", 1, 0, ?p), path.length(?p, ?n), not path.node(?p, _, "config") => ?p, ?n
?p?n
["db","log","util"]2
Facts: the deps dataset

Given an index, path.node gives that node alone. path.nodes(?p, ?l) is the nodes as a list, the path itself when it names no edges:

graph.paths("imports", "db", "util", "shortest", 1, 0, ?p), path.node(?p, ?i, ?node), path.nodes(?p, ?nodes) => ?i, ?node, ?nodes
?i?node?nodes
0db["db","log","util"]
1log["db","log","util"]
2util["db","log","util"]
Facts: the deps dataset

path.step(?p, ?i, ?from, ?to) gives each step with the nodes it joins, which is how a path of nodes finds its edges again, by joining each step back to the relation it walked:

graph.paths("imports", "db", "util", "shortest", 1, 0, ?p), path.step(?p, ?i, ?from, ?to) => ?i, ?from, ?to
?i?from?to
0dblog
1logutil
Facts: the deps dataset

path.edge(?p, ?i, ?e) and path.edges(?p, ?l) read a path that names its edges, one over a relation of three arguments, and give nothing for one that doesn’t:

link(?a, ?r, ?b) :- imports(?a, ?b) as ?r; graph.paths("link", "db", "util", "shortest", 1, 0, ?p), path.edge(?p, ?i, ?e), path.edges(?p, ?all) => ?i, ?e, ?all
?i?e?all
0imports("db", "log")["imports(\"db\", \"log\")","imports(\"log\", \"util\")"]
1imports("log", "util")["imports(\"db\", \"log\")","imports(\"log\", \"util\")"]
Facts: the deps dataset

A value that isn’t a path gives no row rather than an error, since which values reach a function depends on how the query is planned. They live under path, so in a host’s own module named path, a rule’s bare node(...) or edge(...) means these functions, and the module has to write the vocabulary’s relation another way or take another name.

Lists

A list is a value (#283): the list aggregate builds one, a rule hands it on, and a path’s nodes are one too. It’s carried as a JSON list, ["cache","db"], with a number in a unit written {"num": 3.3, "unit": "V"} so it reads back as that number. A Source may serve one in a column it declares list. The list.* functions read it, each needing the list bound. list.member(?l, ?i, ?x) gives each value with its index from 0, which turns a list back into rows:

team(?t, list(?p)) :- owner(?p, ?t); team(?t, ?l), list.member(?l, ?i, ?p), ?t = "data" => ?t, ?i, ?p
?t?i?p
data0cache
data1db
Facts: the deps dataset

With the value bound, list.member is a test that the list holds it. list.length(?l, ?n) is how many values it holds, and list.at(?l, ?i, ?x) the one at an index, a negative index counting from the end:

team(?t, list(?p)) :- owner(?p, ?t); team(?t, ?l), list.length(?l, ?n), ?n > 2, list.at(?l, -1, ?last) => ?t, ?n, ?last
?t?n?last
platform4util
web3legacy
Facts: the deps dataset

A value that isn’t a list gives no row rather than an error, as with paths. Two lists are equal when they hold the same values in the same order, and they order between themselves by their text, not value by value. A rule can’t build a list other than through the aggregate, which keeps a program finite (#1). Lists live under list, so a host’s module named list has the same caveat as one named path.