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

difftreelog

refactor trivial arrays

pynwztymYaroslav Bolyukin2026-05-08parent: #b89fdd3.patch.diff
in: master

5 files changed

modifiedcrates/jrsonnet-evaluator/src/analyze.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/analyze.rs
+++ b/crates/jrsonnet-evaluator/src/analyze.rs
@@ -138,14 +138,12 @@
 #[derive(Debug, Acyclic)]
 pub enum LExpr {
 	Slot(LSlot),
-	Null,
-	Bool(bool),
-	Str(IStr),
-	Num(NumValue),
+	Trivial(TrivialVal),
 	Arr {
 		shape: ClosureShape,
 		items: Rc<Vec<LExpr>>,
 	},
+	ArrConst(Rc<Vec<TrivialVal>>),
 	ArrComp(Box<LArrComp>),
 	Obj(LObjBody),
 	ObjExtend(Box<LExpr>, LObjBody),
@@ -1345,15 +1343,15 @@
 #[allow(clippy::too_many_lines)]
 pub fn analyze(expr: &Expr, stack: &mut AnalysisStack, taint: &mut AnalysisResult) -> LExpr {
 	match expr {
-		Expr::Literal(span, l) => match l {
-			LiteralType::This => stack.use_this(taint).map_or_else(
+		Expr::Identity(span, l) => match l {
+			IdentityKind::This => stack.use_this(taint).map_or_else(
 				|| {
 					stack.report_error("`self` used outside of object", Some(span.clone()));
 					LExpr::BadLocal("self")
 				},
 				LExpr::Slot,
 			),
-			LiteralType::Super => {
+			IdentityKind::Super => {
 				if stack.use_super(taint).is_some() {
 					LExpr::Super
 				} else {
@@ -1361,25 +1359,34 @@
 					LExpr::BadLocal("super")
 				}
 			}
-			LiteralType::Dollar => stack.use_dollar(taint).map_or_else(
+			IdentityKind::Dollar => stack.use_dollar(taint).map_or_else(
 				|| {
 					stack.report_error("`$` used outside of object", Some(span.clone()));
 					LExpr::BadLocal("$")
 				},
 				LExpr::Slot,
 			),
-			LiteralType::Null => LExpr::Null,
-			LiteralType::True => LExpr::Bool(true),
-			LiteralType::False => LExpr::Bool(false),
 		},
-		Expr::Str(s) => LExpr::Str(s.clone()),
-		Expr::Num(n) => LExpr::Num(*n),
+		Expr::Trivial(tv) => LExpr::Trivial(tv.clone()),
 		Expr::Var(v) => stack
 			.use_local(&v.value, v.span.clone(), taint)
 			.map_or_else(|| LExpr::BadLocal("ref"), LExpr::Slot),
 		Expr::Arr(a) => {
-			let (shape, items) = stack
-				.in_using_closure(|stack| a.iter().map(|v| analyze(v, stack, taint)).collect());
+			if a.iter().all(|i| matches!(i, Expr::Trivial(_))) {
+				let trivials: Vec<_> = a
+					.iter()
+					.map(|i| match i {
+						Expr::Trivial(tv) => tv.clone(),
+						_ => unreachable!("checked above"),
+					})
+					.collect();
+				return LExpr::ArrConst(Rc::new(trivials));
+			}
+			let (shape, items) = stack.in_using_closure(|stack| {
+				a.iter()
+					.map(|v| analyze(v, stack, taint))
+					.collect::<Vec<_>>()
+			});
 			LExpr::Arr {
 				shape,
 				items: Rc::new(items),
@@ -1412,7 +1419,7 @@
 		}
 		Expr::LocalExpr(binds, body) => analyze_local_expr(binds, body, stack, taint),
 		Expr::Import(kind, path_expr) => {
-			let Expr::Str(path) = &**path_expr else {
+			let Expr::Trivial(TrivialVal::Str(path)) = &**path_expr else {
 				stack.report_error(
 					"import path must be a string literal",
 					Some(kind.span.clone()),
modifiedcrates/jrsonnet-evaluator/src/arr/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/mod.rs
+++ b/crates/jrsonnet-evaluator/src/arr/mod.rs
@@ -8,12 +8,7 @@
 
 use jrsonnet_gcmodule::{Cc, cc_dyn};
 
-use crate::{
-	Context, Result, Thunk, Val,
-	analyze::{ClosureShape, LExpr},
-	function::NativeFn,
-	typed::IntoUntyped,
-};
+use crate::{Context, Result, Thunk, Val, analyze::LExpr, function::NativeFn, typed::IntoUntyped};
 
 mod spec;
 pub use spec::{ArrayLike, *};
@@ -42,8 +37,8 @@
 		Self::new(())
 	}
 
-	pub fn expr(ctx: Context, shape: &ClosureShape, exprs: Rc<Vec<LExpr>>) -> Self {
-		Self::new(ExprArray::new(ctx, shape, exprs))
+	pub fn expr(ctx: Context, exprs: Rc<Vec<LExpr>>) -> Self {
+		Self::new(ExprArray::new(ctx, exprs))
 	}
 
 	pub fn repeated(data: Self, repeats: u32) -> Option<Self> {
modifiedcrates/jrsonnet-evaluator/src/arr/spec.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/spec.rs
+++ b/crates/jrsonnet-evaluator/src/arr/spec.rs
@@ -8,11 +8,12 @@
 
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::{IBytes, IStr};
+use jrsonnet_ir::TrivialVal;
 
 use super::{ArrValue, arridx};
 use crate::{
 	Context, Error, ObjValue, Result, Thunk, Val,
-	analyze::{ClosureShape, LExpr},
+	analyze::LExpr,
 	error::ErrorKind::InfiniteRecursionDetected,
 	evaluate::evaluate,
 	function::NativeFn,
@@ -108,6 +109,18 @@
 	}
 }
 
+impl ArrayCheap for Rc<Vec<TrivialVal>> {
+	fn get(&self, index: u32) -> Option<Val> {
+		self.as_slice()
+			.get(index as usize)
+			.map(|tv| tv.clone().into())
+	}
+
+	fn len(&self) -> u32 {
+		arridx(self.as_slice().len())
+	}
+}
+
 #[derive(Debug, Trace, Clone)]
 enum ArrayThunk {
 	Computed(Val),
@@ -123,9 +136,9 @@
 	cached: Cc<RefCell<Vec<ArrayThunk>>>,
 }
 impl ExprArray {
-	pub fn new(outer: Context, shape: &ClosureShape, src: Rc<Vec<LExpr>>) -> Self {
+	pub fn new(ctx: Context, src: Rc<Vec<LExpr>>) -> Self {
 		Self {
-			ctx: Context::enter_using(&outer, shape),
+			ctx,
 			cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; src.len()])),
 			src,
 		}
@@ -153,9 +166,17 @@
 			unreachable!()
 		};
 
-		let new_value: Val = evaluate(self.ctx.clone(), &self.src[index as usize])?;
-		self.cached.borrow_mut()[index as usize] = ArrayThunk::Computed(new_value.clone());
-		Ok(Some(new_value))
+		let result = evaluate(self.ctx.clone(), &self.src[index as usize]);
+		match result {
+			Ok(new_value) => {
+				self.cached.borrow_mut()[index as usize] = ArrayThunk::Computed(new_value.clone());
+				Ok(Some(new_value))
+			}
+			Err(e) => {
+				self.cached.borrow_mut()[index as usize] = ArrayThunk::Waiting;
+				Err(e)
+			}
+		}
 	}
 	fn get_lazy32(&self, index: u32) -> Option<Thunk<Val>> {
 		#[derive(Trace)]
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_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/val.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/val.rs
+++ b/crates/jrsonnet-evaluator/src/val.rs
@@ -10,7 +10,7 @@
 
 use jrsonnet_gcmodule::{Acyclic, Cc, Trace, cc_dyn};
 use jrsonnet_interner::IStr;
-use jrsonnet_ir::BinaryOpType;
+use jrsonnet_ir::{BinaryOpType, TrivialVal};
 pub use jrsonnet_macros::Thunk;
 use jrsonnet_types::ValType;
 use rustc_hash::FxHashMap;
@@ -621,6 +621,16 @@
 		Self::Bool(value)
 	}
 }
+impl From<TrivialVal> for Val {
+	fn from(tv: TrivialVal) -> Self {
+		match tv {
+			TrivialVal::Null => Self::Null,
+			TrivialVal::Bool(b) => Self::Bool(b),
+			TrivialVal::Num(n) => Self::Num(n),
+			TrivialVal::Str(s) => Self::string(s),
+		}
+	}
+}
 
 const fn is_function_like(val: &Val) -> bool {
 	matches!(val, Val::Func(_))