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

difftreelog

fix align object iteration with context refactor

wnuqoyquYaroslav Bolyukin2026-04-25parent: #e9c741d.patch.diff
in: master

2 files changed

modifiedcrates/jrsonnet-evaluator/src/evaluate/destructure.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/destructure.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/destructure.rs
@@ -1,7 +1,5 @@
 use jrsonnet_ir::{BindSpec, Destruct};
 
-#[cfg(feature = "exp-preserve-order")]
-use crate::evaluate;
 use crate::{
 	Context, ContextBuilder, Pending, Thunk, Val, error::Result, evaluate_method,
 	evaluate_named_param,
@@ -25,6 +23,8 @@
 		Destruct::Array { start, rest, end } => {
 			use jrsonnet_ir::DestructRest;
 
+			use crate::bail;
+
 			let min_len = start.len() + end.len();
 			let has_rest = rest.is_some();
 			let full = Thunk!(move || {
@@ -102,7 +102,7 @@
 			use jrsonnet_ir::DestructRest;
 			use rustc_hash::FxHashSet;
 
-			use crate::ObjValueBuilder;
+			use crate::{ObjValueBuilder, bail};
 
 			let captured_fields: FxHashSet<_> = fields.iter().map(|f| f.0.clone()).collect();
 			let field_names: Vec<_> = fields
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 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}