Embedding jaala in Go
Serve facts from Go, ship rules as a module, run a query with Bind and Budget, and read its citations, witnesses and errors.
The first four tutorials wrote queries against facts the site provided. A real host provides the facts itself, from whatever it already has: a parsed go.mod, a netlist, a database. This tutorial builds the pieces you need as a host, in Go, over a few of the same imports.
Every code block on this page is a file from docsite/examples/host/, included as is. The last three are Go Example functions, so go test runs them on every build of this site and checks their output against the // Output: comment at the end. If jaala’s API or answers change, the build fails before the page can drift.
Facts from Go ¶
A Source serves a relation’s tuples. ns.MemSource is the simplest one, a set of tuples held in memory, which is pretty much all a test or a small host needs. Each tuple carries its citations, which is how an answer can point back at the line of a file it came from:
package host_test
import (
"fmt"
"github.com/panyam/jaala/ns"
)
// source is the facts a host serves. A MemSource is the simplest Source. You
// declare each relation with its column labels, then add tuples, each with
// the citations an answer that uses it will show. A real host implements
// ns.Source over its own data instead.
func source() *ns.MemSource {
src := ns.NewMemSource().Declare("imports", "from", "to")
edges := [][2]string{
{"app", "api"}, {"api", "auth"}, {"api", "log"},
{"auth", "log"}, {"log", "util"},
}
for i, e := range edges {
src.Add("imports", ns.Tuple{
Vals: []ns.Value{ns.S(e[0]), ns.S(e[1])},
Cites: []string{fmt.Sprintf("imports.txt:%d", i+1)},
})
}
return src
}
A vocabulary and a base ¶
The vocabulary is everything a query can name: the source’s relations, the standard library (str.contains and friends), and modules of rules. Here the rules from the second tutorial become a module called deps, so a query calls deps.depends_on. Check resolves the modules and reports a mistake in one, with the file it came from, before any query runs.
package host_test
import (
"github.com/panyam/jaala/datalog"
"github.com/panyam/jaala/ns"
"github.com/panyam/jaala/stdlib"
)
// deps is a module, the rules a host ships as a library. A query calls them
// by path, as deps.depends_on.
const deps = `
depends_on(?a, ?b) :- imports(?a, ?b);
depends_on(?a, ?c) :- depends_on(?a, ?b), imports(?b, ?c);
`
// base builds what a query runs against. The vocabulary names everything a
// query can call, which is the Source's relations, the standard library and
// the modules. A host builds and checks it once, at startup, and builds a
// Base per dataset. A Base is safe to share across concurrent queries.
func base(src ns.Source) (*datalog.Base, error) {
v, err := ns.NewVocabulary(src)
if err != nil {
return nil, err
}
if err := v.AddLanguage(datalog.Language); err != nil {
return nil, err
}
if err := stdlib.Register(v); err != nil {
return nil, err
}
err = v.AddModule("deps", datalog.LanguageName, deps, "deps.dl")
if err != nil {
return nil, err
}
if err := v.Check(); err != nil {
return nil, err
}
return datalog.NewBase(v, src)
}
A host usually builds the vocabulary once at startup and a Base per dataset. A Base caches what it reads, and it’s safe to share between queries running at the same time.
Running a query ¶
MustParse turns query text into a Query, and SemiNaive.Eval answers it. Bind fills a goal variable from Go, so one query serves every package and you never build query text by hand. Budget caps the work, so a query over a big graph stops instead of running on:
package host_test
import (
"context"
"fmt"
"github.com/panyam/jaala/datalog"
"github.com/panyam/jaala/ns"
)
// A query is parsed once and run as often as needed. Bind gives a goal
// variable its value from the host, so the query text stays fixed while the
// package changes, and Budget caps the work one evaluation may do.
func Example() {
b, err := base(source())
if err != nil {
fmt.Println(err)
return
}
q := datalog.MustParse(`deps.depends_on(?p, ?d) => ?d`)
for _, pkg := range []string{"api", "log"} {
rows, err := datalog.SemiNaive{}.Eval(context.Background(), q, b,
datalog.Bind(map[datalog.Var][]ns.Value{"p": {ns.S(pkg)}}),
datalog.Budget(100_000))
if err != nil {
fmt.Println(err)
return
}
for _, r := range rows {
fmt.Println(pkg, "depends on", r.Bind["d"].S, r.Cites)
}
}
// Output:
// api depends on auth [imports.txt:2]
// api depends on log [imports.txt:3]
// api depends on util [imports.txt:3 imports.txt:5]
// log depends on util [imports.txt:5]
}
Each row’s Bind maps a column to its value, and Cites is the sorted set of citations behind it. api depends on util through log, so that row cites both imports.
Witnesses ¶
Citations say which facts an answer used. Ask for Witnesses and each row also says how they were combined, as a tree of the rules that fired:
package host_test
import (
"context"
"fmt"
"strings"
"github.com/panyam/jaala/datalog"
)
// Witnesses records how each answer was derived, as a tree of the rules that
// fired with the facts at its leaves, in written order.
func Example_witnesses() {
b, err := base(source())
if err != nil {
fmt.Println(err)
return
}
q := datalog.MustParse(`deps.depends_on("app", "util")`)
ctx := context.Background()
rows, err := datalog.SemiNaive{}.Eval(ctx, q, b, datalog.Witnesses())
if err != nil {
fmt.Println(err)
return
}
for _, w := range rows[0].Witness {
show(w, 0)
}
// Output:
// deps.depends_on(app, util)
// deps.depends_on(app, log)
// deps.depends_on(app, api)
// imports(app, api) imports.txt:1
// imports(api, log) imports.txt:3
// imports(log, util) imports.txt:5
}
func show(w *datalog.Witness, depth int) {
vals := make([]string, len(w.Values))
for i, v := range w.Values {
vals[i] = v.S
}
line := strings.Repeat(" ", depth) +
w.Relation + "(" + strings.Join(vals, ", ") + ")"
if len(w.Cites) > 0 {
line += " " + strings.Join(w.Cites, ", ")
}
fmt.Println(line)
for _, c := range w.Children {
show(c, depth+1)
}
}
The tree follows the program as written, even though the evaluator may have rewritten it quite a bit to run faster, so you can show it to the person who wrote the rules.
Errors ¶
A query that can’t be right fails with an error whose text starts with query:, which is safe to show whoever wrote the query. Running out of budget is its own type, so a host can tell “this question is too big” apart from “this question is wrong”:
package host_test
import (
"context"
"errors"
"fmt"
"github.com/panyam/jaala/datalog"
)
// An error is either the query's own fault, whose text starts with "query:"
// and is safe to show to whoever wrote the query, or a BudgetExceeded, which a
// host can test for and handle on its own.
func Example_errors() {
b, err := base(source())
if err != nil {
fmt.Println(err)
return
}
ctx := context.Background()
wrong := datalog.MustParse(`deps.depends_on(?p) => ?p`)
_, err = datalog.SemiNaive{}.Eval(ctx, wrong, b)
fmt.Println(err)
all := datalog.MustParse(`deps.depends_on(?p, ?d) => ?p, ?d`)
_, err = datalog.SemiNaive{}.Eval(ctx, all, b, datalog.Budget(5))
var over *datalog.BudgetExceeded
if errors.As(err, &over) {
fmt.Println("stopped at the budget of", over.Limit)
}
// Output:
// query: relation "deps.depends_on" takes 2 args, got 1
// stopped at the budget of 5
}
A host runs this loop for as long as it lives. Most of the work after this page is deciding which facts to serve and which rules belong in a module. The guide has the language’s rules in full.