Add basic tracing support

Emit messages for four main steps of the evaluation process:

1. new expression (eval)
2. trying to evaluate a plugged expression (try)
3. successfully evaluated a plugged expression (success)
4. finished evaluating a query (finish)

Also refactored query interface slightly to take a params struct. This will be
a bit more future proof.
This commit is contained in:
Torin Sandall
2016-04-20 09:43:05 -07:00
parent 104852c1d8
commit ec7efe1a06
5 changed files with 193 additions and 58 deletions
+7 -3
View File
@@ -4,7 +4,10 @@
package eval
import "sort"
import (
"fmt"
"sort"
)
// Compare returns 0 if a equals b, -1 if a is less than b, and 1 if b is than a.
//
@@ -117,7 +120,8 @@ func Compare(a, b interface{}) int {
return 1
}
}
panic("unreachable")
panic(fmt.Sprintf("illegal arguments of type %T and type %T", a, b))
}
const (
@@ -144,5 +148,5 @@ func sortOrder(v interface{}) int {
case map[string]interface{}:
return objectSort
}
panic("unreachable")
panic(fmt.Sprintf("illegal argument of type %T", v))
}
+89 -53
View File
@@ -19,32 +19,19 @@ type TopDownContext struct {
Index int
Previous *TopDownContext
Store Storage
Tracer Tracer
}
// Current returns the current expression to evaluate.
func (ctx *TopDownContext) Current() *opalog.Expr {
return ctx.Query[ctx.Index]
}
// Step returns a new context to evaluate the next expression.
func (ctx *TopDownContext) Step() *TopDownContext {
return &TopDownContext{
Query: ctx.Query,
Bindings: ctx.Bindings,
Index: ctx.Index + 1,
Previous: ctx.Previous,
Store: ctx.Store,
}
}
// Child returns a new context to evaluate a rule that was referenced by this context.
func (ctx *TopDownContext) Child(rule *opalog.Rule, bindings *hashMap) *TopDownContext {
return &TopDownContext{
Query: rule.Body,
Bindings: bindings,
Previous: ctx,
Store: ctx.Store,
}
// BindRef returns a new TopDownContext with bindings that map the reference to the value.
func (ctx *TopDownContext) BindRef(ref opalog.Ref, value opalog.Value) *TopDownContext {
cpy := *ctx
cpy.Bindings = newHashMap()
ctx.Bindings.Iter(func(k opalog.Value, v opalog.Value) bool {
cpy.Bindings.Put(k, v)
return false
})
cpy.Bindings.Put(ref, value)
return &cpy
}
// BindVar returns a new TopDownContext with bindings that map the variable to the value.
@@ -88,18 +75,52 @@ func (ctx *TopDownContext) BindVar(variable opalog.Var, value opalog.Value) *Top
return &cpy
}
// BindRef returns a new TopDownContext with bindings that map the reference to the value.
func (ctx *TopDownContext) BindRef(ref opalog.Ref, value opalog.Value) *TopDownContext {
// Child returns a new context to evaluate a rule that was referenced by this context.
func (ctx *TopDownContext) Child(rule *opalog.Rule, bindings *hashMap) *TopDownContext {
cpy := *ctx
cpy.Bindings = newHashMap()
ctx.Bindings.Iter(func(k opalog.Value, v opalog.Value) bool {
cpy.Bindings.Put(k, v)
return false
})
cpy.Bindings.Put(ref, value)
cpy.Query = rule.Body
cpy.Bindings = bindings
cpy.Previous = ctx
return &cpy
}
// Current returns the current expression to evaluate.
func (ctx *TopDownContext) Current() *opalog.Expr {
return ctx.Query[ctx.Index]
}
// Step returns a new context to evaluate the next expression.
func (ctx *TopDownContext) Step() *TopDownContext {
cpy := *ctx
cpy.Index++
return &cpy
}
func (ctx *TopDownContext) trace(f string, a ...interface{}) {
if ctx.Tracer == nil {
return
}
if ctx.Tracer.Enabled() {
ctx.Tracer.Trace(ctx, f, a...)
}
}
func (ctx *TopDownContext) traceEval() {
ctx.trace("Eval %v", ctx.Current())
}
func (ctx *TopDownContext) traceTry(expr *opalog.Expr) {
ctx.trace(" Try %v", expr)
}
func (ctx *TopDownContext) traceSuccess(expr *opalog.Expr) {
ctx.trace(" Success %v", expr)
}
func (ctx *TopDownContext) traceFinish() {
ctx.trace(" Finish %v", ctx.Bindings)
}
// TopDownIterator is the interface for processing contexts.
type TopDownIterator func(*TopDownContext) error
@@ -110,19 +131,26 @@ func TopDown(ctx *TopDownContext, iter TopDownIterator) error {
return evalContext(ctx, iter)
}
// TopDownQueryParams defines input parameters for the query interface.
type TopDownQueryParams struct {
Store Storage
Tracer Tracer
Path []string
}
// TopDownQuery returns the document identified by the path.
//
// If the storage node identified by the path is a collection of rules, then the TopDown
// algorithm is run to generate the virtual document defined by the rules.
func TopDownQuery(store Storage, path []string) (interface{}, error) {
func TopDownQuery(params *TopDownQueryParams) (interface{}, error) {
var ref opalog.Ref
ref = append(ref, opalog.VarTerm(path[0]))
for _, v := range path[1:] {
ref = append(ref, opalog.VarTerm(params.Path[0]))
for _, v := range params.Path[1:] {
ref = append(ref, opalog.StringTerm(v))
}
node, err := store.Lookup(ref)
node, err := params.Store.Lookup(ref)
if err != nil {
return nil, err
}
@@ -136,11 +164,11 @@ func TopDownQuery(store Storage, path []string) (interface{}, error) {
// type. This is checked at compile time.
switch node[0].DocKind() {
case opalog.CompleteDoc:
return topDownQueryCompleteDoc(store, node)
return topDownQueryCompleteDoc(params, node)
case opalog.PartialObjectDoc:
return topDownQueryPartialObjectDoc(store, node)
return topDownQueryPartialObjectDoc(params, node)
case opalog.PartialSetDoc:
return topDownQueryPartialSetDoc(store, node)
return topDownQueryPartialSetDoc(params, node)
default:
return nil, fmt.Errorf("invalid document (kind: %v): %v", node[0].DocKind(), ref)
}
@@ -176,9 +204,12 @@ func dereferenceVar(v opalog.Var, ctx *TopDownContext) (interface{}, error) {
func evalContext(ctx *TopDownContext, iter TopDownIterator) error {
if ctx.Index >= len(ctx.Query) {
ctx.traceFinish()
return iter(ctx)
}
ctx.traceEval()
if ctx.Current().Negated {
return evalContextNegated(ctx, iter)
}
@@ -193,16 +224,14 @@ func evalContext(ctx *TopDownContext, iter TopDownIterator) error {
func evalContextNegated(ctx *TopDownContext, iter TopDownIterator) error {
negation := &TopDownContext{
Query: opalog.Body([]*opalog.Expr{ctx.Current().Complement()}),
Bindings: ctx.Bindings,
Previous: ctx,
Store: ctx.Store,
}
negation := *ctx
negation.Query = opalog.Body([]*opalog.Expr{ctx.Current().Complement()})
negation.Index = 0
negation.Previous = ctx
isDefined := false
err := evalContext(negation, func(*TopDownContext) error {
err := evalContext(&negation, func(*TopDownContext) error {
isDefined = true
return nil
})
@@ -224,7 +253,10 @@ func evalEq(ctx *TopDownContext, expr *opalog.Expr, iter TopDownIterator) error
a := operands[1].Value
b := operands[2].Value
return evalEqUnify(ctx, a, b, iter)
return evalEqUnify(ctx, a, b, func(ctx *TopDownContext) error {
ctx.traceSuccess(expr)
return iter(ctx)
})
}
func evalEqGround(ctx *TopDownContext, a opalog.Value, b opalog.Value, iter TopDownIterator) error {
@@ -481,6 +513,7 @@ func evalEqUnifyVar(ctx *TopDownContext, a opalog.Var, b opalog.Value, iter TopD
func evalExpr(ctx *TopDownContext, iter TopDownIterator) error {
expr := plugExpr(ctx.Current(), ctx.Bindings)
ctx.traceTry(expr)
switch tt := expr.Terms.(type) {
case []*opalog.Term:
builtin := builtinFunctions[tt[0].Value.(opalog.Var)]
@@ -997,7 +1030,7 @@ func plugValue(v opalog.Value, bindings *hashMap) opalog.Value {
}
}
func topDownQueryCompleteDoc(store Storage, rules []*opalog.Rule) (interface{}, error) {
func topDownQueryCompleteDoc(params *TopDownQueryParams, rules []*opalog.Rule) (interface{}, error) {
if len(rules) > 1 {
return nil, fmt.Errorf("multiple conflicting rules: %v", rules[0].Name)
@@ -1008,7 +1041,8 @@ func topDownQueryCompleteDoc(store Storage, rules []*opalog.Rule) (interface{},
ctx := &TopDownContext{
Query: rule.Body,
Bindings: newHashMap(),
Store: store,
Store: params.Store,
Tracer: params.Tracer,
}
isDefined := false
@@ -1026,7 +1060,7 @@ func topDownQueryCompleteDoc(store Storage, rules []*opalog.Rule) (interface{},
return valueToInterface(rule.Value.Value, ctx)
}
func topDownQueryPartialObjectDoc(store Storage, rules []*opalog.Rule) (interface{}, error) {
func topDownQueryPartialObjectDoc(params *TopDownQueryParams, rules []*opalog.Rule) (interface{}, error) {
result := map[string]interface{}{}
@@ -1034,7 +1068,8 @@ func topDownQueryPartialObjectDoc(store Storage, rules []*opalog.Rule) (interfac
ctx := &TopDownContext{
Query: rule.Body,
Bindings: newHashMap(),
Store: store,
Store: params.Store,
Tracer: params.Tracer,
}
key := rule.Key.Value.(opalog.Var)
value := rule.Value.Value.(opalog.Var)
@@ -1062,13 +1097,14 @@ func topDownQueryPartialObjectDoc(store Storage, rules []*opalog.Rule) (interfac
return result, nil
}
func topDownQueryPartialSetDoc(store Storage, rules []*opalog.Rule) (interface{}, error) {
func topDownQueryPartialSetDoc(params *TopDownQueryParams, rules []*opalog.Rule) (interface{}, error) {
result := []interface{}{}
for _, rule := range rules {
ctx := &TopDownContext{
Query: rule.Body,
Bindings: newHashMap(),
Store: store,
Store: params.Store,
Tracer: params.Tracer,
}
key := rule.Key.Value.(opalog.Var)
err := TopDown(ctx, func(ctx *TopDownContext) error {
+2 -2
View File
@@ -605,7 +605,7 @@ func runTopDownTestCase(t *testing.T, data map[string]interface{}, i int, note s
switch e := expected.(type) {
case error:
result, err := TopDownQuery(store, []string{"p"})
result, err := TopDownQuery(&TopDownQueryParams{Store: store, Path: []string{"p"}})
if err == nil {
t.Errorf("Test case %d (%v): expected error but got: %v", i+1, note, result)
return
@@ -616,7 +616,7 @@ func runTopDownTestCase(t *testing.T, data map[string]interface{}, i int, note s
case string:
expected := loadExpectedResult(e)
result, err := TopDownQuery(store, []string{"p"})
result, err := TopDownQuery(&TopDownQueryParams{Store: store, Path: []string{"p"}})
if err != nil {
t.Errorf("Test case %d (%v): unexpected error: %v", i+1, note, err)
return
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2016 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package eval
import (
"fmt"
"strings"
)
// Tracer defines the interface for tracing evaluation.
type Tracer interface {
// Enabled returns true if the tracer is enabled.
Enabled() bool
// Trace emits a message if the tracer is enabled.
Trace(ctx *TopDownContext, f string, a ...interface{})
}
// StdoutTracer writes trace messages to stdout.
type StdoutTracer struct{}
// Enabled always returns true.
func (t *StdoutTracer) Enabled() bool { return true }
// Trace writes a trace message to stdout.
func (t *StdoutTracer) Trace(ctx *TopDownContext, f string, a ...interface{}) {
var padding string
i := 0
for ; ctx != nil; ctx = ctx.Previous {
padding += strings.Repeat(" ", ctx.Index+i)
i++
}
f = padding + f + "\n"
fmt.Printf(f, a...)
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2016 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package eval
import (
"fmt"
"testing"
)
type mockTracer struct {
buf []string
}
func (t *mockTracer) Enabled() bool { return true }
func (t *mockTracer) Trace(ctx *TopDownContext, f string, a ...interface{}) {
t.buf = append(t.buf, fmt.Sprintf(f, a))
}
func TestTracer(t *testing.T) {
rules := parseRules([]string{
"p[x] :- q[x] = y",
"q[i] = j :- a[i] = j",
})
store := newStorage(loadSmallTestData(), rules)
tracer := &mockTracer{[]string{}}
params := &TopDownQueryParams{
Store: store,
Tracer: tracer,
Path: []string{"p"}}
result, err := TopDownQuery(params)
if err != nil {
t.Errorf("Unexpected error in tracing test: %v", err)
return
}
expected := []interface{}{float64(0), float64(1), float64(2), float64(3)}
if Compare(result, expected) != 0 {
t.Errorf("Unexpected result in tracing test: %v", result)
return
}
// ((try success finish * 2) * 4) + 2
// 2 rules
// 4 elements in a
if len(tracer.buf) != 26 {
t.Errorf("Unexpected output from tracer:\n%v", tracer.buf)
}
}