planner: Support and/or logical operators (#8827)

Fixes: #8681

---------

Signed-off-by: Johan Fylling <johan.dev@fylling.se>
This commit is contained in:
Johan Fylling
2026-07-01 08:30:40 +02:00
committed by GitHub
parent 1783ac26de
commit 21fe862a52
15 changed files with 1118 additions and 28 deletions
@@ -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
View File
@@ -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
+206 -1
View File
@@ -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"
+1 -2
View File
@@ -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
+129
View File
@@ -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
+90
View File
@@ -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
+124
View File
@@ -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