diff --git a/src/interpreter.rs b/src/interpreter.rs index 463bed0..ebc91d8 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -26,7 +26,7 @@ pub struct Interpreter<'source> { // TODO: handle recursive calls where same expr could have different values. loop_var_values: BTreeMap<&'source Expr<'source>, Value>, contexts: Vec>, - functions: HashMap>, + functions: HashMap>>, rules: HashMap>>, default_rules: HashMap, Option)>>, processed: BTreeSet<&'source Rule<'source>>, @@ -1180,7 +1180,7 @@ impl<'source> Interpreter<'source> { } } - fn lookup_function(&self, fcn: &'source Expr<'source>) -> Result<&'source Rule<'source>> { + fn lookup_function(&self, fcn: &'source Expr<'source>) -> Result<&Vec<&'source Rule<'source>>> { let mut path = Self::get_path_string(fcn, None)?; if !path.starts_with("data.") { path = self.current_module_path.clone() + "." + &path; @@ -1239,7 +1239,7 @@ impl<'source> Interpreter<'source> { fcn: &'source Expr<'source>, params: &'source Vec>, ) -> Result { - let fcn_rule = match self.lookup_function(fcn) { + let fcns_rules = match self.lookup_function(fcn) { Ok(r) => r, _ => { // Look up builtin function. @@ -1256,69 +1256,106 @@ impl<'source> Interpreter<'source> { } }; - let (args, output_expr, bodies) = match fcn_rule { - Rule::Spec { - head: RuleHead::Func { args, assign, .. }, - bodies, - .. - } => (args, assign.as_ref().map(|a| &a.value), bodies), - _ => bail!("internal error not a function"), - }; + let fcns = fcns_rules.clone(); - if args.len() != params.len() { + let mut results: Vec = Vec::new(); + let mut errors: Vec = Vec::new(); + for fcn_rule in fcns { + let (args, output_expr, bodies) = match fcn_rule { + Rule::Spec { + head: RuleHead::Func { args, assign, .. }, + bodies, + .. + } => (args, assign.as_ref().map(|a| &a.value), bodies), + _ => bail!("internal error not a function"), + }; + + if args.len() != params.len() { + return Err(span.source.error( + span.line, + span.col, + format!( + "mismatch in number of arguments. supplied {}, expected {}", + params.len(), + args.len() + ) + .as_str(), + )); + } + + let mut args_scope = Scope::new(); + for (idx, a) in args.iter().enumerate() { + let a = match a { + Expr::Var(s) => s.text(), + _ => unimplemented!("destructuring function arguments"), + }; + //TODO: check call in params + args_scope.insert(a.to_string(), self.eval_expr(¶ms[idx])?); + } + + let ctx = Context { + key_expr: None, + output_expr, + value: Value::new_set(), + }; + + // Back up local variables of current function and empty + // the local variables of callee function. + let scopes = std::mem::take(&mut self.scopes); + + // Set the arguments scope. + self.scopes.push(args_scope); + 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(e); + continue; + } + }; + + let result = match &value { + Value::Set(s) if s.len() == 1 => s.iter().next().unwrap().clone(), + Value::Set(s) if !s.is_empty() => { + return Err(span.source.error( + span.line, + span.col, + format!("function produced multiple outputs {value:?}").as_str(), + )) + } + // If the function successfully executed, but did not return any value, then return true. + 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, + _ => bail!("internal error: function did not return set {value:?}"), + }; + + // Restore local variables for current context. + self.scopes = scopes; + + if result != Value::Undefined { + results.push(result); + } + } + + if results.is_empty() { + if errors.is_empty() { + return Ok(Value::Undefined); + } else { + return Err(anyhow!(errors[0].to_string())); + } + } + + // all defined values should be the equal to the same value that should be returned + if !results.windows(2).all(|w| w[0] == w[1]) { return Err(span.source.error( span.line, span.col, - format!( - "mismatch in number of arguments. supplied {}, expected {}", - params.len(), - args.len() - ) - .as_str(), + "functions must not produce multiple outputs for same inputs", )); } - - let mut args_scope = Scope::new(); - for (idx, a) in args.iter().enumerate() { - let a = match a { - Expr::Var(s) => s.text(), - _ => unimplemented!("destructuring function arguments"), - }; - //TODO: check call in params - args_scope.insert(a.to_string(), self.eval_expr(¶ms[idx])?); - } - - let ctx = Context { - key_expr: None, - output_expr, - value: Value::new_set(), - }; - - // Back up local variables of current function and empty - // the local variables of callee function. - 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)?; - let result = match &value { - Value::Set(s) if s.len() == 1 => Ok(s.iter().next().unwrap().clone()), - Value::Set(s) if !s.is_empty() => Err(span.source.error( - span.line, - span.col, - format!("function produced multiple outputs {value:?}").as_str(), - )), - // If the function successfully executed, but did not return any value, then return true. - Value::Set(s) if s.is_empty() && output_expr.is_none() => Ok(Value::Bool(true)), - - // If the function execution resulted in undefined, then propagate it. - Value::Undefined => Ok(Value::Undefined), - _ => bail!("internal error: function did not return set {value:?}"), - }; - - // Restore local variables for current context. - self.scopes = scopes; - result + Ok(results[0].clone()) } fn lookup_local_var(&self, name: &str) -> Option { @@ -1700,7 +1737,12 @@ impl<'source> Interpreter<'source> { } let full_path = Self::get_path_string(refr, Some(module_path.as_str()))?; - self.functions.insert(full_path, rule); + + if let Some(functions) = self.functions.get_mut(&full_path) { + functions.push(rule); + } else { + self.functions.insert(full_path, vec![rule]); + } } } self.set_current_module(prev_module)?; diff --git a/tests/interpreter/cases/call/basic.yaml b/tests/interpreter/cases/call/basic.yaml index 70a07bf..012df0d 100644 --- a/tests/interpreter/cases/call/basic.yaml +++ b/tests/interpreter/cases/call/basic.yaml @@ -45,3 +45,17 @@ cases: want_result: a: -11 + - note: call-return-undefined + data: {} + modules: + - | + package test + + inc(x) = y { + x > 10 # This will evaluate to false. + y = 100 # y will be undefined. + } + + a1 = inc(5) + query: data.test + want_result: {} diff --git a/tests/interpreter/cases/call/or.yaml b/tests/interpreter/cases/call/or.yaml new file mode 100644 index 0000000..b63b6bd --- /dev/null +++ b/tests/interpreter/cases/call/or.yaml @@ -0,0 +1,154 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: or-error-output + data: {} + modules: + - | + package test + + inc(x) = x + 1 + inc(x) = x + 4 + + a1 = inc(5) + query: data.test + error: functions must not produce multiple outputs for same inputs + + - note: or-ok + data: {} + modules: + - | + package test + + inc(x) = x + 1 + inc(x) = x + 1 + + a1 = inc(5) + query: data.test + want_result: + a1: 6 + + - note: or-true + data: {} + modules: + - | + package test + + inc(x) = 7 == x + inc(x) = 7 == x + + a1 = inc(7) + query: data.test + want_result: + a1: true + + - note: or-false + data: {} + modules: + - | + package test + + inc(x) = 8 == x + inc(x) = 3 == x + + a1 = inc(2) + query: data.test + want_result: + a1: false + + - note: or-undefined + data: {} + modules: + - | + package test + + 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) + query: data.test + want_result: + a1: 6 + + - note: or-first-undefined + data: {} + modules: + - | + package test + + inc(x) = y { + x > 10 # This will evaluate to false. + y = 100 # y will be undefined. + } + inc(x) = x + 1 + inc(x) = x + 1 + + a1 = inc(5) + query: data.test + want_result: + a1: 6 + + - note: or-all-undefined + data: {} + modules: + - | + package test + + inc(x1) = y1 { + x1 > 10 # This will evaluate to false. + y1 = 100 # y will be undefined. + } + inc(x2) = y2 { + x2 > 10 # This will evaluate to false. + y2 = 100 # y will be undefined. + } + + a1 = inc(5) + query: data.test + want_result: {} + + - note: or-one-error + data: {} + modules: + - | + package test + + fcn(x) = y { + y = x + 1 + } + + fcn(x) = y { + y = concat(" ", ["hello", x]) + } + + a1 = fcn("world") + a2 = fcn(5) + query: data.test + want_result: + a1: + set!: [hello world] + a2: + set!: [6] + + - note: or-all-error + data: {} + modules: + - | + package test + + fcn(x) = y { + y = x + 1 + } + + fcn(x) = y { + y = x + 1 + } + + a1 = fcn("world") + query: data.test + error: "`add` expects numeric argument. Got `\"world\"` instead" \ No newline at end of file