git.delta.rocks / jrsonnet / refs/commits / 7cdcae351387

difftreelog

feat simplify Thunk creation with closure syntax

Yaroslav Bolyukin2024-08-26parent: #7d331b6.patch.diff
in: master

9 files changed

modifiedcrates/jrsonnet-evaluator/src/arr/spec.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/spec.rs
+++ b/crates/jrsonnet-evaluator/src/arr/spec.rs
@@ -7,7 +7,7 @@
 use super::ArrValue;
 use crate::{
 	error::ErrorKind::InfiniteRecursionDetected, evaluate, function::FuncVal, typed::Typed,
-	val::ThunkValue, Context, Error, ObjValue, Result, Thunk, Val,
+	Context, Error, ObjValue, Result, Thunk, Val,
 };
 
 pub trait ArrayLike: Any + Trace + Debug {
@@ -182,23 +182,6 @@
 		Ok(Some(new_value))
 	}
 	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
-		#[derive(Trace)]
-		struct ArrayElement {
-			arr_thunk: ExprArray,
-			index: usize,
-		}
-
-		impl ThunkValue for ArrayElement {
-			type Output = Val;
-
-			fn get(self: Box<Self>) -> Result<Self::Output> {
-				self.arr_thunk
-					.get(self.index)
-					.transpose()
-					.expect("index checked")
-			}
-		}
-
 		if index >= self.len() {
 			return None;
 		}
@@ -208,9 +191,9 @@
 			ArrayThunk::Waiting(_) | ArrayThunk::Pending => {}
 		};
 
-		Some(Thunk::new(ArrayElement {
-			arr_thunk: self.clone(),
-			index,
+		let arr_thunk = self.clone();
+		Some(Thunk!(move || {
+			arr_thunk.get(index).transpose().expect("index checked")
 		}))
 	}
 	fn get_cheap(&self, _index: usize) -> Option<Val> {
@@ -492,23 +475,6 @@
 		Ok(Some(new_value))
 	}
 	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
-		#[derive(Trace)]
-		struct ArrayElement<const WITH_INDEX: bool> {
-			arr_thunk: MappedArray<WITH_INDEX>,
-			index: usize,
-		}
-
-		impl<const WITH_INDEX: bool> ThunkValue for ArrayElement<WITH_INDEX> {
-			type Output = Val;
-
-			fn get(self: Box<Self>) -> Result<Self::Output> {
-				self.arr_thunk
-					.get(self.index)
-					.transpose()
-					.expect("index checked")
-			}
-		}
-
 		if index >= self.len() {
 			return None;
 		}
@@ -518,9 +484,9 @@
 			ArrayThunk::Waiting(()) | ArrayThunk::Pending => {}
 		};
 
-		Some(Thunk::new(ArrayElement {
-			arr_thunk: self.clone(),
-			index,
+		let arr_thunk = self.clone();
+		Some(Thunk!(move || {
+			arr_thunk.get(index).transpose().expect("index checked")
 		}))
 	}
 
modifiedcrates/jrsonnet-evaluator/src/evaluate/destructure.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/destructure.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/destructure.rs
@@ -1,13 +1,11 @@
-use jrsonnet_gcmodule::Trace;
 use jrsonnet_interner::IStr;
-use jrsonnet_parser::{BindSpec, Destruct, LocExpr, ParamsDesc};
+use jrsonnet_parser::{BindSpec, Destruct};
 
 use crate::{
 	bail,
 	error::{ErrorKind::*, Result},
 	evaluate, evaluate_method, evaluate_named,
 	gc::GcHashMap,
-	val::ThunkValue,
 	Context, Pending, Thunk, Val,
 };
 
@@ -31,65 +29,34 @@
 		#[cfg(feature = "exp-destruct")]
 		Destruct::Array { start, rest, end } => {
 			use jrsonnet_parser::DestructRest;
-
-			use crate::arr::ArrValue;
-
-			#[derive(Trace)]
-			struct DataThunk {
-				parent: Thunk<Val>,
-				min_len: usize,
-				has_rest: bool,
-			}
-			impl ThunkValue for DataThunk {
-				type Output = ArrValue;
 
-				fn get(self: Box<Self>) -> Result<Self::Output> {
-					let v = self.parent.evaluate()?;
-					let Val::Arr(arr) = v else {
-						bail!("expected array");
-					};
-					if !self.has_rest {
-						if arr.len() != self.min_len {
-							bail!("expected {} elements, got {}", self.min_len, arr.len())
-						}
-					} else if arr.len() < self.min_len {
-						bail!(
-							"expected at least {} elements, but array was only {}",
-							self.min_len,
-							arr.len()
-						)
+			let min_len = start.len() + end.len();
+			let has_rest = rest.is_some();
+			let full = Thunk!(move || {
+				let v = parent.evaluate()?;
+				let Val::Arr(arr) = v else {
+					bail!("expected array");
+				};
+				if !has_rest {
+					if arr.len() != min_len {
+						bail!("expected {} elements, got {}", min_len, arr.len())
 					}
-					Ok(arr)
+				} else if arr.len() < min_len {
+					bail!(
+						"expected at least {} elements, but array was only {}",
+						min_len,
+						arr.len()
+					)
 				}
-			}
-
-			let full = Thunk::new(DataThunk {
-				min_len: start.len() + end.len(),
-				has_rest: rest.is_some(),
-				parent,
+				Ok(arr)
 			});
 
 			{
-				#[derive(Trace)]
-				struct BaseThunk {
-					full: Thunk<ArrValue>,
-					index: usize,
-				}
-				impl ThunkValue for BaseThunk {
-					type Output = Val;
-
-					fn get(self: Box<Self>) -> Result<Self::Output> {
-						let full = self.full.evaluate()?;
-						Ok(full.get(self.index)?.expect("length is checked"))
-					}
-				}
 				for (i, d) in start.iter().enumerate() {
+					let full = full.clone();
 					destruct(
 						d,
-						Thunk::new(BaseThunk {
-							full: full.clone(),
-							index: i,
-						}),
+						Thunk!(move || Ok(full.evaluate()?.get(i)?.expect("length is checked"))),
 						fctx.clone(),
 						new_bindings,
 					)?;
@@ -98,32 +65,19 @@
 
 			match rest {
 				Some(DestructRest::Keep(v)) => {
-					#[derive(Trace)]
-					struct RestThunk {
-						full: Thunk<ArrValue>,
-						start: usize,
-						end: usize,
-					}
-					impl ThunkValue for RestThunk {
-						type Output = Val;
-
-						fn get(self: Box<Self>) -> Result<Self::Output> {
-							let full = self.full.evaluate()?;
-							let to = full.len() - self.end;
+					let start = start.len();
+					let end = end.len();
+					let full = full.clone();
+					destruct(
+						&Destruct::Full(v.clone()),
+						Thunk!(move || {
+							let full = full.evaluate()?;
+							let to = full.len() - end;
 							Ok(Val::Arr(full.slice(
-								Some(self.start as i32),
+								Some(start as i32),
 								Some(to as i32),
 								None,
 							)))
-						}
-					}
-
-					destruct(
-						&Destruct::Full(v.clone()),
-						Thunk::new(RestThunk {
-							full: full.clone(),
-							start: start.len(),
-							end: end.len(),
 						}),
 						fctx.clone(),
 						new_bindings,
@@ -133,29 +87,14 @@
 			}
 
 			{
-				#[derive(Trace)]
-				struct EndThunk {
-					full: Thunk<ArrValue>,
-					index: usize,
-					end: usize,
-				}
-				impl ThunkValue for EndThunk {
-					type Output = Val;
-
-					fn get(self: Box<Self>) -> Result<Self::Output> {
-						let full = self.full.evaluate()?;
-						Ok(full
-							.get(full.len() - self.end + self.index)?
-							.expect("length is checked"))
-					}
-				}
 				for (i, d) in end.iter().enumerate() {
+					let full = full.clone();
+					let end = end.len();
 					destruct(
 						d,
-						Thunk::new(EndThunk {
-							full: full.clone(),
-							index: i,
-							end: end.len(),
+						Thunk!(move || {
+							let full = full.evaluate()?;
+							Ok(full.get(full.len() - end + i)?.expect("length is checked"))
 						}),
 						fctx.clone(),
 						new_bindings,
@@ -165,71 +104,46 @@
 		}
 		#[cfg(feature = "exp-destruct")]
 		Destruct::Object { fields, rest } => {
-			use crate::obj::ObjValue;
-
-			#[derive(Trace)]
-			struct DataThunk {
-				parent: Thunk<Val>,
-				field_names: Vec<(IStr, bool)>,
-				has_rest: bool,
-			}
-			impl ThunkValue for DataThunk {
-				type Output = ObjValue;
-
-				fn get(self: Box<Self>) -> Result<Self::Output> {
-					let v = self.parent.evaluate()?;
-					let Val::Obj(obj) = v else {
-						bail!("expected object");
-					};
-					for (field, has_default) in &self.field_names {
-						if !has_default && !obj.has_field_ex(field.clone(), true) {
-							bail!("missing field: {field}");
-						}
-					}
-					if !self.has_rest {
-						let len = obj.len();
-						if len > self.field_names.len() {
-							bail!("too many fields, and rest not found");
-						}
-					}
-					Ok(obj)
-				}
-			}
 			let field_names: Vec<_> = fields
 				.iter()
 				.map(|f| (f.0.clone(), f.2.is_some()))
 				.collect();
-			let full = Thunk::new(DataThunk {
-				parent,
-				field_names,
-				has_rest: rest.is_some(),
+			let has_rest = rest.is_some();
+			let full = Thunk!(move || {
+				let v = parent.evaluate()?;
+				let Val::Obj(obj) = v else {
+					bail!("expected object");
+				};
+				for (field, has_default) in &field_names {
+					if !has_default && !obj.has_field_ex(field.clone(), true) {
+						bail!("missing field: {field}");
+					}
+				}
+				if !has_rest {
+					let len = obj.len();
+					if len > field_names.len() {
+						bail!("too many fields, and rest not found");
+					}
+				}
+				Ok(obj)
 			});
 
 			for (field, d, default) in fields {
-				#[derive(Trace)]
-				struct FieldThunk {
-					full: Thunk<ObjValue>,
-					field: IStr,
-					default: Option<(Pending<Context>, LocExpr)>,
-				}
-				impl ThunkValue for FieldThunk {
-					type Output = Val;
-
-					fn get(self: Box<Self>) -> Result<Self::Output> {
-						let full = self.full.evaluate()?;
-						if let Some(field) = full.get(self.field)? {
+				let default = default.clone().map(|e| (fctx.clone(), e));
+				let value = {
+					let field = field.clone();
+					let full = full.clone();
+					Thunk!(move || {
+						let full = full.evaluate()?;
+						if let Some(field) = full.get(field)? {
 							Ok(field)
 						} else {
-							let (fctx, expr) = self.default.as_ref().expect("shape is checked");
+							let (fctx, expr) = default.as_ref().expect("shape is checked");
 							Ok(evaluate(fctx.clone().unwrap(), expr)?)
 						}
-					}
-				}
-				let value = Thunk::new(FieldThunk {
-					full: full.clone(),
-					field: field.clone(),
-					default: default.clone().map(|e| (fctx.clone(), e)),
-				});
+					})
+				};
+
 				if let Some(d) = d {
 					destruct(d, value, fctx.clone(), new_bindings)?;
 				} else {
@@ -253,26 +167,15 @@
 ) -> Result<()> {
 	match d {
 		BindSpec::Field { into, value } => {
-			#[derive(Trace)]
-			struct EvaluateThunkValue {
-				name: Option<IStr>,
-				fctx: Pending<Context>,
-				expr: LocExpr,
-			}
-			impl ThunkValue for EvaluateThunkValue {
-				type Output = Val;
-				fn get(self: Box<Self>) -> Result<Self::Output> {
-					self.name.map_or_else(
-						|| evaluate(self.fctx.unwrap(), &self.expr),
-						|name| evaluate_named(self.fctx.unwrap(), &self.expr, name),
-					)
-				}
-			}
-			let data = Thunk::new(EvaluateThunkValue {
-				name: into.name(),
-				fctx: fctx.clone(),
-				expr: value.clone(),
-			});
+			let name = into.name();
+			let value = value.clone();
+			let data = {
+				let fctx = fctx.clone();
+				Thunk!(move || name.map_or_else(
+					|| evaluate(fctx.unwrap(), &value),
+					|name| evaluate_named(fctx.unwrap(), &value, name),
+				))
+			};
 			destruct(into, data, fctx, new_bindings)?;
 		}
 		BindSpec::Function {
@@ -280,37 +183,15 @@
 			params,
 			value,
 		} => {
-			#[derive(Trace)]
-			struct MethodThunk {
-				fctx: Pending<Context>,
-				name: IStr,
-				params: ParamsDesc,
-				value: LocExpr,
-			}
-			impl ThunkValue for MethodThunk {
-				type Output = Val;
-
-				fn get(self: Box<Self>) -> Result<Self::Output> {
-					Ok(evaluate_method(
-						self.fctx.unwrap(),
-						self.name,
-						self.params,
-						self.value,
-					))
-				}
-			}
-
-			let old = new_bindings.insert(
-				name.clone(),
-				Thunk::new(MethodThunk {
-					fctx,
-					name: name.clone(),
-					params: params.clone(),
-					value: value.clone(),
-				}),
-			);
+			let params = params.clone();
+			let name = name.clone();
+			let value = value.clone();
+			let old = new_bindings.insert(name.clone(), {
+				let name = name.clone();
+				Thunk!(move || Ok(evaluate_method(fctx.unwrap(), name, params, value)))
+			});
 			if old.is_some() {
-				bail!(DuplicateLocalVar(name.clone()))
+				bail!(DuplicateLocalVar(name))
 			}
 		}
 	}
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_parser::{6	ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, FieldMember, FieldName,7	ForSpecData, IfSpecData, LiteralType, LocExpr, Member, ObjBody, ParamsDesc,8};9use jrsonnet_types::ValType;1011use self::destructure::destruct;12use crate::{13	arr::ArrValue,14	bail,15	destructure::evaluate_dest,16	error::{suggest_object_fields, ErrorKind::*},17	evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},18	function::{CallLocation, FuncDesc, FuncVal},19	in_frame,20	typed::Typed,21	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk, ThunkValue},22	Context, Error, GcHashMap, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,23	ResultExt, Unbound, Val,24};25pub mod destructure;26pub mod operator;2728// This is the amount of bytes that need to be left on the stack before increasing the size.29// It must be at least as large as the stack required by any code that does not call30// `ensure_sufficient_stack`.31const RED_ZONE: usize = 100 * 1024; // 100k3233// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then34// on. This flag has performance relevant characteristics. Don't set it too high.35const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB3637/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations38/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit39/// from this.40///41/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.42#[inline]43pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {44	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)45}4647pub fn evaluate_trivial(expr: &LocExpr) -> Option<Val> {48	fn is_trivial(expr: &LocExpr) -> bool {49		match expr.expr() {50			Expr::Str(_)51			| Expr::Num(_)52			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,53			Expr::Arr(a) => a.iter().all(is_trivial),54			Expr::Parened(e) => is_trivial(e),55			_ => false,56		}57	}58	Some(match expr.expr() {59		Expr::Str(s) => Val::string(s.clone()),60		Expr::Num(n) => {61			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))62		}63		Expr::Literal(LiteralType::False) => Val::Bool(false),64		Expr::Literal(LiteralType::True) => Val::Bool(true),65		Expr::Literal(LiteralType::Null) => Val::Null,66		Expr::Arr(n) => {67			if n.iter().any(|e| !is_trivial(e)) {68				return None;69			}70			Val::Arr(ArrValue::eager(71				n.iter()72					.map(evaluate_trivial)73					.map(|e| e.expect("checked trivial"))74					.collect(),75			))76		}77		Expr::Parened(e) => evaluate_trivial(e)?,78		_ => return None,79	})80}8182pub fn evaluate_method(ctx: Context, name: IStr, params: ParamsDesc, body: LocExpr) -> 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: &FieldName) -> Result<Option<IStr>> {92	Ok(match field_name {93		FieldName::Fixed(n) => Some(n.clone()),94		FieldName::Dyn(expr) => in_frame(95			CallLocation::new(&expr.span()),96			|| "evaluating field name".to_string(),97			|| {98				let value = evaluate(ctx, expr)?;99				if matches!(value, Val::Null) {100					Ok(None)101				} else {102					Ok(Some(IStr::from_untyped(value)?))103				}104			},105		)?,106	})107}108109pub fn evaluate_comp(110	ctx: Context,111	specs: &[CompSpec],112	callback: &mut impl FnMut(Context) -> Result<()>,113) -> Result<()> {114	match specs.first() {115		None => callback(ctx)?,116		Some(CompSpec::IfSpec(IfSpecData(cond))) => {117			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {118				evaluate_comp(ctx, &specs[1..], callback)?;119			}120		}121		Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {122			Val::Arr(list) => {123				for item in list.iter_lazy() {124					let fctx = Pending::new();125					let mut new_bindings = GcHashMap::with_capacity(var.capacity_hint());126					destruct(var, item, fctx.clone(), &mut new_bindings)?;127					let ctx = ctx128						.clone()129						.extend(new_bindings, None, None, None)130						.into_future(fctx);131132					evaluate_comp(ctx, &specs[1..], callback)?;133				}134			}135			#[cfg(feature = "exp-object-iteration")]136			Val::Obj(obj) => {137				for field in obj.fields(138					// TODO: Should there be ability to preserve iteration order?139					#[cfg(feature = "exp-preserve-order")]140					false,141				) {142					#[derive(Trace)]143					struct ObjectFieldThunk {144						obj: ObjValue,145						field: IStr,146					}147					impl ThunkValue for ObjectFieldThunk {148						type Output = Val;149150						fn get(self: Box<Self>) -> Result<Self::Output> {151							self.obj.get(self.field).transpose().expect(152								"field exists, as field name was obtained from object.fields()",153							)154						}155					}156157					let fctx = Pending::new();158					let mut new_bindings = GcHashMap::with_capacity(var.capacity_hint());159					let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![160						Thunk::evaluated(Val::string(field.clone())),161						Thunk::new(ObjectFieldThunk {162							field: field.clone(),163							obj: obj.clone(),164						}),165					])));166					destruct(var, value, fctx.clone(), &mut new_bindings)?;167					let ctx = ctx168						.clone()169						.extend(new_bindings, None, None, None)170						.into_future(fctx);171172					evaluate_comp(ctx, &specs[1..], callback)?;173				}174			}175			_ => bail!(InComprehensionCanOnlyIterateOverArray),176		},177	}178	Ok(())179}180181trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}182impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}183184fn evaluate_object_locals(185	fctx: Pending<Context>,186	locals: Rc<Vec<BindSpec>>,187) -> impl CloneableUnbound<Context> {188	#[derive(Trace, Clone)]189	struct UnboundLocals {190		fctx: Pending<Context>,191		locals: Rc<Vec<BindSpec>>,192	}193	impl Unbound for UnboundLocals {194		type Bound = Context;195196		fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Context> {197			let fctx = Context::new_future();198			let mut new_bindings =199				GcHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());200			for b in self.locals.iter() {201				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;202			}203204			let ctx = self.fctx.unwrap();205			let new_dollar = ctx.dollar().cloned().or_else(|| this.clone());206207			let ctx = ctx208				.extend(new_bindings, new_dollar, sup, this)209				.into_future(fctx);210211			Ok(ctx)212		}213	}214215	UnboundLocals { fctx, locals }216}217218pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(219	builder: &mut ObjValueBuilder,220	ctx: Context,221	uctx: B,222	field: &FieldMember,223) -> Result<()> {224	let name = evaluate_field_name(ctx, &field.name)?;225	let Some(name) = name else {226		return Ok(());227	};228229	match field {230		FieldMember {231			plus,232			params: None,233			visibility,234			value,235			..236		} => {237			#[derive(Trace)]238			struct UnboundValue<B: Trace> {239				uctx: B,240				value: LocExpr,241				name: IStr,242			}243			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {244				type Bound = Val;245				fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {246					evaluate_named(self.uctx.bind(sup, this)?, &self.value, self.name.clone())247				}248			}249250			builder251				.field(name.clone())252				.with_add(*plus)253				.with_visibility(*visibility)254				.with_location(value.span())255				.bindable(UnboundValue {256					uctx,257					value: value.clone(),258					name,259				})?;260		}261		FieldMember {262			params: Some(params),263			visibility,264			value,265			..266		} => {267			#[derive(Trace)]268			struct UnboundMethod<B: Trace> {269				uctx: B,270				value: LocExpr,271				params: ParamsDesc,272				name: IStr,273			}274			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {275				type Bound = Val;276				fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {277					Ok(evaluate_method(278						self.uctx.bind(sup, this)?,279						self.name.clone(),280						self.params.clone(),281						self.value.clone(),282					))283				}284			}285286			builder287				.field(name.clone())288				.with_visibility(*visibility)289				.with_location(value.span())290				.bindable(UnboundMethod {291					uctx,292					value: value.clone(),293					params: params.clone(),294					name,295				})?;296		}297	}298	Ok(())299}300301#[allow(clippy::too_many_lines)]302pub fn evaluate_member_list_object(ctx: Context, members: &[Member]) -> Result<ObjValue> {303	let mut builder = ObjValueBuilder::new();304	let locals = Rc::new(305		members306			.iter()307			.filter_map(|m| match m {308				Member::BindStmt(bind) => Some(bind.clone()),309				_ => None,310			})311			.collect::<Vec<_>>(),312	);313314	let fctx = Context::new_future();315316	// We have single context for all fields, so we can cache binds317	let uctx = CachedUnbound::new(evaluate_object_locals(fctx.clone(), locals));318319	for member in members {320		match member {321			Member::Field(field) => {322				evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;323			}324			Member::AssertStmt(stmt) => {325				#[derive(Trace)]326				struct ObjectAssert<B: Trace> {327					uctx: B,328					assert: AssertStmt,329				}330				impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {331					fn run(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<()> {332						let ctx = self.uctx.bind(sup, this)?;333						evaluate_assert(ctx, &self.assert)334					}335				}336				builder.assert(ObjectAssert {337					uctx: uctx.clone(),338					assert: stmt.clone(),339				});340			}341			Member::BindStmt(_) => {342				// Already handled343			}344		}345	}346	let this = builder.build();347	fctx.fill(ctx.extend(GcHashMap::new(), None, None, Some(this.clone())));348	Ok(this)349}350351pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {352	Ok(match object {353		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,354		ObjBody::ObjComp(obj) => {355			let mut builder = ObjValueBuilder::new();356			let locals = Rc::new(357				obj.pre_locals358					.iter()359					.chain(obj.post_locals.iter())360					.cloned()361					.collect::<Vec<_>>(),362			);363			let mut ctxs = vec![];364			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {365				let fctx = Context::new_future();366				ctxs.push((ctx.clone(), fctx.clone()));367				let uctx = evaluate_object_locals(fctx, locals.clone());368369				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)370			})?;371372			let this = builder.build();373			for (ctx, fctx) in ctxs {374				let _ctx = ctx375					.extend(GcHashMap::new(), None, None, Some(this.clone()))376					.into_future(fctx);377			}378			this379		}380	})381}382383pub fn evaluate_apply(384	ctx: Context,385	value: &LocExpr,386	args: &ArgsDesc,387	loc: CallLocation<'_>,388	tailstrict: bool,389) -> Result<Val> {390	let value = evaluate(ctx.clone(), value)?;391	Ok(match value {392		Val::Func(f) => {393			let body = || f.evaluate(ctx, loc, args, tailstrict);394			if tailstrict {395				body()?396			} else {397				in_frame(loc, || format!("function <{}> call", f.name()), body)?398			}399		}400		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),401	})402}403404pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {405	let value = &assertion.0;406	let msg = &assertion.1;407	let assertion_result = in_frame(408		CallLocation::new(&value.span()),409		|| "assertion condition".to_owned(),410		|| bool::from_untyped(evaluate(ctx.clone(), value)?),411	)?;412	if !assertion_result {413		in_frame(414			CallLocation::new(&value.span()),415			|| "assertion failure".to_owned(),416			|| {417				if let Some(msg) = msg {418					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));419				}420				bail!(AssertionFailed(Val::Null.to_string()?));421			},422		)?;423	}424	Ok(())425}426427pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result<Val> {428	use Expr::*;429	Ok(match expr.expr() {430		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),431		_ => evaluate(ctx, expr)?,432	})433}434435#[allow(clippy::too_many_lines)]436pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result<Val> {437	use Expr::*;438439	if let Some(trivial) = evaluate_trivial(expr) {440		return Ok(trivial);441	}442	let loc = expr.span();443	Ok(match expr.expr() {444		Literal(LiteralType::This) => {445			Val::Obj(ctx.this().ok_or(CantUseSelfOutsideOfObject)?.clone())446		}447		Literal(LiteralType::Super) => Val::Obj(448			ctx.super_obj().ok_or(NoSuperFound)?.with_this(449				ctx.this()450					.expect("if super exists - then this should too")451					.clone(),452			),453		),454		Literal(LiteralType::Dollar) => {455			Val::Obj(ctx.dollar().ok_or(NoTopLevelObjectFound)?.clone())456		}457		Literal(LiteralType::True) => Val::Bool(true),458		Literal(LiteralType::False) => Val::Bool(false),459		Literal(LiteralType::Null) => Val::Null,460		Parened(e) => evaluate(ctx, e)?,461		Str(v) => Val::string(v.clone()),462		Num(v) => Val::try_num(*v)?,463		// I have tried to remove special behavior from super by implementing standalone-super464		// expresion, but looks like this case still needs special treatment.465		//466		// Note that other jsonnet implementations will fail on `if value in (super)` expression,467		// because the standalone super literal is not supported, that is because in other468		// implementations `in super` treated differently from in `smth_else`.469		BinaryOp(field, BinaryOpType::In, e)470			if matches!(e.expr(), Expr::Literal(LiteralType::Super)) =>471		{472			let Some(super_obj) = ctx.super_obj() else {473				return Ok(Val::Bool(false));474			};475			let field = evaluate(ctx.clone(), field)?;476			Val::Bool(super_obj.has_field_ex(field.to_string()?, true))477		}478		BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,479		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,480		Var(name) => in_frame(481			CallLocation::new(&loc),482			|| format!("variable <{name}> access"),483			|| ctx.binding(name.clone())?.evaluate(),484		)?,485		Index { indexable, parts } => ensure_sufficient_stack(|| {486			let mut parts = parts.iter();487			let mut indexable = if matches!(indexable.expr(), Expr::Literal(LiteralType::Super)) {488				let part = parts.next().expect("at least part should exist");489				let Some(super_obj) = ctx.super_obj() else {490					#[cfg(feature = "exp-null-coaelse")]491					if part.null_coaelse {492						return Ok(Val::Null);493					}494					bail!(NoSuperFound)495				};496				let name = evaluate(ctx.clone(), &part.value)?;497498				let Val::Str(name) = name else {499					bail!(ValueIndexMustBeTypeGot(500						ValType::Obj,501						ValType::Str,502						name.value_type(),503					))504				};505506				let this = ctx507					.this()508					.expect("no this found, while super present, should not happen");509				let name = name.into_flat();510				match super_obj511					.get_for(name.clone(), this.clone())512					.with_description_src(&part.value, || format!("field <{name}> access"))?513				{514					Some(v) => v,515					#[cfg(feature = "exp-null-coaelse")]516					None if part.null_coaelse => return Ok(Val::Null),517					None => {518						let suggestions = suggest_object_fields(super_obj, name.clone());519520						bail!(NoSuchField(name, suggestions))521					}522				}523			} else {524				evaluate(ctx.clone(), indexable)?525			};526527			for part in parts {528				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {529					(Val::Obj(v), Val::Str(key)) => match v530						.get(key.clone().into_flat())531						.with_description_src(&part.value, || format!("field <{key}> access"))?532					{533						Some(v) => v,534						#[cfg(feature = "exp-null-coaelse")]535						None if part.null_coaelse => return Ok(Val::Null),536						None => {537							let suggestions = suggest_object_fields(&v, key.clone().into_flat());538539							return Err(Error::from(NoSuchField(540								key.clone().into_flat(),541								suggestions,542							)))543							.with_description_src(&part.value, || format!("field <{key}> access"));544						}545					},546					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(547						ValType::Obj,548						ValType::Str,549						n.value_type(),550					)),551					(Val::Arr(v), Val::Num(n)) => {552						let n = n.get();553						if n.fract() > f64::EPSILON {554							bail!(FractionalIndex)555						}556						if n < 0.0 {557							bail!(ArrayBoundsError(n as isize, v.len()));558						}559						v.get(n as usize)?560							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?561					}562					(Val::Arr(_), Val::Str(n)) => {563						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))564					}565					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(566						ValType::Arr,567						ValType::Num,568						n.value_type(),569					)),570571					(Val::Str(s), Val::Num(n)) => Val::Str({572						let v: IStr = s573							.clone()574							.into_flat()575							.chars()576							.skip(n.get() as usize)577							.take(1)578							.collect::<String>()579							.into();580						if v.is_empty() {581							let size = s.into_flat().chars().count();582							bail!(StringBoundsError(n.get() as usize, size))583						}584						StrValue::Flat(v)585					}),586					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(587						ValType::Str,588						ValType::Num,589						n.value_type(),590					)),591					#[cfg(feature = "exp-null-coaelse")]592					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),593					(v, _) => bail!(CantIndexInto(v.value_type())),594				};595			}596			Ok(indexable)597		})?,598		LocalExpr(bindings, returned) => {599			let mut new_bindings: GcHashMap<IStr, Thunk<Val>> =600				GcHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());601			let fctx = Context::new_future();602			for b in bindings {603				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;604			}605			let ctx = ctx.extend(new_bindings, None, None, None).into_future(fctx);606			evaluate(ctx, &returned.clone())?607		}608		Arr(items) => {609			if items.is_empty() {610				Val::Arr(ArrValue::empty())611			} else if items.len() == 1 {612				#[derive(Trace)]613				struct ArrayElement {614					ctx: Context,615					item: LocExpr,616				}617				impl ThunkValue for ArrayElement {618					type Output = Val;619					fn get(self: Box<Self>) -> Result<Val> {620						evaluate(self.ctx, &self.item)621					}622				}623				Val::Arr(ArrValue::lazy(vec![Thunk::new(ArrayElement {624					ctx,625					item: items[0].clone(),626				})]))627			} else {628				Val::Arr(ArrValue::expr(ctx, items.iter().cloned()))629			}630		}631		ArrComp(expr, comp_specs) => {632			let mut out = Vec::new();633			evaluate_comp(ctx, comp_specs, &mut |ctx| {634				#[derive(Trace)]635				struct EvaluateThunk {636					ctx: Context,637					expr: LocExpr,638				}639				impl ThunkValue for EvaluateThunk {640					type Output = Val;641					fn get(self: Box<Self>) -> Result<Val> {642						evaluate(self.ctx, &self.expr)643					}644				}645				out.push(Thunk::new(EvaluateThunk {646					ctx,647					expr: expr.clone(),648				}));649				Ok(())650			})?;651			Val::Arr(ArrValue::lazy(out))652		}653		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),654		ObjExtend(a, b) => evaluate_add_op(655			&evaluate(ctx.clone(), a)?,656			&Val::Obj(evaluate_object(ctx, b)?),657		)?,658		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {659			evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)660		})?,661		Function(params, body) => {662			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())663		}664		AssertExpr(assert, returned) => {665			evaluate_assert(ctx.clone(), assert)?;666			evaluate(ctx, returned)?667		}668		ErrorStmt(e) => in_frame(669			CallLocation::new(&loc),670			|| "error statement".to_owned(),671			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),672		)?,673		IfElse {674			cond,675			cond_then,676			cond_else,677		} => {678			if in_frame(679				CallLocation::new(&loc),680				|| "if condition".to_owned(),681				|| bool::from_untyped(evaluate(ctx.clone(), &cond.0)?),682			)? {683				evaluate(ctx, cond_then)?684			} else {685				match cond_else {686					Some(v) => evaluate(ctx, v)?,687					None => Val::Null,688				}689			}690		}691		Slice(value, desc) => {692			fn parse_idx<T: Typed>(693				loc: CallLocation<'_>,694				ctx: &Context,695				expr: Option<&LocExpr>,696				desc: &'static str,697			) -> Result<Option<T>> {698				if let Some(value) = expr {699					Ok(Some(in_frame(700						loc,701						|| format!("slice {desc}"),702						|| T::from_untyped(evaluate(ctx.clone(), value)?),703					)?))704				} else {705					Ok(None)706				}707			}708709			let indexable = evaluate(ctx.clone(), value)?;710			let loc = CallLocation::new(&loc);711712			let start = parse_idx(loc, &ctx, desc.start.as_ref(), "start")?;713			let end = parse_idx(loc, &ctx, desc.end.as_ref(), "end")?;714			let step = parse_idx(loc, &ctx, desc.step.as_ref(), "step")?;715716			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?717		}718		i @ (Import(path) | ImportStr(path) | ImportBin(path)) => {719			let Expr::Str(path) = &path.expr() else {720				bail!("computed imports are not supported")721			};722			let tmp = loc.clone().0;723			let s = ctx.state();724			let resolved_path = s.resolve_from(tmp.source_path(), path as &str)?;725			match i {726				Import(_) => in_frame(727					CallLocation::new(&loc),728					|| format!("import {:?}", path.clone()),729					|| s.import_resolved(resolved_path),730				)?,731				ImportStr(_) => Val::string(s.import_resolved_str(resolved_path)?),732				ImportBin(_) => Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?)),733				_ => unreachable!(),734			}735		}736	})737}
after · crates/jrsonnet-evaluator/src/evaluate/mod.rs
1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_parser::{6	ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, FieldMember, FieldName,7	ForSpecData, IfSpecData, LiteralType, LocExpr, Member, ObjBody, ParamsDesc,8};9use jrsonnet_types::ValType;1011use self::destructure::destruct;12use crate::{13	arr::ArrValue,14	bail,15	destructure::evaluate_dest,16	error::{suggest_object_fields, ErrorKind::*},17	evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},18	function::{CallLocation, FuncDesc, FuncVal},19	in_frame,20	typed::Typed,21	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},22	Context, Error, GcHashMap, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,23	ResultExt, Unbound, Val,24};25pub mod destructure;26pub mod operator;2728// This is the amount of bytes that need to be left on the stack before increasing the size.29// It must be at least as large as the stack required by any code that does not call30// `ensure_sufficient_stack`.31const RED_ZONE: usize = 100 * 1024; // 100k3233// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then34// on. This flag has performance relevant characteristics. Don't set it too high.35const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB3637/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations38/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit39/// from this.40///41/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.42#[inline]43pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {44	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)45}4647pub fn evaluate_trivial(expr: &LocExpr) -> Option<Val> {48	fn is_trivial(expr: &LocExpr) -> bool {49		match expr.expr() {50			Expr::Str(_)51			| Expr::Num(_)52			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,53			Expr::Arr(a) => a.iter().all(is_trivial),54			Expr::Parened(e) => is_trivial(e),55			_ => false,56		}57	}58	Some(match expr.expr() {59		Expr::Str(s) => Val::string(s.clone()),60		Expr::Num(n) => {61			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))62		}63		Expr::Literal(LiteralType::False) => Val::Bool(false),64		Expr::Literal(LiteralType::True) => Val::Bool(true),65		Expr::Literal(LiteralType::Null) => Val::Null,66		Expr::Arr(n) => {67			if n.iter().any(|e| !is_trivial(e)) {68				return None;69			}70			Val::Arr(ArrValue::eager(71				n.iter()72					.map(evaluate_trivial)73					.map(|e| e.expect("checked trivial"))74					.collect(),75			))76		}77		Expr::Parened(e) => evaluate_trivial(e)?,78		_ => return None,79	})80}8182pub fn evaluate_method(ctx: Context, name: IStr, params: ParamsDesc, body: LocExpr) -> 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: &FieldName) -> Result<Option<IStr>> {92	Ok(match field_name {93		FieldName::Fixed(n) => Some(n.clone()),94		FieldName::Dyn(expr) => in_frame(95			CallLocation::new(&expr.span()),96			|| "evaluating field name".to_string(),97			|| {98				let value = evaluate(ctx, expr)?;99				if matches!(value, Val::Null) {100					Ok(None)101				} else {102					Ok(Some(IStr::from_untyped(value)?))103				}104			},105		)?,106	})107}108109pub fn evaluate_comp(110	ctx: Context,111	specs: &[CompSpec],112	callback: &mut impl FnMut(Context) -> Result<()>,113) -> Result<()> {114	match specs.first() {115		None => callback(ctx)?,116		Some(CompSpec::IfSpec(IfSpecData(cond))) => {117			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {118				evaluate_comp(ctx, &specs[1..], callback)?;119			}120		}121		Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {122			Val::Arr(list) => {123				for item in list.iter_lazy() {124					let fctx = Pending::new();125					let mut new_bindings = GcHashMap::with_capacity(var.capacity_hint());126					destruct(var, item, fctx.clone(), &mut new_bindings)?;127					let ctx = ctx128						.clone()129						.extend(new_bindings, None, None, None)130						.into_future(fctx);131132					evaluate_comp(ctx, &specs[1..], callback)?;133				}134			}135			#[cfg(feature = "exp-object-iteration")]136			Val::Obj(obj) => {137				for field in obj.fields(138					// TODO: Should there be ability to preserve iteration order?139					#[cfg(feature = "exp-preserve-order")]140					false,141				) {142					let fctx = Pending::new();143					let mut new_bindings = GcHashMap::with_capacity(var.capacity_hint());144					let obj = obj.clone();145					let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![146						Thunk::evaluated(Val::string(field.clone())),147						Thunk!(move || obj.get(field).transpose().expect(148							"field exists, as field name was obtained from object.fields()",149						)),150					])));151					destruct(var, value, fctx.clone(), &mut new_bindings)?;152					let ctx = ctx153						.clone()154						.extend(new_bindings, None, None, None)155						.into_future(fctx);156157					evaluate_comp(ctx, &specs[1..], callback)?;158				}159			}160			_ => bail!(InComprehensionCanOnlyIterateOverArray),161		},162	}163	Ok(())164}165166trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}167impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}168169fn evaluate_object_locals(170	fctx: Pending<Context>,171	locals: Rc<Vec<BindSpec>>,172) -> impl CloneableUnbound<Context> {173	#[derive(Trace, Clone)]174	struct UnboundLocals {175		fctx: Pending<Context>,176		locals: Rc<Vec<BindSpec>>,177	}178	impl Unbound for UnboundLocals {179		type Bound = Context;180181		fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Context> {182			let fctx = Context::new_future();183			let mut new_bindings =184				GcHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());185			for b in self.locals.iter() {186				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;187			}188189			let ctx = self.fctx.unwrap();190			let new_dollar = ctx.dollar().cloned().or_else(|| this.clone());191192			let ctx = ctx193				.extend(new_bindings, new_dollar, sup, this)194				.into_future(fctx);195196			Ok(ctx)197		}198	}199200	UnboundLocals { fctx, locals }201}202203pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(204	builder: &mut ObjValueBuilder,205	ctx: Context,206	uctx: B,207	field: &FieldMember,208) -> Result<()> {209	let name = evaluate_field_name(ctx, &field.name)?;210	let Some(name) = name else {211		return Ok(());212	};213214	match field {215		FieldMember {216			plus,217			params: None,218			visibility,219			value,220			..221		} => {222			#[derive(Trace)]223			struct UnboundValue<B: Trace> {224				uctx: B,225				value: LocExpr,226				name: IStr,227			}228			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {229				type Bound = Val;230				fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {231					evaluate_named(self.uctx.bind(sup, this)?, &self.value, self.name.clone())232				}233			}234235			builder236				.field(name.clone())237				.with_add(*plus)238				.with_visibility(*visibility)239				.with_location(value.span())240				.bindable(UnboundValue {241					uctx,242					value: value.clone(),243					name,244				})?;245		}246		FieldMember {247			params: Some(params),248			visibility,249			value,250			..251		} => {252			#[derive(Trace)]253			struct UnboundMethod<B: Trace> {254				uctx: B,255				value: LocExpr,256				params: ParamsDesc,257				name: IStr,258			}259			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {260				type Bound = Val;261				fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {262					Ok(evaluate_method(263						self.uctx.bind(sup, this)?,264						self.name.clone(),265						self.params.clone(),266						self.value.clone(),267					))268				}269			}270271			builder272				.field(name.clone())273				.with_visibility(*visibility)274				.with_location(value.span())275				.bindable(UnboundMethod {276					uctx,277					value: value.clone(),278					params: params.clone(),279					name,280				})?;281		}282	}283	Ok(())284}285286#[allow(clippy::too_many_lines)]287pub fn evaluate_member_list_object(ctx: Context, members: &[Member]) -> Result<ObjValue> {288	let mut builder = ObjValueBuilder::new();289	let locals = Rc::new(290		members291			.iter()292			.filter_map(|m| match m {293				Member::BindStmt(bind) => Some(bind.clone()),294				_ => None,295			})296			.collect::<Vec<_>>(),297	);298299	let fctx = Context::new_future();300301	// We have single context for all fields, so we can cache binds302	let uctx = CachedUnbound::new(evaluate_object_locals(fctx.clone(), locals));303304	for member in members {305		match member {306			Member::Field(field) => {307				evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;308			}309			Member::AssertStmt(stmt) => {310				#[derive(Trace)]311				struct ObjectAssert<B: Trace> {312					uctx: B,313					assert: AssertStmt,314				}315				impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {316					fn run(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<()> {317						let ctx = self.uctx.bind(sup, this)?;318						evaluate_assert(ctx, &self.assert)319					}320				}321				builder.assert(ObjectAssert {322					uctx: uctx.clone(),323					assert: stmt.clone(),324				});325			}326			Member::BindStmt(_) => {327				// Already handled328			}329		}330	}331	let this = builder.build();332	fctx.fill(ctx.extend(GcHashMap::new(), None, None, Some(this.clone())));333	Ok(this)334}335336pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {337	Ok(match object {338		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,339		ObjBody::ObjComp(obj) => {340			let mut builder = ObjValueBuilder::new();341			let locals = Rc::new(342				obj.pre_locals343					.iter()344					.chain(obj.post_locals.iter())345					.cloned()346					.collect::<Vec<_>>(),347			);348			let mut ctxs = vec![];349			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {350				let fctx = Context::new_future();351				ctxs.push((ctx.clone(), fctx.clone()));352				let uctx = evaluate_object_locals(fctx, locals.clone());353354				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)355			})?;356357			let this = builder.build();358			for (ctx, fctx) in ctxs {359				let _ctx = ctx360					.extend(GcHashMap::new(), None, None, Some(this.clone()))361					.into_future(fctx);362			}363			this364		}365	})366}367368pub fn evaluate_apply(369	ctx: Context,370	value: &LocExpr,371	args: &ArgsDesc,372	loc: CallLocation<'_>,373	tailstrict: bool,374) -> Result<Val> {375	let value = evaluate(ctx.clone(), value)?;376	Ok(match value {377		Val::Func(f) => {378			let body = || f.evaluate(ctx, loc, args, tailstrict);379			if tailstrict {380				body()?381			} else {382				in_frame(loc, || format!("function <{}> call", f.name()), body)?383			}384		}385		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),386	})387}388389pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {390	let value = &assertion.0;391	let msg = &assertion.1;392	let assertion_result = in_frame(393		CallLocation::new(&value.span()),394		|| "assertion condition".to_owned(),395		|| bool::from_untyped(evaluate(ctx.clone(), value)?),396	)?;397	if !assertion_result {398		in_frame(399			CallLocation::new(&value.span()),400			|| "assertion failure".to_owned(),401			|| {402				if let Some(msg) = msg {403					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));404				}405				bail!(AssertionFailed(Val::Null.to_string()?));406			},407		)?;408	}409	Ok(())410}411412pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result<Val> {413	use Expr::*;414	Ok(match expr.expr() {415		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),416		_ => evaluate(ctx, expr)?,417	})418}419420#[allow(clippy::too_many_lines)]421pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result<Val> {422	use Expr::*;423424	if let Some(trivial) = evaluate_trivial(expr) {425		return Ok(trivial);426	}427	let loc = expr.span();428	Ok(match expr.expr() {429		Literal(LiteralType::This) => {430			Val::Obj(ctx.this().ok_or(CantUseSelfOutsideOfObject)?.clone())431		}432		Literal(LiteralType::Super) => Val::Obj(433			ctx.super_obj().ok_or(NoSuperFound)?.with_this(434				ctx.this()435					.expect("if super exists - then this should too")436					.clone(),437			),438		),439		Literal(LiteralType::Dollar) => {440			Val::Obj(ctx.dollar().ok_or(NoTopLevelObjectFound)?.clone())441		}442		Literal(LiteralType::True) => Val::Bool(true),443		Literal(LiteralType::False) => Val::Bool(false),444		Literal(LiteralType::Null) => Val::Null,445		Parened(e) => evaluate(ctx, e)?,446		Str(v) => Val::string(v.clone()),447		Num(v) => Val::try_num(*v)?,448		// I have tried to remove special behavior from super by implementing standalone-super449		// expresion, but looks like this case still needs special treatment.450		//451		// Note that other jsonnet implementations will fail on `if value in (super)` expression,452		// because the standalone super literal is not supported, that is because in other453		// implementations `in super` treated differently from in `smth_else`.454		BinaryOp(field, BinaryOpType::In, e)455			if matches!(e.expr(), Expr::Literal(LiteralType::Super)) =>456		{457			let Some(super_obj) = ctx.super_obj() else {458				return Ok(Val::Bool(false));459			};460			let field = evaluate(ctx.clone(), field)?;461			Val::Bool(super_obj.has_field_ex(field.to_string()?, true))462		}463		BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,464		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,465		Var(name) => in_frame(466			CallLocation::new(&loc),467			|| format!("variable <{name}> access"),468			|| ctx.binding(name.clone())?.evaluate(),469		)?,470		Index { indexable, parts } => ensure_sufficient_stack(|| {471			let mut parts = parts.iter();472			let mut indexable = if matches!(indexable.expr(), Expr::Literal(LiteralType::Super)) {473				let part = parts.next().expect("at least part should exist");474				let Some(super_obj) = ctx.super_obj() else {475					#[cfg(feature = "exp-null-coaelse")]476					if part.null_coaelse {477						return Ok(Val::Null);478					}479					bail!(NoSuperFound)480				};481				let name = evaluate(ctx.clone(), &part.value)?;482483				let Val::Str(name) = name else {484					bail!(ValueIndexMustBeTypeGot(485						ValType::Obj,486						ValType::Str,487						name.value_type(),488					))489				};490491				let this = ctx492					.this()493					.expect("no this found, while super present, should not happen");494				let name = name.into_flat();495				match super_obj496					.get_for(name.clone(), this.clone())497					.with_description_src(&part.value, || format!("field <{name}> access"))?498				{499					Some(v) => v,500					#[cfg(feature = "exp-null-coaelse")]501					None if part.null_coaelse => return Ok(Val::Null),502					None => {503						let suggestions = suggest_object_fields(super_obj, name.clone());504505						bail!(NoSuchField(name, suggestions))506					}507				}508			} else {509				evaluate(ctx.clone(), indexable)?510			};511512			for part in parts {513				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {514					(Val::Obj(v), Val::Str(key)) => match v515						.get(key.clone().into_flat())516						.with_description_src(&part.value, || format!("field <{key}> access"))?517					{518						Some(v) => v,519						#[cfg(feature = "exp-null-coaelse")]520						None if part.null_coaelse => return Ok(Val::Null),521						None => {522							let suggestions = suggest_object_fields(&v, key.clone().into_flat());523524							return Err(Error::from(NoSuchField(525								key.clone().into_flat(),526								suggestions,527							)))528							.with_description_src(&part.value, || format!("field <{key}> access"));529						}530					},531					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(532						ValType::Obj,533						ValType::Str,534						n.value_type(),535					)),536					(Val::Arr(v), Val::Num(n)) => {537						let n = n.get();538						if n.fract() > f64::EPSILON {539							bail!(FractionalIndex)540						}541						if n < 0.0 {542							bail!(ArrayBoundsError(n as isize, v.len()));543						}544						v.get(n as usize)?545							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?546					}547					(Val::Arr(_), Val::Str(n)) => {548						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))549					}550					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(551						ValType::Arr,552						ValType::Num,553						n.value_type(),554					)),555556					(Val::Str(s), Val::Num(n)) => Val::Str({557						let v: IStr = s558							.clone()559							.into_flat()560							.chars()561							.skip(n.get() as usize)562							.take(1)563							.collect::<String>()564							.into();565						if v.is_empty() {566							let size = s.into_flat().chars().count();567							bail!(StringBoundsError(n.get() as usize, size))568						}569						StrValue::Flat(v)570					}),571					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(572						ValType::Str,573						ValType::Num,574						n.value_type(),575					)),576					#[cfg(feature = "exp-null-coaelse")]577					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),578					(v, _) => bail!(CantIndexInto(v.value_type())),579				};580			}581			Ok(indexable)582		})?,583		LocalExpr(bindings, returned) => {584			let mut new_bindings: GcHashMap<IStr, Thunk<Val>> =585				GcHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());586			let fctx = Context::new_future();587			for b in bindings {588				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;589			}590			let ctx = ctx.extend(new_bindings, None, None, None).into_future(fctx);591			evaluate(ctx, &returned.clone())?592		}593		Arr(items) => {594			if items.is_empty() {595				Val::Arr(ArrValue::empty())596			} else if items.len() == 1 {597				let item = items[0].clone();598				Val::Arr(ArrValue::lazy(vec![Thunk!(move || evaluate(ctx, &item))]))599			} else {600				Val::Arr(ArrValue::expr(ctx, items.iter().cloned()))601			}602		}603		ArrComp(expr, comp_specs) => {604			let mut out = Vec::new();605			evaluate_comp(ctx, comp_specs, &mut |ctx| {606				let expr = expr.clone();607				out.push(Thunk!(move || evaluate(ctx, &expr)));608				Ok(())609			})?;610			Val::Arr(ArrValue::lazy(out))611		}612		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),613		ObjExtend(a, b) => evaluate_add_op(614			&evaluate(ctx.clone(), a)?,615			&Val::Obj(evaluate_object(ctx, b)?),616		)?,617		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {618			evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)619		})?,620		Function(params, body) => {621			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())622		}623		AssertExpr(assert, returned) => {624			evaluate_assert(ctx.clone(), assert)?;625			evaluate(ctx, returned)?626		}627		ErrorStmt(e) => in_frame(628			CallLocation::new(&loc),629			|| "error statement".to_owned(),630			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),631		)?,632		IfElse {633			cond,634			cond_then,635			cond_else,636		} => {637			if in_frame(638				CallLocation::new(&loc),639				|| "if condition".to_owned(),640				|| bool::from_untyped(evaluate(ctx.clone(), &cond.0)?),641			)? {642				evaluate(ctx, cond_then)?643			} else {644				match cond_else {645					Some(v) => evaluate(ctx, v)?,646					None => Val::Null,647				}648			}649		}650		Slice(value, desc) => {651			fn parse_idx<T: Typed>(652				loc: CallLocation<'_>,653				ctx: &Context,654				expr: Option<&LocExpr>,655				desc: &'static str,656			) -> Result<Option<T>> {657				if let Some(value) = expr {658					Ok(Some(in_frame(659						loc,660						|| format!("slice {desc}"),661						|| T::from_untyped(evaluate(ctx.clone(), value)?),662					)?))663				} else {664					Ok(None)665				}666			}667668			let indexable = evaluate(ctx.clone(), value)?;669			let loc = CallLocation::new(&loc);670671			let start = parse_idx(loc, &ctx, desc.start.as_ref(), "start")?;672			let end = parse_idx(loc, &ctx, desc.end.as_ref(), "end")?;673			let step = parse_idx(loc, &ctx, desc.step.as_ref(), "step")?;674675			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?676		}677		i @ (Import(path) | ImportStr(path) | ImportBin(path)) => {678			let Expr::Str(path) = &path.expr() else {679				bail!("computed imports are not supported")680			};681			let tmp = loc.clone().0;682			let s = ctx.state();683			let resolved_path = s.resolve_from(tmp.source_path(), path as &str)?;684			match i {685				Import(_) => in_frame(686					CallLocation::new(&loc),687					|| format!("import {:?}", path.clone()),688					|| s.import_resolved(resolved_path),689				)?,690				ImportStr(_) => Val::string(s.import_resolved_str(resolved_path)?),691				ImportBin(_) => Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?)),692				_ => unreachable!(),693			}694		}695	})696}
modifiedcrates/jrsonnet-evaluator/src/function/parse.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/parse.rs
+++ b/crates/jrsonnet-evaluator/src/function/parse.rs
@@ -90,7 +90,8 @@
 		let fctx = Context::new_future();
 		let mut defaults = GcHashMap::with_capacity(
 			params.iter().map(|p| p.0.capacity_hint()).sum::<usize>()
-				- filled_named - filled_positionals,
+				- filled_named
+				- filled_positionals,
 		);
 
 		for (idx, param) in params.iter().enumerate().filter(|p| p.1 .1.is_some()) {
@@ -232,22 +233,6 @@
 /// Creates Context, which has all argument default values applied
 /// and with unbound values causing error to be returned
 pub fn parse_default_function_call(body_ctx: Context, params: &ParamsDesc) -> Result<Context> {
-	#[derive(Trace)]
-	struct DependsOnUnbound(IStr, ParamsDesc);
-	impl ThunkValue for DependsOnUnbound {
-		type Output = Val;
-		fn get(self: Box<Self>) -> Result<Val> {
-			Err(FunctionParameterNotBoundInCall(
-				Some(self.0.clone()),
-				self.1
-					.iter()
-					.map(|p| (p.0.name(), ParamDefault::exists(p.1.is_some())))
-					.collect(),
-			)
-			.into())
-		}
-	}
-
 	let fctx = Context::new_future();
 
 	let mut bindings = GcHashMap::with_capacity(params.iter().map(|p| p.0.capacity_hint()).sum());
@@ -267,10 +252,18 @@
 		} else {
 			destruct(
 				&param.0,
-				Thunk::new(DependsOnUnbound(
-					param.0.name().unwrap_or_else(|| "<destruct>".into()),
-					params.clone(),
-				)),
+				{
+					let param_name = param.0.name().unwrap_or_else(|| "<destruct>".into());
+					let params = params.clone();
+					Thunk!(move || Err(FunctionParameterNotBoundInCall(
+						Some(param_name),
+						params
+							.iter()
+							.map(|p| (p.0.name(), ParamDefault::exists(p.1.is_some())))
+							.collect(),
+					)
+					.into()))
+				},
 				fctx.clone(),
 				&mut bindings,
 			)?;
modifiedcrates/jrsonnet-evaluator/src/gc.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/gc.rs
+++ b/crates/jrsonnet-evaluator/src/gc.rs
@@ -158,3 +158,5 @@
 		Self::new()
 	}
 }
+
+pub fn assert_trace<T: Trace>(_v: &T) {}
modifiedcrates/jrsonnet-evaluator/src/obj.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/obj.rs
+++ b/crates/jrsonnet-evaluator/src/obj.rs
@@ -20,7 +20,7 @@
 	in_frame,
 	operator::evaluate_add_op,
 	tb,
-	val::{ArrValue, ThunkValue},
+	val::ArrValue,
 	MaybeUnbound, Result, Thunk, Unbound, Val,
 };
 
@@ -444,45 +444,16 @@
 		})
 	}
 	pub fn get_lazy(&self, key: IStr) -> Option<Thunk<Val>> {
-		#[derive(Trace)]
-		struct ThunkGet {
-			obj: ObjValue,
-			key: IStr,
-		}
-		impl ThunkValue for ThunkGet {
-			type Output = Val;
-
-			fn get(self: Box<Self>) -> Result<Self::Output> {
-				Ok(self.obj.get(self.key)?.expect("field exists"))
-			}
-		}
-
 		if !self.has_field_ex(key.clone(), true) {
 			return None;
 		}
-		Some(Thunk::new(ThunkGet {
-			obj: self.clone(),
-			key,
-		}))
+		let obj = self.clone();
+
+		Some(Thunk!(move || Ok(obj.get(key)?.expect("field exists"))))
 	}
 	pub fn get_lazy_or_bail(&self, key: IStr) -> Thunk<Val> {
-		#[derive(Trace)]
-		struct ThunkGet {
-			obj: ObjValue,
-			key: IStr,
-		}
-		impl ThunkValue for ThunkGet {
-			type Output = Val;
-
-			fn get(self: Box<Self>) -> Result<Self::Output> {
-				self.obj.get_or_bail(self.key)
-			}
-		}
-
-		Thunk::new(ThunkGet {
-			obj: self.clone(),
-			key,
-		})
+		let obj = self.clone();
+		Thunk!(move || obj.get_or_bail(key))
 	}
 	pub fn ptr_eq(a: &Self, b: &Self) -> bool {
 		Cc::ptr_eq(&a.0, &b.0)
@@ -733,11 +704,10 @@
 		self.value_cache
 			.borrow_mut()
 			.insert(cache_key.clone(), CacheValue::Pending);
-		let value = self.get_for_uncached(key, this).map_err(|e| {
+		let value = self.get_for_uncached(key, this).inspect_err(|e| {
 			self.value_cache
 				.borrow_mut()
 				.insert(cache_key.clone(), CacheValue::Errored(e.clone()));
-			e
 		})?;
 		self.value_cache.borrow_mut().insert(
 			cache_key,
modifiedcrates/jrsonnet-evaluator/src/val.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/val.rs
+++ b/crates/jrsonnet-evaluator/src/val.rs
@@ -11,6 +11,7 @@
 use derivative::Derivative;
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::IStr;
+pub use jrsonnet_macros::Thunk;
 use jrsonnet_types::ValType;
 use thiserror::Error;
 
@@ -32,6 +33,27 @@
 }
 
 #[derive(Trace)]
+pub struct ThunkValueClosure<D: Trace, O: 'static> {
+	env: D,
+	// Carries no data, as it is not a real closure, all the
+	// captured environment is stored in `env` field.
+	#[trace(skip)]
+	closure: fn(D) -> Result<O>,
+}
+impl<D: Trace, O: 'static> ThunkValueClosure<D, O> {
+	pub fn new(env: D, closure: fn(D) -> Result<O>) -> Self {
+		Self { env, closure }
+	}
+}
+impl<D: Trace, O: 'static> ThunkValue for ThunkValueClosure<D, O> {
+	type Output = O;
+
+	fn get(self: Box<Self>) -> Result<Self::Output> {
+		(self.closure)(self.env)
+	}
+}
+
+#[derive(Trace)]
 enum ThunkInner<T: Trace> {
 	Computed(T),
 	Errored(Error),
@@ -113,28 +135,11 @@
 		M: ThunkMapper<Input>,
 		M::Output: Trace,
 	{
-		#[derive(Trace)]
-		struct Mapped<Input: Trace, Mapper: Trace> {
-			inner: Thunk<Input>,
-			mapper: Mapper,
-		}
-		impl<Input, Mapper> ThunkValue for Mapped<Input, Mapper>
-		where
-			Input: Trace + Clone,
-			Mapper: ThunkMapper<Input>,
-		{
-			type Output = Mapper::Output;
-
-			fn get(self: Box<Self>) -> Result<Self::Output> {
-				let value = self.inner.evaluate()?;
-				let mapped = self.mapper.map(value)?;
-				Ok(mapped)
-			}
-		}
-
-		Thunk::new(Mapped::<Input, M> {
-			inner: self,
-			mapper,
+		let inner = self;
+		Thunk!(move || {
+			let value = inner.evaluate()?;
+			let mapped = mapper.map(value)?;
+			Ok(mapped)
 		})
 	}
 }
modifiedcrates/jrsonnet-macros/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-macros/Cargo.toml
+++ b/crates/jrsonnet-macros/Cargo.toml
@@ -17,3 +17,4 @@
 proc-macro2.workspace = true
 quote.workspace = true
 syn = { workspace = true, features = ["full"] }
+syn-dissect-closure.workspace = true
modifiedcrates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-macros/src/lib.rs
+++ b/crates/jrsonnet-macros/src/lib.rs
@@ -1,7 +1,7 @@
 use std::string::String;
 
 use proc_macro2::TokenStream;
-use quote::quote;
+use quote::{quote, quote_spanned};
 use syn::{
 	parenthesized,
 	parse::{Parse, ParseStream},
@@ -9,8 +9,8 @@
 	punctuated::Punctuated,
 	spanned::Spanned,
 	token::{self, Comma},
-	Attribute, DeriveInput, Error, Expr, FnArg, GenericArgument, Ident, ItemFn, LitStr, Pat, Path,
-	PathArguments, Result, ReturnType, Token, Type,
+	Attribute, DeriveInput, Error, Expr, ExprClosure, FnArg, GenericArgument, Ident, ItemFn,
+	LitStr, Pat, Path, PathArguments, Result, ReturnType, Token, Type,
 };
 
 fn parse_attr<A: Parse, I>(attrs: &[Attribute], ident: I) -> Result<Option<A>>
@@ -815,3 +815,30 @@
 	let input = parse_macro_input!(input as FormatInput);
 	input.expand().into()
 }
+
+/// Create Thunk using closure syntax
+#[proc_macro]
+#[allow(non_snake_case)]
+pub fn Thunk(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+	let input = parse_macro_input!(input as ExprClosure);
+
+	let span = input.inputs.span();
+	let move_check = input.capture.is_none().then(|| {
+		quote_spanned! {span => {
+			compile_error!("Thunk! needs to be called with move closure");
+		}}
+	});
+
+	let (env, closure, args) = syn_dissect_closure::split_env(input);
+
+	let trace_check = args.iter().map(|el| {
+		let span = el.span();
+		quote_spanned! {span => ::jrsonnet_evaluator::gc::assert_trace(&#el);}
+	});
+
+	quote! {{
+		#move_check
+		#(#trace_check)*
+		::jrsonnet_evaluator::Thunk::new(::jrsonnet_evaluator::val::ThunkValueClosure::new(#env, #closure))
+	}}.into()
+}