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 arguments | What 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") => ?pquery: str.contains needs all arguments bound (a variable must appear in a relation before str.contains tests it)
deps datasetString tests ¶
str.contains, str.prefix and str.suffix compare plain substrings:
test(?t, ?p), str.suffix(?t, "_test") => ?t| ?t |
|---|
| api_test |
| auth_test |
| db_test |
| util_test |
deps datasetstr.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:
str.match takes a regular expression in Go’s syntax, unanchored, so anchor it with ^ and $ to match the whole string:
A pattern that doesn’t compile is refused before anything runs, with the compiler’s reason:
package(?p), str.match(?p, "(") => ?pquery: invalid regex "(": error parsing regexp: missing closing ): `(`
deps datasetpackage(?p), str.glob(?p, "[") => ?pquery: invalid glob "[": unclosed [ (write [[] for a literal [)
deps datasetGo 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 |
|---|---|---|
| api | app | 1 |
deps datasetEach 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, ?dquery: the query calls str.distance(?p, ?q, ?d), and nothing binds what str.distance needs first: it needs ?p and ?q bound
deps datasetBuilding 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 |
|---|---|
| cache | cache.go |
| db | db.go |
deps datasetLike 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_test | api |
| auth_test | auth |
| db_test | db |
| util_test | util |
deps datasetstr.replace(?s, ?old, ?new, ?out) replaces every occurrence:
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 |
|---|---|---|---|
| security | SECURITY | Security | security |
deps datasetstr.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 |
|---|---|
| 0 | auth |
| 1 | fuzz |
deps datasetGiven 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_test | test |
| auth_fuzz | fuzz |
| auth_test | test |
| db_test | test |
| util_test | test |
deps datasetNone of them makes text up from nothing, so a call with nothing to read is refused:
str.concat(?a, "_test", ?t) => ?a, ?tquery: 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
deps datasetNumbers 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 |
|---|---|---|
| r1 | 5 | V |
| r2 | 0.5 | A |
| r3 | 7 | absent |
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 |
|---|---|---|---|---|---|
| 300 | 301 | 299 | 600 | 150 | -300 |
deps datasetEach 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:
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) => ?pNo rows.
deps datasetcoalesce ¶
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 |
deps datasetPaths ¶
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 |
deps datasetGiven 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 |
|---|---|---|
| 0 | db | ["db","log","util"] |
| 1 | log | ["db","log","util"] |
| 2 | util | ["db","log","util"] |
deps datasetpath.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 |
|---|---|---|
| 0 | db | log |
| 1 | log | util |
deps datasetpath.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 |
|---|---|---|
| 0 | imports("db", "log") | ["imports(\"db\", \"log\")","imports(\"log\", \"util\")"] |
| 1 | imports("log", "util") | ["imports(\"db\", \"log\")","imports(\"log\", \"util\")"] |
deps datasetA 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 |
|---|---|---|
| data | 0 | cache |
| data | 1 | db |
deps datasetWith 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 |
|---|---|---|
| platform | 4 | util |
| web | 3 | legacy |
deps datasetA 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.