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

difftreelog

refactor AssertStmt to struct

zksvlxnsYaroslav Bolyukin2026-04-25parent: #6fbff53.patch.diff
in: master

6 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_ir::{6	ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, ExprParams, FieldMember,7	FieldName, ForSpecData, IfSpecData, ImportKind, LiteralType, ObjBody, ObjMembers, Spanned,8	function::ParamName,9};10use jrsonnet_types::ValType;1112use self::destructure::destruct;13use crate::{14	Context, ContextBuilder, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,15	ResultExt, SupThis, Unbound, Val,16	arr::ArrValue,17	bail,18	destructure::evaluate_dest,19	error::{ErrorKind::*, suggest_object_fields},20	evaluate::operator::{evaluate_binary_op_special, evaluate_unary_op},21	function::{CallLocation, FuncDesc, FuncVal, PreparedFuncVal},22	in_frame,23	typed::{FromUntyped, IntoUntyped as _, Typed},24	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},25	with_state,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: &Expr) -> Option<Val> {50	fn is_trivial(expr: &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(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(ctx: Context, name: IStr, params: ExprParams, body: Rc<Expr>) -> Val {83	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {84		name,85		ctx,86		params,87		body,88	})))89}9091pub fn evaluate_field_name(ctx: Context, field_name: &Spanned<FieldName>) -> Result<Option<IStr>> {92	Ok(match &field_name.value {93		FieldName::Fixed(n) => Some(n.clone()),94		FieldName::Dyn(expr) => in_frame(95			CallLocation::new(&field_name.span),96			|| "evaluating field name".to_string(),97			|| {98				let v = evaluate(ctx, expr)?;99				Ok(if matches!(v, Val::Null) {100					None101				} else {102					Some(IStr::from_untyped(v)?)103				})104			},105		)?,106	})107}108109pub fn evaluate_comp(110	ctx: Context,111	specs: &[CompSpec],112	mut guaranteed_reserve: usize,113	callback: &mut impl FnMut(Context, usize) -> Result<()>,114) -> Result<()> {115	match specs.first() {116		None => callback(ctx, guaranteed_reserve)?,117		Some(CompSpec::IfSpec(IfSpecData { cond, span: _ })) => {118			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {119				evaluate_comp(ctx, &specs[1..], 0, callback)?;120			}121		}122		Some(CompSpec::ForSpec(ForSpecData {123			destruct: into,124			over,125		})) => {126			match evaluate(ctx.clone(), over)? {127				Val::Arr(list) => {128					guaranteed_reserve = guaranteed_reserve.max(1) * list.len();129					for (i, item) in list.iter_lazy().enumerate() {130						let fctx = Pending::new();131						let mut ctx = ContextBuilder::extend_fast(ctx.clone());132						destruct(into, item, fctx.clone(), &mut ctx)?;133						let ctx = ctx.build().into_future(fctx);134135						let specs = &specs[1..];136						evaluate_comp(137							ctx,138							specs,139							if i == 0 || !specs.is_empty() {140								guaranteed_reserve141							} else {142								0143							},144							callback,145						)?;146					}147				}148				#[cfg(feature = "exp-object-iteration")]149				Val::Obj(obj) => {150					let fields = obj.fields(151						// TODO: Should there be ability to preserve iteration order?152						#[cfg(feature = "exp-preserve-order")]153						false,154					);155					guaranteed_reserve = guaranteed_reserve.max(1) * fields.len();156					for field in fields {157						let fctx = Pending::new();158						let mut new_bindings = FxHashMap::with_capacity(into.binds_len());159						let obj = obj.clone();160						let value = Thunk::evaluated(Val::arr(vec![161							Thunk::evaluated(Val::string(field.clone())),162							obj.get_lazy(field).transpose().expect(163								"field exists, as field name was obtained from object.fields()",164							),165						]));166						destruct(into, value, fctx.clone(), &mut new_bindings)?;167						let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);168169						evaluate_comp(ctx, &specs[1..], callback)?;170					}171				}172				_ => bail!(InComprehensionCanOnlyIterateOverArray),173			}174		}175	}176	Ok(())177}178179fn evaluate_arr_comp(ctx: Context, expr: &Rc<Expr>, comp_specs: &[CompSpec]) -> Result<ArrValue> {180	let ctx = ctx.branch_point();181	'eager: {182		let mut out = Vec::new();183184		if evaluate_comp(ctx.clone(), comp_specs, 0, &mut |ctx, reserve| {185			if reserve != 0 {186				out.reserve(reserve);187			}188			out.push(evaluate(ctx, expr)?);189			Ok(())190		})191		.is_err()192		{193			break 'eager;194		}195196		return Ok(ArrValue::new(out));197	};198	let mut out = Vec::new();199	evaluate_comp(ctx, comp_specs, 0, &mut |ctx, reserve| {200		if reserve != 0 {201			out.reserve(reserve);202		}203		let expr = expr.clone();204		out.push(Thunk!(move || evaluate(ctx, &expr)));205		Ok(())206	})?;207	Ok(ArrValue::new(out))208}209210trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}211impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}212213fn evaluate_object_locals(214	fctx: Context,215	locals: Rc<Vec<BindSpec>>,216) -> impl CloneableUnbound<Context> {217	#[derive(Trace, Clone)]218	struct UnboundLocals {219		fctx: Context,220		locals: Rc<Vec<BindSpec>>,221	}222	impl Unbound for UnboundLocals {223		type Bound = Context;224225		fn bind(&self, sup_this: SupThis) -> Result<Context> {226			let fctx = Context::new_future();227			let ctx = self.fctx.clone();228			let mut ctx = ContextBuilder::extend(ctx);229			for b in self.locals.iter() {230				evaluate_dest(b, fctx.clone(), &mut ctx)?;231			}232233			let ctx = ctx.build_sup_this(sup_this).into_future(fctx);234235			Ok(ctx)236		}237	}238239	UnboundLocals { fctx, locals }240}241242pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(243	builder: &mut ObjValueBuilder,244	ctx: Context,245	uctx: B,246	field: &FieldMember,247) -> Result<()> {248	let name = evaluate_field_name(ctx, &field.name)?;249	let Some(name) = name else {250		return Ok(());251	};252253	match field {254		FieldMember {255			plus,256			params: None,257			visibility,258			value,259			..260		} => {261			#[derive(Trace)]262			struct UnboundValue<B: Trace> {263				uctx: B,264				value: Rc<Expr>,265				name: IStr,266			}267			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {268				type Bound = Val;269				fn bind(&self, sup_this: SupThis) -> Result<Val> {270					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())271				}272			}273274			builder275				.field(name.clone())276				.with_add(*plus)277				.with_visibility(*visibility)278				.with_location(field.name.span.clone())279				.bindable(UnboundValue {280					uctx,281					value: value.clone(),282					name,283				})?;284		}285		FieldMember {286			params: Some(params),287			visibility,288			value,289			..290		} => {291			#[derive(Trace)]292			struct UnboundMethod<B: Trace> {293				uctx: B,294				value: Rc<Expr>,295				params: ExprParams,296				name: IStr,297			}298			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {299				type Bound = Val;300				fn bind(&self, sup_this: SupThis) -> Result<Val> {301					Ok(evaluate_method(302						self.uctx.bind(sup_this)?,303						self.name.clone(),304						self.params.clone(),305						self.value.clone(),306					))307				}308			}309310			builder311				.field(name.clone())312				.with_visibility(*visibility)313				// .with_location(value.span())314				.bindable(UnboundMethod {315					uctx,316					value: value.clone(),317					params: params.clone(),318					name,319				})?;320		}321	}322	Ok(())323}324325#[derive(Trace, Clone)]326struct DirectUnbound(Context);327impl Unbound for DirectUnbound {328	type Bound = Context;329	fn bind(&self, sup_this: SupThis) -> Result<Context> {330		Ok(ContextBuilder::extend(self.0.clone()).build_sup_this(sup_this))331	}332}333334#[allow(clippy::too_many_lines)]335pub fn evaluate_member_list_object(336	super_obj: Option<ObjValue>,337	ctx: Context,338	members: &ObjMembers,339) -> Result<ObjValue> {340	#[derive(Trace)]341	struct ObjectAssert<B: Trace> {342		uctx: B,343		asserts: Rc<Vec<AssertStmt>>,344	}345	impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {346		fn run(&self, sup_this: SupThis) -> Result<()> {347			let ctx = self.uctx.bind(sup_this)?;348			for assert in &*self.asserts {349				evaluate_assert(ctx.clone(), assert)?;350			}351			Ok(())352		}353	}354355	let mut builder = ObjValueBuilder::new();356	if let Some(super_obj) = super_obj {357		builder.with_super(super_obj);358	}359360	if members.locals.is_empty() {361		// We can use the same context for all field evaluation, it doesn't depends on locals, only on this/super362		let uctx = DirectUnbound(ctx.clone());363		for field in &members.fields {364			evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;365		}366		if !members.asserts.is_empty() {367			builder.assert(ObjectAssert {368				uctx,369				asserts: members.asserts.clone(),370			});371		}372	} else {373		let locals = members.locals.clone();374		// We have single context for all fields, so we can cache them together375		let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));376		for field in &members.fields {377			evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;378		}379		if !members.asserts.is_empty() {380			builder.assert(ObjectAssert {381				uctx,382				asserts: members.asserts.clone(),383			});384		}385	}386387	Ok(builder.build())388}389390pub fn evaluate_object(391	super_obj: Option<ObjValue>,392	ctx: Context,393	object: &ObjBody,394) -> Result<ObjValue> {395	Ok(match object {396		ObjBody::MemberList(members) => evaluate_member_list_object(super_obj, ctx, members)?,397		ObjBody::ObjComp(obj) => {398			let mut builder = ObjValueBuilder::new();399			if let Some(super_obj) = super_obj {400				builder.with_super(super_obj);401			}402			let locals = obj.locals.clone();403			evaluate_comp(404				ctx.branch_point(),405				&obj.compspecs,406				0,407				&mut |ctx, reserve| {408					let uctx = evaluate_object_locals(ctx.clone(), locals.clone());409					builder.reserve_fields(reserve);410411					evaluate_field_member(&mut builder, ctx, uctx, &obj.field)412				},413			)?;414415			builder.build()416		}417	})418}419420pub fn evaluate_apply(421	ctx: Context,422	value: &Expr,423	args: &ArgsDesc,424	loc: CallLocation<'_>,425	tailstrict: bool,426) -> Result<Val> {427	let value = evaluate(ctx.clone(), value)?;428	Ok(match value {429		Val::Func(f) => {430			let name = f.name();431			let unnamed = args432				.unnamed433				.iter()434				.cloned()435				.map(|un| evaluate_thunk(ctx.clone(), un, tailstrict))436				.collect::<Result<Vec<_>>>()?;437			let named = args438				.values439				.iter()440				.cloned()441				.map(|un| evaluate_thunk(ctx.clone(), un, tailstrict))442				.collect::<Result<Vec<_>>>()?;443			let prepare = PreparedFuncVal::new(f, args.unnamed.len(), &args.names)444				.with_description_src(loc, || format!("function <{name}> call"))?;445			let body = || prepare.call(loc, &unnamed, &named);446			if tailstrict {447				body()?448			} else {449				in_frame(loc, || format!("function <{name}> call"), body)?450			}451		}452		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),453	})454}455456pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {457	let value = &assertion.0;458	let msg = &assertion.1;459	let assertion_result = in_frame(460		CallLocation::new(&value.span),461		|| "assertion condition".to_owned(),462		|| bool::from_untyped(evaluate(ctx.clone(), value)?),463	)?;464	if !assertion_result {465		in_frame(466			CallLocation::new(&value.span),467			|| "assertion failure".to_owned(),468			|| {469				if let Some(msg) = msg {470					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));471				}472				bail!(AssertionFailed(Val::Null.to_string()?));473			},474		)?;475	}476	Ok(())477}478479pub fn evaluate_named_param(ctx: Context, expr: &Expr, name: ParamName) -> Result<Val> {480	match name {481		ParamName::Named(name) => evaluate_named(ctx, expr, name),482		ParamName::Unnamed => evaluate(ctx, expr),483	}484}485486pub fn evaluate_named(ctx: Context, expr: &Expr, name: IStr) -> Result<Val> {487	use Expr::*;488	Ok(match expr {489		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),490		_ => evaluate(ctx, expr)?,491	})492}493494pub fn evaluate_thunk(ctx: Context, expr: Rc<Expr>, tailstrict: bool) -> Result<Thunk<Val>> {495	Ok(if tailstrict {496		Thunk::evaluated(evaluate(ctx, &expr)?)497	} else {498		Thunk!(move || { evaluate(ctx, &expr) })499	})500}501#[allow(clippy::too_many_lines)]502pub fn evaluate(ctx: Context, expr: &Expr) -> Result<Val> {503	use Expr::*;504505	Ok(match expr {506		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),507		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),508		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),509		Literal(LiteralType::True) => Val::Bool(true),510		Literal(LiteralType::False) => Val::Bool(false),511		Literal(LiteralType::Null) => Val::Null,512		Str(v) => Val::string(v.clone()),513		Num(v) => Val::try_num(*v)?,514		// I have tried to remove special behavior from super by implementing standalone-super515		// expresion, but looks like this case still needs special treatment.516		//517		// Note that other jsonnet implementations will fail on `if value in (super)` expression,518		// because the standalone super literal is not supported, that is because in other519		// implementations `in super` treated differently from `in smth_else`.520		BinaryOp(bin)521			if matches!(&bin.rhs, Expr::Literal(LiteralType::Super))522				&& bin.op == BinaryOpType::In =>523		{524			let sup_this = ctx.try_sup_this()?;525			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.526			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.527			if !sup_this.has_super() {528				return Ok(Val::Bool(false));529			}530			let field = evaluate(ctx, &bin.lhs)?;531			Val::Bool(sup_this.field_in_super(field.to_string()?))532		}533		BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?,534		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,535		Var(name) => in_frame(536			CallLocation::new(&name.span),537			|| format!("local <{}> access", &**name),538			|| ctx.binding((**name).clone())?.evaluate(),539		)?,540		Index { indexable, parts } => ensure_sufficient_stack(|| {541			let mut parts = parts.iter();542			let mut indexable = if matches!(&**indexable, Expr::Literal(LiteralType::Super)) {543				let part = parts.next().expect("at least part should exist");544				// sup_this existence check might also be skipped here for null-coalesce...545				// But I believe this might cause errors.546				let sup_this = ctx.try_sup_this()?;547				if !sup_this.has_super() {548					#[cfg(feature = "exp-null-coaelse")]549					if part.null_coaelse {550						return Ok(Val::Null);551					}552					bail!(NoSuperFound)553				}554				let name = evaluate(ctx.clone(), &part.value)?;555556				let Val::Str(name) = name else {557					bail!(ValueIndexMustBeTypeGot(558						ValType::Obj,559						ValType::Str,560						name.value_type(),561					))562				};563564				let name = name.into_flat();565				match sup_this566					.get_super(name.clone())567					.with_description_src(&part.span, || format!("field <{name}> access"))?568				{569					Some(v) => v,570					#[cfg(feature = "exp-null-coaelse")]571					None if part.null_coaelse => return Ok(Val::Null),572					None => {573						let suggestions = suggest_object_fields(574							&sup_this.standalone_super().expect("super exists"),575							name.clone(),576						);577578						bail!(NoSuchField(name, suggestions))579					}580				}581			} else {582				evaluate(ctx.clone(), indexable)?583			};584585			for part in parts {586				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {587					(Val::Obj(v), Val::Str(key)) => match v588						.get(key.clone().into_flat())589						.with_description_src(&part.span, || format!("field <{key}> access"))?590					{591						Some(v) => v,592						#[cfg(feature = "exp-null-coaelse")]593						None if part.null_coaelse => return Ok(Val::Null),594						None => {595							let suggestions = suggest_object_fields(&v, key.into_flat());596597							return Err(Error::from(NoSuchField(598								key.clone().into_flat(),599								suggestions,600							)))601							.with_description_src(&part.span, || format!("field <{key}> access"));602						}603					},604					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(605						ValType::Obj,606						ValType::Str,607						n.value_type(),608					)),609					(Val::Arr(v), Val::Num(n)) => {610						let n = n.get();611						if n.fract() > f64::EPSILON {612							bail!(FractionalIndex)613						}614						if n < 0.0 {615							#[expect(616								clippy::cast_possible_truncation,617								reason = "it would be truncated anyway"618							)]619							let n = n as isize;620							bail!(ArrayBoundsError(n, v.len()));621						}622						#[expect(623							clippy::cast_possible_truncation,624							clippy::cast_sign_loss,625							reason = "n is checked postive"626						)]627						v.get(n as usize)?628							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?629					}630					(Val::Arr(_), Val::Str(n)) => {631						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))632					}633					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(634						ValType::Arr,635						ValType::Num,636						n.value_type(),637					)),638639					(Val::Str(s), Val::Num(n)) => Val::Str({640						let n = n.get();641						if n.fract() > f64::EPSILON {642							bail!(FractionalIndex)643						}644						if n < 0.0 {645							#[expect(646								clippy::cast_possible_truncation,647								reason = "it would be truncated anyway"648							)]649							let n = n as isize;650							bail!(ArrayBoundsError(n, s.into_flat().chars().count()));651						}652						#[expect(653							clippy::cast_sign_loss,654							clippy::cast_possible_truncation,655							reason = "n is positive, overflow will truncate as expected"656						)]657						let n = n as usize;658						let v: IStr = s659							.clone()660							.into_flat()661							.chars()662							.skip(n)663							.take(1)664							.collect::<String>()665							.into();666						if v.is_empty() {667							bail!(StringBoundsError(n, s.into_flat().chars().count()))668						}669						StrValue::Flat(v)670					}),671					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(672						ValType::Str,673						ValType::Num,674						n.value_type(),675					)),676					#[cfg(feature = "exp-null-coaelse")]677					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),678					(v, _) => bail!(CantIndexInto(v.value_type())),679				};680			}681			Ok(indexable)682		})?,683		LocalExpr(bindings, returned) => {684			let fctx = Context::new_future();685			let mut ctx = ContextBuilder::extend(ctx);686			for b in bindings {687				evaluate_dest(b, fctx.clone(), &mut ctx)?;688			}689			let ctx = ctx.build().into_future(fctx);690			evaluate(ctx, returned)?691		}692		Arr(items) => {693			if items.is_empty() {694				Val::arr(())695			} else {696				Val::Arr(ArrValue::expr(ctx, items.clone()))697			}698		}699		ArrComp(expr, comp_specs) => Val::Arr(evaluate_arr_comp(ctx, expr, comp_specs)?),700		Obj(body) => Val::Obj(evaluate_object(None, ctx, body)?),701		ObjExtend(a, b) => {702			let base = evaluate(ctx.clone(), a)?;703			match base {704				Val::Obj(base_obj) => Val::Obj(evaluate_object(Some(base_obj), ctx, b)?),705				_ => bail!("ObjExtend lhs should be an object value"),706			}707		}708		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {709			evaluate_apply(ctx, value, args, CallLocation::new(&args.span), *tailstrict)710		})?,711		Function(params, body) => {712			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())713		}714		AssertExpr(assert) => {715			evaluate_assert(ctx.clone(), &assert.assert)?;716			evaluate(ctx, &assert.rest)?717		}718		ErrorStmt(s, e) => in_frame(719			CallLocation::new(s),720			|| "error statement".to_owned(),721			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),722		)?,723		IfElse(if_else) => {724			if in_frame(725				CallLocation::new(&if_else.cond.span),726				|| "if condition".to_owned(),727				|| bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.cond)?),728			)? {729				evaluate(ctx, &if_else.cond_then)?730			} else {731				match &if_else.cond_else {732					Some(v) => evaluate(ctx, v)?,733					None => Val::Null,734				}735			}736		}737		Slice(slice) => {738			fn parse_idx<T: Typed + FromUntyped>(739				ctx: Context,740				expr: Option<&Spanned<Expr>>,741				desc: &'static str,742			) -> Result<Option<T>> {743				if let Some(value) = expr {744					Ok(in_frame(745						CallLocation::new(&value.span),746						|| format!("slice {desc}"),747						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),748					)?)749				} else {750					Ok(None)751				}752			}753754			let indexable = evaluate(ctx.clone(), &slice.value)?;755756			let start = parse_idx(ctx.clone(), slice.slice.start.as_ref(), "start")?;757			let end = parse_idx(ctx.clone(), slice.slice.end.as_ref(), "end")?;758			let step = parse_idx(ctx, slice.slice.step.as_ref(), "step")?;759760			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?761		}762		Import(kind, path) => {763			let Expr::Str(path) = &**path else {764				bail!("computed imports are not supported")765			};766			with_state(|s| {767				let span = &kind.span;768				let resolved_path = s.resolve_from(span.0.source_path(), path)?;769				Ok(match &**kind {770					ImportKind::Normal => in_frame(771						CallLocation::new(span),772						|| format!("import {:?}", path.clone()),773						|| s.import_resolved(resolved_path),774					)?,775					ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?),776					ImportKind::Bin => Val::arr(s.import_resolved_bin(resolved_path)?),777				}) as Result<Val>778			})?779		}780	})781}
after · crates/jrsonnet-evaluator/src/evaluate/mod.rs
1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_ir::{6	ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, ExprParams, FieldMember,7	FieldName, ForSpecData, IfSpecData, ImportKind, LiteralType, ObjBody, ObjMembers, Spanned,8	function::ParamName,9};10use jrsonnet_types::ValType;1112use self::destructure::destruct;13use crate::{14	Context, ContextBuilder, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,15	ResultExt, SupThis, Unbound, Val,16	arr::ArrValue,17	bail,18	destructure::evaluate_dest,19	error::{ErrorKind::*, suggest_object_fields},20	evaluate::operator::{evaluate_binary_op_special, evaluate_unary_op},21	function::{CallLocation, FuncDesc, FuncVal, PreparedFuncVal},22	in_frame,23	typed::{FromUntyped, IntoUntyped as _, Typed},24	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},25	with_state,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: &Expr) -> Option<Val> {50	fn is_trivial(expr: &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(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(ctx: Context, name: IStr, params: ExprParams, body: Rc<Expr>) -> Val {83	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {84		name,85		ctx,86		params,87		body,88	})))89}9091pub fn evaluate_field_name(ctx: Context, field_name: &Spanned<FieldName>) -> Result<Option<IStr>> {92	Ok(match &field_name.value {93		FieldName::Fixed(n) => Some(n.clone()),94		FieldName::Dyn(expr) => in_frame(95			CallLocation::new(&field_name.span),96			|| "evaluating field name".to_string(),97			|| {98				let v = evaluate(ctx, expr)?;99				Ok(if matches!(v, Val::Null) {100					None101				} else {102					Some(IStr::from_untyped(v)?)103				})104			},105		)?,106	})107}108109pub fn evaluate_comp(110	ctx: Context,111	specs: &[CompSpec],112	mut guaranteed_reserve: usize,113	callback: &mut impl FnMut(Context, usize) -> Result<()>,114) -> Result<()> {115	match specs.first() {116		None => callback(ctx, guaranteed_reserve)?,117		Some(CompSpec::IfSpec(IfSpecData { cond, span: _ })) => {118			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {119				evaluate_comp(ctx, &specs[1..], 0, callback)?;120			}121		}122		Some(CompSpec::ForSpec(ForSpecData {123			destruct: into,124			over,125		})) => {126			match evaluate(ctx.clone(), over)? {127				Val::Arr(list) => {128					guaranteed_reserve = guaranteed_reserve.max(1) * list.len();129					for (i, item) in list.iter_lazy().enumerate() {130						let fctx = Pending::new();131						let mut ctx = ContextBuilder::extend_fast(ctx.clone());132						destruct(into, item, fctx.clone(), &mut ctx)?;133						let ctx = ctx.build().into_future(fctx);134135						let specs = &specs[1..];136						evaluate_comp(137							ctx,138							specs,139							if i == 0 || !specs.is_empty() {140								guaranteed_reserve141							} else {142								0143							},144							callback,145						)?;146					}147				}148				#[cfg(feature = "exp-object-iteration")]149				Val::Obj(obj) => {150					let fields = obj.fields(151						// TODO: Should there be ability to preserve iteration order?152						#[cfg(feature = "exp-preserve-order")]153						false,154					);155					guaranteed_reserve = guaranteed_reserve.max(1) * fields.len();156					for field in fields {157						let fctx = Pending::new();158						let mut new_bindings = FxHashMap::with_capacity(into.binds_len());159						let obj = obj.clone();160						let value = Thunk::evaluated(Val::arr(vec![161							Thunk::evaluated(Val::string(field.clone())),162							obj.get_lazy(field).transpose().expect(163								"field exists, as field name was obtained from object.fields()",164							),165						]));166						destruct(into, value, fctx.clone(), &mut new_bindings)?;167						let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);168169						evaluate_comp(ctx, &specs[1..], callback)?;170					}171				}172				_ => bail!(InComprehensionCanOnlyIterateOverArray),173			}174		}175	}176	Ok(())177}178179fn evaluate_arr_comp(ctx: Context, expr: &Rc<Expr>, comp_specs: &[CompSpec]) -> Result<ArrValue> {180	let ctx = ctx.branch_point();181	'eager: {182		let mut out = Vec::new();183184		if evaluate_comp(ctx.clone(), comp_specs, 0, &mut |ctx, reserve| {185			if reserve != 0 {186				out.reserve(reserve);187			}188			out.push(evaluate(ctx, expr)?);189			Ok(())190		})191		.is_err()192		{193			break 'eager;194		}195196		return Ok(ArrValue::new(out));197	};198	let mut out = Vec::new();199	evaluate_comp(ctx, comp_specs, 0, &mut |ctx, reserve| {200		if reserve != 0 {201			out.reserve(reserve);202		}203		let expr = expr.clone();204		out.push(Thunk!(move || evaluate(ctx, &expr)));205		Ok(())206	})?;207	Ok(ArrValue::new(out))208}209210trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}211impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}212213fn evaluate_object_locals(214	fctx: Context,215	locals: Rc<Vec<BindSpec>>,216) -> impl CloneableUnbound<Context> {217	#[derive(Trace, Clone)]218	struct UnboundLocals {219		fctx: Context,220		locals: Rc<Vec<BindSpec>>,221	}222	impl Unbound for UnboundLocals {223		type Bound = Context;224225		fn bind(&self, sup_this: SupThis) -> Result<Context> {226			let fctx = Context::new_future();227			let ctx = self.fctx.clone();228			let mut ctx = ContextBuilder::extend(ctx);229			for b in self.locals.iter() {230				evaluate_dest(b, fctx.clone(), &mut ctx)?;231			}232233			let ctx = ctx.build_sup_this(sup_this).into_future(fctx);234235			Ok(ctx)236		}237	}238239	UnboundLocals { fctx, locals }240}241242pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(243	builder: &mut ObjValueBuilder,244	ctx: Context,245	uctx: B,246	field: &FieldMember,247) -> Result<()> {248	let name = evaluate_field_name(ctx, &field.name)?;249	let Some(name) = name else {250		return Ok(());251	};252253	match field {254		FieldMember {255			plus,256			params: None,257			visibility,258			value,259			..260		} => {261			#[derive(Trace)]262			struct UnboundValue<B: Trace> {263				uctx: B,264				value: Rc<Expr>,265				name: IStr,266			}267			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {268				type Bound = Val;269				fn bind(&self, sup_this: SupThis) -> Result<Val> {270					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())271				}272			}273274			builder275				.field(name.clone())276				.with_add(*plus)277				.with_visibility(*visibility)278				.with_location(field.name.span.clone())279				.bindable(UnboundValue {280					uctx,281					value: value.clone(),282					name,283				})?;284		}285		FieldMember {286			params: Some(params),287			visibility,288			value,289			..290		} => {291			#[derive(Trace)]292			struct UnboundMethod<B: Trace> {293				uctx: B,294				value: Rc<Expr>,295				params: ExprParams,296				name: IStr,297			}298			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {299				type Bound = Val;300				fn bind(&self, sup_this: SupThis) -> Result<Val> {301					Ok(evaluate_method(302						self.uctx.bind(sup_this)?,303						self.name.clone(),304						self.params.clone(),305						self.value.clone(),306					))307				}308			}309310			builder311				.field(name.clone())312				.with_visibility(*visibility)313				// .with_location(value.span())314				.bindable(UnboundMethod {315					uctx,316					value: value.clone(),317					params: params.clone(),318					name,319				})?;320		}321	}322	Ok(())323}324325#[derive(Trace, Clone)]326struct DirectUnbound(Context);327impl Unbound for DirectUnbound {328	type Bound = Context;329	fn bind(&self, sup_this: SupThis) -> Result<Context> {330		Ok(ContextBuilder::extend(self.0.clone()).build_sup_this(sup_this))331	}332}333334#[allow(clippy::too_many_lines)]335pub fn evaluate_member_list_object(336	super_obj: Option<ObjValue>,337	ctx: Context,338	members: &ObjMembers,339) -> Result<ObjValue> {340	#[derive(Trace)]341	struct ObjectAssert<B: Trace> {342		uctx: B,343		asserts: Rc<Vec<AssertStmt>>,344	}345	impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {346		fn run(&self, sup_this: SupThis) -> Result<()> {347			let ctx = self.uctx.bind(sup_this)?;348			for assert in &*self.asserts {349				evaluate_assert(ctx.clone(), assert)?;350			}351			Ok(())352		}353	}354355	let mut builder = ObjValueBuilder::new();356	if let Some(super_obj) = super_obj {357		builder.with_super(super_obj);358	}359360	if members.locals.is_empty() {361		// We can use the same context for all field evaluation, it doesn't depends on locals, only on this/super362		let uctx = DirectUnbound(ctx.clone());363		for field in &members.fields {364			evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;365		}366		if !members.asserts.is_empty() {367			builder.assert(ObjectAssert {368				uctx,369				asserts: members.asserts.clone(),370			});371		}372	} else {373		let locals = members.locals.clone();374		// We have single context for all fields, so we can cache them together375		let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));376		for field in &members.fields {377			evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;378		}379		if !members.asserts.is_empty() {380			builder.assert(ObjectAssert {381				uctx,382				asserts: members.asserts.clone(),383			});384		}385	}386387	Ok(builder.build())388}389390pub fn evaluate_object(391	super_obj: Option<ObjValue>,392	ctx: Context,393	object: &ObjBody,394) -> Result<ObjValue> {395	Ok(match object {396		ObjBody::MemberList(members) => evaluate_member_list_object(super_obj, ctx, members)?,397		ObjBody::ObjComp(obj) => {398			let mut builder = ObjValueBuilder::new();399			if let Some(super_obj) = super_obj {400				builder.with_super(super_obj);401			}402			let locals = obj.locals.clone();403			evaluate_comp(404				ctx.branch_point(),405				&obj.compspecs,406				0,407				&mut |ctx, reserve| {408					let uctx = evaluate_object_locals(ctx.clone(), locals.clone());409					builder.reserve_fields(reserve);410411					evaluate_field_member(&mut builder, ctx, uctx, &obj.field)412				},413			)?;414415			builder.build()416		}417	})418}419420pub fn evaluate_apply(421	ctx: Context,422	value: &Expr,423	args: &ArgsDesc,424	loc: CallLocation<'_>,425	tailstrict: bool,426) -> Result<Val> {427	let value = evaluate(ctx.clone(), value)?;428	Ok(match value {429		Val::Func(f) => {430			let name = f.name();431			let unnamed = args432				.unnamed433				.iter()434				.cloned()435				.map(|un| evaluate_thunk(ctx.clone(), un, tailstrict))436				.collect::<Result<Vec<_>>>()?;437			let named = args438				.values439				.iter()440				.cloned()441				.map(|un| evaluate_thunk(ctx.clone(), un, tailstrict))442				.collect::<Result<Vec<_>>>()?;443			let prepare = PreparedFuncVal::new(f, args.unnamed.len(), &args.names)444				.with_description_src(loc, || format!("function <{name}> call"))?;445			let body = || prepare.call(loc, &unnamed, &named);446			if tailstrict {447				body()?448			} else {449				in_frame(loc, || format!("function <{name}> call"), body)?450			}451		}452		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),453	})454}455456pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {457	let AssertStmt { assertion, message } = assertion;458	let assertion_result = in_frame(459		CallLocation::new(&assertion.span),460		|| "assertion condition".to_owned(),461		|| bool::from_untyped(evaluate(ctx.clone(), assertion)?),462	)?;463	if !assertion_result {464		in_frame(465			CallLocation::new(&assertion.span),466			|| "assertion failure".to_owned(),467			|| {468				if let Some(msg) = message {469					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));470				}471				bail!(AssertionFailed(Val::Null.to_string()?));472			},473		)?;474	}475	Ok(())476}477478pub fn evaluate_named_param(ctx: Context, expr: &Expr, name: ParamName) -> Result<Val> {479	match name {480		ParamName::Named(name) => evaluate_named(ctx, expr, name),481		ParamName::Unnamed => evaluate(ctx, expr),482	}483}484485pub fn evaluate_named(ctx: Context, expr: &Expr, name: IStr) -> Result<Val> {486	use Expr::*;487	Ok(match expr {488		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),489		_ => evaluate(ctx, expr)?,490	})491}492493pub fn evaluate_thunk(ctx: Context, expr: Rc<Expr>, tailstrict: bool) -> Result<Thunk<Val>> {494	Ok(if tailstrict {495		Thunk::evaluated(evaluate(ctx, &expr)?)496	} else {497		Thunk!(move || { evaluate(ctx, &expr) })498	})499}500#[allow(clippy::too_many_lines)]501pub fn evaluate(ctx: Context, expr: &Expr) -> Result<Val> {502	use Expr::*;503504	Ok(match expr {505		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),506		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),507		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),508		Literal(LiteralType::True) => Val::Bool(true),509		Literal(LiteralType::False) => Val::Bool(false),510		Literal(LiteralType::Null) => Val::Null,511		Str(v) => Val::string(v.clone()),512		Num(v) => Val::try_num(*v)?,513		// I have tried to remove special behavior from super by implementing standalone-super514		// expresion, but looks like this case still needs special treatment.515		//516		// Note that other jsonnet implementations will fail on `if value in (super)` expression,517		// because the standalone super literal is not supported, that is because in other518		// implementations `in super` treated differently from `in smth_else`.519		BinaryOp(bin)520			if matches!(&bin.rhs, Expr::Literal(LiteralType::Super))521				&& bin.op == BinaryOpType::In =>522		{523			let sup_this = ctx.try_sup_this()?;524			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.525			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.526			if !sup_this.has_super() {527				return Ok(Val::Bool(false));528			}529			let field = evaluate(ctx, &bin.lhs)?;530			Val::Bool(sup_this.field_in_super(field.to_string()?))531		}532		BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?,533		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,534		Var(name) => in_frame(535			CallLocation::new(&name.span),536			|| format!("local <{}> access", &**name),537			|| ctx.binding((**name).clone())?.evaluate(),538		)?,539		Index { indexable, parts } => ensure_sufficient_stack(|| {540			let mut parts = parts.iter();541			let mut indexable = if matches!(&**indexable, Expr::Literal(LiteralType::Super)) {542				let part = parts.next().expect("at least part should exist");543				// sup_this existence check might also be skipped here for null-coalesce...544				// But I believe this might cause errors.545				let sup_this = ctx.try_sup_this()?;546				if !sup_this.has_super() {547					#[cfg(feature = "exp-null-coaelse")]548					if part.null_coaelse {549						return Ok(Val::Null);550					}551					bail!(NoSuperFound)552				}553				let name = evaluate(ctx.clone(), &part.value)?;554555				let Val::Str(name) = name else {556					bail!(ValueIndexMustBeTypeGot(557						ValType::Obj,558						ValType::Str,559						name.value_type(),560					))561				};562563				let name = name.into_flat();564				match sup_this565					.get_super(name.clone())566					.with_description_src(&part.span, || format!("field <{name}> access"))?567				{568					Some(v) => v,569					#[cfg(feature = "exp-null-coaelse")]570					None if part.null_coaelse => return Ok(Val::Null),571					None => {572						let suggestions = suggest_object_fields(573							&sup_this.standalone_super().expect("super exists"),574							name.clone(),575						);576577						bail!(NoSuchField(name, suggestions))578					}579				}580			} else {581				evaluate(ctx.clone(), indexable)?582			};583584			for part in parts {585				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {586					(Val::Obj(v), Val::Str(key)) => match v587						.get(key.clone().into_flat())588						.with_description_src(&part.span, || format!("field <{key}> access"))?589					{590						Some(v) => v,591						#[cfg(feature = "exp-null-coaelse")]592						None if part.null_coaelse => return Ok(Val::Null),593						None => {594							let suggestions = suggest_object_fields(&v, key.into_flat());595596							return Err(Error::from(NoSuchField(597								key.clone().into_flat(),598								suggestions,599							)))600							.with_description_src(&part.span, || format!("field <{key}> access"));601						}602					},603					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(604						ValType::Obj,605						ValType::Str,606						n.value_type(),607					)),608					(Val::Arr(v), Val::Num(n)) => {609						let n = n.get();610						if n.fract() > f64::EPSILON {611							bail!(FractionalIndex)612						}613						if n < 0.0 {614							#[expect(615								clippy::cast_possible_truncation,616								reason = "it would be truncated anyway"617							)]618							let n = n as isize;619							bail!(ArrayBoundsError(n, v.len()));620						}621						#[expect(622							clippy::cast_possible_truncation,623							clippy::cast_sign_loss,624							reason = "n is checked postive"625						)]626						v.get(n as usize)?627							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?628					}629					(Val::Arr(_), Val::Str(n)) => {630						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))631					}632					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(633						ValType::Arr,634						ValType::Num,635						n.value_type(),636					)),637638					(Val::Str(s), Val::Num(n)) => Val::Str({639						let n = n.get();640						if n.fract() > f64::EPSILON {641							bail!(FractionalIndex)642						}643						if n < 0.0 {644							#[expect(645								clippy::cast_possible_truncation,646								reason = "it would be truncated anyway"647							)]648							let n = n as isize;649							bail!(ArrayBoundsError(n, s.into_flat().chars().count()));650						}651						#[expect(652							clippy::cast_sign_loss,653							clippy::cast_possible_truncation,654							reason = "n is positive, overflow will truncate as expected"655						)]656						let n = n as usize;657						let v: IStr = s658							.clone()659							.into_flat()660							.chars()661							.skip(n)662							.take(1)663							.collect::<String>()664							.into();665						if v.is_empty() {666							bail!(StringBoundsError(n, s.into_flat().chars().count()))667						}668						StrValue::Flat(v)669					}),670					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(671						ValType::Str,672						ValType::Num,673						n.value_type(),674					)),675					#[cfg(feature = "exp-null-coaelse")]676					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),677					(v, _) => bail!(CantIndexInto(v.value_type())),678				};679			}680			Ok(indexable)681		})?,682		LocalExpr(bindings, returned) => {683			let fctx = Context::new_future();684			let mut ctx = ContextBuilder::extend(ctx);685			for b in bindings {686				evaluate_dest(b, fctx.clone(), &mut ctx)?;687			}688			let ctx = ctx.build().into_future(fctx);689			evaluate(ctx, returned)?690		}691		Arr(items) => {692			if items.is_empty() {693				Val::arr(())694			} else {695				Val::Arr(ArrValue::expr(ctx, items.clone()))696			}697		}698		ArrComp(expr, comp_specs) => Val::Arr(evaluate_arr_comp(ctx, expr, comp_specs)?),699		Obj(body) => Val::Obj(evaluate_object(None, ctx, body)?),700		ObjExtend(a, b) => {701			let base = evaluate(ctx.clone(), a)?;702			match base {703				Val::Obj(base_obj) => Val::Obj(evaluate_object(Some(base_obj), ctx, b)?),704				_ => bail!("ObjExtend lhs should be an object value"),705			}706		}707		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {708			evaluate_apply(ctx, value, args, CallLocation::new(&args.span), *tailstrict)709		})?,710		Function(params, body) => {711			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())712		}713		AssertExpr(assert) => {714			evaluate_assert(ctx.clone(), &assert.assert)?;715			evaluate(ctx, &assert.rest)?716		}717		ErrorStmt(s, e) => in_frame(718			CallLocation::new(s),719			|| "error statement".to_owned(),720			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),721		)?,722		IfElse(if_else) => {723			if in_frame(724				CallLocation::new(&if_else.cond.span),725				|| "if condition".to_owned(),726				|| bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.cond)?),727			)? {728				evaluate(ctx, &if_else.cond_then)?729			} else {730				match &if_else.cond_else {731					Some(v) => evaluate(ctx, v)?,732					None => Val::Null,733				}734			}735		}736		Slice(slice) => {737			fn parse_idx<T: Typed + FromUntyped>(738				ctx: Context,739				expr: Option<&Spanned<Expr>>,740				desc: &'static str,741			) -> Result<Option<T>> {742				if let Some(value) = expr {743					Ok(in_frame(744						CallLocation::new(&value.span),745						|| format!("slice {desc}"),746						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),747					)?)748				} else {749					Ok(None)750				}751			}752753			let indexable = evaluate(ctx.clone(), &slice.value)?;754755			let start = parse_idx(ctx.clone(), slice.slice.start.as_ref(), "start")?;756			let end = parse_idx(ctx.clone(), slice.slice.end.as_ref(), "end")?;757			let step = parse_idx(ctx, slice.slice.step.as_ref(), "step")?;758759			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?760		}761		Import(kind, path) => {762			let Expr::Str(path) = &**path else {763				bail!("computed imports are not supported")764			};765			with_state(|s| {766				let span = &kind.span;767				let resolved_path = s.resolve_from(span.0.source_path(), path)?;768				Ok(match &**kind {769					ImportKind::Normal => in_frame(770						CallLocation::new(span),771						|| format!("import {:?}", path.clone()),772						|| s.import_resolved(resolved_path),773					)?,774					ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?),775					ImportKind::Bin => Val::arr(s.import_resolved_bin(resolved_path)?),776				}) as Result<Val>777			})?778		}779	})780}
modifiedcrates/jrsonnet-ir-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/lib.rs
+++ b/crates/jrsonnet-ir-parser/src/lib.rs
@@ -237,13 +237,13 @@
 
 fn assert_stmt(p: &mut Parser<'_>) -> Result<AssertStmt> {
 	p.eat(T![assert])?;
-	let cond = spanned(p, expr)?;
-	let msg = if p.try_eat(T![:]) {
-		Some(spanned(p, expr)?)
+	let assertion = spanned(p, expr)?;
+	let message = if p.try_eat(T![:]) {
+		Some(expr(p)?)
 	} else {
 		None
 	};
-	Ok(AssertStmt(cond, msg))
+	Ok(AssertStmt { assertion, message })
 }
 
 fn if_spec_data(p: &mut Parser<'_>) -> Result<IfSpecData> {
modifiedcrates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__basic_test.snapdiffbeforeafterboth
--- a/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__basic_test.snap
+++ b/crates/jrsonnet-ir-parser/src/snapshots/jrsonnet_ir_parser__tests__basic_test.snap
@@ -4,8 +4,8 @@
 ---
 AssertExpr(
     AssertExpr {
-        assert: AssertStmt(
-            Index {
+        assert: AssertStmt {
+            assertion: Index {
                 indexable: Literal(
                     True,
                 ),
@@ -18,12 +18,12 @@
                     },
                 ],
             } from virtual:<test>:7-18,
-            Some(
+            message: Some(
                 Literal(
                     False,
-                ) from virtual:<test>:21-26,
+                ),
             ),
-        ),
+        },
         rest: Literal(
             True,
         ),
modifiedcrates/jrsonnet-ir/src/expr.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir/src/expr.rs
+++ b/crates/jrsonnet-ir/src/expr.rs
@@ -38,7 +38,10 @@
 }
 
 #[derive(Debug, PartialEq, Acyclic)]
-pub struct AssertStmt(pub Spanned<Expr>, pub Option<Spanned<Expr>>);
+pub struct AssertStmt {
+	pub assertion: Spanned<Expr>,
+	pub message: Option<Expr>,
+}
 
 #[derive(Debug, PartialEq, Acyclic)]
 pub struct FieldMember {
modifiedcrates/jrsonnet-ir/src/visit.rsdiffbeforeafterboth
--- a/crates/jrsonnet-ir/src/visit.rs
+++ b/crates/jrsonnet-ir/src/visit.rs
@@ -157,10 +157,10 @@
 }
 
 pub fn visit_assert_stmt<V: Visitor>(v: &mut V, ass: &AssertStmt) {
-	let AssertStmt(cond, msg) = ass;
-	v.visit_expr(cond);
-	if let Some(msg) = msg {
-		v.visit_expr(msg);
+	let AssertStmt { assertion, message } = ass;
+	v.visit_expr(assertion);
+	if let Some(message) = message {
+		v.visit_expr(message);
 	}
 }
 pub fn visit_expr<V: Visitor>(v: &mut V, e: &Expr) {
modifiedcrates/jrsonnet-peg-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/lib.rs
+++ b/crates/jrsonnet-peg-parser/src/lib.rs
@@ -138,7 +138,7 @@
 			/ name:id() _ "(" _ params:params(s) _ ")" _ "=" _ value:expr(s) {BindSpec::Function{name, params, value: Rc::new(value)}}
 
 		pub rule assertion(s: &ParserSettings) -> AssertStmt
-			= keyword("assert") _ cond:spanned(<expr(s)>, s) msg:(_ ":" _ e:spanned(<expr(s)>, s) {e})? { AssertStmt(cond, msg) }
+			= keyword("assert") _ assertion:spanned(<expr(s)>, s) message:(_ ":" _ e:expr(s) {e})? { AssertStmt{assertion, message} }
 
 		pub rule whole_line() -> &'input str
 			= str:$((!['\n'][_])* "\n") {str}