From 53030c4adaec309587914110a4f006a8e498f868 Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi Date: Sun, 12 Feb 2023 17:09:23 -0800 Subject: [PATCH] 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 --- src/builtins/compare.rs | 30 - src/builtins/comparison.rs | 60 ++ src/builtins/mod.rs | 4 +- src/interpreter.rs | 34 +- src/value.rs | 2 +- tests/interpreter/cases/builtins/compare.yaml | 765 +++++++++++++----- 6 files changed, 641 insertions(+), 254 deletions(-) delete mode 100644 src/builtins/compare.rs create mode 100644 src/builtins/comparison.rs diff --git a/src/builtins/compare.rs b/src/builtins/compare.rs deleted file mode 100644 index 747b4b5..0000000 --- a/src/builtins/compare.rs +++ /dev/null @@ -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), - } -} diff --git a/src/builtins/comparison.rs b/src/builtins/comparison.rs new file mode 100644 index 0000000..50c72c3 --- /dev/null +++ b/src/builtins/comparison.rs @@ -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 { + // 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, + })) +} diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index 824402e..cd73d89 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -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::*; diff --git a/src/interpreter.rs b/src/interpreter.rs index 14adf22..eb4dc27 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -262,7 +262,7 @@ impl<'source> Interpreter<'source> { ) -> Result { 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()?; diff --git a/src/value.rs b/src/value.rs index 5d07639..22510ed 100644 --- a/src/value.rs +++ b/src/value.rs @@ -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>), Object(Rc>), diff --git a/tests/interpreter/cases/builtins/compare.yaml b/tests/interpreter/cases/builtins/compare.yaml index b1b7798..3260acc 100644 --- a/tests/interpreter/cases/builtins/compare.yaml +++ b/tests/interpreter/cases/builtins/compare.yaml @@ -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}] + +