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

difftreelog

refactor do not allocate for BuiltinParam vec

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

5 files changed

modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/evaluate/mod.rs
1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_parser::{6	ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, FieldMember, FieldName,7	ForSpecData, IfSpecData, ImportKind, LiteralType, ObjBody, ObjMembers, ParamsDesc, Spanned,8};9use jrsonnet_types::ValType;10use rustc_hash::FxHashMap;1112use self::destructure::destruct;13use crate::{14	arr::ArrValue,15	bail,16	destructure::evaluate_dest,17	error::{suggest_object_fields, ErrorKind::*},18	evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},19	function::{CallLocation, FuncDesc, FuncVal},20	gc::WithCapacityExt as _,21	in_frame,22	typed::Typed,23	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},24	with_state, Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,25	ResultExt, SupThis, Unbound, Val,26};27pub mod destructure;28pub mod operator;2930// This is the amount of bytes that need to be left on the stack before increasing the size.31// It must be at least as large as the stack required by any code that does not call32// `ensure_sufficient_stack`.33const RED_ZONE: usize = 100 * 1024; // 100k3435// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then36// on. This flag has performance relevant characteristics. Don't set it too high.37const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB3839/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations40/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit41/// from this.42///43/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.44#[inline]45pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {46	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)47}4849pub fn evaluate_trivial(expr: &Spanned<Expr>) -> Option<Val> {50	fn is_trivial(expr: &Spanned<Expr>) -> bool {51		match &**expr {52			Expr::Str(_)53			| Expr::Num(_)54			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,55			Expr::Arr(a) => a.iter().all(is_trivial),56			_ => false,57		}58	}59	Some(match &**expr {60		Expr::Str(s) => Val::string(s.clone()),61		Expr::Num(n) => {62			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))63		}64		Expr::Literal(LiteralType::False) => Val::Bool(false),65		Expr::Literal(LiteralType::True) => Val::Bool(true),66		Expr::Literal(LiteralType::Null) => Val::Null,67		Expr::Arr(n) => {68			if n.iter().any(|e| !is_trivial(e)) {69				return None;70			}71			Val::Arr(ArrValue::eager(72				n.iter()73					.map(evaluate_trivial)74					.map(|e| e.expect("checked trivial"))75					.collect(),76			))77		}78		_ => return None,79	})80}8182pub fn evaluate_method(83	ctx: Context,84	name: IStr,85	params: ParamsDesc,86	body: Rc<Spanned<Expr>>,87) -> Val {88	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {89		name,90		ctx,91		params,92		body,93	})))94}9596pub fn evaluate_field_name(ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {97	Ok(match field_name {98		FieldName::Fixed(n) => Some(n.clone()),99		FieldName::Dyn(expr) => in_frame(100			CallLocation::new(&expr.span()),101			|| "evaluating field name".to_string(),102			|| {103				let value = evaluate(ctx, expr)?;104				if matches!(value, Val::Null) {105					Ok(None)106				} else {107					Ok(Some(IStr::from_untyped(value)?))108				}109			},110		)?,111	})112}113114pub fn evaluate_comp(115	ctx: Context,116	specs: &[CompSpec],117	callback: &mut impl FnMut(Context) -> Result<()>,118) -> Result<()> {119	match specs.first() {120		None => callback(ctx)?,121		Some(CompSpec::IfSpec(IfSpecData(cond))) => {122			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {123				evaluate_comp(ctx, &specs[1..], callback)?;124			}125		}126		Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {127			Val::Arr(list) => {128				for item in list.iter_lazy() {129					let fctx = Pending::new();130					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());131					destruct(var, item, fctx.clone(), &mut new_bindings)?;132					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);133134					evaluate_comp(ctx, &specs[1..], callback)?;135				}136			}137			#[cfg(feature = "exp-object-iteration")]138			Val::Obj(obj) => {139				for field in obj.fields(140					// TODO: Should there be ability to preserve iteration order?141					#[cfg(feature = "exp-preserve-order")]142					false,143				) {144					let fctx = Pending::new();145					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());146					let obj = obj.clone();147					let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![148						Thunk::evaluated(Val::string(field.clone())),149						Thunk!(move || obj.get(field).transpose().expect(150							"field exists, as field name was obtained from object.fields()",151						)),152					])));153					destruct(var, value, fctx.clone(), &mut new_bindings)?;154					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);155156					evaluate_comp(ctx, &specs[1..], callback)?;157				}158			}159			_ => bail!(InComprehensionCanOnlyIterateOverArray),160		},161	}162	Ok(())163}164165trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}166impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}167168fn evaluate_object_locals(169	fctx: Context,170	locals: Rc<Vec<BindSpec>>,171) -> impl CloneableUnbound<Context> {172	#[derive(Trace, Clone)]173	struct UnboundLocals {174		fctx: Context,175		locals: Rc<Vec<BindSpec>>,176	}177	impl Unbound for UnboundLocals {178		type Bound = Context;179180		fn bind(&self, sup_this: SupThis) -> Result<Context> {181			let fctx = Context::new_future();182			let mut new_bindings =183				FxHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());184			for b in self.locals.iter() {185				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;186			}187188			let ctx = self.fctx.clone();189190			let ctx = ctx191				.extend_bindings_sup_this(new_bindings, sup_this)192				.into_future(fctx);193194			Ok(ctx)195		}196	}197198	UnboundLocals { fctx, locals }199}200201pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(202	builder: &mut ObjValueBuilder,203	ctx: Context,204	uctx: B,205	field: &FieldMember,206) -> Result<()> {207	let name = evaluate_field_name(ctx, &field.name)?;208	let Some(name) = name else {209		return Ok(());210	};211212	match field {213		FieldMember {214			plus,215			params: None,216			visibility,217			value,218			..219		} => {220			#[derive(Trace)]221			struct UnboundValue<B: Trace> {222				uctx: B,223				value: Rc<Spanned<Expr>>,224				name: IStr,225			}226			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {227				type Bound = Val;228				fn bind(&self, sup_this: SupThis) -> Result<Val> {229					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())230				}231			}232233			builder234				.field(name.clone())235				.with_add(*plus)236				.with_visibility(*visibility)237				.with_location(value.span())238				.bindable(UnboundValue {239					uctx,240					value: value.clone(),241					name,242				})?;243		}244		FieldMember {245			params: Some(params),246			visibility,247			value,248			..249		} => {250			#[derive(Trace)]251			struct UnboundMethod<B: Trace> {252				uctx: B,253				value: Rc<Spanned<Expr>>,254				params: ParamsDesc,255				name: IStr,256			}257			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {258				type Bound = Val;259				fn bind(&self, sup_this: SupThis) -> Result<Val> {260					Ok(evaluate_method(261						self.uctx.bind(sup_this)?,262						self.name.clone(),263						self.params.clone(),264						self.value.clone(),265					))266				}267			}268269			builder270				.field(name.clone())271				.with_visibility(*visibility)272				.with_location(value.span())273				.bindable(UnboundMethod {274					uctx,275					value: value.clone(),276					params: params.clone(),277					name,278				})?;279		}280	}281	Ok(())282}283284#[allow(clippy::too_many_lines)]285pub fn evaluate_member_list_object(ctx: Context, members: &ObjMembers) -> Result<ObjValue> {286	let mut builder = ObjValueBuilder::new();287	let locals = members.locals.clone();288289	// We have single context for all fields, so we can cache binds290	let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));291292	for field in &members.fields {293		evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), &field)?;294	}295296	if !members.asserts.is_empty() {297		#[derive(Trace)]298		struct ObjectAssert<B: Trace> {299			uctx: B,300			asserts: Rc<Vec<AssertStmt>>,301		}302		impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {303			fn run(&self, sup_this: SupThis) -> Result<()> {304				let ctx = self.uctx.bind(sup_this)?;305				for assert in &*self.asserts {306					evaluate_assert(ctx.clone(), &assert)?;307				}308				Ok(())309			}310		}311		builder.assert(ObjectAssert {312			uctx: uctx.clone(),313			asserts: members.asserts.clone(),314		});315	}316317	Ok(builder.build())318}319320pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {321	Ok(match object {322		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,323		ObjBody::ObjComp(obj) => {324			let mut builder = ObjValueBuilder::new();325			let locals = obj.locals.clone();326			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {327				let uctx = evaluate_object_locals(ctx.clone(), locals.clone());328329				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)330			})?;331332			builder.build()333		}334	})335}336337pub fn evaluate_apply(338	ctx: Context,339	value: &Spanned<Expr>,340	args: &ArgsDesc,341	loc: CallLocation<'_>,342	tailstrict: bool,343) -> Result<Val> {344	let value = evaluate(ctx.clone(), value)?;345	Ok(match value {346		Val::Func(f) => {347			let body = || f.evaluate(ctx, loc, args, tailstrict);348			if tailstrict {349				body()?350			} else {351				in_frame(loc, || format!("function <{}> call", f.name()), body)?352			}353		}354		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),355	})356}357358pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {359	let value = &assertion.0;360	let msg = &assertion.1;361	let assertion_result = in_frame(362		CallLocation::new(&value.span()),363		|| "assertion condition".to_owned(),364		|| bool::from_untyped(evaluate(ctx.clone(), value)?),365	)?;366	if !assertion_result {367		in_frame(368			CallLocation::new(&value.span()),369			|| "assertion failure".to_owned(),370			|| {371				if let Some(msg) = msg {372					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));373				}374				bail!(AssertionFailed(Val::Null.to_string()?));375			},376		)?;377	}378	Ok(())379}380381pub fn evaluate_named(ctx: Context, expr: &Spanned<Expr>, name: IStr) -> Result<Val> {382	use Expr::*;383	Ok(match &**expr {384		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),385		_ => evaluate(ctx, expr)?,386	})387}388389#[allow(clippy::too_many_lines)]390pub fn evaluate(ctx: Context, expr: &Spanned<Expr>) -> Result<Val> {391	use Expr::*;392393	if let Some(trivial) = evaluate_trivial(expr) {394		return Ok(trivial);395	}396	let loc = expr.span();397	Ok(match &**expr {398		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),399		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),400		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),401		Literal(LiteralType::True) => Val::Bool(true),402		Literal(LiteralType::False) => Val::Bool(false),403		Literal(LiteralType::Null) => Val::Null,404		Str(v) => Val::string(v.clone()),405		Num(v) => Val::try_num(*v)?,406		// I have tried to remove special behavior from super by implementing standalone-super407		// expresion, but looks like this case still needs special treatment.408		//409		// Note that other jsonnet implementations will fail on `if value in (super)` expression,410		// because the standalone super literal is not supported, that is because in other411		// implementations `in super` treated differently from `in smth_else`.412		BinaryOp(bin)413			if matches!(&*bin.rhs, Expr::Literal(LiteralType::Super))414				&& bin.op == BinaryOpType::In =>415		{416			let sup_this = ctx.try_sup_this()?;417			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.418			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.419			if !sup_this.has_super() {420				return Ok(Val::Bool(false));421			}422			let field = evaluate(ctx, &bin.lhs)?;423			Val::Bool(sup_this.field_in_super(field.to_string()?))424		}425		BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?,426		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,427		Var(name) => in_frame(428			CallLocation::new(&loc),429			|| format!("local <{name}> access"),430			|| ctx.binding(name.clone())?.evaluate(),431		)?,432		Index { indexable, parts } => ensure_sufficient_stack(|| {433			let mut parts = parts.iter();434			let mut indexable = if matches!(&***indexable, Expr::Literal(LiteralType::Super)) {435				let part = parts.next().expect("at least part should exist");436				// sup_this existence check might also be skipped here for null-coalesce...437				// But I believe this might cause errors.438				let sup_this = ctx.try_sup_this()?;439				if !sup_this.has_super() {440					#[cfg(feature = "exp-null-coaelse")]441					if part.null_coaelse {442						return Ok(Val::Null);443					}444					bail!(NoSuperFound)445				}446				let name = evaluate(ctx.clone(), &part.value)?;447448				let Val::Str(name) = name else {449					bail!(ValueIndexMustBeTypeGot(450						ValType::Obj,451						ValType::Str,452						name.value_type(),453					))454				};455456				let name = name.into_flat();457				match sup_this458					.get_super(name.clone())459					.with_description_src(&part.value, || format!("field <{name}> access"))?460				{461					Some(v) => v,462					#[cfg(feature = "exp-null-coaelse")]463					None if part.null_coaelse => return Ok(Val::Null),464					None => {465						let suggestions = suggest_object_fields(466							&sup_this.standalone_super().expect("super exists"),467							name.clone(),468						);469470						bail!(NoSuchField(name, suggestions))471					}472				}473			} else {474				evaluate(ctx.clone(), indexable)?475			};476477			for part in parts {478				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {479					(Val::Obj(v), Val::Str(key)) => match v480						.get(key.clone().into_flat())481						.with_description_src(&part.value, || format!("field <{key}> access"))?482					{483						Some(v) => v,484						#[cfg(feature = "exp-null-coaelse")]485						None if part.null_coaelse => return Ok(Val::Null),486						None => {487							let suggestions = suggest_object_fields(&v, key.clone().into_flat());488489							return Err(Error::from(NoSuchField(490								key.clone().into_flat(),491								suggestions,492							)))493							.with_description_src(&part.value, || format!("field <{key}> access"));494						}495					},496					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(497						ValType::Obj,498						ValType::Str,499						n.value_type(),500					)),501					(Val::Arr(v), Val::Num(n)) => {502						let n = n.get();503						if n.fract() > f64::EPSILON {504							bail!(FractionalIndex)505						}506						if n < 0.0 {507							bail!(ArrayBoundsError(n as isize, v.len()));508						}509						v.get(n as usize)?510							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?511					}512					(Val::Arr(_), Val::Str(n)) => {513						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))514					}515					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(516						ValType::Arr,517						ValType::Num,518						n.value_type(),519					)),520521					(Val::Str(s), Val::Num(n)) => Val::Str({522						let n = n.get();523						if n.fract() > f64::EPSILON {524							bail!(FractionalIndex)525						}526						if n < 0.0 {527							bail!(ArrayBoundsError(n as isize, s.into_flat().chars().count()));528						}529						let v: IStr = s530							.clone()531							.into_flat()532							.chars()533							.skip(n as usize)534							.take(1)535							.collect::<String>()536							.into();537						if v.is_empty() {538							bail!(StringBoundsError(n as usize, s.into_flat().chars().count()))539						}540						StrValue::Flat(v)541					}),542					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(543						ValType::Str,544						ValType::Num,545						n.value_type(),546					)),547					#[cfg(feature = "exp-null-coaelse")]548					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),549					(v, _) => bail!(CantIndexInto(v.value_type())),550				};551			}552			Ok(indexable)553		})?,554		LocalExpr(bindings, returned) => {555			let mut new_bindings: FxHashMap<IStr, Thunk<Val>> =556				FxHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());557			let fctx = Context::new_future();558			for b in bindings {559				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;560			}561			let ctx = ctx.extend_bindings(new_bindings).into_future(fctx);562			evaluate(ctx, &returned.clone())?563		}564		Arr(items) => {565			if items.is_empty() {566				Val::Arr(ArrValue::empty())567			} else {568				Val::Arr(ArrValue::expr(ctx, items.clone()))569			}570		}571		ArrComp(expr, comp_specs) => {572			let mut out = Vec::new();573			evaluate_comp(ctx, comp_specs, &mut |ctx| {574				let expr = expr.clone();575				out.push(Thunk!(move || evaluate(ctx, &expr)));576				Ok(())577			})?;578			Val::Arr(ArrValue::lazy(out))579		}580		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),581		ObjExtend(a, b) => evaluate_add_op(582			&evaluate(ctx.clone(), a)?,583			&Val::Obj(evaluate_object(ctx, b)?),584		)?,585		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {586			evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)587		})?,588		Function(params, body) => {589			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())590		}591		AssertExpr(assert) => {592			evaluate_assert(ctx.clone(), &assert.assert)?;593			evaluate(ctx, &assert.rest)?594		}595		ErrorStmt(e) => in_frame(596			CallLocation::new(&loc),597			|| "error statement".to_owned(),598			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),599		)?,600		IfElse(if_else) => {601			if in_frame(602				CallLocation::new(&loc),603				|| "if condition".to_owned(),604				|| bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.0)?),605			)? {606				evaluate(ctx, &if_else.cond_then)?607			} else {608				match &if_else.cond_else {609					Some(v) => evaluate(ctx, v)?,610					None => Val::Null,611				}612			}613		}614		Slice(slice) => {615			fn parse_idx<T: Typed>(616				loc: CallLocation<'_>,617				ctx: Context,618				expr: Option<&Spanned<Expr>>,619				desc: &'static str,620			) -> Result<Option<T>> {621				if let Some(value) = expr {622					Ok(in_frame(623						loc,624						|| format!("slice {desc}"),625						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),626					)?)627				} else {628					Ok(None)629				}630			}631632			let indexable = evaluate(ctx.clone(), &slice.value)?;633			let loc = CallLocation::new(&loc);634635			let start = parse_idx(loc, ctx.clone(), slice.slice.start.as_ref(), "start")?;636			let end = parse_idx(loc, ctx.clone(), slice.slice.end.as_ref(), "end")?;637			let step = parse_idx(loc, ctx, slice.slice.step.as_ref(), "step")?;638639			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?640		}641		Import(kind, path) => {642			let Expr::Str(path) = &***path else {643				bail!("computed imports are not supported")644			};645			let tmp = loc.clone().0;646			with_state(|s| {647				let resolved_path = s.resolve_from(tmp.source_path(), path)?;648				Ok(match kind {649					ImportKind::Normal => in_frame(650						CallLocation::new(&loc),651						|| format!("import {:?}", path.clone()),652						|| s.import_resolved(resolved_path),653					)?,654					ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?),655					ImportKind::Bin => {656						Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?))657					}658				}) as Result<Val>659			})?660		}661	})662}
after · crates/jrsonnet-evaluator/src/evaluate/mod.rs
1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_parser::{6	ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, FieldMember, FieldName,7	ForSpecData, IfSpecData, ImportKind, LiteralType, ObjBody, ObjMembers, ParamsDesc, Spanned,8};9use jrsonnet_types::ValType;10use rustc_hash::FxHashMap;1112use self::destructure::destruct;13use crate::{14	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_state15};16pub mod destructure;17pub mod operator;1819// This is the amount of bytes that need to be left on the stack before increasing the size.20// It must be at least as large as the stack required by any code that does not call21// `ensure_sufficient_stack`.22const RED_ZONE: usize = 100 * 1024; // 100k2324// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then25// on. This flag has performance relevant characteristics. Don't set it too high.26const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB2728/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations29/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit30/// from this.31///32/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.33#[inline]34pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {35	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)36}3738pub fn evaluate_trivial(expr: &Spanned<Expr>) -> Option<Val> {39	fn is_trivial(expr: &Spanned<Expr>) -> bool {40		match &**expr {41			Expr::Str(_)42			| Expr::Num(_)43			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,44			Expr::Arr(a) => a.iter().all(is_trivial),45			_ => false,46		}47	}48	Some(match &**expr {49		Expr::Str(s) => Val::string(s.clone()),50		Expr::Num(n) => {51			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))52		}53		Expr::Literal(LiteralType::False) => Val::Bool(false),54		Expr::Literal(LiteralType::True) => Val::Bool(true),55		Expr::Literal(LiteralType::Null) => Val::Null,56		Expr::Arr(n) => {57			if n.iter().any(|e| !is_trivial(e)) {58				return None;59			}60			Val::Arr(ArrValue::eager(61				n.iter()62					.map(evaluate_trivial)63					.map(|e| e.expect("checked trivial"))64					.collect(),65			))66		}67		_ => return None,68	})69}7071pub fn evaluate_method(72	ctx: Context,73	name: IStr,74	params: ParamsDesc,75	body: Rc<Spanned<Expr>>,76) -> Val {77	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {78		name,79		ctx,80		params_parse: params81			.iter()82			.map(|p| {83				ParamParse::new(84					p.0.name().map_or(ParamName::ANONYMOUS, ParamName::new),85					ParamDefault::exists(p.1.is_some()),86				)87			})88			.collect(),89		params,90		body,91	})))92}9394pub fn evaluate_field_name(ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {95	Ok(match field_name {96		FieldName::Fixed(n) => Some(n.clone()),97		FieldName::Dyn(expr) => in_frame(98			CallLocation::new(&expr.span()),99			|| "evaluating field name".to_string(),100			|| {101				let value = evaluate(ctx, expr)?;102				if matches!(value, Val::Null) {103					Ok(None)104				} else {105					Ok(Some(IStr::from_untyped(value)?))106				}107			},108		)?,109	})110}111112pub fn evaluate_comp(113	ctx: Context,114	specs: &[CompSpec],115	callback: &mut impl FnMut(Context) -> Result<()>,116) -> Result<()> {117	match specs.first() {118		None => callback(ctx)?,119		Some(CompSpec::IfSpec(IfSpecData(cond))) => {120			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {121				evaluate_comp(ctx, &specs[1..], callback)?;122			}123		}124		Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {125			Val::Arr(list) => {126				for item in list.iter_lazy() {127					let fctx = Pending::new();128					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());129					destruct(var, item, fctx.clone(), &mut new_bindings)?;130					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);131132					evaluate_comp(ctx, &specs[1..], callback)?;133				}134			}135			#[cfg(feature = "exp-object-iteration")]136			Val::Obj(obj) => {137				for field in obj.fields(138					// TODO: Should there be ability to preserve iteration order?139					#[cfg(feature = "exp-preserve-order")]140					false,141				) {142					let fctx = Pending::new();143					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());144					let obj = obj.clone();145					let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![146						Thunk::evaluated(Val::string(field.clone())),147						Thunk!(move || obj.get(field).transpose().expect(148							"field exists, as field name was obtained from object.fields()",149						)),150					])));151					destruct(var, value, fctx.clone(), &mut new_bindings)?;152					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);153154					evaluate_comp(ctx, &specs[1..], callback)?;155				}156			}157			_ => bail!(InComprehensionCanOnlyIterateOverArray),158		},159	}160	Ok(())161}162163trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}164impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}165166fn evaluate_object_locals(167	fctx: Context,168	locals: Rc<Vec<BindSpec>>,169) -> impl CloneableUnbound<Context> {170	#[derive(Trace, Clone)]171	struct UnboundLocals {172		fctx: Context,173		locals: Rc<Vec<BindSpec>>,174	}175	impl Unbound for UnboundLocals {176		type Bound = Context;177178		fn bind(&self, sup_this: SupThis) -> Result<Context> {179			let fctx = Context::new_future();180			let mut new_bindings =181				FxHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());182			for b in self.locals.iter() {183				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;184			}185186			let ctx = self.fctx.clone();187188			let ctx = ctx189				.extend_bindings_sup_this(new_bindings, sup_this)190				.into_future(fctx);191192			Ok(ctx)193		}194	}195196	UnboundLocals { fctx, locals }197}198199pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(200	builder: &mut ObjValueBuilder,201	ctx: Context,202	uctx: B,203	field: &FieldMember,204) -> Result<()> {205	let name = evaluate_field_name(ctx, &field.name)?;206	let Some(name) = name else {207		return Ok(());208	};209210	match field {211		FieldMember {212			plus,213			params: None,214			visibility,215			value,216			..217		} => {218			#[derive(Trace)]219			struct UnboundValue<B: Trace> {220				uctx: B,221				value: Rc<Spanned<Expr>>,222				name: IStr,223			}224			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {225				type Bound = Val;226				fn bind(&self, sup_this: SupThis) -> Result<Val> {227					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())228				}229			}230231			builder232				.field(name.clone())233				.with_add(*plus)234				.with_visibility(*visibility)235				.with_location(value.span())236				.bindable(UnboundValue {237					uctx,238					value: value.clone(),239					name,240				})?;241		}242		FieldMember {243			params: Some(params),244			visibility,245			value,246			..247		} => {248			#[derive(Trace)]249			struct UnboundMethod<B: Trace> {250				uctx: B,251				value: Rc<Spanned<Expr>>,252				params: ParamsDesc,253				name: IStr,254			}255			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {256				type Bound = Val;257				fn bind(&self, sup_this: SupThis) -> Result<Val> {258					Ok(evaluate_method(259						self.uctx.bind(sup_this)?,260						self.name.clone(),261						self.params.clone(),262						self.value.clone(),263					))264				}265			}266267			builder268				.field(name.clone())269				.with_visibility(*visibility)270				.with_location(value.span())271				.bindable(UnboundMethod {272					uctx,273					value: value.clone(),274					params: params.clone(),275					name,276				})?;277		}278	}279	Ok(())280}281282#[allow(clippy::too_many_lines)]283pub fn evaluate_member_list_object(ctx: Context, members: &ObjMembers) -> Result<ObjValue> {284	let mut builder = ObjValueBuilder::new();285	let locals = members.locals.clone();286287	// We have single context for all fields, so we can cache binds288	let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));289290	for field in &members.fields {291		evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), &field)?;292	}293294	if !members.asserts.is_empty() {295		#[derive(Trace)]296		struct ObjectAssert<B: Trace> {297			uctx: B,298			asserts: Rc<Vec<AssertStmt>>,299		}300		impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {301			fn run(&self, sup_this: SupThis) -> Result<()> {302				let ctx = self.uctx.bind(sup_this)?;303				for assert in &*self.asserts {304					evaluate_assert(ctx.clone(), &assert)?;305				}306				Ok(())307			}308		}309		builder.assert(ObjectAssert {310			uctx: uctx.clone(),311			asserts: members.asserts.clone(),312		});313	}314315	Ok(builder.build())316}317318pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {319	Ok(match object {320		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,321		ObjBody::ObjComp(obj) => {322			let mut builder = ObjValueBuilder::new();323			let locals = obj.locals.clone();324			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {325				let uctx = evaluate_object_locals(ctx.clone(), locals.clone());326327				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)328			})?;329330			builder.build()331		}332	})333}334335pub fn evaluate_apply(336	ctx: Context,337	value: &Spanned<Expr>,338	args: &ArgsDesc,339	loc: CallLocation<'_>,340	tailstrict: bool,341) -> Result<Val> {342	let value = evaluate(ctx.clone(), value)?;343	Ok(match value {344		Val::Func(f) => {345			let body = || f.evaluate(ctx, loc, args, tailstrict);346			if tailstrict {347				body()?348			} else {349				in_frame(loc, || format!("function <{}> call", f.name()), body)?350			}351		}352		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),353	})354}355356pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {357	let value = &assertion.0;358	let msg = &assertion.1;359	let assertion_result = in_frame(360		CallLocation::new(&value.span()),361		|| "assertion condition".to_owned(),362		|| bool::from_untyped(evaluate(ctx.clone(), value)?),363	)?;364	if !assertion_result {365		in_frame(366			CallLocation::new(&value.span()),367			|| "assertion failure".to_owned(),368			|| {369				if let Some(msg) = msg {370					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));371				}372				bail!(AssertionFailed(Val::Null.to_string()?));373			},374		)?;375	}376	Ok(())377}378379pub fn evaluate_named(ctx: Context, expr: &Spanned<Expr>, name: IStr) -> Result<Val> {380	use Expr::*;381	Ok(match &**expr {382		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),383		_ => evaluate(ctx, expr)?,384	})385}386387#[allow(clippy::too_many_lines)]388pub fn evaluate(ctx: Context, expr: &Spanned<Expr>) -> Result<Val> {389	use Expr::*;390391	if let Some(trivial) = evaluate_trivial(expr) {392		return Ok(trivial);393	}394	let loc = expr.span();395	Ok(match &**expr {396		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),397		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),398		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),399		Literal(LiteralType::True) => Val::Bool(true),400		Literal(LiteralType::False) => Val::Bool(false),401		Literal(LiteralType::Null) => Val::Null,402		Str(v) => Val::string(v.clone()),403		Num(v) => Val::try_num(*v)?,404		// I have tried to remove special behavior from super by implementing standalone-super405		// expresion, but looks like this case still needs special treatment.406		//407		// Note that other jsonnet implementations will fail on `if value in (super)` expression,408		// because the standalone super literal is not supported, that is because in other409		// implementations `in super` treated differently from `in smth_else`.410		BinaryOp(bin)411			if matches!(&*bin.rhs, Expr::Literal(LiteralType::Super))412				&& bin.op == BinaryOpType::In =>413		{414			let sup_this = ctx.try_sup_this()?;415			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.416			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.417			if !sup_this.has_super() {418				return Ok(Val::Bool(false));419			}420			let field = evaluate(ctx, &bin.lhs)?;421			Val::Bool(sup_this.field_in_super(field.to_string()?))422		}423		BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?,424		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,425		Var(name) => in_frame(426			CallLocation::new(&loc),427			|| format!("local <{name}> access"),428			|| ctx.binding(name.clone())?.evaluate(),429		)?,430		Index { indexable, parts } => ensure_sufficient_stack(|| {431			let mut parts = parts.iter();432			let mut indexable = if matches!(&***indexable, Expr::Literal(LiteralType::Super)) {433				let part = parts.next().expect("at least part should exist");434				// sup_this existence check might also be skipped here for null-coalesce...435				// But I believe this might cause errors.436				let sup_this = ctx.try_sup_this()?;437				if !sup_this.has_super() {438					#[cfg(feature = "exp-null-coaelse")]439					if part.null_coaelse {440						return Ok(Val::Null);441					}442					bail!(NoSuperFound)443				}444				let name = evaluate(ctx.clone(), &part.value)?;445446				let Val::Str(name) = name else {447					bail!(ValueIndexMustBeTypeGot(448						ValType::Obj,449						ValType::Str,450						name.value_type(),451					))452				};453454				let name = name.into_flat();455				match sup_this456					.get_super(name.clone())457					.with_description_src(&part.value, || format!("field <{name}> access"))?458				{459					Some(v) => v,460					#[cfg(feature = "exp-null-coaelse")]461					None if part.null_coaelse => return Ok(Val::Null),462					None => {463						let suggestions = suggest_object_fields(464							&sup_this.standalone_super().expect("super exists"),465							name.clone(),466						);467468						bail!(NoSuchField(name, suggestions))469					}470				}471			} else {472				evaluate(ctx.clone(), indexable)?473			};474475			for part in parts {476				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {477					(Val::Obj(v), Val::Str(key)) => match v478						.get(key.clone().into_flat())479						.with_description_src(&part.value, || format!("field <{key}> access"))?480					{481						Some(v) => v,482						#[cfg(feature = "exp-null-coaelse")]483						None if part.null_coaelse => return Ok(Val::Null),484						None => {485							let suggestions = suggest_object_fields(&v, key.clone().into_flat());486487							return Err(Error::from(NoSuchField(488								key.clone().into_flat(),489								suggestions,490							)))491							.with_description_src(&part.value, || format!("field <{key}> access"));492						}493					},494					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(495						ValType::Obj,496						ValType::Str,497						n.value_type(),498					)),499					(Val::Arr(v), Val::Num(n)) => {500						let n = n.get();501						if n.fract() > f64::EPSILON {502							bail!(FractionalIndex)503						}504						if n < 0.0 {505							bail!(ArrayBoundsError(n as isize, v.len()));506						}507						v.get(n as usize)?508							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?509					}510					(Val::Arr(_), Val::Str(n)) => {511						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))512					}513					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(514						ValType::Arr,515						ValType::Num,516						n.value_type(),517					)),518519					(Val::Str(s), Val::Num(n)) => Val::Str({520						let n = n.get();521						if n.fract() > f64::EPSILON {522							bail!(FractionalIndex)523						}524						if n < 0.0 {525							bail!(ArrayBoundsError(n as isize, s.into_flat().chars().count()));526						}527						let v: IStr = s528							.clone()529							.into_flat()530							.chars()531							.skip(n as usize)532							.take(1)533							.collect::<String>()534							.into();535						if v.is_empty() {536							bail!(StringBoundsError(n as usize, s.into_flat().chars().count()))537						}538						StrValue::Flat(v)539					}),540					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(541						ValType::Str,542						ValType::Num,543						n.value_type(),544					)),545					#[cfg(feature = "exp-null-coaelse")]546					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),547					(v, _) => bail!(CantIndexInto(v.value_type())),548				};549			}550			Ok(indexable)551		})?,552		LocalExpr(bindings, returned) => {553			let mut new_bindings: FxHashMap<IStr, Thunk<Val>> =554				FxHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());555			let fctx = Context::new_future();556			for b in bindings {557				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;558			}559			let ctx = ctx.extend_bindings(new_bindings).into_future(fctx);560			evaluate(ctx, &returned.clone())?561		}562		Arr(items) => {563			if items.is_empty() {564				Val::Arr(ArrValue::empty())565			} else {566				Val::Arr(ArrValue::expr(ctx, items.clone()))567			}568		}569		ArrComp(expr, comp_specs) => {570			let mut out = Vec::new();571			evaluate_comp(ctx, comp_specs, &mut |ctx| {572				let expr = expr.clone();573				out.push(Thunk!(move || evaluate(ctx, &expr)));574				Ok(())575			})?;576			Val::Arr(ArrValue::lazy(out))577		}578		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),579		ObjExtend(a, b) => evaluate_add_op(580			&evaluate(ctx.clone(), a)?,581			&Val::Obj(evaluate_object(ctx, b)?),582		)?,583		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {584			evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)585		})?,586		Function(params, body) => {587			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())588		}589		AssertExpr(assert) => {590			evaluate_assert(ctx.clone(), &assert.assert)?;591			evaluate(ctx, &assert.rest)?592		}593		ErrorStmt(e) => in_frame(594			CallLocation::new(&loc),595			|| "error statement".to_owned(),596			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),597		)?,598		IfElse(if_else) => {599			if in_frame(600				CallLocation::new(&loc),601				|| "if condition".to_owned(),602				|| bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.0)?),603			)? {604				evaluate(ctx, &if_else.cond_then)?605			} else {606				match &if_else.cond_else {607					Some(v) => evaluate(ctx, v)?,608					None => Val::Null,609				}610			}611		}612		Slice(slice) => {613			fn parse_idx<T: Typed>(614				loc: CallLocation<'_>,615				ctx: Context,616				expr: Option<&Spanned<Expr>>,617				desc: &'static str,618			) -> Result<Option<T>> {619				if let Some(value) = expr {620					Ok(in_frame(621						loc,622						|| format!("slice {desc}"),623						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),624					)?)625				} else {626					Ok(None)627				}628			}629630			let indexable = evaluate(ctx.clone(), &slice.value)?;631			let loc = CallLocation::new(&loc);632633			let start = parse_idx(loc, ctx.clone(), slice.slice.start.as_ref(), "start")?;634			let end = parse_idx(loc, ctx.clone(), slice.slice.end.as_ref(), "end")?;635			let step = parse_idx(loc, ctx, slice.slice.step.as_ref(), "step")?;636637			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?638		}639		Import(kind, path) => {640			let Expr::Str(path) = &***path else {641				bail!("computed imports are not supported")642			};643			let tmp = loc.clone().0;644			with_state(|s| {645				let resolved_path = s.resolve_from(tmp.source_path(), path)?;646				Ok(match kind {647					ImportKind::Normal => in_frame(648						CallLocation::new(&loc),649						|| format!("import {:?}", path.clone()),650						|| s.import_resolved(resolved_path),651					)?,652					ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?),653					ImportKind::Bin => {654						Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?))655					}656				}) as Result<Val>657			})?658		}659	})660}
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
--- 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)