Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Arbitrary vars in rule refs #5913

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
525c93d
topdown: Adding support for multiple variables in rule refs
johanfylling Apr 6, 2023
5f70376
Merge branch 'main' into jf/5685/multi-var-rule-refs
johanfylling Jul 3, 2023
d387cfd
Updating commented-out tests.
johanfylling Jul 6, 2023
d1dcb03
Fixing yaml test expecting multiple results.
johanfylling Jul 10, 2023
2073c3d
Not running yaml test for WASM
johanfylling Jul 12, 2023
29c2a2e
Merge branch 'main' into jf/5685/multi-var-rule-refs
johanfylling Aug 1, 2023
659ef2a
Moving type merge from `typeChecker.checkRule()` to `typeTreeNode.Ins…
johanfylling Jul 4, 2023
692770e
Updating type-checker to handle generic rule refs
johanfylling Jul 4, 2023
9707e66
Updating tests
johanfylling Jul 5, 2023
391d469
Generalizing `types.Object.Merge()``
johanfylling Jul 5, 2023
ea52a01
Merging sub-objects in `types.Object.Merge()`
johanfylling Jul 5, 2023
996cdec
Removing comment in test
johanfylling Jul 10, 2023
1065755
Updating tests
johanfylling Jul 5, 2023
67ba943
Aligning hint-key and entry for general refs on virtual partial eval
johanfylling Jul 11, 2023
8ffcf1d
Enforcing no-else parser rule on general ref heads
johanfylling Aug 10, 2023
f02441d
PE impl
johanfylling Aug 14, 2023
68f9436
Removing redundant legacy PE eval branch
johanfylling Aug 14, 2023
bec96d1
Fixing issue where PE-produced query contained non-namespaced local vars
johanfylling Aug 18, 2023
33ebc2f
Adding PE tests
johanfylling Aug 18, 2023
80620a0
Invoking evalOneRulePostUnify() for any ref with unknowns overlapping…
johanfylling Aug 21, 2023
98178fb
fmt: Not using rule ref ground prefix if ref has dynamic part
johanfylling Aug 21, 2023
372def8
Fixing linter warnings
johanfylling Aug 21, 2023
9e544c6
Ignoring additional yaml tests not supported by Wasm
johanfylling Aug 22, 2023
0fa5652
Merge branch 'main' into jf/5685/multi-var-rule-refs
johanfylling Aug 22, 2023
52bb04d
Cleanup
johanfylling Aug 22, 2023
18702a3
Producing cache hint keys containing entire applicable ref for genera…
johanfylling Aug 22, 2023
d4ac3b0
Merge branch 'main' into jf/5685/multi-var-rule-refs
johanfylling Aug 29, 2023
9e46555
Updating type-tree construction
johanfylling Aug 29, 2023
e1c0a23
Merge branch 'main' into jf/5685/multi-var-rule-refs
johanfylling Aug 29, 2023
2e7121b
Removing debug print
johanfylling Aug 29, 2023
a98a6ee
Merge branch 'main' into jf/5685/multi-var-rule-refs
johanfylling Aug 30, 2023
9dcaac8
Adding docs
johanfylling Aug 30, 2023
c0d38ea
Merge branch 'main' into jf/5685/multi-var-rule-refs
johanfylling Aug 31, 2023
ebf1503
Making changes suggested by @srenatus
johanfylling Aug 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
165 changes: 109 additions & 56 deletions ast/compile.go
Expand Up @@ -7,6 +7,7 @@ package ast
import (
"fmt"
"io"
"os"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -81,6 +82,28 @@ type Compiler struct {
// +--- b
// |
// +--- c (1 rule)
//
// Another example with general refs containing vars at arbitrary locations:
//
// package ex
// a.b[x].d { x := "c" } # R1
// a.b.c[x] { x := "d" } # R2
// a.b[x][y] { x := "c"; y := "d" } # R3
// p := true # R4
//
// root
// |
// +--- data (no rules)
// |
// +--- ex (no rules)
// |
// +--- a
// | |
// | +--- b (R1, R3)
// | |
// | +--- c (R2)
// |
// +--- p (R4)
RuleTree *TreeNode

// Graph contains dependencies between rules. An edge (u,v) is added to the
Expand Down Expand Up @@ -123,6 +146,7 @@ type Compiler struct {
keepModules bool // whether to keep the unprocessed, parse modules (below)
parsedModules map[string]*Module // parsed, but otherwise unprocessed modules, kept track of when keepModules is true
useTypeCheckAnnotations bool // whether to provide annotated information (schemas) to the type checker
generalRuleRefsEnabled bool
}

// CompilerStage defines the interface for stages in the compiler.
Expand Down Expand Up @@ -311,6 +335,8 @@ func NewCompiler() *Compiler {
{"BuildComprehensionIndices", "compile_stage_rebuild_comprehension_indices", c.buildComprehensionIndices},
}

_, c.generalRuleRefsEnabled = os.LookupEnv("OPA_ENABLE_GENERAL_RULE_REFS")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh interesting 💡 yeah, why not. We might throw in a _WIP_


return c
}

Expand Down Expand Up @@ -790,17 +816,15 @@ func (c *Compiler) buildRuleIndices() {
rules := extractRules(node.Values)
hasNonGroundKey := false
for _, r := range rules {
if ref := r.Head.Ref(); len(ref) > 1 {
if !ref[len(ref)-1].IsGround() {
hasNonGroundKey = true
}
}
hasNonGroundKey = !r.Head.Ref().IsGround()
}
if hasNonGroundKey {
// collect children: as of now, this cannot go deeper than one level,
// so we grab those, and abort the DepthFirst processing for this branch
for _, n := range node.Children {
rules = append(rules, extractRules(n.Values)...)
// collect children
for _, child := range node.Children {
child.DepthFirst(func(c *TreeNode) bool {
rules = append(rules, extractRules(c.Values)...)
return false
})
}
}

Expand Down Expand Up @@ -870,10 +894,11 @@ func (c *Compiler) checkRuleConflicts() {

kinds := make(map[RuleKind]struct{}, len(node.Values))
defaultRules := 0
completeRules := 0
partialRules := 0
arities := make(map[int]struct{}, len(node.Values))
name := ""
var singleValueConflicts []Ref
var multiValueConflicts []Ref
var conflicts []Ref

for _, rule := range node.Values {
r := rule.(*Rule)
Expand All @@ -885,70 +910,99 @@ func (c *Compiler) checkRuleConflicts() {
defaultRules++
}

// Single-value rules may not have any other rules in their extent: these pairs are invalid:
// Single-value rules may not have any other rules in their extent.
// Rules with vars in their ref are allowed to have rules inside their extent.
// Only the ground portion (terms before the first var term) of a rule's ref is considered when determining
// whether it's inside the extent of another (c.RuleTree is organized this way already).
// These pairs are invalid:
//
// data.p.q.r { true } # data.p.q is { "r": true }
// data.p.q.r.s { true }
//
// data.p.q[r] { r := input.r } # data.p.q could be { "r": true }
// data.p.q.r.s { true }
// data.p.q.r { true }
// data.p.q.r[s].t { s = input.key }
//
// data.p[r] := x { r = input.key; x = input.bar }
// data.p.q[r] := x { r = input.key; x = input.bar }

// But this is allowed:
//
// data.p.q.r { true }
// data.p.q[r].s.t { r = input.key }
//
// data.p[r] := x { r = input.key; x = input.bar }
// data.p.q[r] := x { r = input.key; x = input.bar }
//
// data.p.q[r] { r := input.r }
// data.p.q.r.s { true }
//
// data.p.q[r] = 1 { r := "r" }
// data.p.q.s = 2
//
// data.p[q][r] { q := input.q; r := input.r }
// data.p.q.r { true }
//
// data.p.q[r] { r := input.r }
// data.p[q].r { q := input.q }
//
// data.p.q[r][s] { r := input.r; s := input.s }
// data.p[q].r.s { q := input.q }

if r.Head.RuleKind() == SingleValue && len(node.Children) > 0 {
if len(ref) > 1 && !ref[len(ref)-1].IsGround() { // p.q[x] and p.q.s.t => check grandchildren
for _, c := range node.Children {
grandchildrenFound := false

if len(c.Values) > 0 {
childRules := extractRules(c.Values)
for _, childRule := range childRules {
childRef := childRule.Ref()
if childRule.Head.RuleKind() == SingleValue && !childRef[len(childRef)-1].IsGround() {
// The child is a partial object rule, so it's effectively "generating" grandchildren.
grandchildrenFound = true
break
if c.generalRuleRefsEnabled {
if r.Ref().IsGround() && len(node.Children) > 0 {
conflicts = node.flattenChildren()
}
} else { // TODO: Remove when general rule refs are enabled by default.
if r.Head.RuleKind() == SingleValue && len(node.Children) > 0 {
if len(ref) > 1 && !ref[len(ref)-1].IsGround() { // p.q[x] and p.q.s.t => check grandchildren
for _, c := range node.Children {
grandchildrenFound := false

if len(c.Values) > 0 {
childRules := extractRules(c.Values)
for _, childRule := range childRules {
childRef := childRule.Ref()
if childRule.Head.RuleKind() == SingleValue && !childRef[len(childRef)-1].IsGround() {
// The child is a partial object rule, so it's effectively "generating" grandchildren.
grandchildrenFound = true
break
}
}
}
}

if len(c.Children) > 0 {
grandchildrenFound = true
}
if len(c.Children) > 0 {
grandchildrenFound = true
}

if grandchildrenFound {
singleValueConflicts = node.flattenChildren()
break
if grandchildrenFound {
conflicts = node.flattenChildren()
break
}
}
} else { // p.q.s and p.q.s.t => any children are in conflict
conflicts = node.flattenChildren()
}
} else { // p.q.s and p.q.s.t => any children are in conflict
singleValueConflicts = node.flattenChildren()
}
}

// Multi-value rules may not have any other rules in their extent; e.g.:
//
// data.p[v] { v := ... }
// data.p.q := 42 # In direct conflict with data.p[v], which is constructing a set and cannot have values assigned to a sub-path.
// Multi-value rules may not have any other rules in their extent; e.g.:
//
// data.p[v] { v := ... }
// data.p.q := 42 # In direct conflict with data.p[v], which is constructing a set and cannot have values assigned to a sub-path.

if r.Head.RuleKind() == MultiValue && len(node.Children) > 0 {
multiValueConflicts = node.flattenChildren()
if r.Head.RuleKind() == MultiValue && len(node.Children) > 0 {
conflicts = node.flattenChildren()
}
}

if r.Head.RuleKind() == SingleValue && r.Head.Ref().IsGround() {
completeRules++
} else {
partialRules++
}
}

switch {
case singleValueConflicts != nil:
c.err(NewError(TypeErr, node.Values[0].(*Rule).Loc(), "single-value rule %v conflicts with %v", name, singleValueConflicts))

case multiValueConflicts != nil:
c.err(NewError(TypeErr, node.Values[0].(*Rule).Loc(), "multi-value rule %v conflicts with %v", name, multiValueConflicts))
case conflicts != nil:
c.err(NewError(TypeErr, node.Values[0].(*Rule).Loc(), "rule %v conflicts with %v", name, conflicts))

case len(kinds) > 1 || len(arities) > 1:
case len(kinds) > 1 || len(arities) > 1 || (completeRules >= 1 && partialRules >= 1):
c.err(NewError(TypeErr, node.Values[0].(*Rule).Loc(), "conflicting rules %v found", name))

case defaultRules > 1:
Expand Down Expand Up @@ -1697,13 +1751,12 @@ func (c *Compiler) rewriteRuleHeadRefs() {
}

for i := 1; i < len(ref); i++ {
// NOTE(sr): In the first iteration, non-string values in the refs are forbidden
// NOTE: Unless enabled via the OPA_ENABLE_GENERAL_RULE_REFS env var, non-string values in the refs are forbidden
// except for the last position, e.g.
// OK: p.q.r[s]
// NOT OK: p[q].r.s
// TODO(sr): This is stricter than necessary. We could allow any non-var values there,
// but we'll also have to adjust the type tree, for example.
if i != len(ref)-1 { // last
// TODO: Remove when general rule refs are enabled by default.
if !c.generalRuleRefsEnabled && i != len(ref)-1 { // last
if _, ok := ref[i].Value.(String); !ok {
c.err(NewError(TypeErr, rule.Loc(), "rule head must only contain string terms (except for last): %v", ref[i]))
continue
Expand Down