-
Notifications
You must be signed in to change notification settings - Fork 36
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
Conversation
@microsoft-github-policy-service agree |
@eric-therond Thanks for the contribution. Can you sign off you commit? |
The PR looks good to me. Are there other tests that you can think of? |
@anakrish I signed-off the commit |
src/interpreter.rs
Outdated
let a = match a { | ||
Expr::Var(s) => s.text(), | ||
_ => unimplemented!("destructuring function arguments"), | ||
let mut results: Vec<Result<Value>> = Vec::new(); |
There was a problem hiding this comment.
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>
?
There was a problem hiding this comment.
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
src/interpreter.rs
Outdated
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()), |
There was a problem hiding this comment.
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.
src/interpreter.rs
Outdated
results.push(result); | ||
} | ||
|
||
if let Ok(value0) = &results[0] { |
There was a problem hiding this comment.
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])
There was a problem hiding this 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.
@eric-therond Kindly address suggestions from @mingweishih |
- | | ||
package test | ||
|
||
inc(x) = x + 1 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
src/interpreter.rs
Outdated
// Restore local variables for current context. | ||
self.scopes = scopes; | ||
|
||
// If the function successfully executed and returned true, not necessary to execute the other |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
) | ||
.as_str(), | ||
)); | ||
let mut fcns: Vec<&Rule> = Vec::new(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
let mut fcns: Vec<&Rule> = Vec::new(); | |
let fcns = fcns_rules.clone(); |
src/interpreter.rs
Outdated
|
||
if results | ||
.iter() | ||
.any(|value| *value != results[0] && *value != Value::Undefined) |
There was a problem hiding this comment.
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.
tests/interpreter/cases/call/or.yaml
Outdated
query: data.test | ||
want_result: | ||
a1: 6 | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do think about:
- How we can simplify the logic and possibly avoid extra copying
- 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, |
There was a problem hiding this comment.
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,
- If
results
is empty, that means all the rules evaluated toUndefined
. - If
results
is not empty, we need to check that all the values are the same.
src/interpreter.rs
Outdated
0 => bail!("internal error"), | ||
1 => Ok(results[0].clone()), | ||
_ => { | ||
let mut defined_results = results.clone(); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good tests.
There was a problem hiding this 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: {} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- Also push errors into a vector
errors
within the loop. - Then
- If
results
is not empty then ignoreerrors
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.
- If
- If
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Signed-off-by: eric-therond <[email protected]>
ae7bd01
to
db3d5f7
Compare
src/interpreter.rs
Outdated
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)?; |
There was a problem hiding this comment.
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
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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let mut results: Vec<Value> = Vec::new(); | |
let mut results = Vec::new(); | |
let mut errors = Vec::new(); |
src/interpreter.rs
Outdated
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return Ok(Value::Undefined); | |
if errors.is_empty() { | |
return Ok(Value::Undefined); | |
} else { | |
return errors[0]; | |
} |
Signed-off-by: eric-therond <[email protected]>
@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! |
Probably not a clean code, I am learning both rust and rego languages ...
I tried to add support of mutiple definitions of a function