Destructuring of arrays and objects in some-in expressions

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2023-02-21 07:19:05 -08:00
committed by Anand Krishnamoorthi
parent cf4fbdbb90
commit ae3866b1ab
3 changed files with 268 additions and 76 deletions

View File

@@ -57,19 +57,20 @@ fn is_string(span: &Span, params: &[Expr], args: &[Value]) -> Result<Value> {
Ok(Value::Bool(matches!(&args[0], Value::String(_))))
}
fn type_name(span: &Span, params: &[Expr], args: &[Value]) -> Result<Value> {
ensure_args_count(span, "type_name", params, args, 1)?;
Ok(Value::String(
match &args[0] {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
Value::Set(_) => "set",
Value::Undefined => "undefined",
}
.to_string(),
))
pub fn get_type(value: &Value) -> &str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
Value::Set(_) => "set",
Value::Undefined => "undefined",
}
}
pub fn type_name(span: &Span, params: &[Expr], args: &[Value]) -> Result<Value> {
ensure_args_count(span, "type_name", params, args, 1)?;
Ok(Value::String(get_type(&args[0]).to_string()))
}

View File

@@ -30,6 +30,7 @@ pub struct Interpreter<'source> {
processed: BTreeSet<&'source Rule<'source>>,
active_rules: Vec<&'source Rule<'source>>,
builtins_cache: BTreeMap<(&'static str, Vec<Value>), Value>,
no_rules_lookup: bool,
}
#[derive(Debug, Clone)]
@@ -64,6 +65,7 @@ impl<'source> Interpreter<'source> {
processed: BTreeSet::new(),
active_rules: vec![],
builtins_cache: BTreeMap::new(),
no_rules_lookup: false,
})
}
@@ -129,7 +131,7 @@ impl<'source> Interpreter<'source> {
// Stop path collection upon encountering the leading variable.
Expr::Var(v) => {
path.reverse();
return self.lookup_var(v.text(), &path[..]);
return self.lookup_var(v, &path[..]);
}
// Accumulate chained . field accesses.
Expr::RefDot { refr, field, .. } => {
@@ -489,119 +491,212 @@ impl<'source> Interpreter<'source> {
Ok(r)
}
fn make_bindings(
fn lookup_or_eval_expr(
&mut self,
cache: &mut BTreeMap<&'source Expr<'source>, Value>,
expr: &'source Expr<'source>,
v: &Value,
) -> Result<Value> {
match cache.get(expr) {
Some(v) => Ok(v.clone()),
_ => {
let v = self.eval_expr(expr)?;
cache.insert(expr, v.clone());
Ok(v)
}
}
}
fn make_bindings_impl(
&mut self,
is_last: bool,
type_match: &mut BTreeSet<&'source Expr<'source>>,
cache: &mut BTreeMap<&'source Expr<'source>, Value>,
expr: &'source Expr<'source>,
value: &Value,
) -> Result<bool> {
if v == &Value::Undefined {
// Propagate undefined.
if value == &Value::Undefined {
return Ok(false);
}
match expr {
Expr::Var(ident) => {
self.add_variable(ident.text(), v.clone())?;
let span = expr.span();
let raise_error = is_last && type_match.get(expr).is_none();
match (expr, value) {
(Expr::Var(ident), _) => {
self.add_variable(ident.text(), value.clone())?;
Ok(true)
}
Expr::Array { .. } => {
let expr_v = self.eval_expr(expr)?;
if expr_v == Value::Undefined {
// Destructure arrays
(Expr::Array { items, .. }, Value::Array(a)) => {
if items.len() != a.len() {
if raise_error {
return Err(span.error(
format!(
"array length mismatch. Expected {} got {}.",
items.len(),
a.len()
)
.as_str(),
));
}
return Ok(false);
}
Ok(&expr_v == v)
type_match.insert(expr);
for (idx, item) in items.iter().enumerate() {
self.make_bindings(is_last, type_match, cache, item, &a[idx])?;
}
Ok(true)
}
Expr::Object { .. } => {
let expr_v = self.eval_expr(expr)?;
if expr_v == Value::Undefined {
return Ok(false);
// Destructure objects
(Expr::Object { fields, .. }, Value::Object(_)) => {
for (_, key_expr, value_expr) in fields.iter() {
// Rego does not support bindings in keys.
// Therefore, just eval key_expr.
let key = self.lookup_or_eval_expr(cache, key_expr)?;
let field_value = &value[&key];
if field_value == &Value::Undefined {
if raise_error {}
return Ok(false);
}
// Match patterns in value_expr
self.make_bindings(is_last, type_match, cache, value_expr, field_value)?;
}
Ok(&expr_v == v)
Ok(true)
}
_ => {
let expr_v = match cache.get(expr) {
Some(v) => v.clone(),
None => {
let v = self.eval_expr(expr)?;
cache.insert(expr, v.clone());
v
}
};
if expr_v == Value::Undefined {
let expr_value = self.lookup_or_eval_expr(cache, expr)?;
if expr_value == Value::Undefined {
return Ok(false);
}
// TODO: type checking
Ok(&expr_v == v)
if raise_error {
let expr_t = builtins::types::get_type(&expr_value);
let value_t = builtins::types::get_type(value);
if expr_t != value_t {
return Err(span.error(
format!("Cannot bind pattern of type `{expr_t}` with value of type `{value_t}`. Value is {value}.").as_str()));
}
}
type_match.insert(expr);
Ok(&expr_value == value)
}
}
}
fn make_bindings(
&mut self,
is_last: bool,
type_match: &mut BTreeSet<&'source Expr<'source>>,
cache: &mut BTreeMap<&'source Expr<'source>, Value>,
expr: &'source Expr<'source>,
value: &Value,
) -> Result<bool> {
let prev = self.no_rules_lookup;
self.no_rules_lookup = true;
let r = self.make_bindings_impl(is_last, type_match, cache, expr, value);
self.no_rules_lookup = prev;
r
}
fn make_key_value_bindings(
&mut self,
is_last: bool,
type_match: &mut BTreeSet<&'source Expr<'source>>,
cache: &mut BTreeMap<&'source Expr<'source>, Value>,
exprs: (&'source Option<Expr<'source>>, &'source Expr<'source>),
values: (&Value, &Value),
) -> Result<bool> {
let (key_expr, value_expr) = exprs;
let (key, value) = values;
if let Some(key_expr) = key_expr {
if !self.make_bindings(is_last, type_match, cache, key_expr, key)? {
return Ok(false);
}
}
self.make_bindings(is_last, type_match, cache, value_expr, value)
}
fn eval_some_in(
&mut self,
_span: &'source Span<'source>,
key: &'source Option<Expr<'source>>,
value: &'source Expr<'source>,
key_expr: &'source Option<Expr<'source>>,
value_expr: &'source Expr<'source>,
collection: &'source Expr<'source>,
stmts: &'source [LiteralStmt<'source>],
) -> Result<bool> {
let scope_saved = self.current_scope()?.clone();
let mut type_match = BTreeSet::new();
let mut cache = BTreeMap::new();
let mut count = 0;
match self.eval_expr(collection)? {
Value::Array(a) => {
for (k, v) in a.iter().enumerate() {
if let Some(key) = key {
if !self.make_bindings(&mut cache, key, &Value::from_float(k as Float))? {
continue;
}
}
if !self.make_bindings(&mut cache, value, v)? {
for (idx, value) in a.iter().enumerate() {
if !self.make_key_value_bindings(
idx == a.len() - 1,
&mut type_match,
&mut cache,
(key_expr, value_expr),
(&Value::from_float(idx as Float), value),
)? {
continue;
}
let r = self.eval_stmts(stmts)?;
*self.current_scope_mut()? = scope_saved.clone();
if r {
if self.eval_stmts(stmts)? {
count += 1;
}
*self.current_scope_mut()? = scope_saved.clone();
}
}
Value::Set(s) => {
for v in s.iter() {
if !self.make_bindings(&mut cache, value, v)? {
for (idx, value) in s.iter().enumerate() {
if !self.make_key_value_bindings(
idx == s.len() - 1,
&mut type_match,
&mut cache,
(key_expr, value_expr),
(value, value),
)? {
continue;
}
if let Some(key) = key {
if !self.make_bindings(&mut cache, key, v)? {
continue;
}
}
let r = self.eval_stmts(stmts)?;
*self.current_scope_mut()? = scope_saved.clone();
if r {
if self.eval_stmts(stmts)? {
count += 1;
}
*self.current_scope_mut()? = scope_saved.clone();
}
}
Value::Object(o) => {
for (k, v) in o.iter() {
if !self.make_bindings(&mut cache, value, v)? {
for (idx, (key, value)) in o.iter().enumerate() {
if !self.make_key_value_bindings(
idx == o.len() - 1,
&mut type_match,
&mut cache,
(key_expr, value_expr),
(key, value),
)? {
continue;
}
if let Some(key) = key {
if !self.make_bindings(&mut cache, key, k)? {
continue;
}
}
let r = self.eval_stmts(stmts)?;
*self.current_scope_mut()? = scope_saved.clone();
if r {
if self.eval_stmts(stmts)? {
count += 1;
}
*self.current_scope_mut()? = scope_saved.clone();
}
}
Value::Undefined => (),
v => {
let span = collection.span();
bail!(span.error(
format!("`some .. in collection` expects a collection. Got {v}").as_str()
format!("`some .. in collection` expects array/set/object. Got `{v}`").as_str()
))
}
}
@@ -1253,7 +1348,9 @@ impl<'source> Interpreter<'source> {
Ok(())
}
fn lookup_var(&mut self, name: &str, fields: &[&str]) -> Result<Value> {
fn lookup_var(&mut self, span: &'source Span<'source>, fields: &[&str]) -> Result<Value> {
let name = span.text();
// Return local variable/argument.
if let Some(v) = self.lookup_local_var(name) {
return Ok(Self::get_value_chained(v, fields));
@@ -1264,6 +1361,11 @@ impl<'source> Interpreter<'source> {
return Ok(Self::get_value_chained(self.input.clone(), fields));
}
// TODO: should we return before checking for input?
if self.no_rules_lookup {
return Err(span.error("undefined var"));
}
// Ensure that rules are evaluated
if name == "data" {
// Evaluate rule corresponding to longest matching path.

View File

@@ -9,17 +9,106 @@ cases:
import future.keywords
r1 = y {
# value
y1 = [x | some x in [1, 2, 3]; x >= 2]
# key and value
y2 = [y | some x, y in [1, 2, 3]; x >= 2]
y = [y1, y2]
}
r2[x] {
# Constant value
some x, "l" in ["h", "e", "l", "l", "o"]
}
r3[x] {
# Constant value
some 1, x in ["h", "e", "l", "l", "o"]
}
r4[x] {
# Extract array element
some [5, x] in [[0, 1], [5, 1], [5, 2]]
}
r5[x+y] {
# Extract multiple array elements
some [5, x, y, 9] in [[0, 1], [5, 1, 2, 9], [5, 2, 8, 9]]
}
# Undefined
r6 {
some x in []
}
r7 {
some x in r6
}
query: data.test
want_result:
r1: [ [2, 3], [3]]
r2:
set!: [2, 3]
r3:
set!: ["e"]
r4:
set!: [1, 2]
r5:
set!: [3, 10]
- note: type-mismatch
data: {}
modules:
- |
package test
import future.keywords
x { some [1] in [1] }
query: data.test
error: "Cannot bind pattern of type `array`"
- note: array-length-mismatch
data: {}
modules:
- |
package test
import future.keywords
x { some [1] in [[1, 2]] }
query: data.test
error: "array length mismatch. Expected 1 got 2."
- note: array-length-mismatch-skipped
data: {}
modules:
- |
package test
import future.keywords
x { some [1] in [[1, 2], [1]] }
query: data.test
want_result:
x: true
- note: invalid-null
data: {}
modules: ["package test\nimport future.keywords\nx { some _ in null}"]
query: data.test
error: "`some .. in collection` expects array/set/object."
- note: invalid-bool
data: {}
modules: ["package test\nimport future.keywords\nx { some _ in false}"]
query: data.test
error: "`some .. in collection` expects array/set/object."
- note: invalid-number
data: {}
modules: ["package test\nimport future.keywords\nx { some _ in 0}"]
query: data.test
error: "`some .. in collection` expects array/set/object."
- note: invalid-string
data: {}
modules: ["package test\nimport future.keywords\nx { some _ in \"\"}"]
query: data.test
error: "`some .. in collection` expects array/set/object."