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
after · crates/jrsonnet-evaluator/src/function/mod.rs
1use std::{fmt::Debug, rc::Rc};23pub use arglike::{ArgLike, ArgsLike, TlaArg};4use educe::Educe;5use jrsonnet_gcmodule::{Cc, Trace};6use jrsonnet_interner::IStr;7pub use jrsonnet_macros::builtin;8use jrsonnet_parser::{Destruct, Expr, ParamsDesc, Span, Spanned};910use self::{11	arglike::OptionalContext,12	builtin::{Builtin, ParamParse, StaticBuiltin},13	native::NativeDesc,14	parse::{parse_default_function_call, parse_function_call},15};16use crate::{17	bail, error::ErrorKind::*, evaluate, evaluate_trivial, function::builtin::BuiltinFunc, Context,18	ContextBuilder, Result, Thunk, Val,19};2021pub mod arglike;22pub mod builtin;23pub mod native;24pub mod parse;2526/// Function callsite location.27/// Either from other jsonnet code, specified by expression location, or from native (without location).28#[derive(Clone, Copy)]29pub struct CallLocation<'l>(pub Option<&'l Span>);30impl<'l> CallLocation<'l> {31	/// Construct new location for calls coming from specified jsonnet expression location.32	pub const fn new(loc: &'l Span) -> Self {33		Self(Some(loc))34	}35}36impl CallLocation<'static> {37	/// Construct new location for calls coming from native code.38	pub const fn native() -> Self {39		Self(None)40	}41}4243/// Represents Jsonnet function defined in code.44#[derive(Trace, Educe)]45#[educe(Debug, PartialEq)]46pub struct FuncDesc {47	/// # Example48	///49	/// In expressions like this, deducted to `a`, unspecified otherwise.50	/// ```jsonnet51	/// local a = function() ...52	/// local a() ...53	/// { a: function() ... }54	/// { a() = ... }55	/// ```56	pub name: IStr,57	/// Context, in which this function was evaluated.58	///59	/// # Example60	/// In61	/// ```jsonnet62	/// local a = 2;63	/// function() ...64	/// ```65	/// context will contain `a`.66	pub ctx: Context,6768	/// Function parameter definition69	pub params: ParamsDesc,70	/// Function body71	pub body: Rc<Spanned<Expr>>,7273	#[educe(PartialEq = false, Debug = false)]74	pub(crate) params_parse: Vec<ParamParse>,75}76impl FuncDesc {77	/// Create body context, but fill arguments without defaults with lazy error78	pub fn default_body_context(&self) -> Result<Context> {79		parse_default_function_call(self.ctx.clone(), &self.params)80	}8182	/// Create context, with which body code will run83	pub fn call_body_context(84		&self,85		call_ctx: Context,86		args: &dyn ArgsLike,87		tailstrict: bool,88	) -> Result<Context> {89		parse_function_call(call_ctx, self.ctx.clone(), &self.params, args, tailstrict)90	}9192	pub fn evaluate_trivial(&self) -> Option<Val> {93		evaluate_trivial(&self.body)94	}95}9697/// Represents a Jsonnet function value, including plain functions and user-provided builtins.98#[allow(clippy::module_name_repetitions)]99#[derive(Trace, Clone)]100pub enum FuncVal {101	/// Identity function, kept this way for comparsions.102	Id,103	/// Plain function implemented in jsonnet.104	Normal(Cc<FuncDesc>),105	/// Function without arguments works just as a fancy thunk value.106	Thunk(Thunk<Val>),107	/// Standard library function.108	StaticBuiltin(#[trace(skip)] &'static dyn StaticBuiltin),109	/// User-provided function.110	Builtin(BuiltinFunc),111}112113impl Debug for FuncVal {114	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {115		match self {116			Self::Id => f.debug_tuple("Id").finish(),117			Self::Thunk(arg0) => f.debug_tuple("Thunk").field(arg0).finish(),118			Self::Normal(arg0) => f.debug_tuple("Normal").field(arg0).finish(),119			Self::StaticBuiltin(arg0) => {120				f.debug_tuple("StaticBuiltin").field(&arg0.name()).finish()121			}122			Self::Builtin(arg0) => f.debug_tuple("Builtin").field(&arg0.name()).finish(),123		}124	}125}126127#[allow(clippy::unnecessary_wraps)]128#[builtin]129const fn builtin_id(x: Val) -> Val {130	x131}132static ID: &builtin_id = &builtin_id {};133134impl FuncVal {135	pub fn builtin(builtin: impl Builtin) -> Self {136		Self::Builtin(BuiltinFunc::new(builtin))137	}138	pub fn static_builtin(static_builtin: &'static dyn StaticBuiltin) -> Self {139		Self::StaticBuiltin(static_builtin)140	}141142	pub fn params(&self) -> &[ParamParse] {143		match self {144			Self::Id => ID.params(),145			Self::StaticBuiltin(i) => i.params(),146			Self::Builtin(i) => i.params(),147			Self::Normal(p) => &p.params_parse,148			Self::Thunk(_) => &[],149		}150	}151	/// Amount of non-default required arguments152	pub fn params_len(&self) -> usize {153		match self {154			Self::Id => 1,155			Self::Normal(n) => n.params.iter().filter(|p| p.1.is_none()).count(),156			Self::StaticBuiltin(i) => i.params().iter().filter(|p| !p.has_default()).count(),157			Self::Builtin(i) => i.params().iter().filter(|p| !p.has_default()).count(),158			Self::Thunk(_) => 0,159		}160	}161	/// Function name, as defined in code.162	pub fn name(&self) -> IStr {163		match self {164			Self::Id => "id".into(),165			Self::Normal(normal) => normal.name.clone(),166			Self::StaticBuiltin(builtin) => builtin.name().into(),167			Self::Builtin(builtin) => builtin.name().into(),168			Self::Thunk(_) => "thunk".into(),169		}170	}171	/// Call function using arguments evaluated in specified `call_ctx` [`Context`].172	///173	/// If `tailstrict` is specified - then arguments will be evaluated before being passed to function body.174	pub fn evaluate(175		&self,176		call_ctx: Context,177		loc: CallLocation<'_>,178		args: &dyn ArgsLike,179		tailstrict: bool,180	) -> Result<Val> {181		match self {182			Self::Id => ID.call(call_ctx, loc, args),183			Self::Normal(func) => {184				let body_ctx = func.call_body_context(call_ctx, args, tailstrict)?;185				evaluate(body_ctx, &func.body)186			}187			Self::Thunk(thunk) => {188				if args.is_empty() {189					bail!(TooManyArgsFunctionHas(0, vec![],))190				}191				thunk.evaluate()192			}193			Self::StaticBuiltin(b) => b.call(call_ctx, loc, args),194			Self::Builtin(b) => b.call(call_ctx, loc, args),195		}196	}197	pub fn evaluate_simple<A: ArgsLike + OptionalContext>(198		&self,199		args: &A,200		tailstrict: bool,201	) -> Result<Val> {202		self.evaluate(203			ContextBuilder::new().build(),204			CallLocation::native(),205			args,206			tailstrict,207		)208	}209	/// Convert jsonnet function to plain `Fn` value.210	pub fn into_native<D: NativeDesc>(self) -> D::Value {211		D::into_native(self)212	}213214	/// Is this function an indentity function.215	///216	/// Currently only works for builtin `std.id`, aka `Self::Id` value, and `function(x) x`.217	///218	/// This function should only be used for optimization, not for the conditional logic, i.e code should work with syntetic identity function too219	pub fn is_identity(&self) -> bool {220		match self {221			Self::Id => true,222			Self::Normal(desc) => {223				if desc.params.len() != 1 {224					return false;225				}226				let param = &desc.params[0];227				if param.1.is_some() {228					return false;229				}230				#[allow(clippy::infallible_destructuring_match)]231				let id = match &param.0 {232					Destruct::Full(id) => id,233					#[cfg(feature = "exp-destruct")]234					_ => return false,235				};236				**desc.body == Expr::Var(id.clone())237			}238			_ => false,239		}240	}241	/// Identity function value.242	pub const fn identity() -> Self {243		Self::Id244	}245246	pub fn evaluate_trivial(&self) -> Option<Val> {247		match self {248			Self::Normal(n) => n.evaluate_trivial(),249			_ => None,250		}251	}252}253254impl<T> From<T> for FuncVal255where256	T: Builtin,257{258	fn from(value: T) -> Self {259		Self::builtin(value)260	}261}262impl From<&'static dyn StaticBuiltin> for FuncVal {263	fn from(value: &'static dyn StaticBuiltin) -> Self {264		Self::static_builtin(value)265	}266}
modifiedcrates/jrsonnet-evaluator/src/function/parse.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/parse.rs
+++ b/crates/jrsonnet-evaluator/src/function/parse.rs
@@ -4,7 +4,7 @@
 use jrsonnet_parser::ParamsDesc;
 use rustc_hash::FxHashMap;
 
-use super::{arglike::ArgsLike, builtin::BuiltinParam};
+use super::{arglike::ArgsLike, builtin::ParamParse};
 use crate::{
 	bail,
 	destructure::destruct,
@@ -147,7 +147,7 @@
 /// * `tailstrict`: if set to `true` function arguments are eagerly executed, otherwise - lazily
 pub fn parse_builtin_call(
 	ctx: Context,
-	params: &[BuiltinParam],
+	params: &[ParamParse],
 	args: &dyn ArgsLike,
 	tailstrict: bool,
 ) -> Result<Vec<Option<Thunk<Val>>>> {
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)