From b5434962bef6001feb2207490ba12ddc58ca1623 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Fri, 20 Aug 2021 19:44:57 +0000 Subject: [PATCH] feat: lazy evaluation of function default params Fixes #59 --- --- a/crates/jrsonnet-evaluator/src/ctx.rs +++ b/crates/jrsonnet-evaluator/src/ctx.rs @@ -73,6 +73,9 @@ .cloned() .ok_or(VariableIsNotDefined(name))?) } + pub fn contains_binding(&self, name: IStr) -> bool { + self.0.bindings.contains_key(&name) + } pub fn into_future(self, ctx: FutureWrapper) -> Self { { ctx.0.borrow_mut().replace(self); --- a/crates/jrsonnet-evaluator/src/error.rs +++ b/crates/jrsonnet-evaluator/src/error.rs @@ -56,7 +56,7 @@ BindingParameterASecondTime(IStr), #[error("too many args, function has {0}")] TooManyArgsFunctionHas(usize), - #[error("founction argument is not passed: {0}")] + #[error("function argument is not passed: {0}")] FunctionParameterNotBoundInCall(IStr), #[error("external variable is not defined: {0}")] --- a/crates/jrsonnet-evaluator/src/function.rs +++ b/crates/jrsonnet-evaluator/src/function.rs @@ -1,4 +1,7 @@ -use crate::{error::Error::*, evaluate, throw, Context, LazyVal, LazyValValue, Result, Val}; +use crate::{ + error::Error::*, evaluate, evaluate_named, throw, Context, FutureWrapper, LazyVal, + LazyValValue, Result, Val, +}; use jrsonnet_gc::Trace; use jrsonnet_interner::IStr; use jrsonnet_parser::{ArgsDesc, LocExpr, ParamsDesc}; @@ -8,6 +11,18 @@ const NO_DEFAULT_CONTEXT: &str = "no default context set for call with defined default parameter value"; +#[derive(Trace)] +#[trivially_drop] +struct EvaluateLazyVal { + context: Context, + expr: LocExpr, +} +impl LazyValValue for EvaluateLazyVal { + fn get(self: Box) -> Result { + evaluate(self.context, &self.expr) + } +} + /// Creates correct [context](Context) for function body evaluation returning error on invalid call. /// /// ## Parameters @@ -18,64 +33,119 @@ /// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily pub fn parse_function_call( ctx: Context, - body_ctx: Option, + body_ctx: Context, params: &ParamsDesc, args: &ArgsDesc, tailstrict: bool, ) -> Result { - let mut out = HashMap::with_capacity_and_hasher(params.len(), BuildHasherDefault::default()); - let mut positioned_args = vec![None; params.0.len()]; - for (id, arg) in args.iter().enumerate() { - let idx = if let Some(name) = &arg.0 { - params - .iter() - .position(|p| *p.0 == *name) - .ok_or_else(|| UnknownFunctionParameter(name.clone()))? - } else { - id - }; + let mut passed_args = + HashMap::with_capacity_and_hasher(params.len(), BuildHasherDefault::default()); + if args.unnamed.len() > params.len() { + throw!(TooManyArgsFunctionHas(params.len())) + } + + let mut filled_args = 0; - if idx >= params.len() { - throw!(TooManyArgsFunctionHas(params.len())); + for (id, arg) in args.unnamed.iter().enumerate() { + let name = params[id].0.clone(); + passed_args.insert( + name, + if tailstrict { + LazyVal::new_resolved(evaluate(ctx.clone(), arg)?) + } else { + LazyVal::new(Box::new(EvaluateLazyVal { + context: ctx.clone(), + expr: arg.clone(), + })) + }, + ); + filled_args += 1; + } + + for (name, value) in args.named.iter() { + // FIXME: O(n) for arg existence check + if !params.iter().any(|p| &p.0 == name) { + throw!(UnknownFunctionParameter((name as &str).to_owned())); } - if positioned_args[idx].is_some() { - throw!(BindingParameterASecondTime(params[idx].0.clone())); + if passed_args + .insert( + name.clone(), + if tailstrict { + LazyVal::new_resolved(evaluate(ctx.clone(), value)?) + } else { + LazyVal::new(Box::new(EvaluateLazyVal { + context: ctx.clone(), + expr: value.clone(), + })) + }, + ) + .is_some() + { + throw!(BindingParameterASecondTime(name.clone())); } - positioned_args[idx] = Some(arg.1.clone()); + filled_args += 1; } - // Fill defaults - for (id, p) in params.iter().enumerate() { - let (ctx, expr) = if let Some(arg) = &positioned_args[id] { - (ctx.clone(), arg) - } else if let Some(default) = &p.1 { - (body_ctx.clone().expect(NO_DEFAULT_CONTEXT), default) - } else { - throw!(FunctionParameterNotBoundInCall(p.0.clone())); - }; - let val = if tailstrict { - LazyVal::new_resolved(evaluate(ctx, expr)?) - } else { + + if filled_args < params.len() { + // Some args are unset, but maybe we have defaults for them + // Default values should be created in newly created context + let future_context = FutureWrapper::::new(); + let mut defaults = HashMap::with_capacity_and_hasher( + params.len() - filled_args, + BuildHasherDefault::default(), + ); + + for param in params.iter().filter(|p| p.1.is_some()) { + if passed_args.contains_key(¶m.0.clone()) { + continue; + } #[derive(Trace)] #[trivially_drop] - struct EvaluateLazyVal { - context: Context, - expr: LocExpr, + struct LazyNamedBinding { + future_context: FutureWrapper, + name: IStr, + value: LocExpr, } - impl LazyValValue for EvaluateLazyVal { + impl LazyValValue for LazyNamedBinding { fn get(self: Box) -> Result { - evaluate(self.context, &self.expr) + evaluate_named(self.future_context.unwrap(), &self.value, self.name) } } + LazyVal::new(Box::new(LazyNamedBinding { + future_context: future_context.clone(), + name: param.0.clone(), + value: param.1.clone().unwrap(), + })); - LazyVal::new(Box::new(EvaluateLazyVal { - context: ctx.clone(), - expr: expr.clone(), - })) - }; - out.insert(p.0.clone(), val); - } + defaults.insert( + param.0.clone(), + LazyVal::new(Box::new(LazyNamedBinding { + future_context: future_context.clone(), + name: param.0.clone(), + value: param.1.clone().unwrap(), + })), + ); + filled_args += 1; + } + + // Some args still wasn't filled + if filled_args != params.len() { + for param in params.iter().skip(args.unnamed.len()) { + if !args.named.iter().any(|a| a.0 == param.0) { + throw!(FunctionParameterNotBoundInCall(param.0.clone())); + } + } + unreachable!(); + } - Ok(body_ctx.unwrap_or(ctx).extend(out, None, None, None)) + Ok(body_ctx + .extend(passed_args, None, None, None) + .extend_bound(defaults) + .into_future(future_context)) + } else { + let body_ctx = body_ctx.extend(passed_args, None, None, None); + Ok(body_ctx) + } } pub fn parse_function_call_map( @@ -176,21 +246,25 @@ use $crate::{error::Error::*, throw, evaluate, push_stack_frame, typed::CheckType}; let args = $args; - if args.len() > $total_args { + if args.unnamed.len() + args.named.len() > $total_args { throw!(TooManyArgsFunctionHas($total_args)); } $( - if args.len() <= $id { + if args.unnamed.len() + args.named.len() <= $id { throw!(FunctionParameterNotBoundInCall(stringify!($name).into())); } - let $name = &args[$id]; - if $name.0.is_some() { - if $name.0.as_ref().unwrap() != stringify!($name) { + // Is named + let $name = if $id >= $args.unnamed.len() { + let named = &args.named[$id - $args.unnamed.len()]; + if &named.0 != stringify!($name) { throw!(IntrinsicArgumentReorderingIsNotSupportedYet); } - } + &named.1 + } else { + &$args.unnamed[$id] + }; let $name = push_stack_frame(None, || format!("evaluating argument"), || { - let value = evaluate($ctx.clone(), &$name.1)?; + let value = evaluate($ctx.clone(), &$name)?; $ty.check(&value)?; Ok(value) })?; --- a/crates/jrsonnet-evaluator/src/map.rs +++ b/crates/jrsonnet-evaluator/src/map.rs @@ -29,6 +29,16 @@ .get(key) .or_else(|| self.0.parent.as_ref().and_then(|p| p.get(key))) } + + pub fn contains_key(&self, key: &IStr) -> bool { + (self.0).current.contains_key(key) + || self + .0 + .parent + .as_ref() + .map(|p| p.contains_key(key)) + .unwrap_or(false) + } } impl Clone for LayeredHashMap { --- a/crates/jrsonnet-evaluator/src/val.rs +++ b/crates/jrsonnet-evaluator/src/val.rs @@ -11,7 +11,7 @@ }; use jrsonnet_gc::{Gc, GcCell, Trace}; use jrsonnet_interner::IStr; -use jrsonnet_parser::{el, Arg, ArgsDesc, Expr, ExprLocation, LiteralType, LocExpr, ParamsDesc}; +use jrsonnet_parser::{el, ArgsDesc, Expr, ExprLocation, LiteralType, LocExpr, ParamsDesc}; use jrsonnet_types::ValType; use std::{collections::HashMap, fmt::Debug, rc::Rc}; @@ -127,7 +127,7 @@ Self::Normal(func) => { let ctx = parse_function_call( call_ctx, - Some(func.ctx.clone()), + func.ctx.clone(), &func.params, args, tailstrict, @@ -136,7 +136,8 @@ } Self::Intrinsic(name) => call_builtin(call_ctx, loc, name, args), Self::NativeExt(_name, handler) => { - let args = parse_function_call(call_ctx, None, &handler.params, args, true)?; + let args = + parse_function_call(call_ctx, Context::new(), &handler.params, args, true)?; let mut out_args = Vec::with_capacity(handler.params.len()); for p in handler.params.0.iter() { out_args.push(args.binding(p.0.clone())?.evaluate()?); @@ -554,17 +555,17 @@ el!(Expr::Var("std".into())), el!(Expr::Str("manifestYamlDoc".into())) )), - ArgsDesc(vec![ - Arg(None, el!(Expr::Var("__tmp__to_json__".into()))), - Arg( - None, + ArgsDesc::new( + vec![ + el!(Expr::Var("__tmp__to_json__".into())), el!(Expr::Literal(if padding != 0 { LiteralType::True } else { LiteralType::False - })) - ) - ]), + })), + ], + vec![] + ), false )), )? --- a/crates/jrsonnet-parser/src/expr.rs +++ b/crates/jrsonnet-parser/src/expr.rs @@ -194,18 +194,13 @@ #[cfg_attr(feature = "deserialize", derive(Deserialize))] #[derive(Debug, PartialEq, Trace)] #[trivially_drop] -pub struct Arg(pub Option, pub LocExpr); - -#[cfg_attr(feature = "serialize", derive(Serialize))] -#[cfg_attr(feature = "deserialize", derive(Deserialize))] -#[derive(Debug, PartialEq, Trace)] -#[trivially_drop] -pub struct ArgsDesc(pub Vec); - -impl Deref for ArgsDesc { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 +pub struct ArgsDesc { + pub unnamed: Vec, + pub named: Vec<(IStr, LocExpr)>, +} +impl ArgsDesc { + pub fn new(unnamed: Vec, named: Vec<(IStr, LocExpr)>) -> Self { + Self { unnamed, named } } } --- a/crates/jrsonnet-parser/src/lib.rs +++ b/crates/jrsonnet-parser/src/lib.rs @@ -7,6 +7,7 @@ }; mod expr; pub use expr::*; +pub use jrsonnet_interner::IStr; pub use peg; pub struct ParserSettings { @@ -70,19 +71,29 @@ } / { expr::ParamsDesc(Rc::new(Vec::new())) } - pub rule arg(s: &ParserSettings) -> expr::Arg - = name:$(id()) _ "=" _ expr:expr(s) {expr::Arg(Some(name.into()), expr)} - / expr:expr(s) {expr::Arg(None, expr)} + pub rule arg(s: &ParserSettings) -> (Option, LocExpr) + = quiet! { name:(s:$(id()) _ "=" _ {s})? expr:expr(s) {(name.map(Into::into), expr)} } + / expected!("") + pub rule args(s: &ParserSettings) -> expr::ArgsDesc - = args:arg(s) ** comma() comma()? { + = args:arg(s)**comma() comma()? {? + let unnamed_count = args.iter().take_while(|(n, _)| n.is_none()).count(); + let mut unnamed = Vec::with_capacity(unnamed_count); + let mut named = Vec::with_capacity(args.len() - unnamed_count); let mut named_started = false; - for arg in &args { - named_started = named_started || arg.0.is_some(); - assert_eq!(named_started, arg.0.is_some(), "named args should be used after all positionals"); + for (name, value) in args { + if let Some(name) = name { + named_started = true; + named.push((name, value)); + } else { + if named_started { + return Err("") + } + unnamed.push(value); + } } - expr::ArgsDesc(args) + Ok(expr::ArgsDesc::new(unnamed, named)) } - / { expr::ArgsDesc(Vec::new()) } pub rule bind(s: &ParserSettings) -> expr::BindSpec = name:$(id()) _ "=" _ expr:expr(s) {expr::BindSpec{name:name.into(), params: None, value: expr}} @@ -493,7 +504,7 @@ el!(ArrComp( el!(Apply( el!(Index(el!(Var("std".into())), el!(Str("deepJoin".into())))), - ArgsDesc(vec![Arg(None, el!(Var("x".into())))]), + ArgsDesc::new(vec![el!(Var("x".into()))], vec![]), false, )), vec![CompSpec::ForSpec(ForSpecData( -- gitstuff