git.delta.rocks / jrsonnet / refs/commits / df5053db3f0e

difftreelog

refactor do not allocate for BuiltinParam vec

mqrttluxYaroslav Bolyukin2026-03-21parent: #6a2bca3.patch.diff
in: master

5 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
@@ -11,18 +11,7 @@
 
 use self::destructure::destruct;
 use crate::{
-	arr::ArrValue,
-	bail,
-	destructure::evaluate_dest,
-	error::{suggest_object_fields, ErrorKind::*},
-	evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},
-	function::{CallLocation, FuncDesc, FuncVal},
-	gc::WithCapacityExt as _,
-	in_frame,
-	typed::Typed,
-	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},
-	with_state, Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,
-	ResultExt, SupThis, Unbound, Val,
+	Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result, ResultExt, SupThis, Unbound, Val, arr::ArrValue, bail, destructure::evaluate_dest, error::{ErrorKind::*, suggest_object_fields}, evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op}, function::{CallLocation, FuncDesc, FuncVal, builtin::{ParamDefault, ParamName, ParamParse}}, gc::WithCapacityExt as _, in_frame, typed::Typed, val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk}, with_state
 };
 pub mod destructure;
 pub mod operator;
@@ -88,6 +77,15 @@
 	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {
 		name,
 		ctx,
+		params_parse: params
+			.iter()
+			.map(|p| {
+				ParamParse::new(
+					p.0.name().map_or(ParamName::ANONYMOUS, ParamName::new),
+					ParamDefault::exists(p.1.is_some()),
+				)
+			})
+			.collect(),
 		params,
 		body,
 	})))
modifiedcrates/jrsonnet-evaluator/src/function/builtin.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/builtin.rs
+++ b/crates/jrsonnet-evaluator/src/function/builtin.rs
@@ -1,22 +1,17 @@
-use std::{any::Any, borrow::Cow};
+use std::any::Any;
 
-use jrsonnet_gcmodule::{cc_dyn, Trace, TraceBox};
+use jrsonnet_gcmodule::{cc_dyn, Acyclic, Trace, TraceBox};
 use jrsonnet_interner::IStr;
 
 use super::{arglike::ArgsLike, parse::parse_builtin_call, CallLocation};
 use crate::{Context, Result, Val};
 
-/// Can't have `str` | `IStr`, because constant `BuiltinParam` causes
-/// `E0492: constant functions cannot refer to interior mutable data`
-#[derive(Clone, Trace)]
-pub struct ParamName(Option<Cow<'static, str>>);
+#[derive(Clone, Acyclic)]
+pub struct ParamName(Option<IStr>);
 impl ParamName {
 	pub const ANONYMOUS: Self = Self(None);
-	pub const fn new_static(name: &'static str) -> Self {
-		Self(Some(Cow::Borrowed(name)))
-	}
-	pub fn new_dynamic(name: String) -> Self {
-		Self(Some(Cow::Owned(name)))
+	pub fn new(name: IStr) -> Self {
+		Self(Some(name))
 	}
 	pub fn as_str(&self) -> Option<&str> {
 		self.0.as_deref()
@@ -33,7 +28,7 @@
 	}
 }
 
-#[derive(Clone, Copy, Debug, Trace)]
+#[derive(Clone, Copy, Debug, Acyclic)]
 pub enum ParamDefault {
 	None,
 	Exists,
@@ -49,13 +44,26 @@
 	}
 }
 
-#[derive(Clone, Trace)]
-pub struct BuiltinParam {
+#[macro_export]
+macro_rules! params {
+	(@name unnamed) => { ParamName::ANONYMOUS };
+	(@name named $name:literal) => { ParamName::new($crate::IStr::from($name)) };
+	($($(#[$meta:meta])* [$kind:ident $(($lit:literal))? => $default:expr]),* $(,)?) => {
+		thread_local! {
+			static PARAMS: [ParamParse; { const N: usize = <[u8]>::len(&[$($(#[$meta])* 0u8),*]); N }] = [
+				$($(#[$meta])* ParamParse::new(params!(@name $kind $($lit)?), $default)),*
+			];
+		}
+	};
+}
+
+#[derive(Clone, Acyclic)]
+pub struct ParamParse {
 	name: ParamName,
 	default: ParamDefault,
 }
-impl BuiltinParam {
-	pub const fn new(name: ParamName, default: ParamDefault) -> Self {
+impl ParamParse {
+	pub fn new(name: ParamName, default: ParamDefault) -> Self {
 		Self { name, default }
 	}
 	/// Parameter name for named call parsing
@@ -81,7 +89,7 @@
 		self.0.name()
 	}
 
-	fn params(&self) -> &[BuiltinParam] {
+	fn params(&self) -> &[ParamParse] {
 		self.0.params()
 	}
 
@@ -101,7 +109,7 @@
 	/// Function name to be used in stack traces
 	fn name(&self) -> &str;
 	/// Parameter names for named calls
-	fn params(&self) -> &[BuiltinParam];
+	fn params(&self) -> &[ParamParse];
 	/// Call the builtin
 	fn call(&self, ctx: Context, loc: CallLocation<'_>, args: &dyn ArgsLike) -> Result<Val>;
 
@@ -118,7 +126,7 @@
 
 #[derive(Trace)]
 pub struct NativeCallback {
-	pub(crate) params: Vec<BuiltinParam>,
+	pub(crate) params: Vec<ParamParse>,
 	handler: TraceBox<dyn NativeCallbackHandler>,
 }
 impl NativeCallback {
@@ -127,8 +135,8 @@
 		Self {
 			params: params
 				.into_iter()
-				.map(|n| BuiltinParam {
-					name: ParamName::new_dynamic(n),
+				.map(|n| ParamParse {
+					name: ParamName::new(n.into()),
 					default: ParamDefault::None,
 				})
 				.collect(),
@@ -144,7 +152,7 @@
 		"<native>"
 	}
 
-	fn params(&self) -> &[BuiltinParam] {
+	fn params(&self) -> &[ParamParse] {
 		&self.params
 	}
 
modifiedcrates/jrsonnet-evaluator/src/function/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/mod.rs
+++ b/crates/jrsonnet-evaluator/src/function/mod.rs
@@ -1,6 +1,7 @@
 use std::{fmt::Debug, rc::Rc};
 
 pub use arglike::{ArgLike, ArgsLike, TlaArg};
+use educe::Educe;
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::IStr;
 pub use jrsonnet_macros::builtin;
@@ -8,7 +9,7 @@
 
 use self::{
 	arglike::OptionalContext,
-	builtin::{Builtin, BuiltinParam, ParamDefault, ParamName, StaticBuiltin},
+	builtin::{Builtin, ParamParse, StaticBuiltin},
 	native::NativeDesc,
 	parse::{parse_default_function_call, parse_function_call},
 };
@@ -40,7 +41,8 @@
 }
 
 /// Represents Jsonnet function defined in code.
-#[derive(Debug, Trace, PartialEq)]
+#[derive(Trace, Educe)]
+#[educe(Debug, PartialEq)]
 pub struct FuncDesc {
 	/// # Example
 	///
@@ -67,6 +69,9 @@
 	pub params: ParamsDesc,
 	/// Function body
 	pub body: Rc<Spanned<Expr>>,
+
+	#[educe(PartialEq = false, Debug = false)]
+	pub(crate) params_parse: Vec<ParamParse>,
 }
 impl FuncDesc {
 	/// Create body context, but fill arguments without defaults with lazy error
@@ -134,25 +139,13 @@
 		Self::StaticBuiltin(static_builtin)
 	}
 
-	pub fn params(&self) -> Vec<BuiltinParam> {
+	pub fn params(&self) -> &[ParamParse] {
 		match self {
-			Self::Id => ID.params().to_vec(),
-			Self::StaticBuiltin(i) => i.params().to_vec(),
-			Self::Builtin(i) => i.params().to_vec(),
-			Self::Normal(p) => p
-				.params
-				.iter()
-				.map(|p| {
-					BuiltinParam::new(
-						p.0.name()
-							.as_ref()
-							.map(IStr::to_string)
-							.map_or(ParamName::ANONYMOUS, ParamName::new_dynamic),
-						ParamDefault::exists(p.1.is_some()),
-					)
-				})
-				.collect(),
-			Self::Thunk(_) => vec![],
+			Self::Id => ID.params(),
+			Self::StaticBuiltin(i) => i.params(),
+			Self::Builtin(i) => i.params(),
+			Self::Normal(p) => &p.params_parse,
+			Self::Thunk(_) => &[],
 		}
 	}
 	/// Amount of non-default required arguments
modifiedcrates/jrsonnet-evaluator/src/function/parse.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/function/parse.rs
1use std::mem::replace;23use jrsonnet_interner::IStr;4use jrsonnet_parser::ParamsDesc;5use rustc_hash::FxHashMap;67use super::{arglike::ArgsLike, builtin::BuiltinParam};8use crate::{9	bail,10	destructure::destruct,11	error::{ErrorKind::*, Result},12	evaluate_named,13	function::builtin::ParamDefault,14	gc::WithCapacityExt as _,15	Context, Pending, Thunk, Val,16};1718/// Creates correct [context](Context) for function body evaluation returning error on invalid call.19///20/// ## Parameters21/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set)22/// * `body_ctx`: used for default parameter values' execution and for body execution (if set)23/// * `params`: function parameters' definition24/// * `args`: passed function arguments25/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily26pub fn parse_function_call(27	ctx: Context,28	body_ctx: Context,29	params: &ParamsDesc,30	args: &dyn ArgsLike,31	tailstrict: bool,32) -> Result<Context> {33	let mut passed_args =34		FxHashMap::with_capacity(params.iter().map(|p| p.0.capacity_hint()).sum());35	if args.unnamed_len() > params.len() {36		bail!(TooManyArgsFunctionHas(37			params.len(),38			params39				.iter()40				.map(|p| (p.0.name(), ParamDefault::exists(p.1.is_some())))41				.collect()42		))43	}4445	let mut filled_named = 0;46	let mut filled_positionals = 0;4748	args.unnamed_iter(ctx.clone(), tailstrict, &mut |id, arg| {49		let name = params[id].0.clone();50		destruct(51			&name,52			arg,53			Pending::new_filled(ctx.clone()),54			&mut passed_args,55		)?;56		filled_positionals += 1;57		Ok(())58	})?;5960	args.named_iter(ctx, tailstrict, &mut |name, value| {61		// FIXME: O(n) for arg existence check62		if !params.iter().any(|p| p.0.name().as_ref() == Some(name)) {63			bail!(UnknownFunctionParameter((name as &str).to_owned()));64		}65		if passed_args.insert(name.clone(), value).is_some() {66			bail!(BindingParameterASecondTime(name.clone()));67		}68		filled_named += 1;69		Ok(())70	})?;7172	if filled_named + filled_positionals < params.len() {73		// Some args are unset, but maybe we have defaults for them74		// Default values should be created in newly created context75		let fctx = Context::new_future();76		let mut defaults = FxHashMap::with_capacity(77			params.iter().map(|p| p.0.capacity_hint()).sum::<usize>()78				- filled_named79				- filled_positionals,80		);8182		for (idx, param) in params.iter().enumerate().filter(|p| p.1 .1.is_some()) {83			if let Some(name) = param.0.name() {84				if passed_args.contains_key(&name) {85					continue;86				}87			} else if idx < filled_positionals {88				continue;89			}9091			destruct(92				&param.0,93				{94					let ctx = fctx.clone();95					let name = param.0.name().unwrap_or_else(|| "<destruct>".into());96					let value = param.1.clone().expect("default exists");97					Thunk!(move || evaluate_named(ctx.unwrap(), &value, name))98				},99				fctx.clone(),100				&mut defaults,101			)?;102			if param.0.name().is_some() {103				filled_named += 1;104			} else {105				filled_positionals += 1;106			}107		}108109		// Some args still weren't filled110		if filled_named + filled_positionals != params.len() {111			for param in params.iter().skip(args.unnamed_len()) {112				let mut found = false;113				args.named_names(&mut |name| {114					if Some(name) == param.0.name().as_ref() {115						found = true;116					}117				});118				if !found {119					bail!(FunctionParameterNotBoundInCall(120						param.0.clone().name(),121						params122							.iter()123							.map(|p| (p.0.name(), ParamDefault::exists(p.1.is_some())))124							.collect()125					));126				}127			}128			unreachable!();129		}130131		Ok(body_ctx132			.extend_bindings(passed_args)133			.extend_bindings(defaults)134			.into_future(fctx))135	} else {136		let body_ctx = body_ctx.extend_bindings(passed_args);137		Ok(body_ctx)138	}139}140141/// You shouldn't probally use this function, use `jrsonnet_macros::builtin` instead142///143/// ## Parameters144/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set)145/// * `params`: function parameters' definition146/// * `args`: passed function arguments147/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily148pub fn parse_builtin_call(149	ctx: Context,150	params: &[BuiltinParam],151	args: &dyn ArgsLike,152	tailstrict: bool,153) -> Result<Vec<Option<Thunk<Val>>>> {154	let mut passed_args: Vec<Option<Thunk<Val>>> = vec![None; params.len()];155	if args.unnamed_len() > params.len() {156		bail!(TooManyArgsFunctionHas(157			params.len(),158			params159				.iter()160				.map(|p| (p.name().as_str().map(IStr::from), p.default()))161				.collect()162		))163	}164165	let mut filled_args = 0;166167	args.unnamed_iter(ctx.clone(), tailstrict, &mut |id, arg| {168		passed_args[id] = Some(arg);169		filled_args += 1;170		Ok(())171	})?;172173	args.named_iter(ctx, tailstrict, &mut |name, arg| {174		// FIXME: O(n) for arg existence check175		let id = params176			.iter()177			.position(|p| p.name() == name)178			.ok_or_else(|| UnknownFunctionParameter((name as &str).to_owned()))?;179		if replace(&mut passed_args[id], Some(arg)).is_some() {180			bail!(BindingParameterASecondTime(name.clone()));181		}182		filled_args += 1;183		Ok(())184	})?;185186	if filled_args < params.len() {187		for (id, _) in params.iter().enumerate().filter(|(_, p)| p.has_default()) {188			if passed_args[id].is_some() {189				continue;190			}191			filled_args += 1;192		}193194		// Some args still wasn't filled195		if filled_args != params.len() {196			for param in params.iter().skip(args.unnamed_len()) {197				let mut found = false;198				args.named_names(&mut |name| {199					if param.name() == name {200						found = true;201					}202				});203				if !found {204					bail!(FunctionParameterNotBoundInCall(205						param.name().as_str().map(IStr::from),206						params207							.iter()208							.map(|p| (p.name().as_str().map(IStr::from), p.default()))209							.collect()210					));211				}212			}213			unreachable!();214		}215	}216	Ok(passed_args)217}218219/// Creates Context, which has all argument default values applied220/// and with unbound values causing error to be returned221pub fn parse_default_function_call(body_ctx: Context, params: &ParamsDesc) -> Result<Context> {222	let fctx = Context::new_future();223224	let mut bindings = FxHashMap::with_capacity(params.iter().map(|p| p.0.capacity_hint()).sum());225226	for param in params.iter() {227		if let Some(v) = &param.1 {228			destruct(229				&param.0.clone(),230				{231					let ctx = fctx.clone();232					let name = param.0.name().unwrap_or_else(|| "<destruct>".into());233					let value = v.clone();234					Thunk!(move || evaluate_named(ctx.unwrap(), &value, name))235				},236				fctx.clone(),237				&mut bindings,238			)?;239		} else {240			destruct(241				&param.0,242				{243					let param_name = param.0.name().unwrap_or_else(|| "<destruct>".into());244					let params = params.clone();245					Thunk!(move || Err(FunctionParameterNotBoundInCall(246						Some(param_name),247						params248							.iter()249							.map(|p| (p.0.name(), ParamDefault::exists(p.1.is_some())))250							.collect(),251					)252					.into()))253				},254				fctx.clone(),255				&mut bindings,256			)?;257		}258	}259260	Ok(body_ctx.extend_bindings(bindings).into_future(fctx))261}
after · crates/jrsonnet-evaluator/src/function/parse.rs
1use std::mem::replace;23use jrsonnet_interner::IStr;4use jrsonnet_parser::ParamsDesc;5use rustc_hash::FxHashMap;67use super::{arglike::ArgsLike, builtin::ParamParse};8use crate::{9	bail,10	destructure::destruct,11	error::{ErrorKind::*, Result},12	evaluate_named,13	function::builtin::ParamDefault,14	gc::WithCapacityExt as _,15	Context, Pending, Thunk, Val,16};1718/// Creates correct [context](Context) for function body evaluation returning error on invalid call.19///20/// ## Parameters21/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set)22/// * `body_ctx`: used for default parameter values' execution and for body execution (if set)23/// * `params`: function parameters' definition24/// * `args`: passed function arguments25/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily26pub fn parse_function_call(27	ctx: Context,28	body_ctx: Context,29	params: &ParamsDesc,30	args: &dyn ArgsLike,31	tailstrict: bool,32) -> Result<Context> {33	let mut passed_args =34		FxHashMap::with_capacity(params.iter().map(|p| p.0.capacity_hint()).sum());35	if args.unnamed_len() > params.len() {36		bail!(TooManyArgsFunctionHas(37			params.len(),38			params39				.iter()40				.map(|p| (p.0.name(), ParamDefault::exists(p.1.is_some())))41				.collect()42		))43	}4445	let mut filled_named = 0;46	let mut filled_positionals = 0;4748	args.unnamed_iter(ctx.clone(), tailstrict, &mut |id, arg| {49		let name = params[id].0.clone();50		destruct(51			&name,52			arg,53			Pending::new_filled(ctx.clone()),54			&mut passed_args,55		)?;56		filled_positionals += 1;57		Ok(())58	})?;5960	args.named_iter(ctx, tailstrict, &mut |name, value| {61		// FIXME: O(n) for arg existence check62		if !params.iter().any(|p| p.0.name().as_ref() == Some(name)) {63			bail!(UnknownFunctionParameter((name as &str).to_owned()));64		}65		if passed_args.insert(name.clone(), value).is_some() {66			bail!(BindingParameterASecondTime(name.clone()));67		}68		filled_named += 1;69		Ok(())70	})?;7172	if filled_named + filled_positionals < params.len() {73		// Some args are unset, but maybe we have defaults for them74		// Default values should be created in newly created context75		let fctx = Context::new_future();76		let mut defaults = FxHashMap::with_capacity(77			params.iter().map(|p| p.0.capacity_hint()).sum::<usize>()78				- filled_named79				- filled_positionals,80		);8182		for (idx, param) in params.iter().enumerate().filter(|p| p.1 .1.is_some()) {83			if let Some(name) = param.0.name() {84				if passed_args.contains_key(&name) {85					continue;86				}87			} else if idx < filled_positionals {88				continue;89			}9091			destruct(92				&param.0,93				{94					let ctx = fctx.clone();95					let name = param.0.name().unwrap_or_else(|| "<destruct>".into());96					let value = param.1.clone().expect("default exists");97					Thunk!(move || evaluate_named(ctx.unwrap(), &value, name))98				},99				fctx.clone(),100				&mut defaults,101			)?;102			if param.0.name().is_some() {103				filled_named += 1;104			} else {105				filled_positionals += 1;106			}107		}108109		// Some args still weren't filled110		if filled_named + filled_positionals != params.len() {111			for param in params.iter().skip(args.unnamed_len()) {112				let mut found = false;113				args.named_names(&mut |name| {114					if Some(name) == param.0.name().as_ref() {115						found = true;116					}117				});118				if !found {119					bail!(FunctionParameterNotBoundInCall(120						param.0.clone().name(),121						params122							.iter()123							.map(|p| (p.0.name(), ParamDefault::exists(p.1.is_some())))124							.collect()125					));126				}127			}128			unreachable!();129		}130131		Ok(body_ctx132			.extend_bindings(passed_args)133			.extend_bindings(defaults)134			.into_future(fctx))135	} else {136		let body_ctx = body_ctx.extend_bindings(passed_args);137		Ok(body_ctx)138	}139}140141/// You shouldn't probally use this function, use `jrsonnet_macros::builtin` instead142///143/// ## Parameters144/// * `ctx`: used for passed argument expressions' execution and for body execution (if `body_ctx` is not set)145/// * `params`: function parameters' definition146/// * `args`: passed function arguments147/// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily148pub fn parse_builtin_call(149	ctx: Context,150	params: &[ParamParse],151	args: &dyn ArgsLike,152	tailstrict: bool,153) -> Result<Vec<Option<Thunk<Val>>>> {154	let mut passed_args: Vec<Option<Thunk<Val>>> = vec![None; params.len()];155	if args.unnamed_len() > params.len() {156		bail!(TooManyArgsFunctionHas(157			params.len(),158			params159				.iter()160				.map(|p| (p.name().as_str().map(IStr::from), p.default()))161				.collect()162		))163	}164165	let mut filled_args = 0;166167	args.unnamed_iter(ctx.clone(), tailstrict, &mut |id, arg| {168		passed_args[id] = Some(arg);169		filled_args += 1;170		Ok(())171	})?;172173	args.named_iter(ctx, tailstrict, &mut |name, arg| {174		// FIXME: O(n) for arg existence check175		let id = params176			.iter()177			.position(|p| p.name() == name)178			.ok_or_else(|| UnknownFunctionParameter((name as &str).to_owned()))?;179		if replace(&mut passed_args[id], Some(arg)).is_some() {180			bail!(BindingParameterASecondTime(name.clone()));181		}182		filled_args += 1;183		Ok(())184	})?;185186	if filled_args < params.len() {187		for (id, _) in params.iter().enumerate().filter(|(_, p)| p.has_default()) {188			if passed_args[id].is_some() {189				continue;190			}191			filled_args += 1;192		}193194		// Some args still wasn't filled195		if filled_args != params.len() {196			for param in params.iter().skip(args.unnamed_len()) {197				let mut found = false;198				args.named_names(&mut |name| {199					if param.name() == name {200						found = true;201					}202				});203				if !found {204					bail!(FunctionParameterNotBoundInCall(205						param.name().as_str().map(IStr::from),206						params207							.iter()208							.map(|p| (p.name().as_str().map(IStr::from), p.default()))209							.collect()210					));211				}212			}213			unreachable!();214		}215	}216	Ok(passed_args)217}218219/// Creates Context, which has all argument default values applied220/// and with unbound values causing error to be returned221pub fn parse_default_function_call(body_ctx: Context, params: &ParamsDesc) -> Result<Context> {222	let fctx = Context::new_future();223224	let mut bindings = FxHashMap::with_capacity(params.iter().map(|p| p.0.capacity_hint()).sum());225226	for param in params.iter() {227		if let Some(v) = &param.1 {228			destruct(229				&param.0.clone(),230				{231					let ctx = fctx.clone();232					let name = param.0.name().unwrap_or_else(|| "<destruct>".into());233					let value = v.clone();234					Thunk!(move || evaluate_named(ctx.unwrap(), &value, name))235				},236				fctx.clone(),237				&mut bindings,238			)?;239		} else {240			destruct(241				&param.0,242				{243					let param_name = param.0.name().unwrap_or_else(|| "<destruct>".into());244					let params = params.clone();245					Thunk!(move || Err(FunctionParameterNotBoundInCall(246						Some(param_name),247						params248							.iter()249							.map(|p| (p.0.name(), ParamDefault::exists(p.1.is_some())))250							.collect(),251					)252					.into()))253				},254				fctx.clone(),255				&mut bindings,256			)?;257		}258	}259260	Ok(body_ctx.extend_bindings(bindings).into_future(fctx))261}
modifiedcrates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-macros/src/lib.rs
+++ b/crates/jrsonnet-macros/src/lib.rs
@@ -239,9 +239,7 @@
 			cfg_attrs,
 			..
 		} => {
-			let name = name
-				.as_ref()
-				.map_or_else(|| quote! {None}, |n| quote! {ParamName::new_static(#n)});
+			let name = name.as_ref().map_or_else(|| quote! {unnamed}, |n| quote! {named(#n)});
 			let default = match optionality {
 				Optionality::Required => quote!(ParamDefault::None),
 				Optionality::Optional => quote!(ParamDefault::Exists),
@@ -249,15 +247,13 @@
 			};
 			Some(quote! {
 				#(#cfg_attrs)*
-				BuiltinParam::new(#name, #default),
+				[#name => #default],
 			})
 		}
 		ArgInfo::Lazy { is_option, name } => {
-			let name = name
-				.as_ref()
-				.map_or_else(|| quote! {None}, |n| quote! {ParamName::new_static(#n)});
+			let name = name.as_ref().map_or_else(|| quote! {unnamed}, |n| quote! {named(#n)});
 			Some(quote! {
-				BuiltinParam::new(#name, ParamDefault::exists(#is_option)),
+				[#name => ParamDefault::exists(#is_option)],
 			})
 		}
 		ArgInfo::Context | ArgInfo::Location | ArgInfo::This => None,
@@ -368,13 +364,13 @@
 		const _: () = {
 			use ::jrsonnet_evaluator::{
 				State, Val,
-				function::{builtin::{Builtin, StaticBuiltin, BuiltinParam, ParamName, ParamDefault}, CallLocation, ArgsLike, parse::parse_builtin_call},
+				function::{builtin::{Builtin, StaticBuiltin, ParamParse, ParamName, ParamDefault}, CallLocation, ArgsLike, parse::parse_builtin_call},
 				Result, Context, typed::Typed,
-				parser::Span,
+				parser::Span, params,
 			};
-			const PARAMS: &'static [BuiltinParam] = &[
+			params!(
 				#(#params_desc)*
-			];
+			);
 
 			#static_ext
 			impl Builtin for #name
@@ -384,12 +380,15 @@
 				fn name(&self) -> &str {
 					stringify!(#name)
 				}
-				fn params(&self) -> &[BuiltinParam] {
-					PARAMS
+				fn params(&self) -> &[ParamParse] {
+					/// Safety: ParamParse contains IStr, which is thread-local, thus neither Send or Sync
+					/// The result of this transmute can not outlive the thread, thus 'static here is equivalent to the
+					/// nightly-only 'thread
+					PARAMS.with(|p| unsafe { std::mem::transmute::<&[ParamParse], &'static [ParamParse]>(p.as_slice()) })
 				}
 				#[allow(unused_variables)]
 				fn call(&self, ctx: Context, location: CallLocation, args: &dyn ArgsLike) -> Result<Val> {
-					let parsed = parse_builtin_call(ctx.clone(), &PARAMS, args, false)?;
+					let parsed = parse_builtin_call(ctx.clone(), self.params(), args, false)?;
 
 					let result: #result = #name(#(#pass)*);
 					<_ as Typed>::into_result(result)