Implement comparison operators. Formalize semantics.

Rename compare.rs to comparison.rs to match OPA documentation of builtins.

Also update loop handling to ensure that the current scope is restored after
each iteration of the loop so as to undo side effects of the iteration.

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2023-02-12 17:09:23 -08:00
parent fd5140a5a8
commit 53030c4ada
6 changed files with 641 additions and 254 deletions
-30
View File
@@ -1,30 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::ast::*;
use crate::value::{Value::*, *};
pub fn eq(v1: &Value, v2: &Value) -> Value {
match (v1, v2) {
(Undefined, _) | (_, Undefined) => Undefined,
_ => Bool(v1 == v2),
}
}
pub fn ne(v1: &Value, v2: &Value) -> Value {
match (v1, v2) {
(Undefined, _) | (_, Undefined) => Undefined,
_ => Bool(v1 != v2),
}
}
pub fn compare(op: &BoolOp, v1: &Value, v2: &Value) -> Value {
match op {
BoolOp::Eq => eq(v1, v2),
BoolOp::Ne => ne(v1, v2),
BoolOp::Ge => Bool(v1 >= v2),
BoolOp::Gt => Bool(v1 > v2),
BoolOp::Le => Bool(v1 <= v2),
BoolOp::Lt => Bool(v1 < v2),
}
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use crate::ast::BoolOp;
use crate::value::Value;
use anyhow::Result;
/// compare two values
///
/// When comparing values of different kinds, the following order is honored.
/// That is, null is less than all other kinds of values. bool is greater than
/// null, but less than other kinds of values, and so on.
///
/// 1. null
/// 2. bool
/// 3. number
/// 4. string
/// 5. Array
/// 6. Object
/// 7. Set
///
/// Scalar types like null, bool, number, string follow ordering same as what is seen in other languages when compared with values of the same kind.
///
/// When comparing arrays, each item from the first array is compared with the corresponding item from the second array. The ordering is determined by
// the ordering of the first non-equal items. In case all the compared items
/// are equal, the ordering is determined by comparing the length of the first array
/// with the length of the second array. The smaller array is 'Less' than the larger array.
///
/// Sets compare similar to arrays.
///
/// When comparing objects, each entry (key, value) from the first object is compared with the corresponding
/// entry from the second object. The ordering is determined by the ordering of the first non-equal entries. In case all the compared entries are equal,
/// the ordering is determined by comparing the length of the first object with the length of the second object. The smaller object is 'Less' then the larger object.
///
/// Undefined values are a special case. Comparing an Undefined value with
/// any other value results in Undefined.
///
/// # Arguments
/// * `op` - The comparison operation to perform.
/// * `v1` - The first value.
/// * `v2` - The second value.
pub fn compare(op: &BoolOp, v1: &Value, v2: &Value) -> Result<Value> {
// Handle undefined values.
if v1 == &Value::Undefined || v2 == &Value::Undefined {
return Ok(Value::Undefined);
}
// Rely on generated comparison operators.
// The variants of Value enum are specified in the order necessary to
// obtain the desired semantics.
Ok(Value::Bool(match op {
BoolOp::Eq => v1 == v2,
BoolOp::Ne => v1 != v2,
BoolOp::Lt => v1 < v2,
BoolOp::Le => v1 <= v2,
BoolOp::Gt => v1 > v2,
BoolOp::Ge => v1 >= v2,
}))
}
+2 -2
View File
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
mod compare;
mod comparison;
pub use self::compare::*;
pub use self::comparison::*;
+28 -6
View File
@@ -262,7 +262,7 @@ impl<'source> Interpreter<'source> {
) -> Result<Value> {
let lhs = self.eval_expr(lhs_expr)?;
let rhs = self.eval_expr(rhs_expr)?;
Ok(builtins::compare(op, &lhs, &rhs))
builtins::compare(op, &lhs, &rhs)
}
fn eval_bin_expr(
@@ -531,10 +531,23 @@ impl<'source> Interpreter<'source> {
if loops.is_empty() {
if !stmts.is_empty() {
// Evaluate the current statement whose loop expressions have been hoisted.
if !self.eval_stmt(&stmts[0])? {
return Ok(false);
}
self.eval_stmts(&stmts[1..])
// Save the current scope and restore it after evaluating the statements so
// that the effects of the current loop iteration are cleared.
let scope_saved = match self.scopes.last() {
Some(scope) => scope.clone(),
_ => bail!("internal error: missing scope"),
};
let result = if self.eval_stmt(&stmts[0])? {
self.eval_stmts(&stmts[1..])
} else {
Ok(false)
};
match self.scopes.last_mut() {
Some(scope) => *scope = scope_saved,
_ => bail!("internal error: missing scope"),
};
result
} else {
self.eval_stmts(stmts)
}
@@ -568,7 +581,7 @@ impl<'source> Interpreter<'source> {
));
}
}
self.loop_var_values.remove(loop_expr.expr);
// Return true if at least on iteration returned true
Ok(result)
}
@@ -1617,6 +1630,15 @@ impl<'source> Interpreter<'source> {
if let Some(data) = data {
self.data = data.clone();
}
// Ensure that each module has an empty object
for m in &self.modules {
let path = Parser::get_path_ref_components(&m.package.refr)?;
let path: Vec<&str> = path.iter().map(|s| s.text()).collect();
let vref = Self::make_or_get_value_mut(&mut self.data, &path[..])?;
if *vref == Value::Undefined {
*vref = Value::new_object();
}
}
self.check_default_rules()?;
self.update_function_table()?;
+1 -1
View File
@@ -87,8 +87,8 @@ pub enum Value {
// Json data types. serde will automatically map json to these variants.
Null,
Bool(bool),
String(String),
Number(Number),
String(String),
Array(Rc<Vec<Value>>),
Object(Rc<BTreeMap<Value, Value>>),
+550 -215
View File
@@ -2,233 +2,568 @@
# Licensed under the MIT License.
cases:
- note: number-equals
- note: undefined
data: {}
modules:
- |
package test
import future.keywords.if
# All the following rules will be undefined.
x if false
v = 1
r = [
# Comparison with number
v == 1,
v == 2,
# Comparison with primitives
v == null,
v == "hello",
v == true,
v == false,
# Comparison with arrays
v == [],
v == [v],
# Comparison with sets
v == set(),
v == { v },
# Comparison with objects
v == {},
v == { "a": v},
]
sort_bindings: false
v1 = x == 1
v2 = 1 == x
v3 = x != 1
v4 = 1 != x
v5 = x < 1
v6 = 1 < x
v7 = x <= 1
v8 = 1 <= x
v9 = x > 1
v10 = 1 > x
v11 = x >= 1
v12 = 1 >= x
query: data.test
want_result:
v: 1
r: [ true, false, false, false, false, false,
false, false, false, false, false, false]
- note: null-equals
want_result: {}
- note: eq
data: {}
modules:
- |
package test
v = null
r = [
# Comparison with number
v == 1,
v == 2,
# Comparison with primitives
v == null,
v == "hello",
v == true,
v == false,
# Comparison with arrays
v == [],
v == [v],
# Comparison with sets
v == set(),
v == { v },
# Comparison with objects
v == {},
v == { "a": v},
]
sort_bindings: false
query: data.test
want_result:
v: null
r: [ false, false, true, false, false, false,
false, false, false, false, false, false]
- note: string-equals
data: {}
modules:
- |
package test
v = "hello"
r = [
# Comparison with number
v == 1,
v == 2,
# Comparison with primitives
v == null,
v == "hello",
v == true,
v == false,
# Comparison with arrays
v == [],
v == [v],
# Comparison with sets
v == set(),
v == { v },
# Comparison with objects
v == {},
v == { "a": v},
]
sort_bindings: false
query: data.test
want_result:
v: "hello"
r: [ false, false, false, true, false, false,
false, false, false, false, false, false]
- note: true-equals
data: {}
modules:
- |
package test
v = true
r = [
# Comparison with number
v == 1,
v == 2,
# Comparison with primitives
v == null,
v == "hello",
v == true,
v == false,
# Comparison with arrays
v == [],
v == [v],
# Comparison with sets
v == set(),
v == { v },
# Comparison with objects
v == {},
v == { "a": v},
]
sort_bindings: false
query: data.test
want_result:
v: true
r: [ false, false, false, false, true, false,
false, false, false, false, false, false]
- note: false-equals
data: {}
modules:
- |
package test
v = false
r = [
# Comparison with number
v == 1, # true
v == 1, # false
# Comparison with primitives
v == null,
v == "hello",
v == true,
v == false,
# Comparison with arrays
v == [],
v == [v],
# Comparison with sets
v == set(),
v == { v },
# Comparison with objects
v == {},
v == { "a": v},
]
sort_bindings: false
query: data.test
want_result:
v: false
r: [ false, false, false, false, false, true,
false, false, false, false, false, false]
- note: undefined-equals
data: {}
modules:
- |
package test
import future.keywords
v = 1 if false
r = [
# Comparison with number
v == 1,
v == 2,
# Comparison with primitives
v == null,
v == "hello",
v == true,
v == false,
# Comparison with arrays
v == [],
v == [v],
# Comparison with sets
v == set(),
v == { v },
# Comparison with objects
v == {},
v == { "a": v},
# true
values = [
1,
-1,
null,
"hello",
`hello`,
true,
false,
[],
[1, 2, 3],
[4,"abc", null],
set(),
{1, 2, 3},
{4, "abc", null},
{},
{"a" : 1},
]
r2 = 1 if v
# This variable should appear in output since it is not undefined.
ok = true
sort_bindings: false
query: data.test
values_1 = values
# Evaluate rule so that rule[x] = [ values equal to x ]
results[x] = y {
x = values[_]
y = [ z |
z := values_1[_]
x == z
]
}
query: data.test.results
want_result:
ok: true
object!:
- key: 1
value: [ 1 ]
- key: -1
value: [ -1 ]
- key: null
value: [null]
- key: "hello"
value: ["hello", "hello"]
- key: true
value: [true]
- key: false
value: [false]
- key: []
value: [[]]
- key: [1, 2, 3]
value: [[1, 2, 3]]
- key: [4, "abc", null]
value: [[4, "abc", null]]
- key:
set!: []
value:
- set!: []
- key:
set!: [ 1, 2, 3]
value:
- set!: [1, 2, 3]
- key:
set!: [4, "abc", null]
value:
- set!: [4, "abc", null]
- key:
object!: []
value:
- object!: []
- key:
object!:
- key: a
value: 1
value:
- a: 1
- note: eq-undefined
data: {}
modules:
- |
package test
import future.keywords.if
# All the following rules will be undefined.
x if false
y = x == 1
z = 1 == x
query: data.test
want_result: {}
- note: ne
data: {}
modules:
- |
package test
# true
values = [
1,
-1,
null,
"hello",
`hello`,
true,
false,
[],
[1, 2, 3],
[4,"abc", null],
set(),
{1, 2, 3},
{4, "abc", null},
{},
{"a" : 1},
]
values_1 = values
# Evaluate rule so that rule[x] = [ values equal to x ]
results[x] = y {
x = values[_]
y = [ z |
z := values_1[_]
# Use != and ne
not x != z
]
}
query: data.test.results
want_result:
object!:
- key: 1
value: [ 1 ]
- key: -1
value: [ -1 ]
- key: null
value: [null]
- key: "hello"
value: ["hello", "hello"]
- key: true
value: [true]
- key: false
value: [false]
- key: []
value: [[]]
- key: [1, 2, 3]
value: [[1, 2, 3]]
- key: [4, "abc", null]
value: [[4, "abc", null]]
- key:
set!: []
value:
- set!: []
- key:
set!: [ 1, 2, 3]
value:
- set!: [1, 2, 3]
- key:
set!: [4, "abc", null]
value:
- set!: [4, "abc", null]
- key:
object!: []
value:
- object!: []
- key:
object!:
- key: a
value: 1
value:
- a: 1
- note: order
data: {}
modules:
- |
package test
v = [
null,
false,
0,
"",
[],
{},
set()
]
results = {
"lt" : { x : y |
x = v[_]
y = [z | z = v[_]; x < z]
},
"le" : { x : y |
x = v[_]
y = [z | z = v[_]; x <= z]
},
"gt" : { x : y |
x = v[_]
y = [z | z = v[_]; x > z]
},
"ge" : { x : y |
x = v[_]
y = [z | z = v[_]; x >= z]
}
}
query: data.test.results
want_result:
lt:
object!:
- key: null
value: [false, 0, "", [], {}, {"set!":[]}]
- key: false
value: [0, "", [], {}, {"set!":[]}]
- key: 0
value: ["", [], {}, {"set!":[]}]
- key: ""
value: [[], {}, {"set!":[]}]
- key: []
value: [{}, {"set!":[]}]
- key: {}
value: [{"set!":[]}]
- key: {"set!":[]}
value: []
le:
object!:
- key: null
value: [null, false, 0, "", [], {}, {"set!":[]}]
- key: false
value: [false, 0, "", [], {}, {"set!":[]}]
- key: 0
value: [0, "", [], {}, {"set!":[]}]
- key: ""
value: ["", [], {}, {"set!":[]}]
- key: []
value: [[], {}, {"set!":[]}]
- key: {}
value: [{}, {"set!":[]}]
- key: {"set!":[]}
value: [{"set!":[]}]
gt:
object!:
- key: null
value: []
- key: false
value: [null]
- key: 0
value: [null, false]
- key: ""
value: [null, false, 0]
- key: []
value: [null, false, 0, ""]
- key: {}
value: [null, false, 0, "", []]
- key: {"set!":[]}
value: [null, false, 0, "", [], {}]
ge:
object!:
- key: null
value: [null]
- key: false
value: [null, false]
- key: 0
value: [null, false, 0]
- key: ""
value: [null, false, 0, ""]
- key: []
value: [null, false, 0, "", []]
- key: {}
value: [null, false, 0, "", [], {}]
- key: {"set!":[]}
value: [null, false, 0, "", [], {}, {"set!":[]}]
- note: arrays
data: {}
modules:
- |
package test
v = [ [], [1, 2], [0, 1, 2], [1, 2, 3]]
results = {
"lt" : { x : y |
x = v[_]
y = [z | z = v[_]; x < z]
},
"le" : { x : y |
x = v[_]
y = [z | z = v[_]; x <= z]
},
"gt" : { x : y |
x = v[_]
y = [z | z = v[_]; x > z]
},
"ge" : { x : y |
x = v[_]
y = [z | z = v[_]; x >= z]
}
}
query: data.test.results
want_result:
lt:
object!:
- key: []
value: [[1,2], [0, 1, 2], [1, 2, 3]]
- key: [1, 2]
value: [[1, 2, 3]]
- key: [0, 1, 2]
value: [ [1,2], [1, 2, 3]]
- key: [1, 2, 3]
value: []
le:
object!:
- key: []
value: [[], [1,2], [0, 1, 2], [1, 2, 3]]
- key: [1, 2]
value: [[1, 2], [1, 2, 3]]
- key: [0, 1, 2]
value: [[1,2], [0, 1, 2], [1, 2, 3]]
- key: [1, 2, 3]
value: [[1, 2, 3]]
gt:
object!:
- key: []
value: []
- key: [1, 2]
value: [[], [0, 1, 2]]
- key: [0, 1, 2]
value: [[]]
- key: [1, 2, 3]
value: [[], [1, 2], [0, 1, 2]]
ge:
object!:
- key: []
value: [[]]
- key: [1, 2]
value: [[], [1, 2], [0, 1, 2]]
- key: [0, 1, 2]
value: [[], [0, 1, 2]]
- key: [1, 2, 3]
value: [[], [1, 2], [0, 1, 2], [1, 2, 3]]
- note: sets
data: {}
modules:
- |
package test
v = [ set(), {1, 2}, {0, 1, 2}, {1, 2, 3}]
results = {
"lt" : { x : y |
x = v[_]
y = [z | z = v[_]; x < z]
},
"le" : { x : y |
x = v[_]
y = [z | z = v[_]; x <= z]
},
"gt" : { x : y |
x = v[_]
y = [z | z = v[_]; x > z]
},
"ge" : { x : y |
x = v[_]
y = [z | z = v[_]; x >= z]
}
}
query: data.test.results
want_result:
lt:
object!:
- key:
set!: []
value:
- set!: [1,2]
- set!: [0, 1, 2]
- set!: [1, 2, 3]
- key:
set!: [1, 2]
value:
- set!: [1, 2, 3]
- key:
set!: [0, 1, 2]
value:
- set!: [1,2]
- set!: [1, 2, 3]
- key:
set!: [1, 2, 3]
value: []
le:
object!:
- key:
set!: []
value:
- set!: []
- set!: [1,2]
- set!: [0, 1, 2]
- set!: [1, 2, 3]
- key:
set!: [1, 2]
value:
- set!: [1, 2]
- set!: [1, 2, 3]
- key:
set!: [0, 1, 2]
value:
- set!: [1,2]
- set!: [0, 1, 2]
- set!: [1, 2, 3]
- key:
set!: [1, 2, 3]
value:
- set!: [1, 2, 3]
gt:
object!:
- key:
set!: []
value: []
- key:
set!: [1, 2]
value:
- set!: []
- set!: [0, 1, 2]
- key:
set!: [0, 1, 2]
value:
- set!: []
- key:
set!: [1, 2, 3]
value:
- set!: []
- set!: [1, 2]
- set!: [0, 1, 2]
ge:
object!:
- key:
set!: []
value:
- set!: []
- key:
set!: [1, 2]
value:
- set!: []
- set!: [1, 2]
- set!: [0, 1, 2]
- key:
set!: [0, 1, 2]
value:
- set!: []
- set!: [0, 1, 2]
- key:
set!: [1, 2, 3]
value:
- set!: []
- set!: [1, 2]
- set!: [0, 1, 2]
- set!: [1, 2, 3]
- note: objects
data: {}
modules:
- |
package test
v = [{}, {"a":1}, {"a":2}, {"a":1, "b":0}, {"b":0}]
results = {
"lt" : { x : y |
x = v[_]
y = [z | z = v[_]; x < z]
},
"le" : { x : y |
x = v[_]
y = [z | z = v[_]; x <= z]
},
"gt" : { x : y |
x = v[_]
y = [z | z = v[_]; x > z]
},
"ge" : { x : y |
x = v[_]
y = [z | z = v[_]; x >= z]
}
}
query: data.test.results
want_result:
lt:
object!:
- key: {}
value: [ {"a":1}, {"a":2}, {"a":1, "b":0}, {"b":0}]
- key: {"a":1}
value: [{"a":2}, {"a":1, "b":0}, {"b":0}]
- key: {"a":2}
value: [{"b":0}]
- key: {"a":1, "b":0}
value: [{"a":2}, {"b":0}]
- key: {"b":0}
value: []
le:
object!:
- key: {}
value: [ {}, {"a":1}, {"a":2}, {"a":1, "b":0}, {"b":0}]
- key: {"a":1}
value: [{"a":1}, {"a":2}, {"a":1, "b":0}, {"b":0}]
- key: {"a":2}
value: [{"a":2}, {"b":0}]
- key: {"a":1, "b":0}
value: [{"a":2}, {"a":1, "b":0}, {"b":0}]
- key: {"b":0}
value: [{"b":0}]
gt:
object!:
- key: {}
value: []
- key: {"a":1}
value: [{}]
- key: {"a":2}
value: [{}, {"a":1}, {"a":1, "b":0}]
- key: {"a":1, "b":0}
value: [{}, {"a":1}]
- key: {"b":0}
value: [ {}, {"a":1}, {"a":2}, {"a":1, "b":0}]
ge:
object!:
- key: {}
value: [{}]
- key: {"a":1}
value: [{}, {"a":1}]
- key: {"a":2}
value: [{}, {"a":1}, {"a":2}, {"a":1, "b":0}]
- key: {"a":1, "b":0}
value: [{}, {"a":1}, {"a":1, "b":0}]
- key: {"b":0}
value: [ {}, {"a":1}, {"a":2}, {"a":1, "b":0}, {"b":0}]