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

Conversation

johanfylling
Copy link
Contributor

@johanfylling johanfylling commented May 10, 2023

This PR adds the possibility to declare rules with variables at arbitrary locations int the ref; e.g.

package example

p[q].r[s] := 42 { q := "foo"; s := "bar" }

By default, the compiler won't allow rule refs to have vars in any other position than the last term; to enable general rule refs, set the OPA_ENABLE_GENERAL_RULE_REFS env var (any value). (is this the best way to do this, or do we have precedence of other ways of setting feature flags?)

Not included in this PR:

  • Feature flag to enable/disable
  • Partial eval
  • Planner implementation
  • Type-checker update
  • Language definition update in docs
  • fmt

@netlify
Copy link

netlify bot commented May 29, 2023

Deploy Preview for openpolicyagent ready!

Name Link
🔨 Latest commit ebf1503
🔍 Latest deploy log https://app.netlify.com/sites/openpolicyagent/deploys/64f067600cca9e00073f7213
😎 Deploy Preview https://deploy-preview-5913--openpolicyagent.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@johanfylling johanfylling force-pushed the jf/5685/multi-var-rule-refs branch 2 times, most recently from 8ef1c96 to 9ae279d Compare May 30, 2023 08:42
@johanfylling johanfylling marked this pull request as ready for review May 30, 2023 11:23
@johanfylling johanfylling changed the title WIP: Arbitrary vars in rule refs Arbitrary vars in rule refs May 30, 2023
ast/compile.go Outdated
@@ -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_

Copy link
Contributor

@srenatus srenatus left a comment

Choose a reason for hiding this comment

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

Started looking at it. Still much ground to cover on my side 🔍

ast/index.go Outdated Show resolved Hide resolved
ast/parser.go Outdated Show resolved Hide resolved
test/cases/cases.go Show resolved Hide resolved
topdown/exported_test.go Show resolved Hide resolved
This is the topdown part of supporting generic refs.

Fixes: open-policy-agent#5993

Signed-off-by: Johan Fylling <johan.dev@fylling.se>
@johanfylling johanfylling linked an issue Jun 9, 2023 that may be closed by this pull request
Copy link
Contributor

@srenatus srenatus left a comment

Choose a reason for hiding this comment

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

Sorry it took me so long to get back to this. Some comments and questions inside

ast/parser.go Outdated
@@ -140,6 +142,7 @@ func NewParser() *Parser {
s: &state{},
po: ParserOptions{},
}
_, p.po.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.

💭 treating the environment as a global variable is better avoided, but if we can keep this as a short-term stop-gap, it's probably OK.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is temporary, and will be removed once general refs are feature complete. Hopefully soon 🤞. I'm open to improve this if it's too much of an eyesore, though 😅 .

1: true
c:
2: true
# FIXME: Below test case returns: rego_type_error: undefined ref: data.test.p.b
Copy link
Contributor

Choose a reason for hiding this comment

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

When? 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This has been solved in the type-checker update I have lined up: johanfylling/opa@jf/5685/multi-var-rule-refs...generic_refs/type-check

#
# p.q[r].s := 1 { r := "foo" }
# p.q[r].s := 2 { r := "bar" }
# query: i := ["foo", "bar"][_]; data.test.p.q[i].s = x
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe quotes help?

Suggested change
# query: i := ["foo", "bar"][_]; data.test.p.q[i].s = x
# query: 'i := ["foo", "bar"][_]; data.test.p.q[i].s = x'

Alternatively, you could query data.test.q and add

q = x {
    i := ["foo", "bar"][_]
    p.q[i].s = x
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, looking at this again, I actually get a result with the expected parameters, but with some added "stuff":

{{"$0": 0, "__localq0__": ["foo", "bar"], "i": "foo", "x": 1}, {"$0": 1, "__localq0__": ["foo", "bar"], "i": "bar", "x": 2}}

If I change the query to data.test.p.q[i].s = x, I get a good result:

{{"i": "bar", "x": 2}, {"i": "foo", "x": 1}}

Is this expected, or perhaps an unwanted side effect of my changes 🤔 ? Will try a similar thing on main.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

main exhibit the same behavior. Running opa eval does not output bindings for $0 and __localq0__; so maybe this is a side effect of how the test works?

Copy link
Contributor

Choose a reason for hiding this comment

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

Those should never make it out, I think; but opa eval might have some extra code to filter them. Or maybe that was the rego package, https://github.com/open-policy-agent/opa/blob/main/rego/rego.go#L2166-L2168

topdown/eval.go Outdated

func (e *eval) biunifyDynamicRef(pos int, a, b ast.Ref, b1, b2 *bindings, iter unifyIterator) error {
if pos >= len(a) || pos >= len(b) {
return iter()
Copy link
Contributor

Choose a reason for hiding this comment

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

So if they don't have the same length, but overlap, we'd accept that?

data.foo.bar = data.foo[x].z

we'd add x = "bar" to the bindings and go on?

But I might be missing something here, I haven't fully understood the topdown change yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're simply binding any possibly known vars in a rule's ref before evaluating its body.

If data.foo.bar == data.foo.bar.z then I'd expect your snipped should evaluate. (is that even possible, though? 😵‍💫)

This is an interesting case to test for, though; hadn't thought about it from this angle before 🤔.

Perhaps a bit hacky test, but this should trigger this scenario:

package unification

p := i {
    foo.bar.x.y = foo[i].z
}

foo[a][b] := c {
    a := "bar"
    c := {
        "x": {
            "y": 42
        },
        "z": 42
    }[b]
}
$ OPA_ENABLE_GENERAL_RULE_REFS=true opa eval -d . 'data.unification.p'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or am I completely misunderstanding you?

// `,
// query: `i := ["foo", "bar"][_]; data.test.p.q[i].s = x`,
// exp: `[{"x": 1}, {"x": 2}]`,
//},
Copy link
Contributor

Choose a reason for hiding this comment

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

What's up with these? Do they not work yet; or have they been subsumed by the tests above...? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I missed un-commenting those. Fixed 👍 .

e.e.virtualCache.Put(hint.key, result)
return e.evalTerm(iter, e.pos+1, result, e.bindings)
}

for _, rule := range e.ir.Rules {
Copy link
Contributor

Choose a reason for hiding this comment

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

Given that generic ref head rules are the more general case, you might expect that the base case (below) is also covered by what's added above -- is that so? Or what am I missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think you're right. These two branches should be possible to merge into one; given that there is no performance benefit to keeping them separate.

I think it's around here PE is breaking down too; so I'll dig around here a bit to see what improvements are necessary + possible.

Copy link
Contributor Author

@johanfylling johanfylling Jul 10, 2023

Choose a reason for hiding this comment

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

It's even a bit broken as-is, given that this should be in an elseblock 😅 . I think this rule unification just won't contribute any data in applicable cases, so the result ends up being correct; but we're doing unnecessary work 🤔 .
Scratch that; I'm not even reading my own code right 😄 .

Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
…ert()`

Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Fixes: open-policy-agent#5994
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
instead of `Or`:ing them

Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Nested object created by general ref didn't maintain ground:ness even though containing vars.

Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
… with dynamic portion of a rule ref

Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
* Adding comments
* Removing unused stuff
* Rename evalOneDynamicRefRulePreUnify() -> evalOneRulePreUnify()

Signed-off-by: Johan Fylling <johan.dev@fylling.se>
…l ref heads

Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Copy link
Contributor

@srenatus srenatus left a comment

Choose a reason for hiding this comment

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

Checked the commits since last week, only had a few questions. It's great to see this progress 🚀

@@ -17,3 +17,26 @@ r[x] if x := 10
p.q.r[x] if x := 10

p.q.r[2] = true

g[h].i[j].k = true
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks so wrong but h and j could be rules defined somewhere in the package, right? 😅 (Not that it matters for formatting)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, this isn't internally consistent; but it's valid Rego, and for this rule we're more interested to see what the formatter does with the redundant { true } body.

topdown/eval_test.go Show resolved Hide resolved
topdown/eval_test.go Outdated Show resolved Hide resolved
@@ -579,6 +687,7 @@ func TestCheckInferenceRules(t *testing.T) {
t.Fatalf("Unexpected error %v:", err)
}

fmt.Println(env.tree.String())
Copy link
Member

Choose a reason for hiding this comment

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

Left from testing probably.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops, yea 🤦‍♂️ 😄

Signed-off-by: Johan Fylling <johan.dev@fylling.se>
Signed-off-by: Johan Fylling <johan.dev@fylling.se>
srenatus
srenatus previously approved these changes Aug 31, 2023
Copy link
Contributor

@srenatus srenatus left a comment

Choose a reason for hiding this comment

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

Nitpicks only. Yay, can't wait to get this in. Thanks for the hard work; it's like a summer project 😎

types.NewStaticProperty("c", types.B)},
nil,
expected: types.NewObject(nil,
types.NewDynamicProperty(types.S, types.Any{types.B, types.N, types.S}),
Copy link
Contributor

Choose a reason for hiding this comment

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

📝 took me a moment to understand this, but I had overlooked that it's p.q[r].b = 42 and not p.q[r] = 42. So, the extra S keys would (could) be "a", "b", "c". ✔️

Any term, except the very first, in a rule head's reference can be a variable. These variables can be assigned within the rule, just as for any other partial rule, to dynamically construct a nested collection of objects.

{{< danger >}}
General refs in rule heads is an experimental feature, and can be enabled by setting the `OPA_ENABLE_GENERAL_RULE_REFS` environment variable.
Copy link
Contributor

Choose a reason for hiding this comment

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

I was wondering about the name for a moment, but I think it's OK. We've got another example here, FWIW.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I'll change this to EXPERIMENTAL_GENERAL_RULE_REFS, to align 👍 .

import future.keywords

# A partial object rule that converts a list of users to a mapping by "role" and then "id".
users_by_role[role][id] := user {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's throw in an if here and below

@johanfylling johanfylling merged commit 0431567 into open-policy-agent:main Aug 31, 2023
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

general ref heads: topdown
3 participants