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

support of or-functions #18

Merged
merged 2 commits into from
Jun 16, 2023
Merged

Conversation

eric-therond
Copy link
Contributor

Probably not a clean code, I am learning both rust and rego languages ...

I tried to add support of mutiple definitions of a function

@eric-therond
Copy link
Contributor Author

@microsoft-github-policy-service agree

@anakrish
Copy link
Collaborator

@eric-therond Thanks for the contribution. Can you sign off you commit?

@anakrish
Copy link
Collaborator

The PR looks good to me. Are there other tests that you can think of?

@eric-therond
Copy link
Contributor Author

@anakrish I signed-off the commit
I'am ok with the tests for now

let a = match a {
Expr::Var(s) => s.text(),
_ => unimplemented!("destructuring function arguments"),
let mut results: Vec<Result<Value>> = Vec::new();
Copy link
Contributor

Choose a reason for hiding this comment

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

Can the type just be Vec<Value>?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It makes sense to me, thanks

self.scopes.push(args_scope);
let value = self.eval_rule_bodies(ctx, span, bodies)?;
let result = match &value {
Value::Set(s) if s.len() == 1 => Ok(s.iter().next().unwrap().clone()),
Copy link
Contributor

Choose a reason for hiding this comment

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

If the results is Vec<Value>, you can remove Ok(...) wrapping here and below.

results.push(result);
}

if let Ok(value0) = &results[0] {
Copy link
Contributor

Choose a reason for hiding this comment

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

Assuming results is Vec<Value>, you could do something like:

        if results.is_empty() {
            bail!("internal error");
        }

        if results.iter().any(|value| value != results[0]) {
            return Err(span.source.error(
                span.line,
                span.col,
                "functions must not produce multiple outputs for same inputs",
            ));
        }

        Ok(results[0])

Copy link
Collaborator

@anakrish anakrish left a comment

Choose a reason for hiding this comment

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

LGTM.

Thanks for the contribution.

@anakrish
Copy link
Collaborator

@eric-therond Kindly address suggestions from @mingweishih

- |
package test

inc(x) = x + 1
Copy link
Collaborator

@anakrish anakrish May 31, 2023

Choose a reason for hiding this comment

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

We also need to add another test case for cases where one of the function definitions evaluates to undefined:

        inc(x) = x + 1
        inc(x) = x + 1
        inc(x) = y {
           x > 10 # This will evaluate to false.
           y = 100 # y will be undefined.
        }

        a1 = inc(5)

The logic needs to be fixed to handle this. Undefined definitions are ignored.

a1 = inc(2)
query: data.test
want_result:
a1: true
Copy link
Collaborator

Choose a reason for hiding this comment

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

The above example produces eval_conflict_error: functions must not produce multiple outputs for same inputs in Rego playground.

// Restore local variables for current context.
self.scopes = scopes;

// If the function successfully executed and returned true, not necessary to execute the other
Copy link
Collaborator

Choose a reason for hiding this comment

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

This assumption may not be correct. It seems that Rego requires all the definitions to be executed even if one definition returned true.

Copy link
Contributor Author

@eric-therond eric-therond May 31, 2023

Choose a reason for hiding this comment

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

You're absolutely right, I think I mixed up two different rego objects: "incremental rules" and "multiple function definitions" (I told you I was new to rego !).

To my understanding, rules with the same name should be executed one after another until true is returned, but for functions, as you said, all functions with the same name should be executed, and the return value should be the same, regardless of the value set (defined as in your example).

I will fix that tomorrow

src/interpreter.rs Outdated Show resolved Hide resolved
)
.as_str(),
));
let mut fcns: Vec<&Rule> = Vec::new();
Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't need this copying to fcns anymore.

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 have a borrowing error without the copy that I am not able to fix by myself

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok. The compiler thinks that fcns_rules could be modified by the subsequent code.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You can simplify the code using the clone method.

Suggested change
let mut fcns: Vec<&Rule> = Vec::new();
let fcns = fcns_rules.clone();


if results
.iter()
.any(|value| *value != results[0] && *value != Value::Undefined)
Copy link
Collaborator

Choose a reason for hiding this comment

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

If the first result is Undefined, then this logic won't work.

query: data.test
want_result:
a1: 6

Copy link
Collaborator

Choose a reason for hiding this comment

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

Add test cases for the following scenarios:
1, First definition produces undefined value.
2. All definitions produce undefined value.

Confirm expected behavior in Rego playground.

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 changed a bit my approach to cover all the cases

Copy link
Collaborator

@anakrish anakrish left a comment

Choose a reason for hiding this comment

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

Do think about:

  1. How we can simplify the logic and possibly avoid extra copying
  2. Any more test cases that will lock down the logic.

Value::Set(s) if s.is_empty() && output_expr.is_none() => Value::Bool(true),

// If the function execution resulted in undefined, then propagate it.
Value::Undefined => Value::Undefined,
Copy link
Collaborator

Choose a reason for hiding this comment

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

We could have a strategy where we don't gather Undefined values. In that case,

  1. If results is empty, that means all the rules evaluated to Undefined.
  2. If results is not empty, we need to check that all the values are the same.

0 => bail!("internal error"),
1 => Ok(results[0].clone()),
_ => {
let mut defined_results = results.clone();
Copy link
Collaborator

Choose a reason for hiding this comment

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

We can simplify the logic by avoiding pushing Undefined values altogether.

a1: 6


- note: or-all-undefined
Copy link
Collaborator

Choose a reason for hiding this comment

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

Good tests.

Copy link
Collaborator

@anakrish anakrish left a comment

Choose a reason for hiding this comment

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

@eric-therond Please squash your commits.


a1 = inc(5)
query: data.test
want_result: {}
Copy link
Collaborator

Choose a reason for hiding this comment

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

There is one more case to handle:

fcn(x) = y {
  y = x + 1
}

fcn(x) = y {
   y = concat(" ", ["hello", x])
}

a1 = fcn("world")
a2 = fcn(5)

This produces:

{
    "a1": "hello world",
    "a2": 6
}

The definitions that produce an error are ignored. If all definitions produce errors, then one of the errors is propagated.

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 added the test case without being able to fix it in this PR.
I left a comment to not forget about it.

Unless I missed a simple solution, there is a lot of code to rewrite to support that ? especially how errors are handled.

Copy link
Collaborator

Choose a reason for hiding this comment

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

For minimal changes, we could slightly tweak your current logic:

  1. Also push errors into a vector errors within the loop.
  2. Then
    • If results is not empty then ignore errors and return the result after asserting that all the values are same.
    • If results is empty, then
      • If errors is not empty, then return the first error.
      • If errors is empty, then return Undefined.

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 didn't know that errors are propagated like that in Rust ... thanks for the hint, it's super clear.

I still had a problem, the test case or-one-error returns

{
  "a1": [
    "hello world"
  ],
  "a2": [
    6
  ]
}

but in rego playground it is:

{
    "a1": "hello world",
    "a2": 6
}

I didn't investigate why this difference.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for pointing it out. I will take a look at it.

let scopes = std::mem::take(&mut self.scopes);
// Set the arguments scope.
self.scopes.push(args_scope);
let value = self.eval_rule_bodies(ctx, span, bodies)?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

You could do something like this to accumulate errors

Suggested change
let value = self.eval_rule_bodies(ctx, span, bodies)?;
let value = match self.eval_rule_bodies(ctx, span, bodies) {
Ok(v) => v
Err(e) => {
// If the rule produces an error, save the error.
errors.push(Err(e));
continue;
}
};

.as_str(),
));
}
let mut results: Vec<Value> = Vec::new();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
let mut results: Vec<Value> = Vec::new();
let mut results = Vec::new();
let mut errors = Vec::new();

Value::Set(s) if s.len() == 1 => Ok(s.iter().next().unwrap().clone()),
Value::Set(s) if !s.is_empty() => Err(span.source.error(
if results.is_empty() {
return Ok(Value::Undefined);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
return Ok(Value::Undefined);
if errors.is_empty() {
return Ok(Value::Undefined);
} else {
return errors[0];
}

@anakrish
Copy link
Collaborator

@eric-therond Can you squash you commits?

I will then merge the PR and then investigate fixing the return values in the case you pointed out.

Thanks for your contribution!

@anakrish anakrish merged commit 910ef32 into microsoft:main Jun 16, 2023
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.

3 participants