mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
planner: Support and/or logical operators (#8827)
Fixes: #8681 --------- Signed-off-by: Johan Fylling <johan.dev@fylling.se>
This commit is contained in:
@@ -1,2 +1 @@
|
||||
# Exception Format is <test name>: <reason>
|
||||
"logic_op/**": "No planner support"
|
||||
# Exception Format is <test name>: <reason>
|
||||
@@ -212,6 +212,9 @@ func LoadIrExtendedTestCasesFiltered(filters ...Filters) ([]ExtendedSet, error)
|
||||
rego.Query(tc.Query),
|
||||
rego.SetRegoVersion(ast.RegoV1),
|
||||
}
|
||||
if tc.ExperimentalKeywords {
|
||||
opts = append(opts, rego.Capabilities(ast.CapabilitiesForThisVersion(ast.CapabilitiesExperimentalKeywords(true))))
|
||||
}
|
||||
for i := range tc.Modules {
|
||||
opts = append(opts, rego.Module(fmt.Sprintf("module-%d.rego", i), tc.Modules[i]))
|
||||
}
|
||||
|
||||
+141
-22
@@ -648,6 +648,12 @@ func (p *Planner) planExpr(e *ast.Expr, iter planiter) error {
|
||||
case e.IsNegated():
|
||||
return p.planNot(e, iter)
|
||||
|
||||
case e.IsAnd():
|
||||
return p.planExprLogicalAnd(e, iter)
|
||||
|
||||
case e.IsOr():
|
||||
return p.planExprLogicalOr(e, iter)
|
||||
|
||||
case e.IsCall():
|
||||
return p.planExprCall(e, iter)
|
||||
|
||||
@@ -659,34 +665,34 @@ func (p *Planner) planExpr(e *ast.Expr, iter planiter) error {
|
||||
}
|
||||
|
||||
func (p *Planner) planNot(e *ast.Expr, iter planiter) error {
|
||||
not := &ir.NotStmt{
|
||||
Block: &ir.Block{},
|
||||
}
|
||||
|
||||
prev := p.curr
|
||||
p.curr = not.Block
|
||||
|
||||
if n, ok := e.Terms.(*ast.Not); ok {
|
||||
cond := p.newLocal() // success condition
|
||||
// We're constructing the following plan:
|
||||
//
|
||||
// | not
|
||||
// | | <plan(body)> # assigns Local<cond> = true at each success point
|
||||
// | | is_defined &{Source:Local<cond>} # aborts inner block if body produced no success
|
||||
// | iter() # caller's continuation
|
||||
|
||||
err := p.planQuery(n.Body, 0, func() error {
|
||||
p.appendStmt(&ir.AssignVarStmt{
|
||||
Source: op(ir.Bool(true)),
|
||||
Target: cond,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
cond := p.newLocal()
|
||||
sub, err := p.planBodyAsScope(n.Body, cond)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.appendStmt(&ir.IsDefinedStmt{
|
||||
Source: cond,
|
||||
})
|
||||
} else {
|
||||
if err := p.planExpr(e.Complement(), func() error { return nil }); err != nil {
|
||||
return err
|
||||
}
|
||||
sub.Stmts = append(sub.Stmts, &ir.IsDefinedStmt{Source: cond})
|
||||
p.appendStmt(&ir.NotStmt{Block: sub})
|
||||
|
||||
return iter()
|
||||
}
|
||||
|
||||
// Legacy negation
|
||||
|
||||
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
|
||||
@@ -695,6 +701,119 @@ func (p *Planner) planNot(e *ast.Expr, iter planiter) error {
|
||||
return iter()
|
||||
}
|
||||
|
||||
func (p *Planner) planExprLogicalAnd(e *ast.Expr, iter planiter) error {
|
||||
// We're constructing the following plan:
|
||||
//
|
||||
// | reset &{Target:Local<cond>}
|
||||
// | block lhs
|
||||
// | | <plan(LHS body)>
|
||||
// | | assign_var &{Target:Local<cond>} # Local<cond> = true on success
|
||||
// | is_defined &{Source:Local<cond>} # aborts outer if LHS produced no success
|
||||
// | reset &{Target:Local<cond>} # clear before RHS
|
||||
// | block rhs
|
||||
// | | <plan(RHS body)>
|
||||
// | | assign_var &{Target:Local<cond>} # Local<cond> = true on success
|
||||
// | is_defined &{Source:Local<cond>} # aborts outer if RHS produced no success
|
||||
// | iter() # caller's continuation
|
||||
|
||||
and := e.Terms.(*ast.LogicalAnd)
|
||||
cond := p.newLocal() // success condition
|
||||
|
||||
if err := planLogicalOperand(p, and.Lhs, cond); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := planLogicalOperand(p, and.Rhs, cond); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return iter()
|
||||
}
|
||||
|
||||
func planLogicalOperand(p *Planner, body ast.Body, cond ir.Local) error {
|
||||
p.appendStmt(&ir.ResetLocalStmt{Target: cond})
|
||||
|
||||
sub, err := p.planBodyAsScope(body, cond)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.appendStmt(&ir.BlockStmt{Blocks: []*ir.Block{sub}})
|
||||
p.appendStmt(&ir.IsDefinedStmt{Source: cond})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Planner) planExprLogicalOr(e *ast.Expr, iter planiter) error {
|
||||
// We're constructing the following plan:
|
||||
//
|
||||
// | reset &{Target:Local<cond>}
|
||||
// | block lhs
|
||||
// | | <plan(LHS body)>
|
||||
// | | assign_var &{Target:Local<cond>} # Local<cond> = true on success
|
||||
// | block outer
|
||||
// | | block skip
|
||||
// | | | is_defined &{Source:Local<cond>} # if defined ..
|
||||
// | | | break &{Index:1} # .. break past RHS
|
||||
// | | block rhs
|
||||
// | | | <plan(RHS body)>
|
||||
// | | | assign_var &{Target:Local<cond>} # Local<cond> = true on success
|
||||
// | is_defined &{Source:Local<cond>} # aborts outer if neither produced a success
|
||||
// | iter() # caller's continuation
|
||||
|
||||
or := e.Terms.(*ast.LogicalOr)
|
||||
|
||||
cond := p.newLocal() // success condition
|
||||
p.appendStmt(&ir.ResetLocalStmt{Target: cond})
|
||||
|
||||
lhsBlock, err := p.planBodyAsScope(or.Lhs, cond)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.appendStmt(&ir.BlockStmt{Blocks: []*ir.Block{lhsBlock}})
|
||||
|
||||
rhsBlock, err := p.planBodyAsScope(or.Rhs, cond)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// skip-rhs-if-lhs-succeeded: if cond is defined, break out past the
|
||||
// RHS block; otherwise this inner block aborts and the outer block
|
||||
// falls through into the RHS plan.
|
||||
skip := &ir.Block{Stmts: []ir.Stmt{
|
||||
&ir.IsDefinedStmt{Source: cond},
|
||||
&ir.BreakStmt{Index: 1},
|
||||
}}
|
||||
outer := &ir.Block{Stmts: []ir.Stmt{
|
||||
&ir.BlockStmt{Blocks: []*ir.Block{skip}},
|
||||
&ir.BlockStmt{Blocks: []*ir.Block{rhsBlock}},
|
||||
}}
|
||||
p.appendStmt(&ir.BlockStmt{Blocks: []*ir.Block{outer}})
|
||||
|
||||
p.appendStmt(&ir.IsDefinedStmt{Source: cond})
|
||||
return iter()
|
||||
}
|
||||
|
||||
func (p *Planner) planBodyAsScope(body ast.Body, cond ir.Local) (*ir.Block, error) {
|
||||
sub := &ir.Block{}
|
||||
prev := p.curr
|
||||
p.curr = sub
|
||||
p.vars.Push(map[ast.Var]ir.Local{})
|
||||
|
||||
err := p.planQuery(body, 0, func() error {
|
||||
p.appendStmt(&ir.AssignVarStmt{
|
||||
Source: op(ir.Bool(true)),
|
||||
Target: cond,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
p.vars.Pop()
|
||||
p.curr = prev
|
||||
|
||||
return sub, err
|
||||
}
|
||||
|
||||
func (p *Planner) planWith(e *ast.Expr, iter planiter) error {
|
||||
|
||||
// Plan the values that will be applied by the `with` modifiers. All values
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
package planner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -735,8 +737,211 @@ a if {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleNamedQueries(t *testing.T) {
|
||||
func TestPlannerLogicalOps(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
module string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
note: "and",
|
||||
module: `
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p if {
|
||||
input.x > 0 and input.y > 0
|
||||
}
|
||||
`,
|
||||
want: `| *ir.Funcs Funcs (1 funcs)
|
||||
| | *ir.Func g0.data.test.p (2 params: [Local<0> Local<1>], 3 blocks, path: [g0 test p])
|
||||
| | | *ir.Block Block (8 statements)
|
||||
| | | | *ir.ResetLocalStmt &{Target:Local<3>}
|
||||
| | | | *ir.ResetLocalStmt &{Target:Local<4>}
|
||||
| | | | *ir.BlockStmt BlockStmt (1 blocks)
|
||||
| | | | | *ir.Block Block (5 statements)
|
||||
| | | | | | *ir.DotStmt &{Source:{Value:Local<0>} Key:{Value:String<0>} Target:Local<5>}
|
||||
| | | | | | *ir.MakeNumberRefStmt &{Index:1 Target:Local<6>}
|
||||
| | | | | | *ir.CallStmt &{Func:gt Args:[{Value:Local<5>} {Value:Local<6>}] Result:Local<7>}
|
||||
| | | | | | *ir.NotEqualStmt &{A:{Value:Local<7>} B:{Value:Bool<false>}}
|
||||
| | | | | | *ir.AssignVarStmt &{Source:{Value:Bool<true>} Target:Local<4>}
|
||||
| | | | *ir.IsDefinedStmt &{Source:Local<4>}
|
||||
| | | | *ir.ResetLocalStmt &{Target:Local<4>}
|
||||
| | | | *ir.BlockStmt BlockStmt (1 blocks)
|
||||
| | | | | *ir.Block Block (5 statements)
|
||||
| | | | | | *ir.DotStmt &{Source:{Value:Local<0>} Key:{Value:String<2>} Target:Local<8>}
|
||||
| | | | | | *ir.MakeNumberRefStmt &{Index:1 Target:Local<9>}
|
||||
| | | | | | *ir.CallStmt &{Func:gt Args:[{Value:Local<8>} {Value:Local<9>}] Result:Local<10>}
|
||||
| | | | | | *ir.NotEqualStmt &{A:{Value:Local<10>} B:{Value:Bool<false>}}
|
||||
| | | | | | *ir.AssignVarStmt &{Source:{Value:Bool<true>} Target:Local<4>}
|
||||
| | | | *ir.IsDefinedStmt &{Source:Local<4>}
|
||||
| | | | *ir.AssignVarOnceStmt &{Source:{Value:Bool<true>} Target:Local<3>}
|
||||
| | | *ir.Block Block (2 statements)
|
||||
| | | | *ir.IsDefinedStmt &{Source:Local<3>}
|
||||
| | | | *ir.AssignVarOnceStmt &{Source:{Value:Local<3>} Target:Local<2>}
|
||||
| | | *ir.Block Block (1 statements)
|
||||
| | | | *ir.ReturnLocalStmt &{Source:Local<2>}
|
||||
`,
|
||||
},
|
||||
{
|
||||
note: "or",
|
||||
module: `
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
p if {
|
||||
input.x > 0 or input.y > 0
|
||||
}
|
||||
`,
|
||||
want: `| *ir.Funcs Funcs (1 funcs)
|
||||
| | *ir.Func g0.data.test.p (2 params: [Local<0> Local<1>], 3 blocks, path: [g0 test p])
|
||||
| | | *ir.Block Block (6 statements)
|
||||
| | | | *ir.ResetLocalStmt &{Target:Local<3>}
|
||||
| | | | *ir.ResetLocalStmt &{Target:Local<4>}
|
||||
| | | | *ir.BlockStmt BlockStmt (1 blocks)
|
||||
| | | | | *ir.Block Block (5 statements)
|
||||
| | | | | | *ir.DotStmt &{Source:{Value:Local<0>} Key:{Value:String<0>} Target:Local<5>}
|
||||
| | | | | | *ir.MakeNumberRefStmt &{Index:1 Target:Local<6>}
|
||||
| | | | | | *ir.CallStmt &{Func:gt Args:[{Value:Local<5>} {Value:Local<6>}] Result:Local<7>}
|
||||
| | | | | | *ir.NotEqualStmt &{A:{Value:Local<7>} B:{Value:Bool<false>}}
|
||||
| | | | | | *ir.AssignVarStmt &{Source:{Value:Bool<true>} Target:Local<4>}
|
||||
| | | | *ir.BlockStmt BlockStmt (1 blocks)
|
||||
| | | | | *ir.Block Block (2 statements)
|
||||
| | | | | | *ir.BlockStmt BlockStmt (1 blocks)
|
||||
| | | | | | | *ir.Block Block (2 statements)
|
||||
| | | | | | | | *ir.IsDefinedStmt &{Source:Local<4>}
|
||||
| | | | | | | | *ir.BreakStmt &{Index:1}
|
||||
| | | | | | *ir.BlockStmt BlockStmt (1 blocks)
|
||||
| | | | | | | *ir.Block Block (5 statements)
|
||||
| | | | | | | | *ir.DotStmt &{Source:{Value:Local<0>} Key:{Value:String<2>} Target:Local<8>}
|
||||
| | | | | | | | *ir.MakeNumberRefStmt &{Index:1 Target:Local<9>}
|
||||
| | | | | | | | *ir.CallStmt &{Func:gt Args:[{Value:Local<8>} {Value:Local<9>}] Result:Local<10>}
|
||||
| | | | | | | | *ir.NotEqualStmt &{A:{Value:Local<10>} B:{Value:Bool<false>}}
|
||||
| | | | | | | | *ir.AssignVarStmt &{Source:{Value:Bool<true>} Target:Local<4>}
|
||||
| | | | *ir.IsDefinedStmt &{Source:Local<4>}
|
||||
| | | | *ir.AssignVarOnceStmt &{Source:{Value:Bool<true>} Target:Local<3>}
|
||||
| | | *ir.Block Block (2 statements)
|
||||
| | | | *ir.IsDefinedStmt &{Source:Local<3>}
|
||||
| | | | *ir.AssignVarOnceStmt &{Source:{Value:Local<3>} Target:Local<2>}
|
||||
| | | *ir.Block Block (1 statements)
|
||||
| | | | *ir.ReturnLocalStmt &{Source:Local<2>}
|
||||
`,
|
||||
},
|
||||
{
|
||||
note: "not",
|
||||
module: `
|
||||
package test
|
||||
import future.keywords.not
|
||||
|
||||
p if {
|
||||
not input.x > 0
|
||||
}
|
||||
`,
|
||||
want: `| *ir.Funcs Funcs (1 funcs)
|
||||
| | *ir.Func g0.data.test.p (2 params: [Local<0> Local<1>], 3 blocks, path: [g0 test p])
|
||||
| | | *ir.Block Block (3 statements)
|
||||
| | | | *ir.ResetLocalStmt &{Target:Local<3>}
|
||||
| | | | *ir.NotStmt &{Block:Block (6 statements)}
|
||||
| | | | | *ir.Block Block (6 statements)
|
||||
| | | | | | *ir.DotStmt &{Source:{Value:Local<0>} Key:{Value:String<0>} Target:Local<5>}
|
||||
| | | | | | *ir.MakeNumberRefStmt &{Index:1 Target:Local<6>}
|
||||
| | | | | | *ir.CallStmt &{Func:gt Args:[{Value:Local<5>} {Value:Local<6>}] Result:Local<7>}
|
||||
| | | | | | *ir.NotEqualStmt &{A:{Value:Local<7>} B:{Value:Bool<false>}}
|
||||
| | | | | | *ir.AssignVarStmt &{Source:{Value:Bool<true>} Target:Local<4>}
|
||||
| | | | | | *ir.IsDefinedStmt &{Source:Local<4>}
|
||||
| | | | *ir.AssignVarOnceStmt &{Source:{Value:Bool<true>} Target:Local<3>}
|
||||
| | | *ir.Block Block (2 statements)
|
||||
| | | | *ir.IsDefinedStmt &{Source:Local<3>}
|
||||
| | | | *ir.AssignVarOnceStmt &{Source:{Value:Local<3>} Target:Local<2>}
|
||||
| | | *ir.Block Block (1 statements)
|
||||
| | | | *ir.ReturnLocalStmt &{Source:Local<2>}
|
||||
`,
|
||||
},
|
||||
{
|
||||
note: "not with iteration", // success-cond (Local<4>) distinguishes empty scan from matching scan
|
||||
module: `
|
||||
package test
|
||||
import future.keywords.not
|
||||
|
||||
p if {
|
||||
not { input.xs[i]; i > 0 }
|
||||
}
|
||||
`,
|
||||
want: `| *ir.Funcs Funcs (1 funcs)
|
||||
| | *ir.Func g0.data.test.p (2 params: [Local<0> Local<1>], 3 blocks, path: [g0 test p])
|
||||
| | | *ir.Block Block (3 statements)
|
||||
| | | | *ir.ResetLocalStmt &{Target:Local<3>}
|
||||
| | | | *ir.NotStmt &{Block:Block (3 statements)}
|
||||
| | | | | *ir.Block Block (3 statements)
|
||||
| | | | | | *ir.DotStmt &{Source:{Value:Local<0>} Key:{Value:String<1>} Target:Local<5>}
|
||||
| | | | | | *ir.ScanStmt &{Source:Local<5> Key:Local<6> Value:Local<7> Block:Block (6 statements)}
|
||||
| | | | | | | *ir.Block Block (6 statements)
|
||||
| | | | | | | | *ir.AssignVarStmt &{Source:{Value:Local<6>} Target:Local<8>}
|
||||
| | | | | | | | *ir.NotEqualStmt &{A:{Value:Local<7>} B:{Value:Bool<false>}}
|
||||
| | | | | | | | *ir.MakeNumberRefStmt &{Index:2 Target:Local<9>}
|
||||
| | | | | | | | *ir.CallStmt &{Func:gt Args:[{Value:Local<8>} {Value:Local<9>}] Result:Local<10>}
|
||||
| | | | | | | | *ir.NotEqualStmt &{A:{Value:Local<10>} B:{Value:Bool<false>}}
|
||||
| | | | | | | | *ir.AssignVarStmt &{Source:{Value:Bool<true>} Target:Local<4>}
|
||||
| | | | | | *ir.IsDefinedStmt &{Source:Local<4>}
|
||||
| | | | *ir.AssignVarOnceStmt &{Source:{Value:Bool<true>} Target:Local<3>}
|
||||
| | | *ir.Block Block (2 statements)
|
||||
| | | | *ir.IsDefinedStmt &{Source:Local<3>}
|
||||
| | | | *ir.AssignVarOnceStmt &{Source:{Value:Local<3>} Target:Local<2>}
|
||||
| | | *ir.Block Block (1 statements)
|
||||
| | | | *ir.ReturnLocalStmt &{Source:Local<2>}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
parserOpts := ast.ParserOptions{
|
||||
Capabilities: ast.CapabilitiesForThisVersion(ast.CapabilitiesExperimentalKeywords(true)),
|
||||
FutureKeywords: []string{"and", "or", "not"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
m, err := ast.ParseModuleWithOpts("test.rego", tc.module, parserOpts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
policy, err := New().
|
||||
WithQueries([]QuerySet{{
|
||||
Name: "test",
|
||||
Queries: []ast.Body{ast.MustParseBody("data.test.p = x")},
|
||||
}}).
|
||||
WithModules([]*ast.Module{m}).
|
||||
WithBuiltinDecls(ast.BuiltinMap).
|
||||
Plan()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := ir.Pretty(&buf, policy); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := stripIRLocations(buf.String())
|
||||
|
||||
if !strings.Contains(got, tc.want) {
|
||||
t.Errorf("unexpected plan to contain: \n%s\ngot:\n%s", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
irLocationFieldRE = regexp.MustCompile(` Location:\{[^{}]*(?:\{[^{}]*\}[^{}]*)*}`)
|
||||
irLocationInlineRE = regexp.MustCompile(` &\{[0-9]+ [0-9]+ [0-9]+[^}]*}`)
|
||||
)
|
||||
|
||||
func stripIRLocations(s string) string {
|
||||
s = irLocationFieldRE.ReplaceAllString(s, "")
|
||||
s = irLocationInlineRE.ReplaceAllString(s, "")
|
||||
return s
|
||||
}
|
||||
|
||||
func TestMultipleNamedQueries(t *testing.T) {
|
||||
q1 := []ast.Body{
|
||||
ast.MustParseBody(`a=1`),
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# Exception Format is <test name>: <reason>
|
||||
"data/toplevel integer": "https://github.com/open-policy-agent/opa/issues/3711"
|
||||
"data/nested integer": "https://github.com/open-policy-agent/opa/issues/3711"
|
||||
"logic_op/**": "No planner support"
|
||||
|
||||
@@ -95,8 +95,7 @@ func TestWasmE2E(t *testing.T) {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
|
||||
caps := ast.CapabilitiesForThisVersion()
|
||||
caps.FutureKeywords = append(caps.FutureKeywords, "not")
|
||||
caps := ast.CapabilitiesForThisVersion(ast.CapabilitiesExperimentalKeywords(tc.ExperimentalKeywords))
|
||||
|
||||
opts := []func(*rego.Rego){
|
||||
rego.Query(tc.Query),
|
||||
|
||||
@@ -93,3 +93,75 @@ cases:
|
||||
input:
|
||||
x: 1
|
||||
want_result: []
|
||||
- note: "logic_op/and/basic: virtual rule refs on both sides"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
q if input.x > 0
|
||||
r if input.y > 0
|
||||
|
||||
p if {
|
||||
data.test.q and data.test.r
|
||||
}
|
||||
input:
|
||||
x: 1
|
||||
"y": 2
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/and/basic: virtual rule ref, rhs fails"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
q if input.x > 0
|
||||
r if input.y > 0
|
||||
|
||||
p if {
|
||||
data.test.q and data.test.r
|
||||
}
|
||||
input:
|
||||
x: 1
|
||||
"y": 0
|
||||
want_result: []
|
||||
- note: "logic_op/and/basic: dotted data refs both defined"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p if {
|
||||
data.cfg.foo and data.cfg.bar
|
||||
}
|
||||
data:
|
||||
cfg:
|
||||
foo: true
|
||||
bar: true
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/and/basic: indexed data refs"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p if {
|
||||
data.cfg.flags["a"] and data.cfg.flags["b"]
|
||||
}
|
||||
data:
|
||||
cfg:
|
||||
flags:
|
||||
a: true
|
||||
b: true
|
||||
want_result:
|
||||
- x: true
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
cases:
|
||||
- note: "logic_op/builtin_calls: and with builtins on both sides"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p if {
|
||||
count(input.xs) > 0 and startswith(input.s, "/")
|
||||
}
|
||||
input:
|
||||
xs: [1, 2, 3]
|
||||
s: "/etc/hosts"
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/builtin_calls: and with builtins, rhs fails"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p if {
|
||||
count(input.xs) > 0 and startswith(input.s, "/")
|
||||
}
|
||||
input:
|
||||
xs: [1, 2, 3]
|
||||
s: "no-leading-slash"
|
||||
want_result: []
|
||||
- note: "logic_op/builtin_calls: or with builtins on both sides"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
p if {
|
||||
count(input.xs) > 0 or endswith(input.s, ".txt")
|
||||
}
|
||||
input:
|
||||
xs: []
|
||||
s: "report.txt"
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/builtin_calls: nested builtin call, distinct result temporaries"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p if {
|
||||
count(split(input.a, ",")) > 1 and count(split(input.b, ";")) > 1
|
||||
}
|
||||
input:
|
||||
a: "x,y,z"
|
||||
b: "1;2;3"
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/builtin_calls: comprehension in builtin, or branch"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
p if {
|
||||
count([n | n := input.xs[_]; n > 0]) > 0 or count([n | n := input.ys[_]; n > 0]) > 0
|
||||
}
|
||||
input:
|
||||
xs: [-1, -2]
|
||||
ys: [3]
|
||||
want_result:
|
||||
- x: true
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
cases:
|
||||
- note: "logic_op/chained: a and b and c, all true"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p if {
|
||||
input.a and input.b and input.c
|
||||
}
|
||||
input:
|
||||
a: true
|
||||
b: true
|
||||
c: true
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/chained: a and b and c, middle false"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p if {
|
||||
input.a and input.b and input.c
|
||||
}
|
||||
input:
|
||||
a: true
|
||||
b: false
|
||||
c: true
|
||||
want_result: []
|
||||
- note: "logic_op/chained: a or b or c, only middle true"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
p if {
|
||||
input.a or input.b or input.c
|
||||
}
|
||||
input:
|
||||
a: false
|
||||
b: true
|
||||
c: false
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/chained: a or b or c, all false"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
p if {
|
||||
input.a or input.b or input.c
|
||||
}
|
||||
input:
|
||||
a: false
|
||||
b: false
|
||||
c: false
|
||||
want_result: []
|
||||
- note: "logic_op/chained: a and b or c and d, right side wins"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
import future.keywords.or
|
||||
|
||||
p if {
|
||||
# parses as (a and b) or (c and d)
|
||||
input.a and input.b or input.c and input.d
|
||||
}
|
||||
input:
|
||||
a: false
|
||||
b: true
|
||||
c: true
|
||||
d: true
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/chained: a or b and c or d, b-and-c is the only true conjunct"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
import future.keywords.or
|
||||
|
||||
p if {
|
||||
# parses as: a or (b and c) or d
|
||||
input.a or input.b and input.c or input.d
|
||||
}
|
||||
input:
|
||||
a: false
|
||||
b: true
|
||||
c: true
|
||||
d: false
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/chained: deeply nested explicit bodies"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
import future.keywords.or
|
||||
|
||||
p if {
|
||||
{ input.a and { input.b or input.c } } and { input.d or { input.e and input.f } }
|
||||
}
|
||||
input:
|
||||
a: true
|
||||
b: false
|
||||
c: true
|
||||
d: false
|
||||
e: true
|
||||
f: true
|
||||
want_result:
|
||||
- x: true
|
||||
@@ -93,3 +93,44 @@ cases:
|
||||
- [12, 3, -4]
|
||||
want_result:
|
||||
- x: [[1, 5], [3]]
|
||||
- note: "logic_op/comprehension: walk-bound iteration with and as filter"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p := [v |
|
||||
walk(input, [_, v])
|
||||
is_string(v) and count(v) > 0
|
||||
]
|
||||
input:
|
||||
a: "hello"
|
||||
b: ""
|
||||
c:
|
||||
d: "nested"
|
||||
e: 42
|
||||
sort_bindings: true
|
||||
want_result:
|
||||
- x: ["hello", "nested"]
|
||||
- note: "logic_op/comprehension: walk-bound iteration with or as filter"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
p := [v |
|
||||
walk(input, [_, v])
|
||||
is_number(v) or { is_array(v); count(v) > 1 }
|
||||
]
|
||||
input:
|
||||
a: 1
|
||||
b: "x"
|
||||
c: [10, 20, 30]
|
||||
d: [99]
|
||||
sort_bindings: true
|
||||
want_result:
|
||||
- x: [1, 10, 20, 30, 99, [10, 20, 30]]
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
cases:
|
||||
- note: "logic_op/default: default returns when or body fails"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
default p := false
|
||||
|
||||
p if input.x > 0 or input.y > 0
|
||||
input:
|
||||
x: 0
|
||||
"y": 0
|
||||
want_result:
|
||||
- x: false
|
||||
- note: "logic_op/default: default skipped when or body succeeds"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
default p := false
|
||||
|
||||
p if input.x > 0 or input.y > 0
|
||||
input:
|
||||
x: 0
|
||||
"y": 5
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/default: default returns when and body fails"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
default p := "fallback"
|
||||
|
||||
p := "ok" if input.x > 0 and input.y > 0
|
||||
input:
|
||||
x: 1
|
||||
"y": 0
|
||||
want_result:
|
||||
- x: "fallback"
|
||||
- note: "logic_op/else: else fires when and body fails"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p := "ok" if input.x > 0 and input.y > 0
|
||||
else := "fallback"
|
||||
input:
|
||||
x: 1
|
||||
"y": 0
|
||||
want_result:
|
||||
- x: "fallback"
|
||||
- note: "logic_op/else: else skipped when or body succeeds"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
p := "ok" if input.x > 0 or input.y > 0
|
||||
else := "fallback"
|
||||
input:
|
||||
x: 0
|
||||
"y": 5
|
||||
want_result:
|
||||
- x: "ok"
|
||||
- note: "logic_op/else: chained else with or in primary branch"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
p := "primary" if input.role == "admin" or input.role == "superuser"
|
||||
else := "secondary"
|
||||
input:
|
||||
role: "guest"
|
||||
want_result:
|
||||
- x: "secondary"
|
||||
@@ -107,3 +107,67 @@ cases:
|
||||
]
|
||||
want_result:
|
||||
- x: []
|
||||
- note: "logic_op/iteration: not around or with iteration"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
import future.keywords.not
|
||||
|
||||
# not body iterates xs; succeeds iff NO element satisfies either branch
|
||||
p if {
|
||||
not { some n in input.xs; n < 0 or n > 100 }
|
||||
}
|
||||
input:
|
||||
xs: [0, 5, 99]
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/iteration: not around or with iteration, one element matches"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
import future.keywords.not
|
||||
|
||||
p if {
|
||||
not { some n in input.xs; n < 0 or n > 100 }
|
||||
}
|
||||
input:
|
||||
xs: [0, 5, 200]
|
||||
want_result: []
|
||||
- note: "logic_op/iteration: not around and with iteration"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
import future.keywords.not
|
||||
|
||||
# Succeeds iff no n in xs is both >0 and <10
|
||||
p if {
|
||||
not { some n in input.xs; n > 0 and n < 10 }
|
||||
}
|
||||
input:
|
||||
xs: [-1, 0, 20]
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/iteration: not around and with iteration, one element matches"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
import future.keywords.not
|
||||
|
||||
p if {
|
||||
not { some n in input.xs; n > 0 and n < 10 }
|
||||
}
|
||||
input:
|
||||
xs: [-1, 0, 5]
|
||||
want_result: []
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
cases:
|
||||
- note: "logic_op/local_collision: same name in both and-operand bodies"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p if {
|
||||
{ a := 1; a > 0 } and { a := 2; a > 1 }
|
||||
}
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/local_collision: same name in both or-operand bodies"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
p if {
|
||||
{ a := 0; a > 0 } or { a := 5; a > 0 }
|
||||
}
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/local_collision: same name across and-body and outer scope"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
# outer `a` must not leak into the operand body and vice-versa.
|
||||
p if {
|
||||
a := -100
|
||||
{ a := 1; a > 0 } and { a := 2; a > 1 }
|
||||
a == -100
|
||||
}
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/local_collision: same `some` iter name in both operand bodies"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p if {
|
||||
{ some n in [1, 2, 3]; n > 2 } and { some n in [10, 20, 30]; n > 20 }
|
||||
}
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/local_collision: nested explicit body re-uses same name"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
import future.keywords.or
|
||||
|
||||
p if {
|
||||
{ a := 1; a > 0 } and { { a := 2; a > 1 } or { a := 3; a > 5 } }
|
||||
}
|
||||
want_result:
|
||||
- x: true
|
||||
@@ -108,3 +108,93 @@ cases:
|
||||
}
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/or/basic: virtual rule refs, lhs succeeds"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
q if input.x > 0
|
||||
r if input.y > 0
|
||||
|
||||
p if {
|
||||
data.test.q or data.test.r
|
||||
}
|
||||
input:
|
||||
x: 1
|
||||
"y": 0
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/or/basic: virtual rule refs, rhs succeeds"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
q if input.x > 0
|
||||
r if input.y > 0
|
||||
|
||||
p if {
|
||||
data.test.q or data.test.r
|
||||
}
|
||||
input:
|
||||
x: 0
|
||||
"y": 1
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/or/basic: virtual rule refs, neither succeeds"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
q if input.x > 0
|
||||
r if input.y > 0
|
||||
|
||||
p if {
|
||||
data.test.q or data.test.r
|
||||
}
|
||||
input:
|
||||
x: 0
|
||||
"y": 0
|
||||
want_result: []
|
||||
- note: "logic_op/or/basic: dotted data refs, only one defined"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
p if {
|
||||
data.cfg.foo or data.cfg.bar
|
||||
}
|
||||
data:
|
||||
cfg:
|
||||
bar: true
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/or/basic: indexed data refs"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
p if {
|
||||
data.cfg.flags["a"] or data.cfg.flags["b"]
|
||||
}
|
||||
data:
|
||||
cfg:
|
||||
flags:
|
||||
a: false
|
||||
b: true
|
||||
want_result:
|
||||
- x: true
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
cases:
|
||||
- note: "logic_op/rule_head: and under if-head, both true"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p if input.x > 0 and input.y > 0
|
||||
input:
|
||||
x: 1
|
||||
"y": 2
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/rule_head: and under if-head, lhs false"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p if input.x > 0 and input.y > 0
|
||||
input:
|
||||
x: 0
|
||||
"y": 2
|
||||
want_result: []
|
||||
- note: "logic_op/rule_head: or under if-head, lhs true"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
p if input.x > 0 or input.y > 0
|
||||
input:
|
||||
x: 1
|
||||
"y": 0
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/rule_head: or under if-head, both false"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
p if input.x > 0 or input.y > 0
|
||||
input:
|
||||
x: 0
|
||||
"y": 0
|
||||
want_result: []
|
||||
- note: "logic_op/rule_head: and under assignment head"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
p := 1 if input.x > 0 and input.y > 0
|
||||
input:
|
||||
x: 1
|
||||
"y": 2
|
||||
want_result:
|
||||
- x: 1
|
||||
- note: "logic_op/rule_head: or under assignment head"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
p := "matched" if input.x > 0 or input.y > 0
|
||||
input:
|
||||
x: 0
|
||||
"y": 5
|
||||
want_result:
|
||||
- x: "matched"
|
||||
- note: "logic_op/rule_head: and in function body"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
in_range(n) if n > 0 and n < 10
|
||||
|
||||
p if in_range(5)
|
||||
want_result:
|
||||
- x: true
|
||||
- note: "logic_op/rule_head: and in function body, fails"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.and
|
||||
|
||||
in_range(n) if n > 0 and n < 10
|
||||
|
||||
p if in_range(42)
|
||||
want_result: []
|
||||
- note: "logic_op/rule_head: or in function body"
|
||||
experimental_keywords: true
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.or
|
||||
|
||||
boundary(n) if n < 1 or n > 99
|
||||
|
||||
p if boundary(input.n)
|
||||
input:
|
||||
"n": 100
|
||||
want_result:
|
||||
- x: true
|
||||
Reference in New Issue
Block a user