--- a/crates/jrsonnet-evaluator/src/function/builtin.rs +++ b/crates/jrsonnet-evaluator/src/function/builtin.rs @@ -3,8 +3,8 @@ use jrsonnet_gcmodule::{cc_dyn, Trace, TraceBox}; use jrsonnet_parser::function::{FunctionSignature, ParamDefault, ParamName, ParamParse}; -use super::{arglike::ArgsLike, parse::parse_builtin_call, CallLocation}; -use crate::{Context, Result, Val}; +use super::CallLocation; +use crate::{Result, Thunk, Val}; #[macro_export] macro_rules! params { @@ -34,8 +34,8 @@ self.0.params() } - fn call(&self, ctx: Context, loc: CallLocation<'_>, args: &dyn ArgsLike) -> Result { - self.0.call(ctx, loc, args) + fn call(&self, loc: CallLocation<'_>, args: &[Option>]) -> Result { + self.0.call(loc, args) } fn as_any(&self) -> &dyn Any { @@ -52,7 +52,7 @@ /// Parameter names for named calls fn params(&self) -> FunctionSignature; /// Call the builtin - fn call(&self, ctx: Context, loc: CallLocation<'_>, args: &dyn ArgsLike) -> Result; + fn call(&self, loc: CallLocation<'_>, args: &[Option>]) -> Result; fn as_any(&self) -> &dyn Any; } @@ -96,11 +96,10 @@ self.params.clone() } - fn call(&self, ctx: Context, _loc: CallLocation<'_>, args: &dyn ArgsLike) -> Result { - let args = parse_builtin_call(ctx, self.params.clone(), args, true)?; + fn call(&self, _loc: CallLocation<'_>, args: &[Option>]) -> Result { let args = args .into_iter() - .map(|a| a.expect("legacy natives have no default params")) + .map(|a| a.as_ref().expect("legacy natives have no default params")) .map(|a| a.evaluate()) .collect::>>()?; self.handler.call(&args) --- a/crates/jrsonnet-evaluator/src/function/mod.rs +++ b/crates/jrsonnet-evaluator/src/function/mod.rs @@ -11,7 +11,8 @@ arglike::OptionalContext, builtin::{Builtin, StaticBuiltin}, native::NativeDesc, - parse::{parse_default_function_call, parse_function_call}, + parse::{parse_builtin_call, parse_default_function_call, parse_function_call}, + prepared::{parse_prepared_builtin_call, parse_prepared_function_call, PreparedCall}, }; use crate::{ bail, error::ErrorKind::*, evaluate, evaluate_trivial, function::builtin::BuiltinFunc, Context, @@ -22,7 +23,9 @@ pub mod builtin; pub mod native; pub mod parse; -pub mod prepared; +mod prepared; + +pub use prepared::PreparedFuncVal; pub use jrsonnet_parser::function::*; @@ -173,7 +176,6 @@ tailstrict: bool, ) -> Result { match self { - Self::Id => ID.call(call_ctx, loc, args), Self::Normal(func) => { let body_ctx = func.call_body_context(call_ctx, args, tailstrict)?; evaluate(body_ctx, &func.body) @@ -184,8 +186,18 @@ } thunk.evaluate() } - Self::StaticBuiltin(b) => b.call(call_ctx, loc, args), - Self::Builtin(b) => b.call(call_ctx, loc, args), + Self::Id => { + let args = parse_builtin_call(call_ctx, ID.params(), args, tailstrict)?; + ID.call(loc, &args) + } + Self::StaticBuiltin(b) => { + let args = parse_builtin_call(call_ctx, b.params(), args, tailstrict)?; + b.call(loc, &args) + } + Self::Builtin(b) => { + let args = parse_builtin_call(call_ctx, b.params(), args, tailstrict)?; + b.call(loc, &args) + } } } pub fn evaluate_simple( @@ -200,6 +212,41 @@ tailstrict, ) } + + pub(crate) fn evaluate_prepared( + &self, + prepared: &PreparedCall, + loc: CallLocation<'_>, + unnamed: &[Thunk], + named: &[Thunk], + _tailstrict: bool, + ) -> Result { + match self { + FuncVal::Id => { + let args = parse_prepared_builtin_call(prepared, ID.params(), unnamed, named)?; + ID.call(loc, &args) + } + FuncVal::Normal(func) => { + let body_ctx = parse_prepared_function_call( + func.ctx.clone(), + prepared, + &func.params, + unnamed, + named, + )?; + evaluate(body_ctx, &func.body) + } + FuncVal::Thunk(t) => t.evaluate(), + FuncVal::StaticBuiltin(b) => { + let args = parse_prepared_builtin_call(prepared, b.params(), unnamed, named)?; + b.call(loc, &args) + } + FuncVal::Builtin(b) => { + let args = parse_prepared_builtin_call(prepared, b.params(), unnamed, named)?; + b.call(loc, &args) + } + } + } /// Convert jsonnet function to plain `Fn` value. pub fn into_native(self) -> D::Value { D::into_native(self) --- a/crates/jrsonnet-evaluator/src/function/prepared.rs +++ b/crates/jrsonnet-evaluator/src/function/prepared.rs @@ -1,3 +1,6 @@ +use std::rc::Rc; + +use jrsonnet_gcmodule::{Acyclic, Trace}; use jrsonnet_parser::function::FunctionSignature; use jrsonnet_parser::{ExprParams, IStr}; use rustc_hash::{FxHashMap, FxHashSet}; @@ -7,6 +10,34 @@ use crate::{bail, error::ErrorKind::*, Result}; use crate::{evaluate_named_param, Context, ContextBuilder, Pending, Thunk, Val}; +use super::{CallLocation, FuncVal}; + +#[derive(Debug, Trace, Clone)] +pub struct PreparedFuncVal { + fun: FuncVal, + prepared: Rc, +} + +impl PreparedFuncVal { + pub fn new(fun: FuncVal, unnamed: usize, named: &[IStr]) -> Result { + let prepared = prepare_call(fun.params(), unnamed, named)?; + Ok(Self { + fun, + prepared: Rc::new(prepared), + }) + } + pub fn call( + &self, + loc: CallLocation<'_>, + unnamed: &[Thunk], + named: &[Thunk], + ) -> Result { + self.fun + .evaluate_prepared(&self.prepared, loc, unnamed, named, false) + } +} + +#[derive(Acyclic, Debug)] pub struct PreparedCall { // Param, named input. named: Vec<(usize, usize)>, --- a/crates/jrsonnet-macros/src/lib.rs +++ b/crates/jrsonnet-macros/src/lib.rs @@ -3,16 +3,32 @@ use proc_macro2::TokenStream; use quote::{quote, quote_spanned}; use syn::{ - parenthesized, - parse::{Parse, ParseStream}, - parse_macro_input, - punctuated::Punctuated, - spanned::Spanned, - token::{self, Comma}, - Attribute, DeriveInput, Error, Expr, ExprClosure, FnArg, GenericArgument, Ident, ItemFn, - LitStr, Pat, Path, PathArguments, Result, ReturnType, Token, Type, + Attribute, DeriveInput, Error, Expr, ExprClosure, FnArg, GenericArgument, Ident, ItemFn, LitStr, Meta, Pat, Path, PathArguments, Result, ReturnType, Token, Type, parenthesized, parse::{Parse, ParseStream}, parse_macro_input, punctuated::Punctuated, spanned::Spanned, token::{self, Comma} }; +fn try_parse_attr_noargs(attrs: &[Attribute], ident: I) -> Result +where + Ident: PartialEq, +{ + let attrs = attrs + .iter() + .filter(|a| a.path().is_ident(&ident)) + .collect::>(); + if attrs.len() > 1 { + return Err(Error::new( + attrs[1].span(), + "this attribute may be specified only once", + )); + } else if attrs.is_empty() { + return Ok(false); + } + let attr = attrs[0]; + + match attr.meta { + Meta::Path(_) => Ok(true), + _ => Ok(false), + } +} fn parse_attr(attrs: &[Attribute], ident: I) -> Result> where Ident: PartialEq, @@ -125,9 +141,13 @@ Required, Optional, Default(Expr), + TypeDefault, } -#[allow(clippy::large_enum_variant, reason = "this macro is not that hot for it to matter")] +#[allow( + clippy::large_enum_variant, + reason = "this macro is not that hot for it to matter" +)] enum ArgInfo { Normal { ty: Box, @@ -170,7 +190,10 @@ _ => {} } - let (optionality, ty) = if let Some(default) = parse_attr::<_, _>(&arg.attrs, "default")? { + let (optionality, ty) = if try_parse_attr_noargs(&mut arg.attrs, "default")? { + remove_attr(&mut arg.attrs, "default"); + (Optionality::TypeDefault, ty.clone()) + } else if let Some(default) = parse_attr::<_, _>(&arg.attrs, "default")? { remove_attr(&mut arg.attrs, "default"); (Optionality::Default(default), ty.clone()) } else if let Some(ty) = extract_type_from_option(ty)? { @@ -245,7 +268,7 @@ .map_or_else(|| quote! {unnamed}, |n| quote! {named(#n)}); let default = match optionality { Optionality::Required => quote!(ParamDefault::None), - Optionality::Optional => quote!(ParamDefault::Exists), + Optionality::Optional | Optionality::TypeDefault => quote!(ParamDefault::Exists), Optionality::Default(e) => quote!(ParamDefault::Literal(stringify!(#e))), }; Some(quote! { @@ -305,6 +328,12 @@ let v: #ty = #expr; v },}, + Optionality::TypeDefault => quote! {if let Some(value) = &parsed[#id] { + #eval + } else { + let v: #ty = Default::default(); + v + },}, }; quote! { #(#cfg_attrs)* @@ -371,7 +400,7 @@ State, Val, function::{builtin::{Builtin, StaticBuiltin}, FunctionSignature, ParamParse, ParamName, ParamDefault, CallLocation, ArgsLike, parse::parse_builtin_call}, Result, Context, typed::Typed, - parser::Span, params, + parser::Span, params, Thunk, }; params!( #(#params_desc)* @@ -389,9 +418,7 @@ PARAMS.with(|p| p.clone()) } #[allow(unused_variables)] - fn call(&self, ctx: Context, location: CallLocation, args: &dyn ArgsLike) -> Result { - let parsed = parse_builtin_call(ctx.clone(), self.params(), args, false)?; - + fn call(&self, location: CallLocation<'_>, parsed: &[Option>]) -> Result { let result: #result = #name(#(#pass)*); <_ as Typed>::into_result(result) } --- /dev/null +++ b/crates/jrsonnet-stdlib/src/keyf.rs @@ -0,0 +1,41 @@ +use jrsonnet_evaluator::function::{CallLocation, FuncVal, PreparedFuncVal}; +use jrsonnet_evaluator::typed::{ComplexValType, Typed, ValType}; +use jrsonnet_evaluator::{Error, Result, Thunk, Val}; + +#[derive(Default, Clone)] +pub enum KeyF { + #[default] + Identity, + Prepared(PreparedFuncVal), + PrepareFailure(Error), +} +impl KeyF { + pub fn is_identity(&self) -> bool { + matches!(self, Self::Identity) + } + fn new(val: FuncVal) -> Self { + if val.is_identity() { + Self::Identity + } else { + PreparedFuncVal::new(val, 1, &[]).map_or_else(Self::PrepareFailure, Self::Prepared) + } + } + pub fn eval(&self, val: impl Into>) -> Result { + match self { + KeyF::Identity => val.into().evaluate(), + KeyF::Prepared(p) => p.call(CallLocation::native(), &[val.into()], &[]), + KeyF::PrepareFailure(e) => Err(e.clone()), + } + } +} + +impl Typed for KeyF { + const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Func); + fn from_untyped(untyped: Val) -> Result { + FuncVal::from_untyped(untyped).map(Self::new) + } + + fn into_untyped(_typed: Self) -> Result { + unreachable!("unused, todo: port split of Typed trait from #193") + } +} --- a/crates/jrsonnet-stdlib/src/lib.rs +++ b/crates/jrsonnet-stdlib/src/lib.rs @@ -50,6 +50,7 @@ mod sort; mod strings; mod types; +mod keyf; #[allow(clippy::too_many_lines)] pub fn stdlib_uncached(settings: Cc>) -> ObjValue { --- a/crates/jrsonnet-stdlib/src/sets.rs +++ b/crates/jrsonnet-stdlib/src/sets.rs @@ -1,28 +1,23 @@ use std::cmp::Ordering; use jrsonnet_evaluator::{ - function::{builtin, FuncVal}, - operator::evaluate_compare_op, - val::ArrValue, - Result, Thunk, Val, + function::builtin, operator::evaluate_compare_op, val::ArrValue, Result, Thunk, Val, }; use jrsonnet_parser::BinaryOpType; +use crate::keyf::KeyF; + #[builtin] #[allow(non_snake_case)] -pub fn builtin_set_member(x: Thunk, arr: ArrValue, keyF: Option) -> Result { +pub fn builtin_set_member(x: Thunk, arr: ArrValue, #[default] keyF: KeyF) -> Result { let mut low = 0; let mut high = arr.len(); - let keyF = keyF - .unwrap_or(FuncVal::Id) - .into_native::<((Thunk,), Val)>(); - - let x = keyF(x)?; + let x = keyF.eval(x)?; while low < high { let middle = usize::midpoint(high, low); - let comp = keyF(arr.get_lazy(middle).expect("in bounds"))?; + let comp = keyF.eval(arr.get_lazy(middle).expect("in bounds"))?; match evaluate_compare_op(&comp, &x, BinaryOpType::Lt)? { Ordering::Less => low = middle + 1, Ordering::Equal => return Ok(true), @@ -34,14 +29,11 @@ #[builtin] #[allow(non_snake_case, clippy::redundant_closure)] -pub fn builtin_set_inter(a: ArrValue, b: ArrValue, keyF: Option) -> Result { +pub fn builtin_set_inter(a: ArrValue, b: ArrValue, #[default] keyF: KeyF) -> Result { let mut a = a.iter_lazy(); let mut b = b.iter_lazy(); - let keyF = keyF - .unwrap_or(FuncVal::identity()) - .into_native::<((Thunk,), Val)>(); - let keyF = |v| keyF(v); + let keyF = |v| keyF.eval(v); let mut av = a.next(); let mut bv = b.next(); @@ -73,14 +65,11 @@ #[builtin] #[allow(non_snake_case, clippy::redundant_closure)] -pub fn builtin_set_diff(a: ArrValue, b: ArrValue, keyF: Option) -> Result { +pub fn builtin_set_diff(a: ArrValue, b: ArrValue, #[default] keyF: KeyF) -> Result { let mut a = a.iter_lazy(); let mut b = b.iter_lazy(); - let keyF = keyF - .unwrap_or(FuncVal::identity()) - .into_native::<((Thunk,), Val)>(); - let keyF = |v| keyF(v); + let keyF = |v| keyF.eval(v); let mut av = a.next(); let mut bv = b.next(); @@ -119,14 +108,11 @@ #[builtin] #[allow(non_snake_case, clippy::redundant_closure)] -pub fn builtin_set_union(a: ArrValue, b: ArrValue, keyF: Option) -> Result { +pub fn builtin_set_union(a: ArrValue, b: ArrValue, #[default] keyF: KeyF) -> Result { let mut a = a.iter_lazy(); let mut b = b.iter_lazy(); - let keyF = keyF - .unwrap_or(FuncVal::identity()) - .into_native::<((Thunk,), Val)>(); - let keyF = |v| keyF(v); + let keyF = |v| keyF.eval(v); let mut av = a.next(); let mut bv = b.next(); --- a/crates/jrsonnet-stdlib/src/sort.rs +++ b/crates/jrsonnet-stdlib/src/sort.rs @@ -4,14 +4,14 @@ use jrsonnet_evaluator::{ bail, - function::{builtin, FuncVal}, + function::builtin, operator::evaluate_compare_op, val::{equals, ArrValue}, Result, Thunk, Val, }; use jrsonnet_parser::BinaryOpType; -use crate::eval_on_empty; +use crate::{eval_on_empty, keyf::KeyF}; #[derive(Copy, Clone)] enum SortKeyType { @@ -70,14 +70,11 @@ Ok(values) } -fn sort_keyf(values: ArrValue, keyf: FuncVal) -> Result>> { +fn sort_keyf(values: ArrValue, keyf: KeyF) -> Result>> { // Slow path, user provided key getter let mut vk = Vec::with_capacity(values.len()); for value in values.iter_lazy() { - vk.push(( - value.clone(), - keyf.evaluate_simple(&(value.clone(),), false)?, - )); + vk.push((value.clone(), keyf.eval(value)?)); } let sort_type = get_sort_type(&vk, |v| &v.1)?; match sort_type { @@ -112,7 +109,7 @@ } /// * `key_getter` - None, if identity sort required -pub fn sort(values: ArrValue, key_getter: FuncVal) -> Result { +pub fn sort(values: ArrValue, key_getter: KeyF) -> Result { if values.len() <= 1 { return Ok(values); } @@ -126,11 +123,7 @@ } #[builtin] -pub fn builtin_sort( - arr: ArrValue, - - #[default(FuncVal::identity())] keyF: FuncVal, -) -> Result { +pub fn builtin_sort(arr: ArrValue, #[default] keyF: KeyF) -> Result { super::sort::sort(arr, keyF) } @@ -147,14 +140,14 @@ Ok(out) } -fn uniq_keyf(arr: ArrValue, keyf: FuncVal) -> Result>> { +fn uniq_keyf(arr: ArrValue, keyf: KeyF) -> Result>> { let mut out = Vec::new(); let last_value = arr.get_lazy(0).unwrap(); - let mut last_key = keyf.evaluate_simple(&(last_value.clone(),), false)?; + let mut last_key = keyf.eval(last_value.clone())?; out.push(last_value); for next in arr.iter_lazy().skip(1) { - let next_key = keyf.evaluate_simple(&(next.clone(),), false)?; + let next_key = keyf.eval(next.clone())?; if !equals(&last_key, &next_key)? { out.push(next.clone()); } @@ -165,11 +158,7 @@ #[builtin] #[allow(non_snake_case)] -pub fn builtin_uniq( - arr: ArrValue, - - #[default(FuncVal::identity())] keyF: FuncVal, -) -> Result { +pub fn builtin_uniq(arr: ArrValue, #[default] keyF: KeyF) -> Result { if arr.len() <= 1 { return Ok(arr); } @@ -184,11 +173,7 @@ #[builtin] #[allow(non_snake_case)] -pub fn builtin_set( - arr: ArrValue, - - #[default(FuncVal::identity())] keyF: FuncVal, -) -> Result { +pub fn builtin_set(arr: ArrValue, #[default] keyF: KeyF) -> Result { if arr.len() <= 1 { return Ok(arr); } @@ -201,24 +186,16 @@ let arr = sort_keyf(arr, keyF.clone())?; let arr = uniq_keyf(ArrValue::lazy(arr), keyF)?; Ok(ArrValue::lazy(arr)) - } -} - -fn eval_keyf(val: Val, key_f: Option<&FuncVal>) -> Result { - if let Some(key_f) = key_f { - key_f.evaluate_simple(&(val,), false) - } else { - Ok(val) } } -fn array_top1(arr: ArrValue, key_f: Option<&FuncVal>, ordering: Ordering) -> Result { +fn array_top1(arr: ArrValue, keyf: KeyF, ordering: Ordering) -> Result { let mut iter = arr.iter(); let mut min = iter.next().expect("not empty")?; - let mut min_key = eval_keyf(min.clone(), key_f)?; + let mut min_key = keyf.eval(Thunk::evaluated(min.clone()))?; for item in iter { let cur = item?; - let cur_key = eval_keyf(cur.clone(), key_f)?; + let cur_key = keyf.eval(Thunk::evaluated(cur.clone()))?; if evaluate_compare_op(&cur_key, &min_key, BinaryOpType::Lt)? == ordering { min = cur; min_key = cur_key; @@ -230,22 +207,22 @@ #[builtin] pub fn builtin_min_array( arr: ArrValue, - keyF: Option, + #[default] keyF: KeyF, onEmpty: Option>, ) -> Result { if arr.is_empty() { return eval_on_empty(onEmpty); } - array_top1(arr, keyF.as_ref(), Ordering::Less) + array_top1(arr, keyF, Ordering::Less) } #[builtin] pub fn builtin_max_array( arr: ArrValue, - keyF: Option, + #[default] keyF: KeyF, onEmpty: Option>, ) -> Result { if arr.is_empty() { return eval_on_empty(onEmpty); } - array_top1(arr, keyF.as_ref(), Ordering::Greater) + array_top1(arr, keyF, Ordering::Greater) } --- a/tests/tests/builtin.rs +++ b/tests/tests/builtin.rs @@ -18,8 +18,7 @@ #[test] fn basic_function() -> Result<()> { let a: a = a {}; - let v = - u32::from_untyped(a.call(ContextBuilder::new().build(), CallLocation::native(), &())?)?; + let v = u32::from_untyped(a.call(CallLocation::native(), &[])?)?; ensure_eq!(v, 1); Ok(())