git.delta.rocks / jrsonnet / refs/commits / 3c251da01a10

difftreelog

feat minimal tco

yzrzokypYaroslav Bolyukin2026-05-07parent: #e468723.patch.diff
in: master

3 files changed

modifiedcrates/jrsonnet-evaluator/src/evaluate/destructure.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/destructure.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/destructure.rs
@@ -4,9 +4,7 @@
 
 use crate::{
 	Context, LocalsFrame, PackedContext, Result, SupThis, Thunk, Unbound, Val,
-	analyze::{
-		ClosureShape, LBind, LDestruct, LDestructField, LDestructRest, LLocalExpr, LocalSlot,
-	},
+	analyze::{ClosureShape, LBind, LDestruct, LDestructField, LDestructRest, LocalSlot},
 	bail,
 	evaluate::evaluate,
 };
@@ -187,15 +185,6 @@
 			ctx,
 		);
 	}
-}
-
-pub fn evaluate_local_expr(parent: Context, l: &LLocalExpr) -> Result<Val> {
-	let ctx = parent
-		.pack_captures_sup_this(&l.frame_shape)
-		.enter(|fill, ctx| {
-			fill_letrec_binds(fill, ctx, &l.binds);
-		});
-	evaluate(ctx, &l.body)
 }
 
 pub trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}
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_ir::ImportKind;6use jrsonnet_types::ValType;78use self::{9	compspec::{evaluate_arr_comp, evaluate_obj_comp},10	destructure::{evaluate_local_expr, evaluate_locals_unbound},11	operator::evaluate_binary_op_special,12};13use crate::{14	Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Result, ResultExt as _, SupThis,15	Unbound, Val,16	analyze::{17		ClosureShape, LArgsDesc, LAssertStmt, LExpr, LFieldMember, LFieldName, LFunction,18		LIndexPart, LObjAsserts, LObjBody, LObjMembers, LSlot,19	},20	arr::ArrValue,21	bail,22	error::{ErrorKind::*, suggest_object_fields},23	evaluate::{destructure::fill_letrec_binds, operator::evaluate_unary_op},24	function::{CallLocation, FuncDesc, FuncVal, prepared::PreparedFuncVal},25	in_frame,26	typed::{BoundedUsize, FromUntyped as _},27	val::{CachedUnbound, Thunk},28	with_state,29};3031pub mod compspec;32pub mod destructure;33pub mod operator;3435// This is the amount of bytes that need to be left on the stack before increasing the size.36// It must be at least as large as the stack required by any code that does not call37// `ensure_sufficient_stack`.38const RED_ZONE: usize = 100 * 1024;3940// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then41// on. This flag has performance relevant characteristics. Don't set it too high.42const STACK_PER_RECURSION: usize = 1024 * 1024;4344/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations45/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit46/// from this.47///48/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.49#[inline]50pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {51	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)52}5354pub fn evaluate_trivial(expr: &LExpr) -> Option<Val> {55	// TODO: Eager trivial array56	Some(match expr {57		LExpr::Str(s) => Val::string(s.clone()),58		LExpr::Num(n) => Val::Num(*n),59		LExpr::Bool(false) => Val::Bool(false),60		LExpr::Bool(true) => Val::Bool(true),61		LExpr::Null => Val::Null,62		_ => return None,63	})64}6566pub fn evaluate_method(ctx: Context, name: IStr, func: &Rc<LFunction>) -> Val {67	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {68		name,69		body_captures: ctx.pack_captures_sup_this(&func.body_shape),70		func: func.clone(),71	})))72}7374pub fn evaluate_field_name(ctx: Context, field_name: &LFieldName) -> Result<Option<IStr>> {75	Ok(match field_name {76		LFieldName::Fixed(n) => Some(n.clone()),77		LFieldName::Dyn(expr) => in_frame(78			// TODO: Spanned<LFieldName>79			CallLocation::native(),80			|| "evaluating field name".to_string(),81			|| {82				let v = evaluate(ctx.clone(), expr)?;83				Ok(if matches!(v, Val::Null) {84					None85				} else {86					Some(IStr::from_untyped(v)?)87				})88			},89		)?,90	})91}9293pub fn evaluate_thunk(ctx: Context, expr: Rc<LExpr>, tailstrict: bool) -> Result<Thunk<Val>> {94	match &*expr {95		LExpr::Slot(LSlot::Local(i)) => return Ok(ctx.local(*i)),96		LExpr::Slot(LSlot::Capture(i)) => return Ok(ctx.capture(*i)),97		_ => {98			if let Some(v) = evaluate_trivial(&expr) {99				return Ok(Thunk::evaluated(v));100			}101		}102	}103	Ok(if tailstrict {104		Thunk::evaluated(evaluate(ctx, &expr)?)105	} else {106		Thunk!(move || { evaluate(ctx, &expr) })107	})108}109110mod names {111	use crate::names;112113	names! {114		anonymous: "anonymous",115	}116}117118#[allow(clippy::too_many_lines)]119pub fn evaluate(ctx: Context, expr: &LExpr) -> Result<Val> {120	Ok(match expr {121		LExpr::Null => Val::Null,122		LExpr::Bool(b) => Val::Bool(*b),123		LExpr::Str(s) => Val::string(s.clone()),124		LExpr::Num(n) => Val::Num(*n),125		LExpr::Slot(slot) => ctx.slot(*slot).evaluate()?,126		LExpr::BadLocal(name) => panic!("unresolvable reference: {name}"),127		LExpr::Arr { shape, items } => Val::Arr(ArrValue::expr(ctx, shape, items.clone())),128		LExpr::UnaryOp(op, value) => {129			let value = evaluate(ctx, value)?;130			evaluate_unary_op(*op, &value)?131		}132		LExpr::BinaryOp { lhs, op, rhs } => evaluate_binary_op_special(ctx, lhs, *op, rhs)?,133		LExpr::LocalExpr(local_expr) => evaluate_local_expr(ctx, local_expr)?,134		LExpr::IfElse {135			cond,136			cond_then,137			cond_else,138		} => {139			let cond_val = evaluate(ctx.clone(), cond)?;140			let Val::Bool(b) = cond_val else {141				bail!(TypeMismatch(142					"if condition",143					vec![ValType::Bool],144					cond_val.value_type()145				))146			};147			if b {148				evaluate(ctx, cond_then)?149			} else if let Some(e) = cond_else {150				evaluate(ctx, e)?151			} else {152				Val::Null153			}154		}155		LExpr::Error(s, e) => in_frame(156			CallLocation::new(s),157			|| "error statement".to_owned(),158			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),159		)?,160		LExpr::AssertExpr { assert, rest } => {161			evaluate_assert(ctx.clone(), assert)?;162			evaluate(ctx, rest)?163		}164165		LExpr::Function(func) => evaluate_method(166			ctx,167			func.name.clone().unwrap_or_else(names::anonymous),168			func,169		),170		LExpr::IdentityFunction => Val::Func(FuncVal::identity()),171		LExpr::Apply {172			applicable,173			args,174			tailstrict,175		} => evaluate_apply(176			ctx,177			applicable,178			args,179			CallLocation::new(&args.span),180			*tailstrict,181		)?,182		LExpr::Index { indexable, parts } => evaluate_index(ctx, indexable, parts)?,183		LExpr::Obj(body) => evaluate_obj_body(None, ctx, body)?,184		LExpr::ObjExtend(lhs, body) => {185			let lhs_val = evaluate(ctx.clone(), lhs)?;186			let Val::Obj(lhs_obj) = lhs_val else {187				bail!(TypeMismatch(188					"object extend lhs",189					vec![ValType::Obj],190					lhs_val.value_type(),191				))192			};193			evaluate_obj_body(Some(lhs_obj), ctx, body)?194		}195		LExpr::ArrComp(comp) => evaluate_arr_comp(ctx, comp)?,196		LExpr::Slice(slice) => {197			let val = evaluate(ctx.clone(), &slice.value)?;198			let indexable = val.into_indexable()?;199			let start = slice200				.start201				.as_ref()202				.map(|e| evaluate(ctx.clone(), e))203				.transpose()?204				.map(|v| -> Result<i32> { i32::from_untyped(v).description("slice start value") })205				.transpose()?;206			let end = slice207				.end208				.as_ref()209				.map(|e| evaluate(ctx.clone(), e))210				.transpose()?211				.map(|v| -> Result<i32> { i32::from_untyped(v).description("slice end value") })212				.transpose()?;213			let step = slice214				.step215				.as_ref()216				.map(|e| evaluate(ctx, e))217				.transpose()?218				.map(|v| -> Result<BoundedUsize<1, { i32::MAX as usize }>> {219					BoundedUsize::from_untyped(v).description("slice step value")220				})221				.transpose()?;222			Val::from(indexable.slice32(start, end, step)?)223		}224		LExpr::Super => Val::Obj(ctx.try_sup_this()?.standalone_super().ok_or(NoSuperFound)?),225		LExpr::Import {226			kind,227			kind_span,228			path,229		} => with_state(|state| {230			let resolved = state.resolve_from(kind_span.0.source_path(), &path.clone())?;231			Ok::<_, Error>(match kind.value {232				ImportKind::Normal => in_frame(233					CallLocation::new(&kind.span),234					|| "import".to_string(),235					|| state.import_resolved(resolved),236				)?,237				ImportKind::Str => Val::string(state.import_resolved_str(resolved)?),238				ImportKind::Bin => Val::arr(state.import_resolved_bin(resolved)?),239			})240		})?,241	})242}243244fn evaluate_apply(245	ctx: Context,246	applicable: &LExpr,247	args: &LArgsDesc,248	loc: CallLocation<'_>,249	tailstrict: bool,250) -> Result<Val> {251	let func_val = evaluate(ctx.clone(), applicable)?;252	let Val::Func(func) = func_val else {253		bail!(OnlyFunctionsCanBeCalledGot(func_val.value_type()))254	};255256	if func.is_identity() && args.names.is_empty() && args.unnamed.len() == 1 {257		return evaluate_thunk(ctx, args.unnamed[0].clone(), tailstrict)?.evaluate();258	}259260	let name = func.name();261262	if args.names.is_empty() && args.unnamed.len() == 1 && func.params().len() == 1 {263		use crate::function::prepared::PreparedCall;264		let prepared_inline = PreparedCall::empty();265		let arg = evaluate_thunk(ctx, args.unnamed[0].clone(), tailstrict)?;266		let arg_slice = std::slice::from_ref(&arg);267		return in_frame(268			loc,269			|| format!("function <{name}> call"),270			|| {271				func.evaluate_prepared(272					&prepared_inline,273					CallLocation::native(),274					arg_slice,275					&[],276					tailstrict,277				)278			},279		);280	}281282	let unnamed = args283		.unnamed284		.iter()285		.cloned()286		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))287		.collect::<Result<Vec<_>>>()?;288289	// Fast path: positional-only multi-arg call fully covering the290	// params, no defaults.291	if args.names.is_empty() && unnamed.len() == func.params().len() {292		use crate::function::prepared::PreparedCall;293		let prepared_inline = PreparedCall::empty();294		return in_frame(295			loc,296			|| format!("function <{name}> call"),297			|| {298				func.evaluate_prepared(299					&prepared_inline,300					CallLocation::native(),301					&unnamed,302					&[],303					tailstrict,304				)305			},306		);307	}308309	let named = args310		.values311		.iter()312		.cloned()313		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))314		.collect::<Result<Vec<_>>>()?;315	let prepare = PreparedFuncVal::new(func, unnamed.len(), &args.names)316		.with_description_src(loc, || format!("function <{name}> preparation"))?;317	in_frame(318		loc,319		|| format!("function <{name}> call"),320		|| prepare.call(CallLocation::native(), &unnamed, &named),321	)322}323324#[allow(clippy::too_many_lines)]325fn evaluate_index(ctx: Context, indexable: &LExpr, parts: &[LIndexPart]) -> Result<Val> {326	let mut parts = parts.iter();327	let mut indexable = if matches!(indexable, LExpr::Super) {328		let part = parts.next().expect("at least part should exist");329		// sup_this existence check might also be skipped here for null-coalesce...330		// But I believe this might cause errors.331		let sup_this = ctx.try_sup_this()?;332333		if !sup_this.has_super() {334			#[cfg(feature = "exp-null-coaelse")]335			if part.null_coaelse {336				return Ok(Val::Null);337			}338			bail!(NoSuperFound);339		}340		let name = evaluate(ctx.clone(), &part.value)?;341342		let Val::Str(name) = name else {343			bail!(ValueIndexMustBeTypeGot(344				ValType::Obj,345				ValType::Str,346				name.value_type(),347			))348		};349350		let name = name.into_flat();351		match sup_this352			.get_super(name.clone())353			.with_description_src(&part.span, || format!("super field <{name}> access"))?354		{355			Some(v) => v,356			#[cfg(feature = "exp-null-coaelse")]357			None if part.null_coaelse => return Ok(Val::Null),358			None => {359				let suggestions = suggest_object_fields(360					&sup_this.standalone_super().expect("super exists"),361					name.clone(),362				);363				bail!(NoSuchField(name, suggestions))364			}365		}366	} else {367		evaluate(ctx.clone(), indexable)?368	};369370	for part in parts {371		let ctx = ctx.clone();372		let loc = CallLocation::new(&part.span);373		let value = indexable;374		let key_val = evaluate(ctx, &part.value)?;375		indexable = match (&value, &key_val) {376			(Val::Obj(obj), Val::Str(key)) => {377				let key = key.clone().into_flat();378				match obj379					.get(key.clone())380					.with_description_src(loc, || format!("field <{key}> access"))?381				{382					Some(v) => v,383					#[cfg(feature = "exp-null-coaelse")]384					None if part.null_coaelse => return Ok(Val::Null),385					None => {386						return Err(Error::from(NoSuchField(387							key.clone(),388							suggest_object_fields(obj, key.clone()),389						)))390						.with_description_src(loc, || format!("field <{key}> access"));391					}392				}393			}394			(Val::Arr(arr), Val::Num(idx)) => {395				let n = idx.get();396				if n.fract() > f64::EPSILON {397					bail!(FractionalIndex)398				}399				let len = arr.len32();400				if n < 0.0 || n > f64::from(len) {401					bail!(ArrayBoundsError(n, len));402				}403				#[expect(404					clippy::cast_possible_truncation,405					clippy::cast_sign_loss,406					reason = "n is checked range"407				)]408				let i = n as u32;409				arr.get32(i)410					.with_description_src(loc, || format!("element <{i}> access"))?411					.ok_or_else(|| ArrayBoundsError(n, len))?412			}413			(Val::Str(s), Val::Num(idx)) => {414				let n = idx.get();415				if n.fract() > f64::EPSILON {416					bail!(FractionalIndex)417				}418				#[expect(419					clippy::cast_possible_truncation,420					clippy::cast_sign_loss,421					reason = "n is checked positive, overflow will truncate as expected"422				)]423				let i = n as usize;424				let flat = s.clone().into_flat();425				#[allow(clippy::cast_possible_truncation, reason = "string is max 4g")]426				if n >= 0.0427					&& n <= f64::from(u32::MAX)428					&& let Some(char) = flat.chars().nth(i)429				{430					Val::string(char)431				} else {432					let len = flat.chars().count();433					bail!(StringBoundsError(n, len as u32))434				}435			}436			#[cfg(feature = "exp-null-coaelse")]437			(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),438			_ => bail!(ValueIndexMustBeTypeGot(439				value.value_type(),440				ValType::Str,441				key_val.value_type()442			)),443		};444	}445	Ok(indexable)446}447448fn evaluate_obj_body(super_obj: Option<ObjValue>, ctx: Context, body: &LObjBody) -> Result<Val> {449	match body {450		LObjBody::MemberList(members) => evaluate_obj_members(super_obj, ctx, members),451		LObjBody::ObjComp(comp) => evaluate_obj_comp(super_obj, ctx, comp),452	}453}454455pub fn evaluate_field_member_unbound<B: Unbound<Bound = Context> + Clone>(456	builder: &mut ObjValueBuilder,457	ctx: Context,458	uctx: B,459	field: &LFieldMember,460) -> Result<()> {461	#[derive(Trace)]462	struct UnboundValue<B: Trace> {463		uctx: B,464		value: Rc<(ClosureShape, LExpr)>,465		name: IStr,466	}467	impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {468		type Bound = Val;469		fn bind(&self, sup_this: SupThis) -> Result<Val> {470			let a_ctx = self.uctx.bind(sup_this)?;471			let b_ctx = Context::enter_using(&a_ctx, &self.value.0);472			evaluate(b_ctx, &self.value.1)473		}474	}475476	let LFieldMember {477		name,478		plus,479		visibility,480		value,481	} = field;482	let Some(name) = evaluate_field_name(ctx, name)? else {483		return Ok(());484	};485486	builder487		.field(name.clone())488		.with_add(*plus)489		.with_visibility(*visibility)490		.bindable(UnboundValue {491			uctx,492			value: value.clone(),493			name,494		})495}496pub fn evaluate_field_member_static(497	builder: &mut ObjValueBuilder,498	field_ctx: Context,499	value_ctx: Context,500	field: &LFieldMember,501) -> Result<()> {502	let LFieldMember {503		name,504		plus,505		visibility,506		value,507	} = field;508	let Some(name) = evaluate_field_name(field_ctx, name)? else {509		return Ok(());510	};511512	let env = Context::enter_using(&value_ctx, &value.0);513	let value = value.clone();514	builder515		.field(name)516		.with_add(*plus)517		.with_visibility(*visibility)518		.try_thunk(Thunk!(move || evaluate(env, &value.1)))?;519	Ok(())520}521522fn evaluate_obj_members(523	super_obj: Option<ObjValue>,524	ctx: Context,525	members: &LObjMembers,526) -> Result<Val> {527	let mut builder = ObjValueBuilder::with_capacity(members.fields.len());528	if let Some(sup) = super_obj {529		builder.with_super(sup);530	}531532	let needs_unbound = members.this.is_some() || members.uses_super;533534	if needs_unbound {535		let uctx = CachedUnbound::new(evaluate_locals_unbound(536			&ctx,537			&members.frame_shape,538			members.this,539			members.locals.clone(),540		));541		for field in &members.fields {542			evaluate_field_member_unbound(&mut builder, ctx.clone(), uctx.clone(), field)?;543		}544		if let Some(asserts_block) = &members.asserts {545			builder.assert(evaluate_object_assertions_unbound(546				uctx,547				asserts_block.clone(),548			));549		}550	} else {551		let a_ctx = ctx552			.pack_captures_sup_this(&members.frame_shape)553			.enter(|fill, ctx| {554				fill_letrec_binds(fill, ctx, &members.locals);555			});556		for field in &members.fields {557			evaluate_field_member_static(&mut builder, ctx.clone(), a_ctx.clone(), field)?;558		}559		if let Some(asserts_block) = &members.asserts {560			builder.assert(evaluate_object_assertions_static(561				a_ctx,562				asserts_block.clone(),563			));564		}565	}566567	Ok(Val::Obj(builder.build()))568}569570pub fn evaluate_assert(ctx: Context, assertion: &LAssertStmt) -> Result<()> {571	let LAssertStmt { cond, message } = assertion;572	let assertion_result = in_frame(573		CallLocation::new(&cond.span),574		|| "assertion condition".to_owned(),575		|| bool::from_untyped(evaluate(ctx.clone(), cond)?),576	)?;577	if !assertion_result {578		in_frame(579			CallLocation::new(&cond.span),580			|| "assertion failure".to_owned(),581			|| {582				if let Some(msg) = message {583					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));584				}585				bail!(AssertionFailed(Val::Null.to_string()?));586			},587		)?;588	}589	Ok(())590}591592fn evaluate_object_assertions_unbound<B: Unbound<Bound = Context>>(593	uctx: B,594	asserts: Rc<LObjAsserts>,595) -> impl ObjectAssertion {596	#[derive(Trace)]597	struct ObjectAssert<B: Trace> {598		uctx: B,599		asserts: Rc<LObjAsserts>,600	}601	impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {602		fn run(&self, sup_this: SupThis) -> Result<()> {603			let a_ctx = self.uctx.bind(sup_this)?;604			let assert_env = Context::enter_using(&a_ctx, &self.asserts.shape);605			for assert in &self.asserts.asserts {606				evaluate_assert(assert_env.clone(), assert)?;607			}608			Ok(())609		}610	}611	ObjectAssert { uctx, asserts }612}613fn evaluate_object_assertions_static(614	a_ctx: Context,615	asserts: Rc<LObjAsserts>,616) -> impl ObjectAssertion {617	#[derive(Trace)]618	struct ObjectAssert {619		assert_env: Context,620		asserts: Rc<LObjAsserts>,621	}622	impl ObjectAssertion for ObjectAssert {623		fn run(&self, _sup_this: SupThis) -> Result<()> {624			for assert in &self.asserts.asserts {625				evaluate_assert(self.assert_env.clone(), assert)?;626			}627			Ok(())628		}629	}630	let assert_env = Context::enter_using(&a_ctx, &asserts.shape);631	ObjectAssert {632		assert_env,633		asserts,634	}635}
after · crates/jrsonnet-evaluator/src/evaluate/mod.rs
1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_ir::ImportKind;6use jrsonnet_types::ValType;78use self::{9	compspec::{evaluate_arr_comp, evaluate_obj_comp},10	destructure::evaluate_locals_unbound,11	operator::evaluate_binary_op_special,12};13use crate::{14	Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Result, ResultExt as _, SupThis,15	Unbound, Val,16	analyze::{17		ClosureShape, LArgsDesc, LAssertStmt, LExpr, LFieldMember, LFieldName, LFunction,18		LIndexPart, LObjAsserts, LObjBody, LObjMembers, LSlot,19	},20	arr::ArrValue,21	bail,22	error::{ErrorKind::*, suggest_object_fields},23	evaluate::{destructure::fill_letrec_binds, operator::evaluate_unary_op},24	function::{CallLocation, FuncDesc, FuncVal, prepared::PreparedFuncVal},25	in_frame,26	typed::{BoundedUsize, FromUntyped as _},27	val::{CachedUnbound, Thunk},28	with_state,29};3031pub mod compspec;32pub mod destructure;33pub mod operator;3435// This is the amount of bytes that need to be left on the stack before increasing the size.36// It must be at least as large as the stack required by any code that does not call37// `ensure_sufficient_stack`.38const RED_ZONE: usize = 100 * 1024;3940// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then41// on. This flag has performance relevant characteristics. Don't set it too high.42const STACK_PER_RECURSION: usize = 1024 * 1024;4344/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations45/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit46/// from this.47///48/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.49#[inline]50pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {51	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)52}5354pub fn evaluate_trivial(expr: &LExpr) -> Option<Val> {55	// TODO: Eager trivial array56	Some(match expr {57		LExpr::Str(s) => Val::string(s.clone()),58		LExpr::Num(n) => Val::Num(*n),59		LExpr::Bool(false) => Val::Bool(false),60		LExpr::Bool(true) => Val::Bool(true),61		LExpr::Null => Val::Null,62		_ => return None,63	})64}6566pub fn evaluate_method(ctx: Context, name: IStr, func: &Rc<LFunction>) -> Val {67	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {68		name,69		body_captures: ctx.pack_captures_sup_this(&func.body_shape),70		func: func.clone(),71	})))72}7374pub fn evaluate_field_name(ctx: Context, field_name: &LFieldName) -> Result<Option<IStr>> {75	Ok(match field_name {76		LFieldName::Fixed(n) => Some(n.clone()),77		LFieldName::Dyn(expr) => in_frame(78			// TODO: Spanned<LFieldName>79			CallLocation::native(),80			|| "evaluating field name".to_string(),81			|| {82				let v = evaluate(ctx.clone(), expr)?;83				Ok(if matches!(v, Val::Null) {84					None85				} else {86					Some(IStr::from_untyped(v)?)87				})88			},89		)?,90	})91}9293pub fn evaluate_thunk(ctx: Context, expr: Rc<LExpr>, tailstrict: bool) -> Result<Thunk<Val>> {94	match &*expr {95		LExpr::Slot(LSlot::Local(i)) => return Ok(ctx.local(*i)),96		LExpr::Slot(LSlot::Capture(i)) => return Ok(ctx.capture(*i)),97		_ => {98			if let Some(v) = evaluate_trivial(&expr) {99				return Ok(Thunk::evaluated(v));100			}101		}102	}103	Ok(if tailstrict {104		Thunk::evaluated(evaluate(ctx, &expr)?)105	} else {106		Thunk!(move || { evaluate(ctx, &expr) })107	})108}109110mod names {111	use crate::names;112113	names! {114		anonymous: "anonymous",115	}116}117118#[allow(clippy::too_many_lines)]119pub fn evaluate(mut ctx: Context, mut expr: &LExpr) -> Result<Val> {120	loop {121		return Ok(match expr {122			LExpr::Null => Val::Null,123			LExpr::Bool(b) => Val::Bool(*b),124			LExpr::Str(s) => Val::string(s.clone()),125			LExpr::Num(n) => Val::Num(*n),126			LExpr::Slot(slot) => ctx.slot(*slot).evaluate()?,127			LExpr::BadLocal(name) => panic!("unresolvable reference: {name}"),128			LExpr::Arr { shape, items } => Val::Arr(ArrValue::expr(ctx, shape, items.clone())),129			LExpr::UnaryOp(op, value) => {130				let value = evaluate(ctx, value)?;131				evaluate_unary_op(*op, &value)?132			}133			LExpr::BinaryOp { lhs, op, rhs } => evaluate_binary_op_special(ctx, lhs, *op, rhs)?,134			LExpr::LocalExpr(l) => {135				ctx = ctx136					.pack_captures_sup_this(&l.frame_shape)137					.enter(|fill, ctx| {138						fill_letrec_binds(fill, ctx, &l.binds);139					});140				expr = &l.body;141				continue;142			}143			LExpr::IfElse {144				cond,145				cond_then,146				cond_else,147			} => {148				let cond_val = evaluate(ctx.clone(), cond)?;149				let Val::Bool(b) = cond_val else {150					bail!(TypeMismatch(151						"if condition",152						vec![ValType::Bool],153						cond_val.value_type()154					))155				};156				if b {157					expr = cond_then;158					continue;159				} else if let Some(e) = cond_else {160					expr = e;161					continue;162				}163				Val::Null164			}165			LExpr::Error(s, e) => in_frame(166				CallLocation::new(s),167				|| "error statement".to_owned(),168				|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),169			)?,170			LExpr::AssertExpr { assert, rest } => {171				evaluate_assert(ctx.clone(), assert)?;172				expr = rest;173				continue;174			}175176			LExpr::Function(func) => evaluate_method(177				ctx,178				func.name.clone().unwrap_or_else(names::anonymous),179				func,180			),181			LExpr::IdentityFunction => Val::Func(FuncVal::identity()),182			LExpr::Apply {183				applicable,184				args,185				tailstrict,186			} => evaluate_apply(187				ctx,188				applicable,189				args,190				CallLocation::new(&args.span),191				*tailstrict,192			)?,193			LExpr::Index { indexable, parts } => evaluate_index(ctx, indexable, parts)?,194			LExpr::Obj(body) => evaluate_obj_body(None, ctx, body)?,195			LExpr::ObjExtend(lhs, body) => {196				let lhs_val = evaluate(ctx.clone(), lhs)?;197				let Val::Obj(lhs_obj) = lhs_val else {198					bail!(TypeMismatch(199						"object extend lhs",200						vec![ValType::Obj],201						lhs_val.value_type(),202					))203				};204				evaluate_obj_body(Some(lhs_obj), ctx, body)?205			}206			LExpr::ArrComp(comp) => evaluate_arr_comp(ctx, comp)?,207			LExpr::Slice(slice) => {208				let val = evaluate(ctx.clone(), &slice.value)?;209				let indexable = val.into_indexable()?;210				let start = slice211					.start212					.as_ref()213					.map(|e| evaluate(ctx.clone(), e))214					.transpose()?215					.map(|v| -> Result<i32> {216						i32::from_untyped(v).description("slice start value")217					})218					.transpose()?;219				let end = slice220					.end221					.as_ref()222					.map(|e| evaluate(ctx.clone(), e))223					.transpose()?224					.map(|v| -> Result<i32> { i32::from_untyped(v).description("slice end value") })225					.transpose()?;226				let step = slice227					.step228					.as_ref()229					.map(|e| evaluate(ctx, e))230					.transpose()?231					.map(|v| -> Result<BoundedUsize<1, { i32::MAX as usize }>> {232						BoundedUsize::from_untyped(v).description("slice step value")233					})234					.transpose()?;235				Val::from(indexable.slice32(start, end, step)?)236			}237			LExpr::Super => Val::Obj(ctx.try_sup_this()?.standalone_super().ok_or(NoSuperFound)?),238			LExpr::Import {239				kind,240				kind_span,241				path,242			} => with_state(|state| {243				let resolved = state.resolve_from(kind_span.0.source_path(), &path.clone())?;244				Ok::<_, Error>(match kind.value {245					ImportKind::Normal => in_frame(246						CallLocation::new(&kind.span),247						|| "import".to_string(),248						|| state.import_resolved(resolved),249					)?,250					ImportKind::Str => Val::string(state.import_resolved_str(resolved)?),251					ImportKind::Bin => Val::arr(state.import_resolved_bin(resolved)?),252				})253			})?,254		});255	}256}257258fn evaluate_apply(259	ctx: Context,260	applicable: &LExpr,261	args: &LArgsDesc,262	loc: CallLocation<'_>,263	tailstrict: bool,264) -> Result<Val> {265	let func_val = evaluate(ctx.clone(), applicable)?;266	let Val::Func(func) = func_val else {267		bail!(OnlyFunctionsCanBeCalledGot(func_val.value_type()))268	};269270	if func.is_identity() && args.names.is_empty() && args.unnamed.len() == 1 {271		return evaluate_thunk(ctx, args.unnamed[0].clone(), tailstrict)?.evaluate();272	}273274	let name = func.name();275276	if args.names.is_empty() && args.unnamed.len() == 1 && func.params().len() == 1 {277		use crate::function::prepared::PreparedCall;278		let prepared_inline = PreparedCall::empty();279		let arg = evaluate_thunk(ctx, args.unnamed[0].clone(), tailstrict)?;280		let arg_slice = std::slice::from_ref(&arg);281		return in_frame(282			loc,283			|| format!("function <{name}> call"),284			|| {285				func.evaluate_prepared(286					&prepared_inline,287					CallLocation::native(),288					arg_slice,289					&[],290					tailstrict,291				)292			},293		);294	}295296	let unnamed = args297		.unnamed298		.iter()299		.cloned()300		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))301		.collect::<Result<Vec<_>>>()?;302303	// Fast path: positional-only multi-arg call fully covering the304	// params, no defaults.305	if args.names.is_empty() && unnamed.len() == func.params().len() {306		use crate::function::prepared::PreparedCall;307		let prepared_inline = PreparedCall::empty();308		return in_frame(309			loc,310			|| format!("function <{name}> call"),311			|| {312				func.evaluate_prepared(313					&prepared_inline,314					CallLocation::native(),315					&unnamed,316					&[],317					tailstrict,318				)319			},320		);321	}322323	let named = args324		.values325		.iter()326		.cloned()327		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))328		.collect::<Result<Vec<_>>>()?;329	let prepare = PreparedFuncVal::new(func, unnamed.len(), &args.names)330		.with_description_src(loc, || format!("function <{name}> preparation"))?;331	in_frame(332		loc,333		|| format!("function <{name}> call"),334		|| prepare.call(CallLocation::native(), &unnamed, &named),335	)336}337338#[allow(clippy::too_many_lines)]339fn evaluate_index(ctx: Context, indexable: &LExpr, parts: &[LIndexPart]) -> Result<Val> {340	let mut parts = parts.iter();341	let mut indexable = if matches!(indexable, LExpr::Super) {342		let part = parts.next().expect("at least part should exist");343		// sup_this existence check might also be skipped here for null-coalesce...344		// But I believe this might cause errors.345		let sup_this = ctx.try_sup_this()?;346347		if !sup_this.has_super() {348			#[cfg(feature = "exp-null-coaelse")]349			if part.null_coaelse {350				return Ok(Val::Null);351			}352			bail!(NoSuperFound);353		}354		let name = evaluate(ctx.clone(), &part.value)?;355356		let Val::Str(name) = name else {357			bail!(ValueIndexMustBeTypeGot(358				ValType::Obj,359				ValType::Str,360				name.value_type(),361			))362		};363364		let name = name.into_flat();365		match sup_this366			.get_super(name.clone())367			.with_description_src(&part.span, || format!("super field <{name}> access"))?368		{369			Some(v) => v,370			#[cfg(feature = "exp-null-coaelse")]371			None if part.null_coaelse => return Ok(Val::Null),372			None => {373				let suggestions = suggest_object_fields(374					&sup_this.standalone_super().expect("super exists"),375					name.clone(),376				);377				bail!(NoSuchField(name, suggestions))378			}379		}380	} else {381		evaluate(ctx.clone(), indexable)?382	};383384	for part in parts {385		let ctx = ctx.clone();386		let loc = CallLocation::new(&part.span);387		let value = indexable;388		let key_val = evaluate(ctx, &part.value)?;389		indexable = match (&value, &key_val) {390			(Val::Obj(obj), Val::Str(key)) => {391				let key = key.clone().into_flat();392				match obj393					.get(key.clone())394					.with_description_src(loc, || format!("field <{key}> access"))?395				{396					Some(v) => v,397					#[cfg(feature = "exp-null-coaelse")]398					None if part.null_coaelse => return Ok(Val::Null),399					None => {400						return Err(Error::from(NoSuchField(401							key.clone(),402							suggest_object_fields(obj, key.clone()),403						)))404						.with_description_src(loc, || format!("field <{key}> access"));405					}406				}407			}408			(Val::Arr(arr), Val::Num(idx)) => {409				let n = idx.get();410				if n.fract() > f64::EPSILON {411					bail!(FractionalIndex)412				}413				let len = arr.len32();414				if n < 0.0 || n > f64::from(len) {415					bail!(ArrayBoundsError(n, len));416				}417				#[expect(418					clippy::cast_possible_truncation,419					clippy::cast_sign_loss,420					reason = "n is checked range"421				)]422				let i = n as u32;423				arr.get32(i)424					.with_description_src(loc, || format!("element <{i}> access"))?425					.ok_or_else(|| ArrayBoundsError(n, len))?426			}427			(Val::Str(s), Val::Num(idx)) => {428				let n = idx.get();429				if n.fract() > f64::EPSILON {430					bail!(FractionalIndex)431				}432				#[expect(433					clippy::cast_possible_truncation,434					clippy::cast_sign_loss,435					reason = "n is checked positive, overflow will truncate as expected"436				)]437				let i = n as usize;438				let flat = s.clone().into_flat();439				#[allow(clippy::cast_possible_truncation, reason = "string is max 4g")]440				if n >= 0.0441					&& n <= f64::from(u32::MAX)442					&& let Some(char) = flat.chars().nth(i)443				{444					Val::string(char)445				} else {446					let len = flat.chars().count();447					bail!(StringBoundsError(n, len as u32))448				}449			}450			#[cfg(feature = "exp-null-coaelse")]451			(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),452			_ => bail!(ValueIndexMustBeTypeGot(453				value.value_type(),454				ValType::Str,455				key_val.value_type()456			)),457		};458	}459	Ok(indexable)460}461462fn evaluate_obj_body(super_obj: Option<ObjValue>, ctx: Context, body: &LObjBody) -> Result<Val> {463	match body {464		LObjBody::MemberList(members) => evaluate_obj_members(super_obj, ctx, members),465		LObjBody::ObjComp(comp) => evaluate_obj_comp(super_obj, ctx, comp),466	}467}468469pub fn evaluate_field_member_unbound<B: Unbound<Bound = Context> + Clone>(470	builder: &mut ObjValueBuilder,471	ctx: Context,472	uctx: B,473	field: &LFieldMember,474) -> Result<()> {475	#[derive(Trace)]476	struct UnboundValue<B: Trace> {477		uctx: B,478		value: Rc<(ClosureShape, LExpr)>,479		name: IStr,480	}481	impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {482		type Bound = Val;483		fn bind(&self, sup_this: SupThis) -> Result<Val> {484			let a_ctx = self.uctx.bind(sup_this)?;485			let b_ctx = Context::enter_using(&a_ctx, &self.value.0);486			evaluate(b_ctx, &self.value.1)487		}488	}489490	let LFieldMember {491		name,492		plus,493		visibility,494		value,495	} = field;496	let Some(name) = evaluate_field_name(ctx, name)? else {497		return Ok(());498	};499500	builder501		.field(name.clone())502		.with_add(*plus)503		.with_visibility(*visibility)504		.bindable(UnboundValue {505			uctx,506			value: value.clone(),507			name,508		})509}510pub fn evaluate_field_member_static(511	builder: &mut ObjValueBuilder,512	field_ctx: Context,513	value_ctx: Context,514	field: &LFieldMember,515) -> Result<()> {516	let LFieldMember {517		name,518		plus,519		visibility,520		value,521	} = field;522	let Some(name) = evaluate_field_name(field_ctx, name)? else {523		return Ok(());524	};525526	let env = Context::enter_using(&value_ctx, &value.0);527	let value = value.clone();528	builder529		.field(name)530		.with_add(*plus)531		.with_visibility(*visibility)532		.try_thunk(Thunk!(move || evaluate(env, &value.1)))?;533	Ok(())534}535536fn evaluate_obj_members(537	super_obj: Option<ObjValue>,538	ctx: Context,539	members: &LObjMembers,540) -> Result<Val> {541	let mut builder = ObjValueBuilder::with_capacity(members.fields.len());542	if let Some(sup) = super_obj {543		builder.with_super(sup);544	}545546	let needs_unbound = members.this.is_some() || members.uses_super;547548	if needs_unbound {549		let uctx = CachedUnbound::new(evaluate_locals_unbound(550			&ctx,551			&members.frame_shape,552			members.this,553			members.locals.clone(),554		));555		for field in &members.fields {556			evaluate_field_member_unbound(&mut builder, ctx.clone(), uctx.clone(), field)?;557		}558		if let Some(asserts_block) = &members.asserts {559			builder.assert(evaluate_object_assertions_unbound(560				uctx,561				asserts_block.clone(),562			));563		}564	} else {565		let a_ctx = ctx566			.pack_captures_sup_this(&members.frame_shape)567			.enter(|fill, ctx| {568				fill_letrec_binds(fill, ctx, &members.locals);569			});570		for field in &members.fields {571			evaluate_field_member_static(&mut builder, ctx.clone(), a_ctx.clone(), field)?;572		}573		if let Some(asserts_block) = &members.asserts {574			builder.assert(evaluate_object_assertions_static(575				a_ctx,576				asserts_block.clone(),577			));578		}579	}580581	Ok(Val::Obj(builder.build()))582}583584pub fn evaluate_assert(ctx: Context, assertion: &LAssertStmt) -> Result<()> {585	let LAssertStmt { cond, message } = assertion;586	let assertion_result = in_frame(587		CallLocation::new(&cond.span),588		|| "assertion condition".to_owned(),589		|| bool::from_untyped(evaluate(ctx.clone(), cond)?),590	)?;591	if !assertion_result {592		in_frame(593			CallLocation::new(&cond.span),594			|| "assertion failure".to_owned(),595			|| {596				if let Some(msg) = message {597					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));598				}599				bail!(AssertionFailed(Val::Null.to_string()?));600			},601		)?;602	}603	Ok(())604}605606fn evaluate_object_assertions_unbound<B: Unbound<Bound = Context>>(607	uctx: B,608	asserts: Rc<LObjAsserts>,609) -> impl ObjectAssertion {610	#[derive(Trace)]611	struct ObjectAssert<B: Trace> {612		uctx: B,613		asserts: Rc<LObjAsserts>,614	}615	impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {616		fn run(&self, sup_this: SupThis) -> Result<()> {617			let a_ctx = self.uctx.bind(sup_this)?;618			let assert_env = Context::enter_using(&a_ctx, &self.asserts.shape);619			for assert in &self.asserts.asserts {620				evaluate_assert(assert_env.clone(), assert)?;621			}622			Ok(())623		}624	}625	ObjectAssert { uctx, asserts }626}627fn evaluate_object_assertions_static(628	a_ctx: Context,629	asserts: Rc<LObjAsserts>,630) -> impl ObjectAssertion {631	#[derive(Trace)]632	struct ObjectAssert {633		assert_env: Context,634		asserts: Rc<LObjAsserts>,635	}636	impl ObjectAssertion for ObjectAssert {637		fn run(&self, _sup_this: SupThis) -> Result<()> {638			for assert in &self.asserts.asserts {639				evaluate_assert(self.assert_env.clone(), assert)?;640			}641			Ok(())642		}643	}644	let assert_env = Context::enter_using(&a_ctx, &asserts.shape);645	ObjectAssert {646		assert_env,647		asserts,648	}649}
modifiedcrates/jrsonnet-evaluator/src/function/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/mod.rs
+++ b/crates/jrsonnet-evaluator/src/function/mod.rs
@@ -14,7 +14,8 @@
 	Context, PackedContextSupThis, Result, Thunk, Val,
 	analyze::LFunction,
 	arr::arridx,
-	evaluate::{destructure::destruct, ensure_sufficient_stack, evaluate, evaluate_trivial},
+	ensure_sufficient_stack,
+	evaluate::{destructure::destruct, evaluate, evaluate_trivial},
 	function::builtin::BuiltinFunc,
 };
 
@@ -68,7 +69,7 @@
 		self.func.signature.clone()
 	}
 
-	pub fn call(
+	fn call(
 		&self,
 		unnamed: &[Thunk<Val>],
 		named: &[Thunk<Val>],