git.delta.rocks / jrsonnet / refs/commits / 36af281ff0ca

difftreelog

test pointer-size invariant snapshots

vqqnoupnYaroslav Bolyukin2026-05-06parent: #752087c.patch.diff
in: master

10 files changed

modifiedcrates/jrsonnet-evaluator/src/analyze.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/analyze.rs
+++ b/crates/jrsonnet-evaluator/src/analyze.rs
@@ -1959,48 +1959,6 @@
 	}
 }
 
-#[cfg(test)]
-fn render_diagnostics(src: &str, diags: &[Diagnostic]) -> String {
-	use std::fmt::Write;
-
-	use hi_doc::{Formatting, SnippetBuilder, Text};
-
-	let mut out = String::new();
-	let mut unspanned = Vec::new();
-	let mut spanned: Vec<&Diagnostic> = Vec::new();
-	for d in diags {
-		if d.span.is_some() {
-			spanned.push(d);
-		} else {
-			unspanned.push(d);
-		}
-	}
-	if !spanned.is_empty() {
-		let mut builder = SnippetBuilder::new(src);
-		for d in spanned {
-			let span = d.span.as_ref().expect("spanned");
-			let ab = match d.level {
-				DiagLevel::Error => {
-					builder.error(Text::fragment(d.message.clone(), Formatting::default()))
-				}
-				DiagLevel::Warning => {
-					builder.warning(Text::fragment(d.message.clone(), Formatting::default()))
-				}
-			};
-			ab.range(span.range()).build();
-		}
-		out.push_str(&hi_doc::source_to_ansi(&builder.build()));
-	}
-	for d in unspanned {
-		let prefix = match d.level {
-			DiagLevel::Error => "error",
-			DiagLevel::Warning => "warning",
-		};
-		writeln!(out, "{prefix}: {}", d.message).expect("fmt");
-	}
-	out
-}
-
 pub struct AnalysisReport {
 	pub lir: LExpr,
 	pub root_shape: ClosureShape,
@@ -2011,16 +1969,63 @@
 
 #[cfg(test)]
 mod tests {
-	use std::fs;
+	#[test]
+	#[cfg(not(feature = "exp-null-coaelse"))]
+	fn snapshots() {
+		use std::fs;
 
-	use insta::{assert_snapshot, glob};
-	use jrsonnet_ir::Source;
+		use insta::{assert_snapshot, glob};
+		use jrsonnet_ir::Source;
 
-	use super::*;
+		use super::*;
 
-	#[test]
-	#[cfg(not(feature = "exp-null-coaelse"))]
-	fn snapshots() {
+		fn render_diagnostics(src: &str, diags: &[Diagnostic]) -> String {
+			use std::fmt::Write;
+
+			use hi_doc::{Formatting, SnippetBuilder, Text};
+
+			let mut out = String::new();
+			let mut unspanned = Vec::new();
+			let mut spanned: Vec<&Diagnostic> = Vec::new();
+			for d in diags {
+				if d.span.is_some() {
+					spanned.push(d);
+				} else {
+					unspanned.push(d);
+				}
+			}
+			if !spanned.is_empty() {
+				let mut builder = SnippetBuilder::new(src);
+				for d in spanned {
+					let span = d.span.as_ref().expect("spanned");
+					let ab = match d.level {
+						DiagLevel::Error => {
+							builder.error(Text::fragment(d.message.clone(), Formatting::default()))
+						}
+						DiagLevel::Warning => builder
+							.warning(Text::fragment(d.message.clone(), Formatting::default())),
+					};
+					ab.range(span.range()).build();
+				}
+				out.push_str(&hi_doc::source_to_ansi(&builder.build()));
+			}
+			for d in unspanned {
+				let prefix = match d.level {
+					DiagLevel::Error => "error",
+					DiagLevel::Warning => "warning",
+				};
+				writeln!(out, "{prefix}: {}", d.message).expect("fmt");
+			}
+			out
+		}
+		fn fmt_depth(d: u32) -> String {
+			if d == u32::MAX {
+				"none".into()
+			} else {
+				d.to_string()
+			}
+		}
+
 		glob!("analysis_tests/*.jsonnet", |path| {
 			let code = fs::read_to_string(path).expect("read test file");
 			let src = Source::new_virtual("<test>".into(), code.clone().into());
@@ -2041,13 +2046,5 @@
 			);
 			assert_snapshot!(rendered);
 		});
-	}
-
-	fn fmt_depth(d: u32) -> String {
-		if d == u32::MAX {
-			"none".into()
-		} else {
-			d.to_string()
-		}
 	}
 }
modifiedcrates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -123,9 +123,9 @@
 	EagerCompspecCaptured,
 
 	#[error("array out of bounds: {0} is not within [0,{1})")]
-	ArrayBoundsError(isize, u32),
+	ArrayBoundsError(f64, u32),
 	#[error("string out of bounds: {0} is not within [0,{1})")]
-	StringBoundsError(usize, usize),
+	StringBoundsError(f64, u32),
 
 	#[error("assert failed: {}", format_empty_str(.0))]
 	AssertionFailed(IStr),
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::{build_b_thunk_uno, 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, error,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::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}117118pub fn evaluate(ctx: Context, expr: &LExpr) -> Result<Val> {119	Ok(match expr {120		LExpr::Null => Val::Null,121		LExpr::Bool(b) => Val::Bool(*b),122		LExpr::Str(s) => Val::string(s.clone()),123		LExpr::Num(n) => Val::Num(*n),124		LExpr::Slot(slot) => ctx.slot(*slot).evaluate()?,125		LExpr::BadLocal(name) => panic!("unresolvable reference: {name}"),126		LExpr::Arr { shape, items } => Val::Arr(ArrValue::expr(ctx, shape, items.clone())),127		LExpr::UnaryOp(op, value) => {128			let value = evaluate(ctx, value)?;129			evaluate_unary_op(*op, &value)?130		}131		LExpr::BinaryOp { lhs, op, rhs } => evaluate_binary_op_special(ctx, lhs, *op, rhs)?,132		LExpr::LocalExpr(local_expr) => evaluate_local_expr(ctx, local_expr)?,133		LExpr::IfElse {134			cond,135			cond_then,136			cond_else,137		} => {138			let cond_val = evaluate(ctx.clone(), cond)?;139			let Val::Bool(b) = cond_val else {140				bail!(TypeMismatch(141					"if condition",142					vec![ValType::Bool],143					cond_val.value_type()144				))145			};146			if b {147				evaluate(ctx, cond_then)?148			} else if let Some(e) = cond_else {149				evaluate(ctx, e)?150			} else {151				Val::Null152			}153		}154		LExpr::Error(s, e) => in_frame(155			CallLocation::new(s),156			|| "error statement".to_owned(),157			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),158		)?,159		LExpr::AssertExpr { assert, rest } => {160			evaluate_assert(ctx.clone(), assert)?;161			evaluate(ctx, rest)?162		}163164		LExpr::Function(func) => evaluate_method(165			ctx,166			func.name.clone().unwrap_or_else(names::anonymous),167			func,168		),169		LExpr::IdentityFunction => Val::Func(FuncVal::identity()),170		LExpr::Apply {171			applicable,172			args,173			tailstrict,174		} => evaluate_apply(175			ctx,176			applicable,177			args,178			CallLocation::new(&args.span),179			*tailstrict,180		)?,181		LExpr::Index { indexable, parts } => evaluate_index(ctx, indexable, parts)?,182		LExpr::Obj(body) => evaluate_obj_body(None, ctx, body)?,183		LExpr::ObjExtend(lhs, body) => {184			let lhs_val = evaluate(ctx.clone(), lhs)?;185			let Val::Obj(lhs_obj) = lhs_val else {186				bail!(TypeMismatch(187					"object extend lhs",188					vec![ValType::Obj],189					lhs_val.value_type(),190				))191			};192			evaluate_obj_body(Some(lhs_obj), ctx, body)?193		}194		LExpr::ArrComp(comp) => evaluate_arr_comp(ctx, comp)?,195		LExpr::Slice(slice) => {196			use crate::typed::BoundedUsize;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> {205					v.as_num()206						.ok_or_else(|| {207							TypeMismatch("slice start", vec![ValType::Num], v.value_type()).into()208						})209						.map(|n| n as i32)210				})211				.transpose()?;212			let end = slice213				.end214				.as_ref()215				.map(|e| evaluate(ctx.clone(), e))216				.transpose()?217				.map(|v| -> Result<i32> {218					v.as_num()219						.ok_or_else(|| {220							TypeMismatch("slice end", vec![ValType::Num], v.value_type()).into()221						})222						.map(|n| n as i32)223				})224				.transpose()?;225			let step = slice226				.step227				.as_ref()228				.map(|e| evaluate(ctx, e))229				.transpose()?230				.map(|v| -> Result<BoundedUsize<1, { i32::MAX as usize }>> {231					let n = v.as_num().ok_or_else(|| -> crate::Error {232						TypeMismatch("slice step", vec![ValType::Num], v.value_type()).into()233					})?;234					BoundedUsize::new(n as usize).ok_or_else(|| error!("slice step must be >= 1"))235				})236				.transpose()?;237			Val::from(indexable.slice(start, end, step)?)238		}239		LExpr::Super => Val::Obj(ctx.try_sup_this()?.standalone_super().ok_or(NoSuperFound)?),240		LExpr::Import {241			kind,242			kind_span,243			path,244		} => with_state(|state| {245			let resolved = state.resolve_from(kind_span.0.source_path(), &path.clone())?;246			Ok::<_, Error>(match kind.value {247				ImportKind::Normal => in_frame(248					CallLocation::new(&kind.span),249					|| "import".to_string(),250					|| state.import_resolved(resolved),251				)?,252				ImportKind::Str => Val::string(state.import_resolved_str(resolved)?),253				ImportKind::Bin => Val::arr(state.import_resolved_bin(resolved)?),254			})255		})?,256	})257}258259fn evaluate_apply(260	ctx: Context,261	applicable: &LExpr,262	args: &LArgsDesc,263	loc: CallLocation<'_>,264	tailstrict: bool,265) -> Result<Val> {266	let func_val = evaluate(ctx.clone(), applicable)?;267	let Val::Func(func) = func_val else {268		bail!(OnlyFunctionsCanBeCalledGot(func_val.value_type()))269	};270271	if func.is_identity() && args.names.is_empty() && args.unnamed.len() == 1 {272		return evaluate_thunk(ctx, args.unnamed[0].clone(), tailstrict)?.evaluate();273	}274275	let name = func.name();276277	if args.names.is_empty() && args.unnamed.len() == 1 && func.params().len() == 1 {278		use crate::function::prepared::PreparedCall;279		let prepared_inline = PreparedCall::empty();280		let arg = evaluate_thunk(ctx, args.unnamed[0].clone(), tailstrict)?;281		let arg_slice = std::slice::from_ref(&arg);282		return in_frame(283			loc,284			|| format!("function <{name}> call"),285			|| {286				func.evaluate_prepared(287					&prepared_inline,288					CallLocation::native(),289					arg_slice,290					&[],291					tailstrict,292				)293			},294		);295	}296297	let unnamed = args298		.unnamed299		.iter()300		.cloned()301		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))302		.collect::<Result<Vec<_>>>()?;303304	// Fast path: positional-only multi-arg call fully covering the305	// params, no defaults.306	if args.names.is_empty() && unnamed.len() == func.params().len() {307		use crate::function::prepared::PreparedCall;308		let prepared_inline = PreparedCall::empty();309		return in_frame(310			loc,311			|| format!("function <{name}> call"),312			|| {313				func.evaluate_prepared(314					&prepared_inline,315					CallLocation::native(),316					&unnamed,317					&[],318					tailstrict,319				)320			},321		);322	}323324	let named = args325		.values326		.iter()327		.cloned()328		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))329		.collect::<Result<Vec<_>>>()?;330	let prepare = PreparedFuncVal::new(func, unnamed.len(), &args.names)331		.with_description_src(loc, || format!("function <{name}> preparation"))?;332	in_frame(333		loc,334		|| format!("function <{name}> call"),335		|| prepare.call(CallLocation::native(), &unnamed, &named),336	)337}338339fn 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				if n < 0.0 {414					bail!(ArrayBoundsError(415						n as isize, // truncation is fine for error display416						arr.len()417					));418				}419				#[expect(420					clippy::cast_possible_truncation,421					clippy::cast_sign_loss,422					reason = "n is checked positive"423				)]424				let i = n as u32;425				arr.get(i)426					.with_description_src(loc, || format!("element <{i}> access"))?427					.ok_or_else(|| ArrayBoundsError(i as isize, arr.len()))?428			}429			(Val::Str(s), Val::Num(idx)) => {430				let n = idx.get();431				if n.fract() > f64::EPSILON {432					bail!(FractionalIndex)433				}434				let flat = s.clone().into_flat();435				if n < 0.0 {436					bail!(ArrayBoundsError(437						n as isize, // truncation is fine for error display438						flat.chars().count() as u32439					));440				}441				#[expect(442					clippy::cast_possible_truncation,443					clippy::cast_sign_loss,444					reason = "n is checked positive, overflow will truncate as expected"445				)]446				let i = n as usize;447				let Some(char) = flat.chars().nth(i) else {448					bail!(StringBoundsError(i, flat.chars().count()))449				};450				Val::string(char)451			}452			#[cfg(feature = "exp-null-coaelse")]453			(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),454			_ => bail!(ValueIndexMustBeTypeGot(455				value.value_type(),456				ValType::Str,457				key_val.value_type()458			)),459		};460	}461	Ok(indexable)462}463464fn evaluate_obj_body(super_obj: Option<ObjValue>, ctx: Context, body: &LObjBody) -> Result<Val> {465	match body {466		LObjBody::MemberList(members) => evaluate_obj_members(super_obj, ctx, members),467		LObjBody::ObjComp(comp) => evaluate_obj_comp(super_obj, ctx, comp),468	}469}470471pub fn evaluate_field_member_unbound<B: Unbound<Bound = Context> + Clone>(472	builder: &mut ObjValueBuilder,473	ctx: Context,474	uctx: B,475	field: &LFieldMember,476) -> Result<()> {477	#[derive(Trace)]478	struct UnboundValue<B: Trace> {479		uctx: B,480		value: Rc<(ClosureShape, LExpr)>,481		name: IStr,482	}483	impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {484		type Bound = Val;485		fn bind(&self, sup_this: SupThis) -> Result<Val> {486			let a_ctx = self.uctx.bind(sup_this)?;487			let b_ctx = Context::enter_using(&a_ctx, &self.value.0);488			evaluate(b_ctx, &self.value.1)489		}490	}491492	let LFieldMember {493		name,494		plus,495		visibility,496		value,497	} = field;498	let Some(name) = evaluate_field_name(ctx, name)? else {499		return Ok(());500	};501502	builder503		.field(name.clone())504		.with_add(*plus)505		.with_visibility(*visibility)506		.bindable(UnboundValue {507			uctx,508			value: value.clone(),509			name,510		})511}512pub fn evaluate_field_member_static(513	builder: &mut ObjValueBuilder,514	field_ctx: Context,515	value_ctx: Context,516	field: &LFieldMember,517) -> Result<()> {518	let LFieldMember {519		name,520		plus,521		visibility,522		value,523	} = field;524	let Some(name) = evaluate_field_name(field_ctx, name)? else {525		return Ok(());526	};527528	let thunk = build_b_thunk_uno(&value_ctx, value.clone());529	builder530		.field(name)531		.with_add(*plus)532		.with_visibility(*visibility)533		.try_thunk(thunk)?;534	Ok(())535}536537fn evaluate_obj_members(538	super_obj: Option<ObjValue>,539	ctx: Context,540	members: &LObjMembers,541) -> Result<Val> {542	let mut builder = ObjValueBuilder::with_capacity(members.fields.len());543	if let Some(sup) = super_obj {544		builder.with_super(sup);545	}546547	let needs_unbound = members.this.is_some() || members.uses_super;548549	if needs_unbound {550		let uctx = CachedUnbound::new(evaluate_locals_unbound(551			&ctx,552			&members.frame_shape,553			members.this,554			members.locals.clone(),555		));556		for field in &members.fields {557			evaluate_field_member_unbound(&mut builder, ctx.clone(), uctx.clone(), field)?;558		}559		if let Some(asserts_block) = &members.asserts {560			builder.assert(evaluate_object_assertions_unbound(561				uctx,562				asserts_block.clone(),563			));564		}565	} else {566		let a_ctx = ctx567			.pack_captures_sup_this(&members.frame_shape)568			.enter(|fill, ctx| {569				fill_letrec_binds(fill, &ctx, &members.locals);570			});571		for field in &members.fields {572			evaluate_field_member_static(&mut builder, ctx.clone(), a_ctx.clone(), field)?;573		}574		if let Some(asserts_block) = &members.asserts {575			builder.assert(evaluate_object_assertions_static(576				a_ctx,577				asserts_block.clone(),578			));579		}580	}581582	Ok(Val::Obj(builder.build()))583}584585pub fn evaluate_assert(ctx: Context, assertion: &LAssertStmt) -> Result<()> {586	let LAssertStmt { cond, message } = assertion;587	let assertion_result = in_frame(588		CallLocation::new(&cond.span),589		|| "assertion condition".to_owned(),590		|| bool::from_untyped(evaluate(ctx.clone(), cond)?),591	)?;592	if !assertion_result {593		in_frame(594			CallLocation::new(&cond.span),595			|| "assertion failure".to_owned(),596			|| {597				if let Some(msg) = message {598					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));599				}600				bail!(AssertionFailed(Val::Null.to_string()?));601			},602		)?;603	}604	Ok(())605}606607fn evaluate_object_assertions_unbound<B: Unbound<Bound = Context>>(608	uctx: B,609	asserts: Rc<LObjAsserts>,610) -> impl ObjectAssertion {611	#[derive(Trace)]612	struct ObjectAssert<B: Trace> {613		uctx: B,614		asserts: Rc<LObjAsserts>,615	}616	impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {617		fn run(&self, sup_this: SupThis) -> Result<()> {618			let a_ctx = self.uctx.bind(sup_this)?;619			let assert_env = Context::enter_using(&a_ctx, &self.asserts.shape);620			for assert in &self.asserts.asserts {621				evaluate_assert(assert_env.clone(), assert)?;622			}623			Ok(())624		}625	}626	ObjectAssert { uctx, asserts }627}628fn evaluate_object_assertions_static(629	a_ctx: Context,630	asserts: Rc<LObjAsserts>,631) -> impl ObjectAssertion {632	#[derive(Trace)]633	struct ObjectAssert {634		assert_env: Context,635		asserts: Rc<LObjAsserts>,636	}637	impl ObjectAssertion for ObjectAssert {638		fn run(&self, _sup_this: SupThis) -> Result<()> {639			for assert in &self.asserts.asserts {640				evaluate_assert(self.assert_env.clone(), assert)?;641			}642			Ok(())643		}644	}645	let assert_env = Context::enter_using(&a_ctx, &asserts.shape);646	ObjectAssert {647		assert_env,648		asserts,649	}650}
modifiedcrates/jrsonnet-evaluator/src/typed/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/typed/mod.rs
+++ b/crates/jrsonnet-evaluator/src/typed/mod.rs
@@ -157,9 +157,7 @@
 			Self::BoundedNumber(from, to) => {
 				if let Val::Num(n) = value {
 					let n = n.get();
-					if from.map(|from| from > n).unwrap_or(false)
-						|| to.map(|to| to < n).unwrap_or(false)
-					{
+					if from.is_some_and(|from| from > n) || to.is_some_and(|to| to < n) {
 						return Err(TypeError::BoundsFailed(n, *from, *to).into());
 					}
 					Ok(())
modifiedcrates/jrsonnet-interner/src/inner.rsdiffbeforeafterboth
--- a/crates/jrsonnet-interner/src/inner.rs
+++ b/crates/jrsonnet-interner/src/inner.rs
@@ -161,6 +161,12 @@
 		// SAFETY: header is initialized
 		unsafe { (*header).refcnt() }
 	}
+
+	pub fn len32(&self) -> u32 {
+		let header = Self::header(self);
+		// SAFETY: header is initialized
+		unsafe { (*header).size }
+	}
 }
 
 impl Clone for Inner {
modifiedcrates/jrsonnet-interner/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-interner/src/lib.rs
+++ b/crates/jrsonnet-interner/src/lib.rs
@@ -53,6 +53,10 @@
 	pub fn cast_bytes(self) -> IBytes {
 		IBytes(self.0.clone())
 	}
+
+	pub fn len32(&self) -> u32 {
+		self.0.len32()
+	}
 }
 
 impl Deref for IStr {
modifiedcrates/jrsonnet-ir-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/lib.rs
+++ b/crates/jrsonnet-ir-parser/src/lib.rs
@@ -1038,7 +1038,7 @@
 	}
 	let e = expr(&mut p)?;
 	if !p.at_eof() {
-		return Err(p.error(format!("expected end of file, got {}", p.current_desc(),)));
+		return Err(p.error(format!("expected end of file, got {}", p.current_desc())));
 	}
 	Ok(e)
 }
@@ -1051,10 +1051,7 @@
 
 #[cfg(test)]
 mod tests {
-	use std::fs;
-
-	use insta::{assert_snapshot, glob};
-	use jrsonnet_ir::{IStr, Source};
+	use insta::assert_snapshot;
 
 	use super::*;
 
@@ -1159,6 +1156,11 @@
 	#[test]
 	#[cfg(not(feature = "exp-null-coaelse"))]
 	fn peg_snapshots() {
+		use std::fs;
+
+		use insta::glob;
+		use jrsonnet_ir::{IStr, Source};
+
 		glob!("../../jrsonnet-peg-parser/src", "tests/*.jsonnet", |path| {
 			let input = fs::read_to_string(path).expect("read test file");
 			let source = Source::new_virtual("<test>".into(), IStr::empty());
modifiedcrates/jrsonnet-peg-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/lib.rs
+++ b/crates/jrsonnet-peg-parser/src/lib.rs
@@ -433,16 +433,16 @@
 
 #[cfg(test)]
 mod tests {
-	use std::fs;
+	#[test]
+	#[cfg(not(feature = "exp-null-coaelse"))]
+	fn snapshots() {
+		use std::fs;
 
-	use insta::{assert_snapshot, glob};
-	use jrsonnet_ir::{IStr, Source};
+		use insta::{assert_snapshot, glob};
+		use jrsonnet_ir::{IStr, Source};
 
-	use crate::{ParserSettings, parse};
+		use crate::{ParserSettings, parse};
 
-	#[test]
-	#[cfg(not(feature = "exp-null-coaelse"))]
-	fn snapshots() {
 		glob!("tests/*.jsonnet", |path| {
 			let input = fs::read_to_string(path).expect("read test file");
 			let v = parse(
modifiedtests/cpp_test_suite_golden_override/error.array_large_index.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.array_large_index.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.array_large_index.jsonnet.golden
@@ -1 +1 @@
-array out of bounds: 4294967295 is not within [0,3)
\ No newline at end of file
+array out of bounds: 18446744073709552000 is not within [0,3)
\ No newline at end of file
modifiedtests/go_testdata_golden_override/string_index_negative.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/string_index_negative.jsonnet.golden
+++ b/tests/go_testdata_golden_override/string_index_negative.jsonnet.golden
@@ -1 +1 @@
-array out of bounds: -1 is not within [0,4)
\ No newline at end of file
+string out of bounds: -1 is not within [0,4)
\ No newline at end of file