git.delta.rocks / jrsonnet / refs/commits / 150f28648ad3

difftreelog

refactor always use prepared calls

wulxrvxzYaroslav Bolyukin2026-04-04parent: #0f5b614.patch.diff
in: master

17 files changed

modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs
@@ -19,7 +19,7 @@
 	destructure::evaluate_dest,
 	error::{ErrorKind::*, suggest_object_fields},
 	evaluate::operator::{evaluate_binary_op_special, evaluate_unary_op},
-	function::{CallLocation, FuncDesc, FuncVal},
+	function::{CallLocation, FuncDesc, FuncVal, PreparedFuncVal},
 	gc::WithCapacityExt as _,
 	in_frame,
 	typed::{FromUntyped, IntoUntyped as _, Typed},
@@ -430,11 +430,25 @@
 	let value = evaluate(ctx.clone(), value)?;
 	Ok(match value {
 		Val::Func(f) => {
-			let body = || f.evaluate(ctx, loc, args, tailstrict);
+			let name = f.name();
+			let prepare = PreparedFuncVal::new(f, args.unnamed.len(), &args.names)?;
+			let unnamed = args
+				.unnamed
+				.iter()
+				.cloned()
+				.map(|un| evaluate_thunk(ctx.clone(), un, tailstrict))
+				.collect::<Result<Vec<_>>>()?;
+			let named = args
+				.values
+				.iter()
+				.cloned()
+				.map(|un| evaluate_thunk(ctx.clone(), un, tailstrict))
+				.collect::<Result<Vec<_>>>()?;
+			let body = || prepare.call(loc, &unnamed, &named);
 			if tailstrict {
 				body()?
 			} else {
-				in_frame(loc, || format!("function <{}> call", f.name()), body)?
+				in_frame(loc, || format!("function <{name}> call"), body)?
 			}
 		}
 		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),
@@ -479,6 +493,13 @@
 	})
 }
 
+pub fn evaluate_thunk(ctx: Context, expr: Rc<Expr>, tailstrict: bool) -> Result<Thunk<Val>> {
+	Ok(if tailstrict {
+		Thunk::evaluated(evaluate(ctx, &expr)?)
+	} else {
+		Thunk!(move || { evaluate(ctx, &expr) })
+	})
+}
 #[allow(clippy::too_many_lines)]
 pub fn evaluate(ctx: Context, expr: &Expr) -> Result<Val> {
 	use Expr::*;
modifiedcrates/jrsonnet-evaluator/src/function/mod.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/function/mod.rs
1use std::{fmt::Debug, rc::Rc};23use educe::Educe;4use jrsonnet_gcmodule::{Cc, Trace};5use jrsonnet_interner::IStr;6use jrsonnet_ir::{ArgsDesc, Destruct, Expr, ExprParams, Span};7pub use jrsonnet_macros::builtin;89use self::{10	builtin::Builtin,11	parse::{parse_builtin_call, parse_default_function_call, parse_function_call},12	prepared::{PreparedCall, parse_prepared_builtin_call, parse_prepared_function_call},13};14use crate::{15	Context, Result, Thunk, Val, evaluate, evaluate_trivial, function::builtin::BuiltinFunc,16};1718pub mod builtin;19mod native;20mod parse;21mod prepared;2223pub use jrsonnet_ir::function::*;24pub use native::NativeFn;25pub use prepared::PreparedFuncVal;2627/// Function callsite location.28/// Either from other jsonnet code, specified by expression location, or from native (without location).29#[derive(Clone, Copy)]30pub struct CallLocation<'l>(pub Option<&'l Span>);31impl<'l> CallLocation<'l> {32	/// Construct new location for calls coming from specified jsonnet expression location.33	pub const fn new(loc: &'l Span) -> Self {34		Self(Some(loc))35	}36}37impl CallLocation<'static> {38	/// Construct new location for calls coming from native code.39	pub const fn native() -> Self {40		Self(None)41	}42}4344/// Represents Jsonnet function defined in code.45#[derive(Trace, Educe)]46#[educe(Debug, PartialEq)]47pub struct FuncDesc {48	/// # Example49	///50	/// In expressions like this, deducted to `a`, unspecified otherwise.51	/// ```jsonnet52	/// local a = function() ...53	/// local a() ...54	/// { a: function() ... }55	/// { a() = ... }56	/// ```57	pub name: IStr,58	/// Context, in which this function was evaluated.59	///60	/// # Example61	/// In62	/// ```jsonnet63	/// local a = 2;64	/// function() ...65	/// ```66	/// context will contain `a`.67	pub ctx: Context,6869	/// Function parameter definition70	pub params: ExprParams,71	/// Function body72	pub body: Rc<Expr>,73}74impl FuncDesc {75	/// Create body context, but fill arguments without defaults with lazy error76	pub fn default_body_context(&self) -> Result<Context> {77		parse_default_function_call(self.ctx.clone(), &self.params)78	}7980	/// Create context, with which body code will run81	pub(crate) fn call_body_context(82		&self,83		call_ctx: Context,84		args: &ArgsDesc,85		tailstrict: bool,86	) -> Result<Context> {87		parse_function_call(call_ctx, self.ctx.clone(), &self.params, args, tailstrict)88	}8990	pub fn evaluate_trivial(&self) -> Option<Val> {91		evaluate_trivial(&self.body)92	}93}9495/// Represents a Jsonnet function value, including plain functions and user-provided builtins.96#[allow(clippy::module_name_repetitions)]97#[derive(Trace, Clone)]98pub enum FuncVal {99	/// Plain function implemented in jsonnet.100	Normal(Cc<FuncDesc>),101	/// User-provided function.102	Builtin(BuiltinFunc),103}104105impl Debug for FuncVal {106	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {107		match self {108			Self::Normal(arg0) => f.debug_tuple("Normal").field(arg0).finish(),109			Self::Builtin(arg0) => f.debug_tuple("Builtin").field(&arg0.name()).finish(),110		}111	}112}113114#[allow(clippy::unnecessary_wraps)]115#[builtin]116pub const fn builtin_id(x: Thunk<Val>) -> Thunk<Val> {117	x118}119120impl FuncVal {121	pub fn builtin(builtin: impl Builtin) -> Self {122		Self::Builtin(BuiltinFunc::new(builtin))123	}124125	pub fn params(&self) -> FunctionSignature {126		match self {127			Self::Builtin(i) => i.params(),128			Self::Normal(p) => p.params.signature.clone(),129		}130	}131	/// Amount of non-default required arguments132	pub fn params_len(&self) -> usize {133		self.params().iter().filter(|p| !p.has_default()).count()134	}135	/// Function name, as defined in code.136	pub fn name(&self) -> IStr {137		match self {138			Self::Normal(normal) => normal.name.clone(),139			Self::Builtin(builtin) => builtin.name().into(),140		}141	}142	/// Call function using arguments evaluated in specified `call_ctx` [`Context`].143	///144	/// If `tailstrict` is specified - then arguments will be evaluated before being passed to function body.145	pub fn evaluate(146		&self,147		call_ctx: Context,148		loc: CallLocation<'_>,149		args: &ArgsDesc,150		tailstrict: bool,151	) -> Result<Val> {152		match self {153			Self::Normal(func) => {154				let body_ctx = func.call_body_context(call_ctx, args, tailstrict)?;155				evaluate(body_ctx, &func.body)156			}157			Self::Builtin(b) => {158				let args = parse_builtin_call(call_ctx, b.params(), args, tailstrict)?;159				b.call(loc, &args)160			}161		}162	}163164	pub(crate) fn evaluate_prepared(165		&self,166		prepared: &PreparedCall,167		loc: CallLocation<'_>,168		unnamed: &[Thunk<Val>],169		named: &[Thunk<Val>],170		_tailstrict: bool,171	) -> Result<Val> {172		match self {173			FuncVal::Normal(func) => {174				let body_ctx = parse_prepared_function_call(175					func.ctx.clone(),176					prepared,177					&func.params,178					unnamed,179					named,180				)?;181				evaluate(body_ctx, &func.body)182			}183			FuncVal::Builtin(b) => {184				let args = parse_prepared_builtin_call(prepared, b.params(), unnamed, named);185				b.call(loc, &args)186			}187		}188	}189190	/// Is this function an indentity function.191	///192	/// Currently only works for builtin `std.id`, aka `Self::Id` value, and `function(x) x`.193	///194	/// This function should only be used for optimization, not for the conditional logic, i.e code should work with syntetic identity function too195	pub fn is_identity(&self) -> bool {196		match self {197			Self::Builtin(b) => b.as_any().downcast_ref::<builtin_id>().is_some(),198			Self::Normal(desc) => {199				if desc.params.len() != 1 {200					return false;201				}202				let param = &desc.params.exprs[0];203				if param.default.is_some() {204					return false;205				}206207				#[allow(clippy::infallible_destructuring_match)]208				let id = match &param.destruct {209					Destruct::Full(id) => id,210					#[cfg(feature = "exp-destruct")]211					_ => return false,212				};213				matches!(&*desc.body, Expr::Var(v) if &**v == id)214			}215		}216	}217218	pub fn evaluate_trivial(&self) -> Option<Val> {219		match self {220			Self::Normal(n) => n.evaluate_trivial(),221			Self::Builtin(_) => None,222		}223	}224}225226impl<T> From<T> for FuncVal227where228	T: Builtin,229{230	fn from(value: T) -> Self {231		Self::builtin(value)232	}233}
after · crates/jrsonnet-evaluator/src/function/mod.rs
1use std::{fmt::Debug, rc::Rc};23use educe::Educe;4use jrsonnet_gcmodule::{Cc, Trace};5use jrsonnet_interner::IStr;6use jrsonnet_ir::{Destruct, Expr, ExprParams, Span};7pub use jrsonnet_macros::builtin;89use self::{10	builtin::Builtin,11	parse::parse_default_function_call,12	prepared::{PreparedCall, parse_prepared_builtin_call, parse_prepared_function_call},13};14use crate::{15	Context, Result, Thunk, Val, evaluate, evaluate_trivial, function::builtin::BuiltinFunc,16};1718pub mod builtin;19mod native;20mod parse;21mod prepared;2223pub use jrsonnet_ir::function::*;24pub use native::NativeFn;25pub(crate) use prepared::PreparedFuncVal;2627/// Function callsite location.28/// Either from other jsonnet code, specified by expression location, or from native (without location).29#[derive(Clone, Copy)]30pub struct CallLocation<'l>(pub Option<&'l Span>);31impl<'l> CallLocation<'l> {32	/// Construct new location for calls coming from specified jsonnet expression location.33	pub const fn new(loc: &'l Span) -> Self {34		Self(Some(loc))35	}36}37impl CallLocation<'static> {38	/// Construct new location for calls coming from native code.39	pub const fn native() -> Self {40		Self(None)41	}42}4344/// Represents Jsonnet function defined in code.45#[derive(Trace, Educe)]46#[educe(Debug, PartialEq)]47pub struct FuncDesc {48	/// # Example49	///50	/// In expressions like this, deducted to `a`, unspecified otherwise.51	/// ```jsonnet52	/// local a = function() ...53	/// local a() ...54	/// { a: function() ... }55	/// { a() = ... }56	/// ```57	pub name: IStr,58	/// Context, in which this function was evaluated.59	///60	/// # Example61	/// In62	/// ```jsonnet63	/// local a = 2;64	/// function() ...65	/// ```66	/// context will contain `a`.67	pub ctx: Context,6869	/// Function parameter definition70	pub params: ExprParams,71	/// Function body72	pub body: Rc<Expr>,73}74impl FuncDesc {75	/// Create body context, but fill arguments without defaults with lazy error76	pub fn default_body_context(&self) -> Result<Context> {77		parse_default_function_call(self.ctx.clone(), &self.params)78	}7980	pub fn evaluate_trivial(&self) -> Option<Val> {81		evaluate_trivial(&self.body)82	}83}8485/// Represents a Jsonnet function value, including plain functions and user-provided builtins.86#[allow(clippy::module_name_repetitions)]87#[derive(Trace, Clone)]88pub enum FuncVal {89	/// Plain function implemented in jsonnet.90	Normal(Cc<FuncDesc>),91	/// User-provided function.92	Builtin(BuiltinFunc),93}9495impl Debug for FuncVal {96	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {97		match self {98			Self::Normal(arg0) => f.debug_tuple("Normal").field(arg0).finish(),99			Self::Builtin(arg0) => f.debug_tuple("Builtin").field(&arg0.name()).finish(),100		}101	}102}103104#[allow(clippy::unnecessary_wraps)]105#[builtin]106pub const fn builtin_id(x: Thunk<Val>) -> Thunk<Val> {107	x108}109110impl FuncVal {111	pub fn builtin(builtin: impl Builtin) -> Self {112		Self::Builtin(BuiltinFunc::new(builtin))113	}114115	pub fn params(&self) -> FunctionSignature {116		match self {117			Self::Builtin(i) => i.params(),118			Self::Normal(p) => p.params.signature.clone(),119		}120	}121	/// Amount of non-default required arguments122	pub fn params_len(&self) -> usize {123		self.params().iter().filter(|p| !p.has_default()).count()124	}125	/// Function name, as defined in code.126	pub fn name(&self) -> IStr {127		match self {128			Self::Normal(normal) => normal.name.clone(),129			Self::Builtin(builtin) => builtin.name().into(),130		}131	}132133	pub(crate) fn evaluate_prepared(134		&self,135		prepared: &PreparedCall,136		loc: CallLocation<'_>,137		unnamed: &[Thunk<Val>],138		named: &[Thunk<Val>],139		_tailstrict: bool,140	) -> Result<Val> {141		match self {142			FuncVal::Normal(func) => {143				let body_ctx = parse_prepared_function_call(144					func.ctx.clone(),145					prepared,146					&func.params,147					unnamed,148					named,149				)?;150				evaluate(body_ctx, &func.body)151			}152			FuncVal::Builtin(b) => {153				let args = parse_prepared_builtin_call(prepared, b.params(), unnamed, named);154				b.call(loc, &args)155			}156		}157	}158159	/// Is this function an indentity function.160	///161	/// Currently only works for builtin `std.id`, aka `Self::Id` value, and `function(x) x`.162	///163	/// This function should only be used for optimization, not for the conditional logic, i.e code should work with syntetic identity function too164	pub fn is_identity(&self) -> bool {165		match self {166			Self::Builtin(b) => b.as_any().downcast_ref::<builtin_id>().is_some(),167			Self::Normal(desc) => {168				if desc.params.len() != 1 {169					return false;170				}171				let param = &desc.params.exprs[0];172				if param.default.is_some() {173					return false;174				}175176				#[allow(clippy::infallible_destructuring_match)]177				let id = match &param.destruct {178					Destruct::Full(id) => id,179					#[cfg(feature = "exp-destruct")]180					_ => return false,181				};182				matches!(&*desc.body, Expr::Var(v) if &**v == id)183			}184		}185	}186187	pub fn evaluate_trivial(&self) -> Option<Val> {188		match self {189			Self::Normal(n) => n.evaluate_trivial(),190			Self::Builtin(_) => None,191		}192	}193}194195impl<T> From<T> for FuncVal196where197	T: Builtin,198{199	fn from(value: T) -> Self {200		Self::builtin(value)201	}202}
modifiedcrates/jrsonnet-evaluator/src/function/native.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/native.rs
+++ b/crates/jrsonnet-evaluator/src/function/native.rs
@@ -39,12 +39,19 @@
 		impl<$($gen,)* O> FromUntyped for NativeFn<($($gen,)* O,)> {
 			fn from_untyped(untyped: Val) -> Result<Self> {
 				let func = FuncVal::from_untyped(untyped)?;
+				Self::try_from(func)
+			}
+		}
+		impl<$($gen,)* O> TryFrom<FuncVal> for NativeFn<($($gen,)* O,)> {
+			type Error = crate::Error;
+			fn try_from(v: FuncVal) -> Result<Self> {
 				Ok(Self(
-					PreparedFuncVal::new(func, $i, &[])?,
+					PreparedFuncVal::new(v, $i, &[])?,
 					PhantomData,
 				))
 			}
 		}
+
 	};
 	($i:expr; $($cur:ident)* @ $c:ident $($rest:ident)*) => {
 		impl_native_desc!($i; $($cur)*);
modifiedcrates/jrsonnet-evaluator/src/function/parse.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/parse.rs
+++ b/crates/jrsonnet-evaluator/src/function/parse.rs
@@ -1,215 +1,13 @@
-use std::rc::Rc;
-
-use jrsonnet_ir::{
-	ArgsDesc, Expr, ExprParams,
-	function::{FunctionSignature, ParamName},
-};
+use jrsonnet_ir::ExprParams;
 use rustc_hash::FxHashMap;
 
 use crate::{
-	Context, Pending, Thunk, Val, bail,
+	Context, Thunk,
 	destructure::destruct,
 	error::{ErrorKind::*, Result},
-	evaluate, evaluate_named_param,
+	evaluate_named_param,
 	gc::WithCapacityExt as _,
 };
-
-fn eval_arg(ctx: Context, arg: &Rc<Expr>, tailstrict: bool) -> Result<Thunk<Val>> {
-	if tailstrict {
-		Ok(Thunk::evaluated(evaluate(ctx, arg)?))
-	} else {
-		let arg = arg.clone();
-		Ok(Thunk!(move || evaluate(ctx, &arg)))
-	}
-}
-
-/// Creates correct [context](Context) for function body evaluation returning error on invalid call.
-///
-/// ## Parameters
-/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set)
-/// * `body_ctx`: used for default parameter values' execution and for body execution (if set)
-/// * `params`: function parameters' definition
-/// * `args`: passed function arguments
-/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily
-pub(crate) fn parse_function_call(
-	ctx: Context,
-	body_ctx: Context,
-	params: &ExprParams,
-	args: &ArgsDesc,
-	tailstrict: bool,
-) -> Result<Context> {
-	let mut passed_args = FxHashMap::with_capacity(params.binds_len());
-	if args.unnamed.len() > params.signature.len() {
-		bail!(TooManyArgsFunctionHas(
-			params.signature.len(),
-			params.signature.clone(),
-		))
-	}
-
-	let mut filled_named = 0;
-	let mut filled_positionals = 0;
-
-	for (id, arg) in args.unnamed.iter().enumerate() {
-		destruct(
-			&params.exprs[id].destruct,
-			eval_arg(ctx.clone(), arg, tailstrict)?,
-			Pending::new_filled(ctx.clone()),
-			&mut passed_args,
-		)?;
-		filled_positionals += 1;
-	}
-
-	for (name, value) in &args.named {
-		// FIXME: O(n) for arg existence check
-		if !params.exprs.iter().any(|p| &p.destruct.name() == name) {
-			bail!(UnknownFunctionParameter(name.clone()));
-		}
-		if passed_args
-			.insert(name.clone(), eval_arg(ctx.clone(), value, tailstrict)?)
-			.is_some()
-		{
-			bail!(BindingParameterASecondTime(name.clone()));
-		}
-		filled_named += 1;
-	}
-
-	if filled_named + filled_positionals < params.len() {
-		// Some args are unset, but maybe we have defaults for them
-		// Default values should be created in newly created context
-		let fctx = Context::new_future();
-		let mut defaults =
-			FxHashMap::with_capacity(params.binds_len() - filled_named - filled_positionals);
-
-		for (idx, into, default) in params
-			.exprs
-			.iter()
-			.enumerate()
-			.filter_map(|(i, p)| Some((i, &p.destruct, p.default.as_ref()?)))
-		{
-			if let ParamName::Named(name) = into.name() {
-				if passed_args.contains_key(&name) {
-					continue;
-				}
-			} else if idx < filled_positionals {
-				continue;
-			}
-
-			destruct(
-				into,
-				{
-					let ctx = fctx.clone();
-					let name = into.name();
-					let value = default.clone();
-					Thunk!(move || evaluate_named_param(ctx.unwrap(), &value, name))
-				},
-				fctx.clone(),
-				&mut defaults,
-			)?;
-			if into.name().is_named() {
-				filled_named += 1;
-			} else {
-				filled_positionals += 1;
-			}
-		}
-
-		// Some args still weren't filled
-		if filled_named + filled_positionals != params.len() {
-			for param in params.exprs.iter().skip(args.unnamed.len()) {
-				let mut found = false;
-				for (name, _) in &args.named {
-					if &param.destruct.name() == name {
-						found = true;
-					}
-				}
-				if !found {
-					bail!(FunctionParameterNotBoundInCall(
-						param.destruct.name(),
-						params.signature.clone()
-					));
-				}
-			}
-			unreachable!();
-		}
-
-		Ok(body_ctx
-			.extend_bindings(passed_args)
-			.extend_bindings(defaults)
-			.into_future(fctx))
-	} else {
-		let body_ctx = body_ctx.extend_bindings(passed_args);
-		Ok(body_ctx)
-	}
-}
-
-/// You shouldn't probally use this function, use `jrsonnet_macros::builtin` instead
-///
-/// ## Parameters
-/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set)
-/// * `params`: function parameters' definition
-/// * `args`: passed function arguments
-/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily
-pub fn parse_builtin_call(
-	ctx: Context,
-	params: FunctionSignature,
-	args: &ArgsDesc,
-	tailstrict: bool,
-) -> Result<Vec<Option<Thunk<Val>>>> {
-	let mut passed_args: Vec<Option<Thunk<Val>>> = vec![None; params.len()];
-	if args.unnamed.len() > params.len() {
-		bail!(TooManyArgsFunctionHas(params.len(), params,))
-	}
-
-	let mut filled_args = 0;
-
-	for (id, arg) in args.unnamed.iter().enumerate() {
-		passed_args[id] = Some(eval_arg(ctx.clone(), arg, tailstrict)?);
-		filled_args += 1;
-	}
-
-	for (name, arg) in &args.named {
-		// FIXME: O(n) for arg existence check
-		let id = params
-			.iter()
-			.position(|p| p.name() == name)
-			.ok_or_else(|| UnknownFunctionParameter(name.clone()))?;
-		if passed_args[id]
-			.replace(eval_arg(ctx.clone(), arg, tailstrict)?)
-			.is_some()
-		{
-			bail!(BindingParameterASecondTime(name.clone()));
-		}
-		filled_args += 1;
-	}
-
-	if filled_args < params.len() {
-		for (id, _) in params.iter().enumerate().filter(|(_, p)| p.has_default()) {
-			if passed_args[id].is_some() {
-				continue;
-			}
-			filled_args += 1;
-		}
-
-		// Some args still wasn't filled
-		if filled_args != params.len() {
-			for param in params.iter().skip(args.unnamed.len()) {
-				let mut found = false;
-				for (name, _) in &args.named {
-					if param.name() == name {
-						found = true;
-					}
-				}
-				if !found {
-					bail!(FunctionParameterNotBoundInCall(
-						param.name().clone(),
-						params,
-					));
-				}
-			}
-			unreachable!();
-		}
-	}
-	Ok(passed_args)
-}
 
 /// Creates Context, which has all argument default values applied
 /// and with unbound values causing error to be returned
modifiedcrates/jrsonnet-ir-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/lib.rs
+++ b/crates/jrsonnet-ir-parser/src/lib.rs
@@ -416,10 +416,11 @@
 
 fn args(p: &mut Parser<'_>) -> Result<ArgsDesc> {
 	if p.at(T![')']) {
-		return Ok(ArgsDesc::new(Vec::new(), Vec::new()));
+		return Ok(ArgsDesc::new(Vec::new(), Vec::new(), Vec::new()));
 	}
 	let mut unnamed = Vec::new();
-	let mut named = Vec::new();
+	let mut names = Vec::new();
+	let mut values = Vec::new();
 	let mut named_started = false;
 	loop {
 		let is_named = p.at(SyntaxKind::IDENT) && {
@@ -430,7 +431,9 @@
 			let name: IStr = ident(p)?;
 			p.eat(T![=])?;
 			let value = Rc::new(expr(p)?);
-			named.push((name, value));
+
+			names.push(name);
+			values.push(value);
 			named_started = true;
 		} else {
 			if named_started {
@@ -445,7 +448,7 @@
 			break;
 		}
 	}
-	Ok(ArgsDesc::new(unnamed, named))
+	Ok(ArgsDesc::new(unnamed, names, values))
 }
 
 fn bind(p: &mut Parser<'_>) -> Result<BindSpec> {
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__function_and_call.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__function_and_call.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__function_and_call.snap
@@ -66,12 +66,12 @@
                     2.0,
                 ),
             ],
-            named: [
-                (
-                    "y",
-                    Num(
-                        3.0,
-                    ),
+            names: [
+                "y",
+            ],
+            values: [
+                Num(
+                    3.0,
                 ),
             ],
         } from virtual:<test>:26-34,
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__index_and_suffix.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__index_and_suffix.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__index_and_suffix.snap
@@ -23,7 +23,8 @@
                     2.0,
                 ),
             ],
-            named: [],
+            names: [],
+            values: [],
         } from virtual:<test>:8-11,
         false,
     ),
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@array_comp.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@array_comp.jsonnet.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@array_comp.jsonnet.snap
@@ -26,7 +26,8 @@
                             "x" from virtual:<test>:16-17,
                         ),
                     ],
-                    named: [],
+                    names: [],
+                    values: [],
                 } from virtual:<test>:15-18,
                 false,
             ),
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@reserved.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@reserved.jsonnet.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@reserved.jsonnet.snap
@@ -24,7 +24,8 @@
                         "null_fields" from virtual:<test>:20-31,
                     ),
                 ],
-                named: [],
+                names: [],
+                values: [],
             } from virtual:<test>:16-32,
             false,
         ),
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@suffix.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@suffix.jsonnet.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__peg_snapshots@suffix.jsonnet.snap
@@ -28,7 +28,8 @@
                         2.0,
                     ),
                 ],
-                named: [],
+                names: [],
+                values: [],
             } from virtual:<test>:15-18,
             false,
         ),
@@ -52,7 +53,8 @@
                         2.0,
                     ),
                 ],
-                named: [],
+                names: [],
+                values: [],
             } from virtual:<test>:28-31,
             false,
         ),
modifiedcrates/jrsonnet-ir/src/expr.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir/src/expr.rs
+++ b/crates/jrsonnet-ir/src/expr.rs
@@ -195,11 +195,16 @@
 #[derive(Debug, PartialEq, Acyclic)]
 pub struct ArgsDesc {
 	pub unnamed: Vec<Rc<Expr>>,
-	pub named: Vec<(IStr, Rc<Expr>)>,
+	pub names: Vec<IStr>,
+	pub values: Vec<Rc<Expr>>,
 }
 impl ArgsDesc {
-	pub fn new(unnamed: Vec<Rc<Expr>>, named: Vec<(IStr, Rc<Expr>)>) -> Self {
-		Self { unnamed, named }
+	pub fn new(unnamed: Vec<Rc<Expr>>, names: Vec<IStr>, values: Vec<Rc<Expr>>) -> Self {
+		Self {
+			unnamed,
+			names,
+			values,
+		}
 	}
 }
 
modifiedcrates/jrsonnet-ir/src/visit.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir/src/visit.rs
+++ b/crates/jrsonnet-ir/src/visit.rs
@@ -216,11 +216,15 @@
 		}
 		Expr::Apply(expr, spanned, _) => {
 			v.visit_expr(expr);
-			let ArgsDesc { unnamed, named } = &**spanned;
+			let ArgsDesc {
+				unnamed,
+				names: _,
+				values,
+			} = &**spanned;
 			for unnamed in unnamed {
 				v.visit_expr(unnamed);
 			}
-			for (_name, named) in named {
+			for named in values {
 				v.visit_expr(named);
 			}
 		}
modifiedcrates/jrsonnet-peg-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/lib.rs
+++ b/crates/jrsonnet-peg-parser/src/lib.rs
@@ -73,12 +73,14 @@
 			= 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 names = Vec::with_capacity(args.len() - unnamed_count);
+				let mut values = Vec::with_capacity(args.len() - unnamed_count);
 				let mut named_started = false;
 				for (name, value) in args {
 					if let Some(name) = name {
 						named_started = true;
-						named.push((name, value));
+						names.push(name);
+						values.push(value);
 					} else {
 						if named_started {
 							return Err("<named argument>")
@@ -86,7 +88,7 @@
 						unnamed.push(value);
 					}
 				}
-				Ok(ArgsDesc::new(unnamed, named))
+				Ok(ArgsDesc{unnamed, names, values})
 			}
 
 		pub rule destruct_rest() -> DestructRest
modifiedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@array_comp.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@array_comp.jsonnet.snap
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@array_comp.jsonnet.snap
@@ -26,7 +26,8 @@
                             "x" from virtual:<test>:16-17,
                         ),
                     ],
-                    named: [],
+                    names: [],
+                    values: [],
                 } from virtual:<test>:15-18,
                 false,
             ),
modifiedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@reserved.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@reserved.jsonnet.snap
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@reserved.jsonnet.snap
@@ -24,7 +24,8 @@
                         "null_fields" from virtual:<test>:20-31,
                     ),
                 ],
-                named: [],
+                names: [],
+                values: [],
             } from virtual:<test>:16-32,
             false,
         ),
modifiedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@suffix.jsonnet.snapdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@suffix.jsonnet.snap
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__snapshots@suffix.jsonnet.snap
@@ -28,7 +28,8 @@
                         2.0,
                     ),
                 ],
-                named: [],
+                names: [],
+                values: [],
             } from virtual:<test>:15-18,
             false,
         ),
@@ -52,7 +53,8 @@
                         2.0,
                     ),
                 ],
-                named: [],
+                names: [],
+                values: [],
             } from virtual:<test>:28-31,
             false,
         ),
modifiedcrates/jrsonnet-stdlib/src/keyf.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/keyf.rs
+++ b/crates/jrsonnet-stdlib/src/keyf.rs
@@ -1,14 +1,16 @@
 use jrsonnet_evaluator::{
 	Error, Result, Thunk, Val,
-	function::{CallLocation, FuncVal, PreparedFuncVal},
+	function::{FuncVal, NativeFn},
 	typed::{ComplexValType, FromUntyped, Typed, ValType},
 };
 
+type PreparedKeyF = NativeFn!((Thunk<Val>) -> Val);
+
 #[derive(Default, Clone)]
 pub enum KeyF {
 	#[default]
 	Identity,
-	Prepared(PreparedFuncVal),
+	Prepared(PreparedKeyF),
 	PrepareFailure(Error),
 }
 impl KeyF {
@@ -19,13 +21,13 @@
 		if val.is_identity() {
 			Self::Identity
 		} else {
-			PreparedFuncVal::new(val, 1, &[]).map_or_else(Self::PrepareFailure, Self::Prepared)
+			PreparedKeyF::try_from(val).map_or_else(Self::PrepareFailure, Self::Prepared)
 		}
 	}
 	pub fn eval(&self, val: impl Into<Thunk<Val>>) -> Result<Val> {
 		match self {
 			KeyF::Identity => val.into().evaluate(),
-			KeyF::Prepared(p) => p.call(CallLocation::native(), &[val.into()], &[]),
+			KeyF::Prepared(p) => p.call(val.into()),
 			KeyF::PrepareFailure(e) => Err(e.clone()),
 		}
 	}