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
before · crates/jrsonnet-evaluator/src/evaluate/destructure.rs
1use jrsonnet_gcmodule::Trace;2use jrsonnet_interner::IStr;3use jrsonnet_parser::{BindSpec, Destruct, LocExpr, ParamsDesc};45use crate::{6	bail,7	error::{ErrorKind::*, Result},8	evaluate, evaluate_method, evaluate_named,9	gc::GcHashMap,10	val::ThunkValue,11	Context, Pending, Thunk, Val,12};1314#[allow(clippy::too_many_lines)]15#[allow(unused_variables)]16pub fn destruct(17	d: &Destruct,18	parent: Thunk<Val>,19	fctx: Pending<Context>,20	new_bindings: &mut GcHashMap<IStr, Thunk<Val>>,21) -> Result<()> {22	match d {23		Destruct::Full(v) => {24			let old = new_bindings.insert(v.clone(), parent);25			if old.is_some() {26				bail!(DuplicateLocalVar(v.clone()))27			}28		}29		#[cfg(feature = "exp-destruct")]30		Destruct::Skip => {}31		#[cfg(feature = "exp-destruct")]32		Destruct::Array { start, rest, end } => {33			use jrsonnet_parser::DestructRest;3435			use crate::arr::ArrValue;3637			#[derive(Trace)]38			struct DataThunk {39				parent: Thunk<Val>,40				min_len: usize,41				has_rest: bool,42			}43			impl ThunkValue for DataThunk {44				type Output = ArrValue;4546				fn get(self: Box<Self>) -> Result<Self::Output> {47					let v = self.parent.evaluate()?;48					let Val::Arr(arr) = v else {49						bail!("expected array");50					};51					if !self.has_rest {52						if arr.len() != self.min_len {53							bail!("expected {} elements, got {}", self.min_len, arr.len())54						}55					} else if arr.len() < self.min_len {56						bail!(57							"expected at least {} elements, but array was only {}",58							self.min_len,59							arr.len()60						)61					}62					Ok(arr)63				}64			}6566			let full = Thunk::new(DataThunk {67				min_len: start.len() + end.len(),68				has_rest: rest.is_some(),69				parent,70			});7172			{73				#[derive(Trace)]74				struct BaseThunk {75					full: Thunk<ArrValue>,76					index: usize,77				}78				impl ThunkValue for BaseThunk {79					type Output = Val;8081					fn get(self: Box<Self>) -> Result<Self::Output> {82						let full = self.full.evaluate()?;83						Ok(full.get(self.index)?.expect("length is checked"))84					}85				}86				for (i, d) in start.iter().enumerate() {87					destruct(88						d,89						Thunk::new(BaseThunk {90							full: full.clone(),91							index: i,92						}),93						fctx.clone(),94						new_bindings,95					)?;96				}97			}9899			match rest {100				Some(DestructRest::Keep(v)) => {101					#[derive(Trace)]102					struct RestThunk {103						full: Thunk<ArrValue>,104						start: usize,105						end: usize,106					}107					impl ThunkValue for RestThunk {108						type Output = Val;109110						fn get(self: Box<Self>) -> Result<Self::Output> {111							let full = self.full.evaluate()?;112							let to = full.len() - self.end;113							Ok(Val::Arr(full.slice(114								Some(self.start as i32),115								Some(to as i32),116								None,117							)))118						}119					}120121					destruct(122						&Destruct::Full(v.clone()),123						Thunk::new(RestThunk {124							full: full.clone(),125							start: start.len(),126							end: end.len(),127						}),128						fctx.clone(),129						new_bindings,130					)?;131				}132				Some(DestructRest::Drop) | None => {}133			}134135			{136				#[derive(Trace)]137				struct EndThunk {138					full: Thunk<ArrValue>,139					index: usize,140					end: usize,141				}142				impl ThunkValue for EndThunk {143					type Output = Val;144145					fn get(self: Box<Self>) -> Result<Self::Output> {146						let full = self.full.evaluate()?;147						Ok(full148							.get(full.len() - self.end + self.index)?149							.expect("length is checked"))150					}151				}152				for (i, d) in end.iter().enumerate() {153					destruct(154						d,155						Thunk::new(EndThunk {156							full: full.clone(),157							index: i,158							end: end.len(),159						}),160						fctx.clone(),161						new_bindings,162					)?;163				}164			}165		}166		#[cfg(feature = "exp-destruct")]167		Destruct::Object { fields, rest } => {168			use crate::obj::ObjValue;169170			#[derive(Trace)]171			struct DataThunk {172				parent: Thunk<Val>,173				field_names: Vec<(IStr, bool)>,174				has_rest: bool,175			}176			impl ThunkValue for DataThunk {177				type Output = ObjValue;178179				fn get(self: Box<Self>) -> Result<Self::Output> {180					let v = self.parent.evaluate()?;181					let Val::Obj(obj) = v else {182						bail!("expected object");183					};184					for (field, has_default) in &self.field_names {185						if !has_default && !obj.has_field_ex(field.clone(), true) {186							bail!("missing field: {field}");187						}188					}189					if !self.has_rest {190						let len = obj.len();191						if len > self.field_names.len() {192							bail!("too many fields, and rest not found");193						}194					}195					Ok(obj)196				}197			}198			let field_names: Vec<_> = fields199				.iter()200				.map(|f| (f.0.clone(), f.2.is_some()))201				.collect();202			let full = Thunk::new(DataThunk {203				parent,204				field_names,205				has_rest: rest.is_some(),206			});207208			for (field, d, default) in fields {209				#[derive(Trace)]210				struct FieldThunk {211					full: Thunk<ObjValue>,212					field: IStr,213					default: Option<(Pending<Context>, LocExpr)>,214				}215				impl ThunkValue for FieldThunk {216					type Output = Val;217218					fn get(self: Box<Self>) -> Result<Self::Output> {219						let full = self.full.evaluate()?;220						if let Some(field) = full.get(self.field)? {221							Ok(field)222						} else {223							let (fctx, expr) = self.default.as_ref().expect("shape is checked");224							Ok(evaluate(fctx.clone().unwrap(), expr)?)225						}226					}227				}228				let value = Thunk::new(FieldThunk {229					full: full.clone(),230					field: field.clone(),231					default: default.clone().map(|e| (fctx.clone(), e)),232				});233				if let Some(d) = d {234					destruct(d, value, fctx.clone(), new_bindings)?;235				} else {236					destruct(237						&Destruct::Full(field.clone()),238						value,239						fctx.clone(),240						new_bindings,241					)?;242				}243			}244		}245	}246	Ok(())247}248249pub fn evaluate_dest(250	d: &BindSpec,251	fctx: Pending<Context>,252	new_bindings: &mut GcHashMap<IStr, Thunk<Val>>,253) -> Result<()> {254	match d {255		BindSpec::Field { into, value } => {256			#[derive(Trace)]257			struct EvaluateThunkValue {258				name: Option<IStr>,259				fctx: Pending<Context>,260				expr: LocExpr,261			}262			impl ThunkValue for EvaluateThunkValue {263				type Output = Val;264				fn get(self: Box<Self>) -> Result<Self::Output> {265					self.name.map_or_else(266						|| evaluate(self.fctx.unwrap(), &self.expr),267						|name| evaluate_named(self.fctx.unwrap(), &self.expr, name),268					)269				}270			}271			let data = Thunk::new(EvaluateThunkValue {272				name: into.name(),273				fctx: fctx.clone(),274				expr: value.clone(),275			});276			destruct(into, data, fctx, new_bindings)?;277		}278		BindSpec::Function {279			name,280			params,281			value,282		} => {283			#[derive(Trace)]284			struct MethodThunk {285				fctx: Pending<Context>,286				name: IStr,287				params: ParamsDesc,288				value: LocExpr,289			}290			impl ThunkValue for MethodThunk {291				type Output = Val;292293				fn get(self: Box<Self>) -> Result<Self::Output> {294					Ok(evaluate_method(295						self.fctx.unwrap(),296						self.name,297						self.params,298						self.value,299					))300				}301			}302303			let old = new_bindings.insert(304				name.clone(),305				Thunk::new(MethodThunk {306					fctx,307					name: name.clone(),308					params: params.clone(),309					value: value.clone(),310				}),311			);312			if old.is_some() {313				bail!(DuplicateLocalVar(name.clone()))314			}315		}316	}317	Ok(())318}
after · crates/jrsonnet-evaluator/src/evaluate/destructure.rs
1use jrsonnet_interner::IStr;2use jrsonnet_parser::{BindSpec, Destruct};34use crate::{5	bail,6	error::{ErrorKind::*, Result},7	evaluate, evaluate_method, evaluate_named,8	gc::GcHashMap,9	Context, Pending, Thunk, Val,10};1112#[allow(clippy::too_many_lines)]13#[allow(unused_variables)]14pub fn destruct(15	d: &Destruct,16	parent: Thunk<Val>,17	fctx: Pending<Context>,18	new_bindings: &mut GcHashMap<IStr, Thunk<Val>>,19) -> Result<()> {20	match d {21		Destruct::Full(v) => {22			let old = new_bindings.insert(v.clone(), parent);23			if old.is_some() {24				bail!(DuplicateLocalVar(v.clone()))25			}26		}27		#[cfg(feature = "exp-destruct")]28		Destruct::Skip => {}29		#[cfg(feature = "exp-destruct")]30		Destruct::Array { start, rest, end } => {31			use jrsonnet_parser::DestructRest;3233			let min_len = start.len() + end.len();34			let has_rest = rest.is_some();35			let full = Thunk!(move || {36				let v = parent.evaluate()?;37				let Val::Arr(arr) = v else {38					bail!("expected array");39				};40				if !has_rest {41					if arr.len() != min_len {42						bail!("expected {} elements, got {}", min_len, arr.len())43					}44				} else if arr.len() < min_len {45					bail!(46						"expected at least {} elements, but array was only {}",47						min_len,48						arr.len()49					)50				}51				Ok(arr)52			});5354			{55				for (i, d) in start.iter().enumerate() {56					let full = full.clone();57					destruct(58						d,59						Thunk!(move || Ok(full.evaluate()?.get(i)?.expect("length is checked"))),60						fctx.clone(),61						new_bindings,62					)?;63				}64			}6566			match rest {67				Some(DestructRest::Keep(v)) => {68					let start = start.len();69					let end = end.len();70					let full = full.clone();71					destruct(72						&Destruct::Full(v.clone()),73						Thunk!(move || {74							let full = full.evaluate()?;75							let to = full.len() - end;76							Ok(Val::Arr(full.slice(77								Some(start as i32),78								Some(to as i32),79								None,80							)))81						}),82						fctx.clone(),83						new_bindings,84					)?;85				}86				Some(DestructRest::Drop) | None => {}87			}8889			{90				for (i, d) in end.iter().enumerate() {91					let full = full.clone();92					let end = end.len();93					destruct(94						d,95						Thunk!(move || {96							let full = full.evaluate()?;97							Ok(full.get(full.len() - end + i)?.expect("length is checked"))98						}),99						fctx.clone(),100						new_bindings,101					)?;102				}103			}104		}105		#[cfg(feature = "exp-destruct")]106		Destruct::Object { fields, rest } => {107			let field_names: Vec<_> = fields108				.iter()109				.map(|f| (f.0.clone(), f.2.is_some()))110				.collect();111			let has_rest = rest.is_some();112			let full = Thunk!(move || {113				let v = parent.evaluate()?;114				let Val::Obj(obj) = v else {115					bail!("expected object");116				};117				for (field, has_default) in &field_names {118					if !has_default && !obj.has_field_ex(field.clone(), true) {119						bail!("missing field: {field}");120					}121				}122				if !has_rest {123					let len = obj.len();124					if len > field_names.len() {125						bail!("too many fields, and rest not found");126					}127				}128				Ok(obj)129			});130131			for (field, d, default) in fields {132				let default = default.clone().map(|e| (fctx.clone(), e));133				let value = {134					let field = field.clone();135					let full = full.clone();136					Thunk!(move || {137						let full = full.evaluate()?;138						if let Some(field) = full.get(field)? {139							Ok(field)140						} else {141							let (fctx, expr) = default.as_ref().expect("shape is checked");142							Ok(evaluate(fctx.clone().unwrap(), expr)?)143						}144					})145				};146147				if let Some(d) = d {148					destruct(d, value, fctx.clone(), new_bindings)?;149				} else {150					destruct(151						&Destruct::Full(field.clone()),152						value,153						fctx.clone(),154						new_bindings,155					)?;156				}157			}158		}159	}160	Ok(())161}162163pub fn evaluate_dest(164	d: &BindSpec,165	fctx: Pending<Context>,166	new_bindings: &mut GcHashMap<IStr, Thunk<Val>>,167) -> Result<()> {168	match d {169		BindSpec::Field { into, value } => {170			let name = into.name();171			let value = value.clone();172			let data = {173				let fctx = fctx.clone();174				Thunk!(move || name.map_or_else(175					|| evaluate(fctx.unwrap(), &value),176					|name| evaluate_named(fctx.unwrap(), &value, name),177				))178			};179			destruct(into, data, fctx, new_bindings)?;180		}181		BindSpec::Function {182			name,183			params,184			value,185		} => {186			let params = params.clone();187			let name = name.clone();188			let value = value.clone();189			let old = new_bindings.insert(name.clone(), {190				let name = name.clone();191				Thunk!(move || Ok(evaluate_method(fctx.unwrap(), name, params, value)))192			});193			if old.is_some() {194				bail!(DuplicateLocalVar(name))195			}196		}197	}198	Ok(())199}
modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs
@@ -18,7 +18,7 @@
 	function::{CallLocation, FuncDesc, FuncVal},
 	in_frame,
 	typed::Typed,
-	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk, ThunkValue},
+	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},
 	Context, Error, GcHashMap, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,
 	ResultExt, Unbound, Val,
 };
@@ -139,29 +139,14 @@
 					#[cfg(feature = "exp-preserve-order")]
 					false,
 				) {
-					#[derive(Trace)]
-					struct ObjectFieldThunk {
-						obj: ObjValue,
-						field: IStr,
-					}
-					impl ThunkValue for ObjectFieldThunk {
-						type Output = Val;
-
-						fn get(self: Box<Self>) -> Result<Self::Output> {
-							self.obj.get(self.field).transpose().expect(
-								"field exists, as field name was obtained from object.fields()",
-							)
-						}
-					}
-
 					let fctx = Pending::new();
 					let mut new_bindings = GcHashMap::with_capacity(var.capacity_hint());
+					let obj = obj.clone();
 					let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![
 						Thunk::evaluated(Val::string(field.clone())),
-						Thunk::new(ObjectFieldThunk {
-							field: field.clone(),
-							obj: obj.clone(),
-						}),
+						Thunk!(move || obj.get(field).transpose().expect(
+							"field exists, as field name was obtained from object.fields()",
+						)),
 					])));
 					destruct(var, value, fctx.clone(), &mut new_bindings)?;
 					let ctx = ctx
@@ -609,21 +594,8 @@
 			if items.is_empty() {
 				Val::Arr(ArrValue::empty())
 			} else if items.len() == 1 {
-				#[derive(Trace)]
-				struct ArrayElement {
-					ctx: Context,
-					item: LocExpr,
-				}
-				impl ThunkValue for ArrayElement {
-					type Output = Val;
-					fn get(self: Box<Self>) -> Result<Val> {
-						evaluate(self.ctx, &self.item)
-					}
-				}
-				Val::Arr(ArrValue::lazy(vec![Thunk::new(ArrayElement {
-					ctx,
-					item: items[0].clone(),
-				})]))
+				let item = items[0].clone();
+				Val::Arr(ArrValue::lazy(vec![Thunk!(move || evaluate(ctx, &item))]))
 			} else {
 				Val::Arr(ArrValue::expr(ctx, items.iter().cloned()))
 			}
@@ -631,21 +603,8 @@
 		ArrComp(expr, comp_specs) => {
 			let mut out = Vec::new();
 			evaluate_comp(ctx, comp_specs, &mut |ctx| {
-				#[derive(Trace)]
-				struct EvaluateThunk {
-					ctx: Context,
-					expr: LocExpr,
-				}
-				impl ThunkValue for EvaluateThunk {
-					type Output = Val;
-					fn get(self: Box<Self>) -> Result<Val> {
-						evaluate(self.ctx, &self.expr)
-					}
-				}
-				out.push(Thunk::new(EvaluateThunk {
-					ctx,
-					expr: expr.clone(),
-				}));
+				let expr = expr.clone();
+				out.push(Thunk!(move || evaluate(ctx, &expr)));
 				Ok(())
 			})?;
 			Val::Arr(ArrValue::lazy(out))
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()
+}