mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
81a774a60d
* wasm: optimize package access with non-ground refs using call_indrect
We now
1. write an object corresponding to data paths into the module data
2. initialize an opa_object_t from that using `_initialize`, called
as the module's Start function
3. write out CallDynamicStmts in IR when the ref is not all ground,
but its vars have been seen
4. compile those CallDynamicStmts to call_indirect invocations in
WASM, preceded by a lookup using the path in the object prepared
in (2.)
5. if the lookup fails to come up with a result, the eval goes
undefined
What it looks like:
With t.rego as
package t
p {
data.foo[input.x].bar.p
}
and foo.rego as
package foo.a.bar
p = true
when building policy.wasm using `opa build -t wasm -e t/p t.rego foo.rego`,
the body of function `g0.data.t.p` will contain
i32.const 5
call $opa_array_with_cap
local.set $11
local.get $11
local.get $7
call $opa_array_append
local.get $11
local.get $8
call $opa_array_append
local.get $11
local.get $6
call $opa_array_append
local.get $11
local.get $9
call $opa_array_append
local.get $11
local.get $10
call $opa_array_append
local.get $0
local.get $1
local.get $11
call $opa_mapping_lookup
local.tee $12
i32.eqz
br_if $block
local.get $12
call_indirect $29 (type $1)
local.tee $13
i32.eqz
br_if $block
Where the array-related functions build an array of
["g0", "foo", input.x, "bar", "p"]
and pass that to `opa_mapping_lookup` to determine the element index to
pass to `call_indirect`. The lookup function returns 74 from the JSON
blob put into the data section,
(data $38 (i32.const 56485)
"{\"g0\": {\"foo\": {\"a\": {\"bar\": {\"p\": 74}}}, \"t\": {\"p\": 75}}}")
iff input.x happens to be "a". Otherwise, it'll return 0, and the result
will end up being undefined.
Element 74 of the modules func table is, of course, $g0.data.foo.a.bar.p:
(elem $33 (i32.const 74)
$g0.data.foo.a.bar.p $g0.data.t.p)
($33 is some id of that piece of function table, an artifact of the
`wavm disassemble` output.)
* compiler/wasm: add memoization to call_indirect logic
- adds a data segment for mapping element indices (used with call_indirect)
to function indices (as used with opa_memoize_{get,insert})
- emits mapping function elem -> func idx that uses that data segment
- wires up memoization lookup and insert in call_indirect code path
The added test case would cause `make wasm-rego-test` to fail like this
if memoization wasn't happening:
ERROR 019_call_indirect_optimization.json: memoization: should have been memoized
* planner: add debug messages, carry them over into the compiler
Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
2147 lines
49 KiB
Go
2147 lines
49 KiB
Go
// Copyright 2018 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 planner contains a query planner for Rego queries.
|
|
package planner
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/open-policy-agent/opa/ast"
|
|
"github.com/open-policy-agent/opa/ast/location"
|
|
"github.com/open-policy-agent/opa/internal/ir"
|
|
)
|
|
|
|
// QuerySet represents the input to the planner.
|
|
type QuerySet struct {
|
|
Name string
|
|
Queries []ast.Body
|
|
RewrittenVars map[ast.Var]ast.Var
|
|
}
|
|
|
|
type planiter func() error
|
|
type binaryiter func(ir.Local, ir.Local) error
|
|
|
|
type wasmBuiltin struct {
|
|
*ast.Builtin
|
|
WasmFunction string
|
|
}
|
|
|
|
// Planner implements a query planner for Rego queries.
|
|
type Planner struct {
|
|
policy *ir.Policy // result of planning
|
|
queries []QuerySet // input queries to plan
|
|
modules []*ast.Module // input modules to support queries
|
|
strings map[string]int // global string constant indices
|
|
files map[string]int // global file constant indices
|
|
externs map[string]struct{} // built-in functions that are required in execution environment
|
|
decls map[string]*ast.Builtin // built-in functions that may be provided in execution environment
|
|
rules *ruletrie // rules that may be planned
|
|
funcs *funcstack // functions that have been planned
|
|
plan *ir.Plan // in-progress query plan
|
|
curr *ir.Block // in-progress query block
|
|
vars *varstack // in-scope variables
|
|
ltarget ir.Local // target variable of last planned statement
|
|
lnext ir.Local // next variable to use
|
|
loc *location.Location // location currently "being planned"
|
|
debugs []Debug // debug information produced during planning
|
|
}
|
|
|
|
func (p *Planner) addDebug(info Debug) {
|
|
if info.Location == nil {
|
|
info.Location = p.loc
|
|
}
|
|
p.debugs = append(p.debugs, info)
|
|
}
|
|
|
|
// Debug contains debugging information produced by the build about optimizations and other operations.
|
|
type Debug struct {
|
|
Location *location.Location
|
|
Message string
|
|
}
|
|
|
|
// New returns a new Planner object.
|
|
func New() *Planner {
|
|
return &Planner{
|
|
policy: &ir.Policy{
|
|
Static: &ir.Static{},
|
|
Plans: &ir.Plans{},
|
|
Funcs: &ir.Funcs{},
|
|
},
|
|
strings: map[string]int{},
|
|
files: map[string]int{},
|
|
externs: map[string]struct{}{},
|
|
lnext: ir.Unused,
|
|
vars: newVarstack(map[ast.Var]ir.Local{
|
|
ast.InputRootDocument.Value.(ast.Var): ir.Input,
|
|
ast.DefaultRootDocument.Value.(ast.Var): ir.Data,
|
|
}),
|
|
rules: newRuletrie(),
|
|
funcs: newFuncstack(),
|
|
}
|
|
}
|
|
|
|
// Debug returns a list of debug events produced by the planner.
|
|
func (p *Planner) Debug() []Debug {
|
|
return p.debugs
|
|
}
|
|
|
|
func (p *Planner) debug(msg string) {
|
|
p.addDebug(Debug{Message: msg})
|
|
}
|
|
|
|
// WithBuiltinDecls tells the planner what built-in function may be available
|
|
// inside the execution environment.
|
|
func (p *Planner) WithBuiltinDecls(decls map[string]*ast.Builtin) *Planner {
|
|
p.decls = decls
|
|
return p
|
|
}
|
|
|
|
// WithQueries sets the query sets to generate a plan for. The rewritten collection provides
|
|
// a mapping of rewritten query vars for each query set. The planner uses rewritten variables
|
|
// but the result set key will be the original variable name.
|
|
func (p *Planner) WithQueries(queries []QuerySet) *Planner {
|
|
p.queries = queries
|
|
return p
|
|
}
|
|
|
|
// WithModules sets the module set that contains query dependencies.
|
|
func (p *Planner) WithModules(modules []*ast.Module) *Planner {
|
|
p.modules = modules
|
|
return p
|
|
}
|
|
|
|
// Plan returns a IR plan for the policy query.
|
|
func (p *Planner) Plan() (*ir.Policy, error) {
|
|
|
|
if err := p.buildFunctrie(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := p.planQueries(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := p.planExterns(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return p.policy, nil
|
|
}
|
|
|
|
func (p *Planner) buildFunctrie() error {
|
|
|
|
for _, module := range p.modules {
|
|
|
|
// Create functrie node for empty packages so that extent queries return
|
|
// empty objects. For example:
|
|
//
|
|
// package x.y
|
|
//
|
|
// Query: data.x
|
|
//
|
|
// Expected result: {"y": {}}
|
|
if len(module.Rules) == 0 {
|
|
_ = p.rules.LookupOrInsert(module.Package.Path)
|
|
continue
|
|
}
|
|
|
|
for _, rule := range module.Rules {
|
|
val := p.rules.LookupOrInsert(rule.Path())
|
|
val.rules = append(val.rules, rule)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Planner) planRules(rules []*ast.Rule) (string, error) {
|
|
|
|
pathRef := rules[0].Path()
|
|
path := pathRef.String()
|
|
|
|
var pathPieces []string
|
|
for i := 1; /* skip `data` */ i < len(pathRef); i++ {
|
|
pathPieces = append(pathPieces, string(pathRef[i].Value.(ast.String)))
|
|
}
|
|
|
|
if funcName, ok := p.funcs.Get(path); ok {
|
|
return funcName, nil
|
|
}
|
|
|
|
// Save current state of planner.
|
|
//
|
|
// TODO(tsandall): perhaps we would be better off using stacks here or
|
|
// splitting block planner into separate struct that could be instantiated
|
|
// for rule and comprehension bodies.
|
|
pvars := p.vars
|
|
pcurr := p.curr
|
|
pltarget := p.ltarget
|
|
plnext := p.lnext
|
|
ploc := p.loc
|
|
|
|
// Reset the variable counter for the function plan.
|
|
p.lnext = ir.Input
|
|
|
|
// Set the location to the rule head.
|
|
p.loc = rules[0].Head.Loc()
|
|
|
|
// Create function definition for rules.
|
|
fn := &ir.Func{
|
|
Name: fmt.Sprintf("g%d.%s", p.funcs.gen, path),
|
|
Params: []ir.Local{
|
|
p.newLocal(), // input document
|
|
p.newLocal(), // data document
|
|
},
|
|
Return: p.newLocal(),
|
|
Path: append([]string{fmt.Sprintf("g%d", p.funcs.gen)}, pathPieces...),
|
|
}
|
|
|
|
// Initialize parameters for functions.
|
|
for i := 0; i < len(rules[0].Head.Args); i++ {
|
|
fn.Params = append(fn.Params, p.newLocal())
|
|
}
|
|
|
|
params := fn.Params[2:]
|
|
|
|
// Initialize return value for partial set/object rules. Complete document
|
|
// rules assign directly to `fn.Return`.
|
|
switch rules[0].Head.DocKind() {
|
|
case ast.PartialObjectDoc:
|
|
fn.Blocks = append(fn.Blocks, p.blockWithStmt(&ir.MakeObjectStmt{Target: fn.Return}))
|
|
case ast.PartialSetDoc:
|
|
fn.Blocks = append(fn.Blocks, p.blockWithStmt(&ir.MakeSetStmt{Target: fn.Return}))
|
|
}
|
|
|
|
// For complete document rules, allocate one local variable for output
|
|
// of the rule body + else branches.
|
|
// It is used to let ordered rules (else blocks) check if the previous
|
|
// rule body returned a value.
|
|
lresult := p.newLocal()
|
|
|
|
// At this point the locals for the params and return value have been
|
|
// allocated. This will be the first local that can be used in each block.
|
|
lnext := p.lnext
|
|
|
|
var defaultRule *ast.Rule
|
|
var ruleLoc *location.Location
|
|
|
|
// Generate function blocks for rules.
|
|
for i := range rules {
|
|
|
|
// Save location of first encountered rule for the ReturnLocalStmt below
|
|
if i == 0 {
|
|
ruleLoc = p.loc
|
|
}
|
|
|
|
// Save default rule for the end.
|
|
if rules[i].Default {
|
|
defaultRule = rules[i]
|
|
continue
|
|
}
|
|
|
|
// Ordered rules are nested inside an additional block so that execution
|
|
// can short-circuit. For unordered rules, blocks can be added directly
|
|
// to the function.
|
|
var blocks *[]*ir.Block
|
|
|
|
if rules[i].Else == nil {
|
|
blocks = &fn.Blocks
|
|
} else {
|
|
stmt := &ir.BlockStmt{}
|
|
block := &ir.Block{Stmts: []ir.Stmt{stmt}}
|
|
fn.Blocks = append(fn.Blocks, block)
|
|
blocks = &stmt.Blocks
|
|
}
|
|
|
|
var prev *ast.Rule
|
|
|
|
// Unordered rules are treated as a special case of ordered rules.
|
|
for rule := rules[i]; rule != nil; prev, rule = rule, rule.Else {
|
|
|
|
// Update the location for each ordered rule.
|
|
p.loc = rule.Head.Loc()
|
|
|
|
// Setup planner for block.
|
|
p.lnext = lnext
|
|
p.vars = newVarstack(map[ast.Var]ir.Local{
|
|
ast.InputRootDocument.Value.(ast.Var): fn.Params[0],
|
|
ast.DefaultRootDocument.Value.(ast.Var): fn.Params[1],
|
|
})
|
|
|
|
curr := &ir.Block{}
|
|
*blocks = append(*blocks, curr)
|
|
p.curr = curr
|
|
|
|
if prev != nil {
|
|
// Ordered rules are handled by short circuiting execution. The
|
|
// plan will jump out to the extra block that was planned above.
|
|
p.appendStmt(&ir.IsUndefinedStmt{Source: lresult})
|
|
} else {
|
|
// The first rule body resets the local, so it can be reused.
|
|
p.appendStmt(&ir.ResetLocalStmt{Target: lresult})
|
|
}
|
|
|
|
// Complete and partial rules are treated as special cases of
|
|
// functions. If there are no args, the first step is a no-op.
|
|
err := p.planFuncParams(params, rule.Head.Args, 0, func() error {
|
|
|
|
// Run planner on the rule body.
|
|
err := p.planQuery(rule.Body, 0, func() error {
|
|
|
|
// Run planner on the result.
|
|
switch rule.Head.DocKind() {
|
|
case ast.CompleteDoc:
|
|
return p.planTerm(rule.Head.Value, func() error {
|
|
p.appendStmt(&ir.AssignVarOnceStmt{
|
|
Target: lresult,
|
|
Source: p.ltarget,
|
|
})
|
|
return nil
|
|
})
|
|
case ast.PartialSetDoc:
|
|
return p.planTerm(rule.Head.Key, func() error {
|
|
p.appendStmt(&ir.SetAddStmt{
|
|
Set: fn.Return,
|
|
Value: p.ltarget,
|
|
})
|
|
return nil
|
|
})
|
|
case ast.PartialObjectDoc:
|
|
return p.planTerm(rule.Head.Key, func() error {
|
|
key := p.ltarget
|
|
return p.planTerm(rule.Head.Value, func() error {
|
|
value := p.ltarget
|
|
p.appendStmt(&ir.ObjectInsertOnceStmt{
|
|
Object: fn.Return,
|
|
Key: key,
|
|
Value: value,
|
|
})
|
|
return nil
|
|
})
|
|
})
|
|
default:
|
|
return fmt.Errorf("illegal rule kind")
|
|
}
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
// rule[i] and its else-rule(s), if present, are done
|
|
if rules[i].Head.DocKind() == ast.CompleteDoc {
|
|
end := &ir.Block{}
|
|
p.appendStmtToBlock(&ir.IsDefinedStmt{Source: lresult}, end)
|
|
p.appendStmtToBlock(
|
|
&ir.AssignVarOnceStmt{
|
|
Target: fn.Return,
|
|
Source: lresult,
|
|
},
|
|
end)
|
|
*blocks = append(*blocks, end)
|
|
}
|
|
}
|
|
|
|
// Default rules execute if the return is undefined.
|
|
if defaultRule != nil {
|
|
|
|
// Set the location for the default rule head.
|
|
p.loc = defaultRule.Head.Loc()
|
|
// NOTE(sr) for `default p = 1`,
|
|
// defaultRule.Loc() is `default`,
|
|
// defaultRule.Head.Loc() is `p = 1`.
|
|
|
|
fn.Blocks = append(fn.Blocks, p.blockWithStmt(&ir.IsUndefinedStmt{Source: fn.Return}))
|
|
|
|
p.curr = fn.Blocks[len(fn.Blocks)-1]
|
|
|
|
err := p.planQuery(defaultRule.Body, 0, func() error {
|
|
p.loc = defaultRule.Head.Loc()
|
|
return p.planTerm(defaultRule.Head.Value, func() error {
|
|
p.appendStmt(&ir.AssignVarOnceStmt{
|
|
Target: fn.Return,
|
|
Source: p.ltarget,
|
|
})
|
|
return nil
|
|
})
|
|
})
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
p.loc = ruleLoc
|
|
|
|
// All rules return a value.
|
|
fn.Blocks = append(fn.Blocks, p.blockWithStmt(&ir.ReturnLocalStmt{Source: fn.Return}))
|
|
|
|
p.appendFunc(fn)
|
|
p.funcs.Add(path, fn.Name)
|
|
|
|
// Restore the state of the planner.
|
|
p.lnext = plnext
|
|
p.ltarget = pltarget
|
|
p.vars = pvars
|
|
p.curr = pcurr
|
|
p.loc = ploc
|
|
|
|
return fn.Name, nil
|
|
}
|
|
|
|
func (p *Planner) planFuncParams(params []ir.Local, args ast.Args, idx int, iter planiter) error {
|
|
if idx >= len(args) {
|
|
return iter()
|
|
}
|
|
return p.planUnifyLocal(params[idx], args[idx], func() error {
|
|
return p.planFuncParams(params, args, idx+1, iter)
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planQueries() error {
|
|
|
|
for _, qs := range p.queries {
|
|
|
|
// Initialize the plan with a block that prepares the query result.
|
|
p.plan = &ir.Plan{Name: qs.Name}
|
|
p.policy.Plans.Plans = append(p.policy.Plans.Plans, p.plan)
|
|
p.curr = &ir.Block{}
|
|
|
|
// Build a set of variables appearing in the query and allocate strings for
|
|
// each one. The strings will be used in the result set objects.
|
|
qvs := ast.NewVarSet()
|
|
|
|
for _, q := range qs.Queries {
|
|
vs := q.Vars(ast.VarVisitorParams{SkipRefCallHead: true, SkipClosures: true}).Diff(ast.ReservedVars)
|
|
qvs.Update(vs)
|
|
}
|
|
|
|
lvarnames := make(map[ast.Var]ir.Local, len(qvs))
|
|
|
|
for _, qv := range qvs.Sorted() {
|
|
qv = rewrittenVar(qs.RewrittenVars, qv)
|
|
if !qv.IsGenerated() && !qv.IsWildcard() {
|
|
// NOTE(sr): We have no location for these: they could appear in multiple
|
|
// queries, and we've lost track when building the ast.VarSet.
|
|
stmt := &ir.MakeStringStmt{
|
|
Index: p.getStringConst(string(qv)),
|
|
Target: p.newLocal(),
|
|
}
|
|
p.appendStmt(stmt)
|
|
lvarnames[qv] = stmt.Target
|
|
}
|
|
}
|
|
|
|
if len(p.curr.Stmts) > 0 {
|
|
p.appendBlock(p.curr)
|
|
}
|
|
|
|
lnext := p.lnext
|
|
|
|
for _, q := range qs.Queries {
|
|
p.loc = q.Loc()
|
|
p.lnext = lnext
|
|
p.vars.Push(map[ast.Var]ir.Local{})
|
|
p.curr = &ir.Block{}
|
|
defined := false
|
|
qvs := q.Vars(ast.VarVisitorParams{SkipRefCallHead: true, SkipClosures: true}).Diff(ast.ReservedVars).Sorted()
|
|
|
|
if err := p.planQuery(q, 0, func() error {
|
|
|
|
// Add an object containing variable bindings into the result set.
|
|
lr := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeObjectStmt{
|
|
Target: lr,
|
|
})
|
|
|
|
for _, qv := range qvs {
|
|
rw := rewrittenVar(qs.RewrittenVars, qv)
|
|
if !rw.IsGenerated() && !rw.IsWildcard() {
|
|
p.appendStmt(&ir.ObjectInsertStmt{
|
|
Object: lr,
|
|
Key: lvarnames[rw],
|
|
Value: p.vars.GetOrEmpty(qv),
|
|
})
|
|
}
|
|
}
|
|
|
|
p.appendStmt(&ir.ResultSetAdd{
|
|
Value: lr,
|
|
})
|
|
|
|
defined = true
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
p.vars.Pop()
|
|
|
|
if defined {
|
|
p.appendBlock(p.curr)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Planner) planQuery(q ast.Body, index int, iter planiter) error {
|
|
|
|
if index >= len(q) {
|
|
return iter()
|
|
}
|
|
|
|
old := p.loc
|
|
p.loc = q[index].Loc()
|
|
|
|
err := p.planExpr(q[index], func() error {
|
|
return p.planQuery(q, index+1, func() error {
|
|
curr := p.loc
|
|
p.loc = old
|
|
err := iter()
|
|
p.loc = curr
|
|
return err
|
|
})
|
|
})
|
|
|
|
p.loc = old
|
|
return err
|
|
}
|
|
|
|
// TODO(tsandall): improve errors to include location information.
|
|
func (p *Planner) planExpr(e *ast.Expr, iter planiter) error {
|
|
|
|
if e.Negated {
|
|
return p.planNot(e, iter)
|
|
}
|
|
|
|
if len(e.With) > 0 {
|
|
return p.planWith(e, iter)
|
|
}
|
|
|
|
if e.IsCall() {
|
|
return p.planExprCall(e, iter)
|
|
}
|
|
|
|
return p.planExprTerm(e, iter)
|
|
}
|
|
|
|
func (p *Planner) planNot(e *ast.Expr, iter planiter) error {
|
|
|
|
not := &ir.NotStmt{
|
|
Block: &ir.Block{},
|
|
}
|
|
|
|
prev := p.curr
|
|
p.curr = not.Block
|
|
|
|
if err := p.planExpr(e.Complement(), func() error {
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
p.curr = prev
|
|
p.appendStmt(not)
|
|
|
|
return iter()
|
|
}
|
|
|
|
func (p *Planner) planWith(e *ast.Expr, iter planiter) error {
|
|
|
|
// Plan the values that will be applied by the with modifiers. All values
|
|
// must be defined for the overall expression to evaluate.
|
|
values := make([]*ast.Term, len(e.With))
|
|
|
|
for i := range e.With {
|
|
values[i] = e.With[i].Value
|
|
}
|
|
|
|
return p.planTermSlice(values, func(locals []ir.Local) error {
|
|
|
|
paths := make([][]int, len(e.With))
|
|
saveVars := ast.NewVarSet()
|
|
dataRefs := []ast.Ref{}
|
|
|
|
for i := range e.With {
|
|
|
|
target := e.With[i].Target.Value.(ast.Ref)
|
|
paths[i] = make([]int, len(target)-1)
|
|
|
|
for j := 1; j < len(target); j++ {
|
|
if s, ok := target[j].Value.(ast.String); ok {
|
|
paths[i][j-1] = p.getStringConst(string(s))
|
|
} else {
|
|
return errors.New("invalid with target")
|
|
}
|
|
}
|
|
|
|
head := target[0].Value.(ast.Var)
|
|
saveVars.Add(head)
|
|
|
|
if head.Equal(ast.DefaultRootDocument.Value) {
|
|
dataRefs = append(dataRefs, target)
|
|
}
|
|
}
|
|
|
|
restore := make([][2]ir.Local, len(saveVars))
|
|
|
|
for i, v := range saveVars.Sorted() {
|
|
lorig := p.vars.GetOrEmpty(v)
|
|
lsave := p.newLocal()
|
|
p.appendStmt(&ir.AssignVarStmt{Source: lorig, Target: lsave})
|
|
restore[i] = [2]ir.Local{lorig, lsave}
|
|
}
|
|
|
|
// If any of the with statements targeted the data document we shadow
|
|
// the existing planned functions during expression planning. This
|
|
// causes the planner to re-plan any rules that may be required during
|
|
// planning of this expression (transitively).
|
|
if len(dataRefs) > 0 {
|
|
p.funcs.Push(map[string]string{})
|
|
for _, ref := range dataRefs {
|
|
p.rules.Push(ref)
|
|
}
|
|
}
|
|
|
|
err := p.planWithRec(e, paths, locals, 0, func() error {
|
|
if len(dataRefs) > 0 {
|
|
p.funcs.Pop()
|
|
for i := len(dataRefs) - 1; i >= 0; i-- {
|
|
p.rules.Pop(dataRefs[i])
|
|
}
|
|
}
|
|
|
|
err := p.planWithUndoRec(restore, 0, func() error {
|
|
|
|
err := iter()
|
|
|
|
if len(dataRefs) > 0 {
|
|
p.funcs.Push(map[string]string{})
|
|
for _, ref := range dataRefs {
|
|
p.rules.Push(ref)
|
|
}
|
|
}
|
|
return err
|
|
})
|
|
|
|
return err
|
|
})
|
|
|
|
if len(dataRefs) > 0 {
|
|
p.funcs.Pop()
|
|
for i := len(dataRefs) - 1; i >= 0; i-- {
|
|
p.rules.Pop(dataRefs[i])
|
|
}
|
|
}
|
|
return err
|
|
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planWithRec(e *ast.Expr, targets [][]int, values []ir.Local, index int, iter planiter) error {
|
|
if index >= len(e.With) {
|
|
return p.planExpr(e.NoWith(), iter)
|
|
}
|
|
|
|
prev := p.curr
|
|
p.curr = &ir.Block{}
|
|
|
|
err := p.planWithRec(e, targets, values, index+1, iter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
block := p.curr
|
|
p.curr = prev
|
|
target := e.With[index].Target.Value.(ast.Ref)
|
|
head := target[0].Value.(ast.Var)
|
|
|
|
p.appendStmt(&ir.WithStmt{
|
|
Local: p.vars.GetOrEmpty(head),
|
|
Path: targets[index],
|
|
Value: values[index],
|
|
Block: block,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Planner) planWithUndoRec(restore [][2]ir.Local, index int, iter planiter) error {
|
|
|
|
if index >= len(restore) {
|
|
return iter()
|
|
}
|
|
|
|
prev := p.curr
|
|
p.curr = &ir.Block{}
|
|
|
|
if err := p.planWithUndoRec(restore, index+1, iter); err != nil {
|
|
return err
|
|
}
|
|
|
|
block := p.curr
|
|
p.curr = prev
|
|
lorig := restore[index][0]
|
|
lsave := restore[index][1]
|
|
|
|
p.appendStmt(&ir.WithStmt{
|
|
Local: lorig,
|
|
Value: lsave,
|
|
Block: block,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Planner) planExprTerm(e *ast.Expr, iter planiter) error {
|
|
return p.planTerm(e.Terms.(*ast.Term), func() error {
|
|
falsy := p.newLocal()
|
|
p.appendStmt(&ir.MakeBooleanStmt{
|
|
Value: false,
|
|
Target: falsy,
|
|
})
|
|
p.appendStmt(&ir.NotEqualStmt{
|
|
A: p.ltarget,
|
|
B: falsy,
|
|
})
|
|
return iter()
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planExprCall(e *ast.Expr, iter planiter) error {
|
|
operator := e.Operator().String()
|
|
switch operator {
|
|
case ast.Equality.Name:
|
|
return p.planUnify(e.Operand(0), e.Operand(1), iter)
|
|
case ast.Equal.Name:
|
|
return p.planBinaryExpr(e, func(a, b ir.Local) error {
|
|
p.appendStmt(&ir.EqualStmt{
|
|
A: a,
|
|
B: b,
|
|
})
|
|
return iter()
|
|
})
|
|
case ast.LessThan.Name:
|
|
return p.planBinaryExpr(e, func(a, b ir.Local) error {
|
|
p.appendStmt(&ir.LessThanStmt{
|
|
A: a,
|
|
B: b,
|
|
})
|
|
return iter()
|
|
})
|
|
case ast.LessThanEq.Name:
|
|
return p.planBinaryExpr(e, func(a, b ir.Local) error {
|
|
p.appendStmt(&ir.LessThanEqualStmt{
|
|
A: a,
|
|
B: b,
|
|
})
|
|
return iter()
|
|
})
|
|
case ast.GreaterThan.Name:
|
|
return p.planBinaryExpr(e, func(a, b ir.Local) error {
|
|
p.appendStmt(&ir.GreaterThanStmt{
|
|
A: a,
|
|
B: b,
|
|
})
|
|
return iter()
|
|
})
|
|
case ast.GreaterThanEq.Name:
|
|
return p.planBinaryExpr(e, func(a, b ir.Local) error {
|
|
p.appendStmt(&ir.GreaterThanEqualStmt{
|
|
A: a,
|
|
B: b,
|
|
})
|
|
return iter()
|
|
})
|
|
case ast.NotEqual.Name:
|
|
return p.planBinaryExpr(e, func(a, b ir.Local) error {
|
|
p.appendStmt(&ir.NotEqualStmt{
|
|
A: a,
|
|
B: b,
|
|
})
|
|
return iter()
|
|
})
|
|
default:
|
|
|
|
var relation bool
|
|
var name string
|
|
var arity int
|
|
var args []ir.Local
|
|
|
|
node := p.rules.Lookup(e.Operator())
|
|
|
|
if node != nil {
|
|
var err error
|
|
name, err = p.planRules(node.Rules())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
arity = node.Arity()
|
|
args = []ir.Local{
|
|
p.vars.GetOrEmpty(ast.InputRootDocument.Value.(ast.Var)),
|
|
p.vars.GetOrEmpty(ast.DefaultRootDocument.Value.(ast.Var)),
|
|
}
|
|
} else if decl, ok := p.decls[operator]; ok {
|
|
relation = decl.Relation
|
|
arity = len(decl.Decl.Args())
|
|
name = operator
|
|
p.externs[operator] = struct{}{}
|
|
} else {
|
|
return fmt.Errorf("illegal call: unknown operator %q", operator)
|
|
}
|
|
|
|
operands := e.Operands()
|
|
|
|
if len(operands) < arity || len(operands) > arity+1 {
|
|
return fmt.Errorf("illegal call: wrong number of operands: got %v, want %v)", len(operands), arity)
|
|
}
|
|
|
|
if relation {
|
|
return p.planExprCallRelation(name, arity, operands, args, iter)
|
|
}
|
|
|
|
return p.planExprCallFunc(name, arity, operands, args, iter)
|
|
}
|
|
}
|
|
|
|
func (p *Planner) planExprCallRelation(name string, arity int, operands []*ast.Term, args []ir.Local, iter planiter) error {
|
|
|
|
if len(operands) == arity {
|
|
return p.planCallArgs(operands, 0, args, func(args []ir.Local) error {
|
|
p.ltarget = p.newLocal()
|
|
p.appendStmt(&ir.CallStmt{
|
|
Func: name,
|
|
Args: args,
|
|
Result: p.ltarget,
|
|
})
|
|
|
|
lsize := p.newLocal()
|
|
|
|
p.appendStmt(&ir.LenStmt{
|
|
Source: p.ltarget,
|
|
Target: lsize,
|
|
})
|
|
|
|
lzero := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeNumberIntStmt{
|
|
Value: 0,
|
|
Target: lzero,
|
|
})
|
|
|
|
p.appendStmt(&ir.NotEqualStmt{
|
|
A: lsize,
|
|
B: lzero,
|
|
})
|
|
|
|
return iter()
|
|
})
|
|
}
|
|
|
|
return p.planCallArgs(operands[:len(operands)-1], 0, args, func(args []ir.Local) error {
|
|
|
|
p.ltarget = p.newLocal()
|
|
|
|
p.appendStmt(&ir.CallStmt{
|
|
Func: name,
|
|
Args: args,
|
|
Result: p.ltarget,
|
|
})
|
|
|
|
return p.planScanValues(operands[len(operands)-1], func(ir.Local) error {
|
|
return iter()
|
|
})
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planExprCallFunc(name string, arity int, operands []*ast.Term, args []ir.Local, iter planiter) error {
|
|
|
|
if len(operands) == arity {
|
|
// definition: f(x) = y { ... }
|
|
// call: f(x) # result not captured
|
|
return p.planCallArgs(operands, 0, args, func(args []ir.Local) error {
|
|
p.ltarget = p.newLocal()
|
|
p.appendStmt(&ir.CallStmt{
|
|
Func: name,
|
|
Args: args,
|
|
Result: p.ltarget,
|
|
})
|
|
|
|
falsy := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeBooleanStmt{
|
|
Value: false,
|
|
Target: falsy,
|
|
})
|
|
|
|
p.appendStmt(&ir.NotEqualStmt{
|
|
A: p.ltarget,
|
|
B: falsy,
|
|
})
|
|
|
|
return iter()
|
|
})
|
|
}
|
|
|
|
// definition: f(x) = y { ... }
|
|
// call: f(x, 1) # caller captures result
|
|
return p.planCallArgs(operands[:len(operands)-1], 0, args, func(args []ir.Local) error {
|
|
result := p.newLocal()
|
|
p.appendStmt(&ir.CallStmt{
|
|
Func: name,
|
|
Args: args,
|
|
Result: result,
|
|
})
|
|
return p.planUnifyLocal(result, operands[len(operands)-1], iter)
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planCallArgs(terms []*ast.Term, idx int, args []ir.Local, iter func([]ir.Local) error) error {
|
|
if idx >= len(terms) {
|
|
return iter(args)
|
|
}
|
|
return p.planTerm(terms[idx], func() error {
|
|
args = append(args, p.ltarget)
|
|
return p.planCallArgs(terms, idx+1, args, iter)
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planUnify(a, b *ast.Term, iter planiter) error {
|
|
|
|
switch va := a.Value.(type) {
|
|
case ast.Null, ast.Boolean, ast.Number, ast.String, ast.Ref, ast.Set, *ast.SetComprehension, *ast.ArrayComprehension, *ast.ObjectComprehension:
|
|
return p.planTerm(a, func() error {
|
|
return p.planUnifyLocal(p.ltarget, b, iter)
|
|
})
|
|
case ast.Var:
|
|
return p.planUnifyVar(va, b, iter)
|
|
case *ast.Array:
|
|
switch vb := b.Value.(type) {
|
|
case ast.Var:
|
|
return p.planUnifyVar(vb, a, iter)
|
|
case ast.Ref:
|
|
return p.planTerm(b, func() error {
|
|
return p.planUnifyLocalArray(p.ltarget, va, iter)
|
|
})
|
|
case *ast.Array:
|
|
if va.Len() == vb.Len() {
|
|
return p.planUnifyArraysRec(va, vb, 0, iter)
|
|
}
|
|
return nil
|
|
}
|
|
case ast.Object:
|
|
switch vb := b.Value.(type) {
|
|
case ast.Var:
|
|
return p.planUnifyVar(vb, a, iter)
|
|
case ast.Ref:
|
|
return p.planTerm(b, func() error {
|
|
return p.planUnifyLocalObject(p.ltarget, va, iter)
|
|
})
|
|
case ast.Object:
|
|
if va.Len() == vb.Len() {
|
|
return p.planUnifyObjectsRec(va, vb, va.Keys(), 0, iter)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("not implemented: unify(%v, %v)", a, b)
|
|
}
|
|
|
|
func (p *Planner) planUnifyVar(a ast.Var, b *ast.Term, iter planiter) error {
|
|
|
|
if la, ok := p.vars.Get(a); ok {
|
|
return p.planUnifyLocal(la, b, iter)
|
|
}
|
|
|
|
return p.planTerm(b, func() error {
|
|
target := p.newLocal()
|
|
p.vars.Put(a, target)
|
|
p.appendStmt(&ir.AssignVarStmt{
|
|
Source: p.ltarget,
|
|
Target: target,
|
|
})
|
|
return iter()
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planUnifyLocal(a ir.Local, b *ast.Term, iter planiter) error {
|
|
switch vb := b.Value.(type) {
|
|
case ast.Null, ast.Boolean, ast.Number, ast.String, ast.Ref, ast.Set, *ast.SetComprehension, *ast.ArrayComprehension, *ast.ObjectComprehension:
|
|
return p.planTerm(b, func() error {
|
|
p.appendStmt(&ir.EqualStmt{
|
|
A: a,
|
|
B: p.ltarget,
|
|
})
|
|
return iter()
|
|
})
|
|
case ast.Var:
|
|
if lv, ok := p.vars.Get(vb); ok {
|
|
p.appendStmt(&ir.EqualStmt{
|
|
A: a,
|
|
B: lv,
|
|
})
|
|
return iter()
|
|
}
|
|
lv := p.newLocal()
|
|
p.vars.Put(vb, lv)
|
|
p.appendStmt(&ir.AssignVarStmt{
|
|
Source: a,
|
|
Target: lv,
|
|
})
|
|
return iter()
|
|
case *ast.Array:
|
|
return p.planUnifyLocalArray(a, vb, iter)
|
|
case ast.Object:
|
|
return p.planUnifyLocalObject(a, vb, iter)
|
|
}
|
|
|
|
return fmt.Errorf("not implemented: unifyLocal(%v, %v)", a, b)
|
|
}
|
|
|
|
func (p *Planner) planUnifyLocalArray(a ir.Local, b *ast.Array, iter planiter) error {
|
|
p.appendStmt(&ir.IsArrayStmt{
|
|
Source: a,
|
|
})
|
|
|
|
blen := p.newLocal()
|
|
alen := p.newLocal()
|
|
|
|
p.appendStmt(&ir.LenStmt{
|
|
Source: a,
|
|
Target: alen,
|
|
})
|
|
|
|
p.appendStmt(&ir.MakeNumberIntStmt{
|
|
Value: int64(b.Len()),
|
|
Target: blen,
|
|
})
|
|
|
|
p.appendStmt(&ir.EqualStmt{
|
|
A: alen,
|
|
B: blen,
|
|
})
|
|
|
|
lkey := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeNumberIntStmt{
|
|
Target: lkey,
|
|
})
|
|
|
|
lval := p.newLocal()
|
|
|
|
return p.planUnifyLocalArrayRec(a, 0, b, lkey, lval, iter)
|
|
}
|
|
|
|
func (p *Planner) planUnifyLocalArrayRec(a ir.Local, index int, b *ast.Array, lkey, lval ir.Local, iter planiter) error {
|
|
if b.Len() == index {
|
|
return iter()
|
|
}
|
|
|
|
p.appendStmt(&ir.AssignIntStmt{
|
|
Value: int64(index),
|
|
Target: lkey,
|
|
})
|
|
|
|
p.appendStmt(&ir.DotStmt{
|
|
Source: a,
|
|
Key: lkey,
|
|
Target: lval,
|
|
})
|
|
|
|
return p.planUnifyLocal(lval, b.Elem(index), func() error {
|
|
return p.planUnifyLocalArrayRec(a, index+1, b, lkey, lval, iter)
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planUnifyLocalObject(a ir.Local, b ast.Object, iter planiter) error {
|
|
p.appendStmt(&ir.IsObjectStmt{
|
|
Source: a,
|
|
})
|
|
|
|
blen := p.newLocal()
|
|
alen := p.newLocal()
|
|
|
|
p.appendStmt(&ir.LenStmt{
|
|
Source: a,
|
|
Target: alen,
|
|
})
|
|
|
|
p.appendStmt(&ir.MakeNumberIntStmt{
|
|
Value: int64(b.Len()),
|
|
Target: blen,
|
|
})
|
|
|
|
p.appendStmt(&ir.EqualStmt{
|
|
A: alen,
|
|
B: blen,
|
|
})
|
|
|
|
lkey := p.newLocal()
|
|
lval := p.newLocal()
|
|
bkeys := b.Keys()
|
|
|
|
return p.planUnifyLocalObjectRec(a, 0, bkeys, b, lkey, lval, iter)
|
|
}
|
|
|
|
func (p *Planner) planUnifyLocalObjectRec(a ir.Local, index int, keys []*ast.Term, b ast.Object, lkey, lval ir.Local, iter planiter) error {
|
|
|
|
if index == len(keys) {
|
|
return iter()
|
|
}
|
|
|
|
return p.planTerm(keys[index], func() error {
|
|
p.appendStmt(&ir.AssignVarStmt{
|
|
Source: p.ltarget,
|
|
Target: lkey,
|
|
})
|
|
p.appendStmt(&ir.DotStmt{
|
|
Source: a,
|
|
Key: lkey,
|
|
Target: lval,
|
|
})
|
|
return p.planUnifyLocal(lval, b.Get(keys[index]), func() error {
|
|
return p.planUnifyLocalObjectRec(a, index+1, keys, b, lkey, lval, iter)
|
|
})
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planUnifyArraysRec(a, b *ast.Array, index int, iter planiter) error {
|
|
if index == a.Len() {
|
|
return iter()
|
|
}
|
|
return p.planUnify(a.Elem(index), b.Elem(index), func() error {
|
|
return p.planUnifyArraysRec(a, b, index+1, iter)
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planUnifyObjectsRec(a, b ast.Object, keys []*ast.Term, index int, iter planiter) error {
|
|
if index == len(keys) {
|
|
return iter()
|
|
}
|
|
|
|
aval := a.Get(keys[index])
|
|
bval := b.Get(keys[index])
|
|
if aval == nil || bval == nil {
|
|
return nil
|
|
}
|
|
|
|
return p.planUnify(aval, bval, func() error {
|
|
return p.planUnifyObjectsRec(a, b, keys, index+1, iter)
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planBinaryExpr(e *ast.Expr, iter binaryiter) error {
|
|
return p.planTerm(e.Operand(0), func() error {
|
|
a := p.ltarget
|
|
return p.planTerm(e.Operand(1), func() error {
|
|
b := p.ltarget
|
|
return iter(a, b)
|
|
})
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planTerm(t *ast.Term, iter planiter) error {
|
|
|
|
switch v := t.Value.(type) {
|
|
case ast.Null:
|
|
return p.planNull(v, iter)
|
|
case ast.Boolean:
|
|
return p.planBoolean(v, iter)
|
|
case ast.Number:
|
|
return p.planNumber(v, iter)
|
|
case ast.String:
|
|
return p.planString(v, iter)
|
|
case ast.Var:
|
|
return p.planVar(v, iter)
|
|
case ast.Ref:
|
|
return p.planRef(v, iter)
|
|
case *ast.Array:
|
|
return p.planArray(v, iter)
|
|
case ast.Object:
|
|
return p.planObject(v, iter)
|
|
case ast.Set:
|
|
return p.planSet(v, iter)
|
|
case *ast.SetComprehension:
|
|
p.loc = t.Loc()
|
|
return p.planSetComprehension(v, iter)
|
|
case *ast.ArrayComprehension:
|
|
p.loc = t.Loc()
|
|
return p.planArrayComprehension(v, iter)
|
|
case *ast.ObjectComprehension:
|
|
p.loc = t.Loc()
|
|
return p.planObjectComprehension(v, iter)
|
|
default:
|
|
return fmt.Errorf("%v term not implemented", ast.TypeName(v))
|
|
}
|
|
}
|
|
|
|
func (p *Planner) planNull(null ast.Null, iter planiter) error {
|
|
|
|
target := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeNullStmt{
|
|
Target: target,
|
|
})
|
|
|
|
p.ltarget = target
|
|
|
|
return iter()
|
|
}
|
|
|
|
func (p *Planner) planBoolean(b ast.Boolean, iter planiter) error {
|
|
|
|
target := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeBooleanStmt{
|
|
Value: bool(b),
|
|
Target: target,
|
|
})
|
|
|
|
p.ltarget = target
|
|
|
|
return iter()
|
|
}
|
|
|
|
func (p *Planner) planNumber(num ast.Number, iter planiter) error {
|
|
|
|
index := p.getStringConst(string(num))
|
|
target := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeNumberRefStmt{
|
|
Index: index,
|
|
Target: target,
|
|
})
|
|
|
|
p.ltarget = target
|
|
return iter()
|
|
}
|
|
|
|
func (p *Planner) planNumberFloat(f float64, iter planiter) error {
|
|
|
|
target := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeNumberFloatStmt{
|
|
Value: f,
|
|
Target: target,
|
|
})
|
|
|
|
p.ltarget = target
|
|
|
|
return iter()
|
|
}
|
|
|
|
func (p *Planner) planNumberInt(i int64, iter planiter) error {
|
|
|
|
target := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeNumberIntStmt{
|
|
Value: i,
|
|
Target: target,
|
|
})
|
|
|
|
p.ltarget = target
|
|
|
|
return iter()
|
|
}
|
|
|
|
func (p *Planner) planString(str ast.String, iter planiter) error {
|
|
|
|
index := p.getStringConst(string(str))
|
|
target := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeStringStmt{
|
|
Index: index,
|
|
Target: target,
|
|
})
|
|
|
|
p.ltarget = target
|
|
|
|
return iter()
|
|
}
|
|
|
|
func (p *Planner) planVar(v ast.Var, iter planiter) error {
|
|
p.ltarget = p.vars.GetOrElse(v, func() ir.Local {
|
|
return p.newLocal()
|
|
})
|
|
return iter()
|
|
}
|
|
|
|
func (p *Planner) planArray(arr *ast.Array, iter planiter) error {
|
|
|
|
larr := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeArrayStmt{
|
|
Capacity: int32(arr.Len()),
|
|
Target: larr,
|
|
})
|
|
|
|
return p.planArrayRec(arr, 0, larr, iter)
|
|
}
|
|
|
|
func (p *Planner) planArrayRec(arr *ast.Array, index int, larr ir.Local, iter planiter) error {
|
|
if index == arr.Len() {
|
|
p.ltarget = larr
|
|
return iter()
|
|
}
|
|
|
|
return p.planTerm(arr.Elem(index), func() error {
|
|
|
|
p.appendStmt(&ir.ArrayAppendStmt{
|
|
Value: p.ltarget,
|
|
Array: larr,
|
|
})
|
|
|
|
return p.planArrayRec(arr, index+1, larr, iter)
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planObject(obj ast.Object, iter planiter) error {
|
|
|
|
lobj := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeObjectStmt{
|
|
Target: lobj,
|
|
})
|
|
|
|
return p.planObjectRec(obj, 0, obj.Keys(), lobj, iter)
|
|
}
|
|
|
|
func (p *Planner) planObjectRec(obj ast.Object, index int, keys []*ast.Term, lobj ir.Local, iter planiter) error {
|
|
if index == len(keys) {
|
|
p.ltarget = lobj
|
|
return iter()
|
|
}
|
|
|
|
return p.planTerm(keys[index], func() error {
|
|
lkey := p.ltarget
|
|
|
|
return p.planTerm(obj.Get(keys[index]), func() error {
|
|
lval := p.ltarget
|
|
p.appendStmt(&ir.ObjectInsertStmt{
|
|
Key: lkey,
|
|
Value: lval,
|
|
Object: lobj,
|
|
})
|
|
|
|
return p.planObjectRec(obj, index+1, keys, lobj, iter)
|
|
})
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planSet(set ast.Set, iter planiter) error {
|
|
lset := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeSetStmt{
|
|
Target: lset,
|
|
})
|
|
|
|
return p.planSetRec(set, 0, set.Slice(), lset, iter)
|
|
}
|
|
|
|
func (p *Planner) planSetRec(set ast.Set, index int, elems []*ast.Term, lset ir.Local, iter planiter) error {
|
|
if index == len(elems) {
|
|
p.ltarget = lset
|
|
return iter()
|
|
}
|
|
|
|
return p.planTerm(elems[index], func() error {
|
|
p.appendStmt(&ir.SetAddStmt{
|
|
Value: p.ltarget,
|
|
Set: lset,
|
|
})
|
|
return p.planSetRec(set, index+1, elems, lset, iter)
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planSetComprehension(sc *ast.SetComprehension, iter planiter) error {
|
|
|
|
lset := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeSetStmt{
|
|
Target: lset,
|
|
})
|
|
|
|
return p.planComprehension(sc.Body, func() error {
|
|
return p.planTerm(sc.Term, func() error {
|
|
p.appendStmt(&ir.SetAddStmt{
|
|
Value: p.ltarget,
|
|
Set: lset,
|
|
})
|
|
return nil
|
|
})
|
|
}, lset, iter)
|
|
}
|
|
|
|
func (p *Planner) planArrayComprehension(ac *ast.ArrayComprehension, iter planiter) error {
|
|
|
|
larr := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeArrayStmt{
|
|
Target: larr,
|
|
})
|
|
|
|
return p.planComprehension(ac.Body, func() error {
|
|
return p.planTerm(ac.Term, func() error {
|
|
p.appendStmt(&ir.ArrayAppendStmt{
|
|
Value: p.ltarget,
|
|
Array: larr,
|
|
})
|
|
return nil
|
|
})
|
|
}, larr, iter)
|
|
}
|
|
|
|
func (p *Planner) planObjectComprehension(oc *ast.ObjectComprehension, iter planiter) error {
|
|
|
|
lobj := p.newLocal()
|
|
|
|
p.appendStmt(&ir.MakeObjectStmt{
|
|
Target: lobj,
|
|
})
|
|
return p.planComprehension(oc.Body, func() error {
|
|
return p.planTerm(oc.Key, func() error {
|
|
lkey := p.ltarget
|
|
return p.planTerm(oc.Value, func() error {
|
|
p.appendStmt(&ir.ObjectInsertOnceStmt{
|
|
Key: lkey,
|
|
Value: p.ltarget,
|
|
Object: lobj,
|
|
})
|
|
return nil
|
|
})
|
|
})
|
|
}, lobj, iter)
|
|
}
|
|
|
|
func (p *Planner) planComprehension(body ast.Body, closureIter planiter, target ir.Local, iter planiter) error {
|
|
|
|
prev := p.curr
|
|
p.curr = &ir.Block{}
|
|
ploc := p.loc
|
|
|
|
if err := p.planQuery(body, 0, func() error {
|
|
return closureIter()
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
block := p.curr
|
|
p.curr = prev
|
|
p.loc = ploc
|
|
|
|
p.appendStmt(&ir.BlockStmt{
|
|
Blocks: []*ir.Block{
|
|
block,
|
|
},
|
|
})
|
|
|
|
p.ltarget = target
|
|
return iter()
|
|
}
|
|
|
|
func (p *Planner) planRef(ref ast.Ref, iter planiter) error {
|
|
|
|
head, ok := ref[0].Value.(ast.Var)
|
|
if !ok {
|
|
return fmt.Errorf("illegal ref: non-var head")
|
|
}
|
|
|
|
if head.Compare(ast.DefaultRootDocument.Value) == 0 {
|
|
virtual := p.rules.Get(ref[0].Value)
|
|
base := &baseptr{local: p.vars.GetOrEmpty(ast.DefaultRootDocument.Value.(ast.Var))}
|
|
return p.planRefData(virtual, base, ref, 1, iter)
|
|
}
|
|
|
|
p.ltarget, ok = p.vars.Get(head)
|
|
if !ok {
|
|
return fmt.Errorf("illegal ref: unsafe head")
|
|
}
|
|
|
|
return p.planRefRec(ref, 1, iter)
|
|
}
|
|
|
|
func (p *Planner) planRefRec(ref ast.Ref, index int, iter planiter) error {
|
|
|
|
if len(ref) == index {
|
|
return iter()
|
|
}
|
|
|
|
scan := false
|
|
|
|
ast.WalkVars(ref[index], func(v ast.Var) bool {
|
|
if !scan {
|
|
_, exists := p.vars.Get(v)
|
|
if !exists {
|
|
scan = true
|
|
}
|
|
}
|
|
return scan
|
|
})
|
|
|
|
if !scan {
|
|
return p.planDot(ref[index], func() error {
|
|
return p.planRefRec(ref, index+1, iter)
|
|
})
|
|
}
|
|
|
|
return p.planScan(ref[index], func(ir.Local) error {
|
|
return p.planRefRec(ref, index+1, iter)
|
|
})
|
|
}
|
|
|
|
type baseptr struct {
|
|
local ir.Local
|
|
path ast.Ref
|
|
}
|
|
|
|
// planRefData implements the virtual document model by generating the value of
|
|
// the ref parameter and invoking the iterator with the planner target set to
|
|
// the virtual document and all variables in the reference assigned.
|
|
func (p *Planner) planRefData(virtual *ruletrie, base *baseptr, ref ast.Ref, index int, iter planiter) error {
|
|
|
|
// Early-exit if the end of the reference has been reached. In this case the
|
|
// plan has to materialize the full extent of the referenced value.
|
|
if index >= len(ref) {
|
|
return p.planRefDataExtent(virtual, base, iter)
|
|
}
|
|
|
|
// On the first iteration, we check if this can be optimized using a
|
|
// CallDynamicStatement
|
|
// NOTE(sr): we do it on the first index because later on, the recursion
|
|
// on subtrees of virtual already lost parts of the path we've taken.
|
|
if index == 1 {
|
|
rulesets, stmts, locals, index, optimize := p.optimizeLookup(virtual, ref)
|
|
if optimize {
|
|
// plan rules
|
|
for _, rules := range rulesets {
|
|
if _, err := p.planRules(rules); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// plan MakeStringStmts needed for ground ref[j]
|
|
for _, stmt := range stmts {
|
|
p.appendStmt(stmt)
|
|
}
|
|
|
|
p.ltarget = p.newLocal()
|
|
p.appendStmt(&ir.CallDynamicStmt{
|
|
Args: []ir.Local{
|
|
p.vars.GetOrEmpty(ast.InputRootDocument.Value.(ast.Var)),
|
|
p.vars.GetOrEmpty(ast.DefaultRootDocument.Value.(ast.Var)),
|
|
},
|
|
Path: locals,
|
|
Result: p.ltarget,
|
|
})
|
|
return p.planRefRec(ref, index+1, iter)
|
|
}
|
|
}
|
|
|
|
// If the reference operand is ground then either continue to the next
|
|
// operand or invoke the function for the rule referred to by this operand.
|
|
if ref[index].IsGround() {
|
|
|
|
var vchild *ruletrie
|
|
|
|
if virtual != nil {
|
|
vchild = virtual.Get(ref[index].Value)
|
|
}
|
|
|
|
rules := vchild.Rules()
|
|
|
|
if len(rules) > 0 {
|
|
p.ltarget = p.newLocal()
|
|
|
|
funcName, err := p.planRules(rules)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p.appendStmt(&ir.CallStmt{
|
|
Func: funcName,
|
|
Args: []ir.Local{
|
|
p.vars.GetOrEmpty(ast.InputRootDocument.Value.(ast.Var)),
|
|
p.vars.GetOrEmpty(ast.DefaultRootDocument.Value.(ast.Var)),
|
|
},
|
|
Result: p.ltarget,
|
|
})
|
|
|
|
return p.planRefRec(ref, index+1, iter)
|
|
}
|
|
|
|
bchild := *base
|
|
bchild.path = append(bchild.path, ref[index])
|
|
|
|
return p.planRefData(vchild, &bchild, ref, index+1, iter)
|
|
}
|
|
|
|
exclude := ast.NewSet()
|
|
|
|
// The planner does not support dynamic dispatch so generate blocks to
|
|
// evaluate each of the rulesets on the child nodes.
|
|
if virtual != nil {
|
|
|
|
stmt := &ir.BlockStmt{}
|
|
|
|
for _, child := range virtual.Children() {
|
|
|
|
block := &ir.Block{}
|
|
prev := p.curr
|
|
p.curr = block
|
|
key := ast.NewTerm(child)
|
|
exclude.Add(key)
|
|
|
|
// Assignments in each block due to local unification must be undone
|
|
// so create a new frame that will be popped after this key is
|
|
// processed.
|
|
p.vars.Push(map[ast.Var]ir.Local{})
|
|
|
|
if err := p.planTerm(key, func() error {
|
|
return p.planUnifyLocal(p.ltarget, ref[index], func() error {
|
|
// Create a copy of the reference with this operand plugged.
|
|
// This will result in evaluation of the rulesets on the
|
|
// child node.
|
|
cpy := ref.Copy()
|
|
cpy[index] = key
|
|
return p.planRefData(virtual, base, cpy, index, iter)
|
|
})
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
p.vars.Pop()
|
|
p.curr = prev
|
|
stmt.Blocks = append(stmt.Blocks, block)
|
|
}
|
|
|
|
p.appendStmt(stmt)
|
|
}
|
|
|
|
// If the virtual tree was enumerated then we do not want to enumerate base
|
|
// trees that are rooted at the same key as any of the virtual sub trees. To
|
|
// prevent this we build a set of keys that are to be excluded and check
|
|
// below during the base scan.
|
|
var lexclude *ir.Local
|
|
|
|
if exclude.Len() > 0 {
|
|
if err := p.planSet(exclude, func() error {
|
|
v := p.ltarget
|
|
lexclude = &v
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
p.ltarget = base.local
|
|
|
|
// Perform a scan of the base documents starting from the location referred
|
|
// to by the data pointer. Use the set we built above to avoid revisiting
|
|
// sub trees.
|
|
return p.planRefRec(base.path, 0, func() error {
|
|
return p.planScan(ref[index], func(lkey ir.Local) error {
|
|
if lexclude != nil {
|
|
lignore := p.newLocal()
|
|
p.appendStmt(&ir.NotStmt{
|
|
Block: p.blockWithStmt(&ir.DotStmt{
|
|
Source: *lexclude,
|
|
Key: lkey,
|
|
Target: lignore,
|
|
})})
|
|
}
|
|
|
|
// Assume that virtual sub trees have been visited already so
|
|
// recurse without the virtual node.
|
|
return p.planRefData(nil, &baseptr{local: p.ltarget}, ref, index+1, iter)
|
|
})
|
|
})
|
|
}
|
|
|
|
// planRefDataExtent generates the full extent (combined) of the base and
|
|
// virtual nodes and then invokes the iterator with the planner target set to
|
|
// the full extent.
|
|
func (p *Planner) planRefDataExtent(virtual *ruletrie, base *baseptr, iter planiter) error {
|
|
|
|
vtarget := p.newLocal()
|
|
|
|
// Generate the virtual document out of rules contained under the virtual
|
|
// node (recursively). This document will _ONLY_ contain values generated by
|
|
// rules. No base document values will be included.
|
|
if virtual != nil {
|
|
|
|
p.appendStmt(&ir.MakeObjectStmt{
|
|
Target: vtarget,
|
|
})
|
|
|
|
for _, key := range virtual.Children() {
|
|
child := virtual.Get(key)
|
|
|
|
// Skip functions.
|
|
if child.Arity() > 0 {
|
|
continue
|
|
}
|
|
|
|
lkey := p.newLocal()
|
|
idx := p.getStringConst(string(key.(ast.String)))
|
|
p.appendStmt(&ir.MakeStringStmt{
|
|
Index: idx,
|
|
Target: lkey,
|
|
})
|
|
|
|
rules := child.Rules()
|
|
|
|
// Build object hierarchy depth-first.
|
|
if len(rules) == 0 {
|
|
err := p.planRefDataExtent(child, nil, func() error {
|
|
p.appendStmt(&ir.ObjectInsertStmt{
|
|
Object: vtarget,
|
|
Key: lkey,
|
|
Value: p.ltarget,
|
|
})
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Generate virtual document for leaf.
|
|
lvalue := p.newLocal()
|
|
|
|
funcName, err := p.planRules(rules)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add leaf to object if defined.
|
|
b := &ir.Block{}
|
|
p.appendStmtToBlock(&ir.CallStmt{
|
|
Func: funcName,
|
|
Args: []ir.Local{
|
|
p.vars.GetOrEmpty(ast.InputRootDocument.Value.(ast.Var)),
|
|
p.vars.GetOrEmpty(ast.DefaultRootDocument.Value.(ast.Var)),
|
|
},
|
|
Result: lvalue,
|
|
}, b)
|
|
p.appendStmtToBlock(&ir.ObjectInsertStmt{
|
|
Object: vtarget,
|
|
Key: lkey,
|
|
Value: lvalue,
|
|
}, b)
|
|
p.appendStmt(&ir.BlockStmt{Blocks: []*ir.Block{b}})
|
|
}
|
|
|
|
// At this point vtarget refers to the full extent of the virtual
|
|
// document at ref. If the base pointer is unset, no further processing
|
|
// is required.
|
|
if base == nil {
|
|
p.ltarget = vtarget
|
|
return iter()
|
|
}
|
|
}
|
|
|
|
// Obtain the base document value and merge (recursively) with the virtual
|
|
// document value above if needed.
|
|
prev := p.curr
|
|
p.curr = &ir.Block{}
|
|
p.ltarget = base.local
|
|
target := p.newLocal()
|
|
|
|
err := p.planRefRec(base.path, 0, func() error {
|
|
|
|
if virtual == nil {
|
|
target = p.ltarget
|
|
} else {
|
|
stmt := &ir.ObjectMergeStmt{
|
|
A: p.ltarget,
|
|
B: vtarget,
|
|
Target: target,
|
|
}
|
|
p.appendStmt(stmt)
|
|
}
|
|
|
|
p.appendStmt(&ir.BreakStmt{Index: 1})
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
inner := p.curr
|
|
|
|
// Fallback to virtual document value if base document is undefined.
|
|
// Otherwise, this block is undefined.
|
|
p.curr = &ir.Block{}
|
|
p.appendStmt(&ir.BlockStmt{Blocks: []*ir.Block{inner}})
|
|
|
|
if virtual != nil {
|
|
p.appendStmt(&ir.AssignVarStmt{
|
|
Source: vtarget,
|
|
Target: target,
|
|
})
|
|
} else {
|
|
p.appendStmt(&ir.BreakStmt{Index: 1})
|
|
}
|
|
|
|
outer := p.curr
|
|
p.curr = prev
|
|
p.appendStmt(&ir.BlockStmt{Blocks: []*ir.Block{outer}})
|
|
|
|
// At this point, target refers to either the full extent of the base and
|
|
// virtual documents at ref or just the base document at ref.
|
|
p.ltarget = target
|
|
|
|
return iter()
|
|
}
|
|
|
|
func (p *Planner) planDot(key *ast.Term, iter planiter) error {
|
|
|
|
source := p.ltarget
|
|
|
|
return p.planTerm(key, func() error {
|
|
|
|
target := p.newLocal()
|
|
|
|
p.appendStmt(&ir.DotStmt{
|
|
Source: source,
|
|
Key: p.ltarget,
|
|
Target: target,
|
|
})
|
|
|
|
p.ltarget = target
|
|
|
|
return iter()
|
|
})
|
|
}
|
|
|
|
type scaniter func(ir.Local) error
|
|
|
|
func (p *Planner) planScan(key *ast.Term, iter scaniter) error {
|
|
|
|
scan := &ir.ScanStmt{
|
|
Source: p.ltarget,
|
|
Key: p.newLocal(),
|
|
Value: p.newLocal(),
|
|
Block: &ir.Block{},
|
|
}
|
|
|
|
prev := p.curr
|
|
p.curr = scan.Block
|
|
|
|
if err := p.planUnifyLocal(scan.Key, key, func() error {
|
|
p.ltarget = scan.Value
|
|
return iter(scan.Key)
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
p.curr = prev
|
|
p.appendStmt(scan)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
func (p *Planner) planScanValues(val *ast.Term, iter scaniter) error {
|
|
|
|
scan := &ir.ScanStmt{
|
|
Source: p.ltarget,
|
|
Key: p.newLocal(),
|
|
Value: p.newLocal(),
|
|
Block: &ir.Block{},
|
|
}
|
|
|
|
prev := p.curr
|
|
p.curr = scan.Block
|
|
|
|
if err := p.planUnifyLocal(scan.Value, val, func() error {
|
|
p.ltarget = scan.Value
|
|
return iter(scan.Value)
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
p.curr = prev
|
|
p.appendStmt(scan)
|
|
|
|
return nil
|
|
}
|
|
|
|
// planSaveLocals returns a slice of locals holding temporary variables that
|
|
// have been assigned from the supplied vars.
|
|
func (p *Planner) planSaveLocals(vars ...ir.Local) []ir.Local {
|
|
|
|
lsaved := make([]ir.Local, len(vars))
|
|
|
|
for i := range vars {
|
|
|
|
lsaved[i] = p.newLocal()
|
|
|
|
p.appendStmt(&ir.AssignVarStmt{
|
|
Source: vars[i],
|
|
Target: lsaved[i],
|
|
})
|
|
}
|
|
|
|
return lsaved
|
|
}
|
|
|
|
type termsliceiter func([]ir.Local) error
|
|
|
|
func (p *Planner) planTermSlice(terms []*ast.Term, iter termsliceiter) error {
|
|
return p.planTermSliceRec(terms, make([]ir.Local, len(terms)), 0, iter)
|
|
}
|
|
|
|
func (p *Planner) planTermSliceRec(terms []*ast.Term, locals []ir.Local, index int, iter termsliceiter) error {
|
|
if index >= len(terms) {
|
|
return iter(locals)
|
|
}
|
|
|
|
return p.planTerm(terms[index], func() error {
|
|
locals[index] = p.ltarget
|
|
return p.planTermSliceRec(terms, locals, index+1, iter)
|
|
})
|
|
}
|
|
|
|
func (p *Planner) planExterns() error {
|
|
|
|
p.policy.Static.BuiltinFuncs = make([]*ir.BuiltinFunc, 0, len(p.externs))
|
|
|
|
for name := range p.externs {
|
|
p.policy.Static.BuiltinFuncs = append(p.policy.Static.BuiltinFuncs, &ir.BuiltinFunc{Name: name})
|
|
}
|
|
|
|
sort.Slice(p.policy.Static.BuiltinFuncs, func(i, j int) bool {
|
|
return p.policy.Static.BuiltinFuncs[i].Name < p.policy.Static.BuiltinFuncs[j].Name
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Planner) getStringConst(s string) int {
|
|
index, ok := p.strings[s]
|
|
if !ok {
|
|
index = len(p.policy.Static.Strings)
|
|
p.policy.Static.Strings = append(p.policy.Static.Strings, &ir.StringConst{
|
|
Value: s,
|
|
})
|
|
p.strings[s] = index
|
|
}
|
|
return index
|
|
}
|
|
|
|
func (p *Planner) getFileConst(s string) int {
|
|
index, ok := p.files[s]
|
|
if !ok {
|
|
index = len(p.policy.Static.Files)
|
|
p.policy.Static.Files = append(p.policy.Static.Files, &ir.StringConst{
|
|
Value: s,
|
|
})
|
|
p.files[s] = index
|
|
}
|
|
return index
|
|
}
|
|
|
|
func (p *Planner) appendStmt(s ir.Stmt) {
|
|
p.appendStmtToBlock(s, p.curr)
|
|
}
|
|
|
|
func (p *Planner) appendStmtToBlock(s ir.Stmt, b *ir.Block) {
|
|
if p.loc != nil {
|
|
str := p.loc.File
|
|
if str == "" {
|
|
str = `<query>`
|
|
}
|
|
s.SetLocation(p.getFileConst(str), p.loc.Row, p.loc.Col, str, string(p.loc.Text))
|
|
}
|
|
b.Stmts = append(b.Stmts, s)
|
|
}
|
|
|
|
func (p *Planner) blockWithStmt(s ir.Stmt) *ir.Block {
|
|
b := &ir.Block{}
|
|
p.appendStmtToBlock(s, b)
|
|
return b
|
|
}
|
|
|
|
func (p *Planner) appendBlock(b *ir.Block) {
|
|
p.plan.Blocks = append(p.plan.Blocks, b)
|
|
}
|
|
|
|
func (p *Planner) appendFunc(f *ir.Func) {
|
|
p.policy.Funcs.Funcs = append(p.policy.Funcs.Funcs, f)
|
|
}
|
|
|
|
func (p *Planner) newLocal() ir.Local {
|
|
x := p.lnext
|
|
p.lnext++
|
|
return x
|
|
}
|
|
|
|
func rewrittenVar(vars map[ast.Var]ast.Var, k ast.Var) ast.Var {
|
|
rw, ok := vars[k]
|
|
if !ok {
|
|
return k
|
|
}
|
|
return rw
|
|
}
|
|
|
|
// optimizeLookup returns a set of rulesets and required statements planning
|
|
// the locals (strings) needed with the used local variables, and the index
|
|
// into ref's parth that is still to be planned; if the passed ref's vars
|
|
// allow for optimization using CallDynamicStmt.
|
|
//
|
|
// It's possible iff
|
|
// - all vars in ref have been seen
|
|
// - all ground terms (strings) match some child key on their respective
|
|
// layer of the ruletrie
|
|
// - there are no child trees left (only rulesets) if we're done checking
|
|
// ref
|
|
//
|
|
// The last condition is necessary because we don't deal with _which key a
|
|
// var actually matched_ -- so we don't know which subtree to evaluate
|
|
// with the results.
|
|
func (p *Planner) optimizeLookup(t *ruletrie, ref ast.Ref) ([][]*ast.Rule, []ir.Stmt, []ir.Local, int, bool) {
|
|
dont := func(format string, args ...interface{}) ([][]*ast.Rule, []ir.Stmt, []ir.Local, int, bool) {
|
|
p.debug(fmt.Sprintf("no optimization of %s: "+format, append([]interface{}{ref}, args...)...))
|
|
return nil, nil, nil, 0, false
|
|
}
|
|
if t == nil {
|
|
return dont("trie is nil")
|
|
}
|
|
|
|
nodes := []*ruletrie{t}
|
|
opt := false
|
|
var index int
|
|
|
|
// ref[0] is data, ignore
|
|
for i := 1; i < len(ref); i++ {
|
|
index = i
|
|
r := ref[i]
|
|
var nextNodes []*ruletrie
|
|
|
|
switch r := r.Value.(type) {
|
|
case ast.Var:
|
|
// check if it's been "seen" before
|
|
_, ok := p.vars.Get(r)
|
|
if !ok {
|
|
return dont("ref[%d] is unseen var: %v", i, r)
|
|
}
|
|
opt = true
|
|
// take all children, they might match
|
|
for _, node := range nodes {
|
|
for _, child := range node.Children() {
|
|
if node := node.Get(child); node != nil {
|
|
nextNodes = append(nextNodes, node)
|
|
}
|
|
}
|
|
}
|
|
case ast.String:
|
|
// take matching children
|
|
for _, node := range nodes {
|
|
if node := node.Get(r); node != nil {
|
|
nextNodes = append(nextNodes, node)
|
|
}
|
|
}
|
|
default:
|
|
return dont("ref[%d] is type %T", i, r) // TODO(sr): think more about this
|
|
}
|
|
|
|
nodes = nextNodes
|
|
|
|
// if all nodes have 0 children, abort ref check and optimize
|
|
all := true
|
|
for _, node := range nodes {
|
|
all = all && len(node.Children()) == 0
|
|
}
|
|
if all {
|
|
break
|
|
}
|
|
}
|
|
|
|
var res [][]*ast.Rule
|
|
|
|
// if there hasn't been any var, we're not making things better by
|
|
// introducing CallDynamicStmt
|
|
if !opt {
|
|
return dont("no vars at all")
|
|
}
|
|
|
|
for _, node := range nodes {
|
|
// we're done with ref, check if there's only ruleset leaves; collect rules
|
|
if index == len(ref)-1 {
|
|
if len(node.Children()) > 0 {
|
|
return dont("unbalanced ruletrie")
|
|
}
|
|
}
|
|
if rules := node.Rules(); len(rules) > 0 {
|
|
res = append(res, rules)
|
|
}
|
|
}
|
|
if len(res) == 0 {
|
|
return dont("no rule leaves")
|
|
}
|
|
|
|
// If we've made it here, optimization is possible -- let's plan strings!
|
|
// NOTE(sr): this is a bit of a dirty side-effect; the code here assumes
|
|
// that the returned block and locals are going to be used. Technically,
|
|
// it might still be discarded, and we've planned a few strings we don't
|
|
// actually need.
|
|
var stmts []ir.Stmt
|
|
var locals []ir.Local
|
|
|
|
// plan generation
|
|
lvar := p.newLocal()
|
|
stmts = append(stmts, &ir.MakeStringStmt{
|
|
Index: p.getStringConst(fmt.Sprintf("g%d", p.funcs.gen)),
|
|
Target: lvar,
|
|
})
|
|
locals = append(locals, lvar)
|
|
|
|
for i := 1; i <= index; i++ {
|
|
switch r := ref[i].Value.(type) {
|
|
case ast.Var:
|
|
lv, ok := p.vars.Get(r)
|
|
if !ok {
|
|
return dont("ref[%d] not a seen var: %v", i, ref[i])
|
|
}
|
|
locals = append(locals, lv)
|
|
case ast.String:
|
|
// plan string
|
|
lvar := p.newLocal()
|
|
stmts = append(stmts, &ir.MakeStringStmt{
|
|
Index: p.getStringConst(string(r)),
|
|
Target: lvar,
|
|
})
|
|
locals = append(locals, lvar)
|
|
}
|
|
}
|
|
|
|
return res, stmts, locals, index, true
|
|
}
|
|
|
|
func (p *Planner) seenVar(ref *ast.Term) (bool, bool) {
|
|
if v, ok := ref.Value.(ast.Var); ok {
|
|
_, ok := p.vars.Get(v)
|
|
return true, ok
|
|
}
|
|
return false, false
|
|
}
|