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

difftreelog

refactor closure interpreter

yoxtmquyYaroslav Bolyukin2026-05-05parent: #644f9f3.patch.diff
in: master

14 files changed

modifiedcrates/jrsonnet-evaluator/src/arr/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/mod.rs
+++ b/crates/jrsonnet-evaluator/src/arr/mod.rs
@@ -7,7 +7,12 @@
 
 use jrsonnet_gcmodule::{Cc, cc_dyn};
 
-use crate::{Context, Result, Thunk, Val, analyze::LExpr, function::NativeFn, typed::IntoUntyped};
+use crate::{
+	Context, Result, Thunk, Val,
+	analyze::{ClosureShape, LExpr},
+	function::NativeFn,
+	typed::IntoUntyped,
+};
 
 mod spec;
 pub use spec::{ArrayLike, *};
@@ -36,8 +41,8 @@
 		Self::new(())
 	}
 
-	pub fn expr(ctx: Context, exprs: Rc<Vec<LExpr>>) -> Self {
-		Self::new(ExprArray::new(ctx, exprs))
+	pub fn expr(ctx: Context, shape: &ClosureShape, exprs: Rc<Vec<LExpr>>) -> Self {
+		Self::new(ExprArray::new(ctx, shape, exprs))
 	}
 
 	pub fn repeated(data: Self, repeats: u32) -> Option<Self> {
modifiedcrates/jrsonnet-evaluator/src/arr/spec.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/spec.rs
+++ b/crates/jrsonnet-evaluator/src/arr/spec.rs
@@ -12,7 +12,7 @@
 use super::ArrValue;
 use crate::{
 	Context, Error, ObjValue, Result, Thunk, Val,
-	analyze::LExpr,
+	analyze::{ClosureShape, LExpr},
 	error::ErrorKind::InfiniteRecursionDetected,
 	evaluate::evaluate,
 	function::NativeFn,
@@ -123,9 +123,9 @@
 	cached: Cc<RefCell<Vec<ArrayThunk>>>,
 }
 impl ExprArray {
-	pub fn new(ctx: Context, src: Rc<Vec<LExpr>>) -> Self {
+	pub fn new(outer: Context, shape: &ClosureShape, src: Rc<Vec<LExpr>>) -> Self {
 		Self {
-			ctx,
+			ctx: Context::enter_using(&outer, shape),
 			cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; src.len()])),
 			src,
 		}
modifiedcrates/jrsonnet-evaluator/src/async_import.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/async_import.rs
+++ b/crates/jrsonnet-evaluator/src/async_import.rs
@@ -4,7 +4,7 @@
 use jrsonnet_ir::{IStr, Source, SourcePath, visit::Visitor};
 use rustc_hash::FxHashMap;
 
-use crate::{AsPathLike, FileData, ImportResolver, ResolvePathOwned, State};
+use crate::{AsPathLike, FileData, ImportResolver, ResolvePathOwned, Result, State};
 
 pub struct Import {
 	path: ResolvePathOwned,
@@ -109,23 +109,23 @@
 				}
 			}
 			Job::ParseFile(path) => {
-				if let Some(file) = s.0.file_cache.borrow_mut().get_mut(&path) {
-					if file.parsed.is_none() {
-						let Some(code) = file.get_string() else {
-							continue;
-						};
-						let source = Source::new(path.clone(), code.clone());
-						// If failed - then skip import
-						file.parsed = crate::parse_jsonnet(&code, source).map(Rc::new).ok();
-						if let Some(parsed) = &file.parsed {
-							let mut imports = FoundImports(vec![]);
-							imports.visit_expr(parsed);
-							for import in imports.0 {
-								queue.push(Job::ResolveImport {
-									from: path.clone(),
-									import,
-								});
-							}
+				if let Some(file) = s.0.file_cache.borrow_mut().get_mut(&path)
+					&& file.parsed.is_none()
+				{
+					let Some(code) = file.get_string() else {
+						continue;
+					};
+					let source = Source::new(path.clone(), code.clone());
+					// If failed - then skip import
+					file.parsed = crate::parse_jsonnet(&code, source).map(Rc::new).ok();
+					if let Some(parsed) = &file.parsed {
+						let mut imports = FoundImports(vec![]);
+						imports.visit_expr(parsed);
+						for import in imports.0 {
+							queue.push(Job::ResolveImport {
+								from: path.clone(),
+								import,
+							});
 						}
 					}
 				}
modifiedcrates/jrsonnet-evaluator/src/ctx.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/ctx.rs
+++ b/crates/jrsonnet-evaluator/src/ctx.rs
@@ -1,151 +1,296 @@
-use std::{clone::Clone, fmt::Debug};
+use std::{
+	cell::{Cell, OnceCell, RefCell},
+	clone::Clone,
+	fmt::{self, Debug},
+};
 
 use educe::Educe;
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::IStr;
 
-use crate::{Pending, Result, SupThis, Thunk, Val, analyze::LocalId, error, error::ErrorKind::*};
+use crate::{
+	Result, SupThis, Thunk, Val,
+	analyze::{CaptureSlot, ClosureShape, LSlot, LocalId, LocalSlot},
+	bail, error,
+	error::ErrorKind::*,
+};
 
 #[derive(Debug, Trace, Clone, Educe)]
 #[educe(PartialEq)]
-pub struct Context(#[educe(PartialEq(method = Cc::ptr_eq))] Cc<ContextInternal>);
+pub struct Context(#[educe(PartialEq(method = Cc::ptr_eq))] pub(crate) Cc<ContextInternal>);
 
-#[derive(Debug, Trace, Clone)]
-struct ContextInternal {
-	sup_this: Option<SupThis>,
-	/// `bindings[i]` corresponds to `LocalId(offset + i)`.
-	bindings: Vec<Option<Thunk<Val>>>,
-	offset: u32,
-	parent: Option<Context>,
+#[derive(Trace)]
+pub(crate) struct ContextInternal {
+	/// Immutable, packed at closure-create time.
+	pub(crate) captures: Cc<Vec<Thunk<Val>>>,
+	/// Filled during closure initialization
+	pub(crate) locals: Cc<LocalsFrame>,
+	pub(crate) sup_this: Option<SupThis>,
 }
 
-impl Context {
-	pub fn new_future() -> Pending<Self> {
-		Pending::new()
+impl Debug for ContextInternal {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		f.debug_struct("ContextInternal")
+			.field("captures", &self.captures.len())
+			.field("locals", &self.locals)
+			.field("sup_this", &self.sup_this.is_some())
+			.finish()
 	}
+}
 
-	pub fn sup_this(&self) -> Option<&SupThis> {
-		self.0.sup_this.as_ref()
+#[derive(Trace, Debug)]
+pub(crate) struct IterFrame {
+	slots: Vec<RefCell<Option<Thunk<Val>>>>,
+	captured: Cell<bool>,
+}
+impl IterFrame {
+	pub fn new(n: u16) -> IterFrame {
+		let cells: Vec<RefCell<Option<Thunk<Val>>>> = (0..n).map(|_| RefCell::new(None)).collect();
+		IterFrame {
+			slots: cells,
+			captured: Cell::new(false),
+		}
 	}
-
-	pub fn try_sup_this(&self) -> Result<SupThis> {
-		self.0
-			.sup_this
-			.clone()
-			.ok_or_else(|| error!(CantUseSelfSupOutsideOfObject))
+	pub fn set(&self, slot: LocalSlot, value: Thunk<Val>) {
+		*self.slots[slot.0 as usize].borrow_mut() = Some(value);
 	}
-
-	/// Update binding in `CoW` fashion. Only useful for eager comprehension
-	/// fast-path, as it requires Cc refcount to be 1; Use `ContextBuilder` otherwise.
-	pub(crate) fn cow_fill_binding(&mut self, id: LocalId, value: Thunk<Val>) {
-		let mut value = Some(Some(value));
+}
 
-		self.0.update_with(|inner| {
-			let local_idx = (id.0 - inner.offset) as usize;
-			while inner.bindings.len() <= local_idx {
-				inner.bindings.push(None);
+#[derive(Trace, Debug)]
+pub(crate) enum LocalsFrame {
+	Once1(OnceCell<Thunk<Val>>),
+	/// Letrec/function/object/for frames - slots are filled during frame setup
+	Once(Vec<OnceCell<Thunk<Val>>>),
+	/// Comp-eager fast-path, cells are reset per iteration for the unique frames (i.e for the non-capturing thunks)
+	Iter(IterFrame),
+}
+impl LocalsFrame {
+	pub fn set(&self, slot: LocalSlot, value: Thunk<Val>) {
+		match self {
+			LocalsFrame::Once1(cell) => {
+				debug_assert_eq!(slot.0, 0, "Once1 only holds slot 0");
+				cell.set(value)
+					.map_err(|_| ())
+					.expect("slot already filled");
 			}
-			inner.bindings[local_idx] = value.take().expect("called once");
-		});
-	}
-
-	pub fn binding(&self, id: LocalId) -> Option<Thunk<Val>> {
-		let id_num = id.0;
-		if id_num >= self.0.offset {
-			let local_idx = (id_num - self.0.offset) as usize;
-			if let Some(Some(thunk)) = self.0.bindings.get(local_idx) {
-				return Some(thunk.clone());
+			LocalsFrame::Once(cells) => {
+				cells[slot.0 as usize]
+					.set(value)
+					.map_err(|_| ())
+					.expect("slot already filled");
 			}
+			LocalsFrame::Iter(_) => unreachable!("iter frame has different constructors"),
 		}
-		if let Some(parent) = &self.0.parent {
-			return parent.binding(id);
+	}
+}
+
+impl LocalsFrame {
+	pub(crate) fn new_once(n: u16) -> Cc<Self> {
+		if n == 1 {
+			return Cc::new(Self::Once1(OnceCell::new()));
 		}
-		None
+		let cells: Vec<OnceCell<Thunk<Val>>> = (0..n).map(|_| OnceCell::new()).collect();
+		Cc::new(Self::Once(cells))
 	}
+}
 
-	#[must_use]
-	pub fn into_future(self, ctx: Pending<Self>) -> Self {
-		{
-			ctx.clone().fill(self);
+pub(crate) struct IterContext {
+	context: Context,
+}
+impl IterContext {
+	pub(crate) fn create(&self, build: impl FnOnce(&IterFrame)) -> Result<Context> {
+		if !Cc::is_unique(&self.context.0.locals) {
+			bail!(EagerCompspecCaptured);
+		}
+		let LocalsFrame::Iter(frame) = &*self.context.0.locals else {
+			unreachable!("IterContext is only created for Iter ctx");
+		};
+		if frame.captured.get() {
+			bail!(EagerCompspecCaptured);
 		}
-		ctx.unwrap()
+		build(frame);
+		Ok(self.context.clone())
 	}
 }
 
-#[derive(Clone)]
-pub struct ContextBuilder {
+#[derive(Trace, Clone)]
+pub(crate) struct PackedContext {
+	captures: Cc<Vec<Thunk<Val>>>,
+	n_locals: u16,
+}
+impl PackedContext {
+	pub fn enter(self, sup_this: SupThis, build: impl FnOnce(&LocalsFrame, &Context)) -> Context {
+		let locals = LocalsFrame::new_once(self.n_locals);
+		let val = Context(Cc::new(ContextInternal {
+			captures: self.captures.clone(),
+			locals,
+			sup_this: Some(sup_this),
+		}));
+		build(&val.0.locals, &val);
+		val
+	}
+}
+#[derive(Trace, Clone, Educe, Debug)]
+#[educe(PartialEq)]
+pub(crate) struct PackedContextSupThis {
+	#[educe(PartialEq(method = Cc::ptr_eq))]
+	captures: Cc<Vec<Thunk<Val>>>,
+	n_locals: u16,
 	sup_this: Option<SupThis>,
-	bindings: Vec<Option<Thunk<Val>>>,
-	offset: u32,
-	parent: Option<Context>,
 }
+impl PackedContextSupThis {
+	pub fn enter(self, build: impl FnOnce(&LocalsFrame, &Context)) -> Context {
+		let locals = LocalsFrame::new_once(self.n_locals);
+		let val = Context(Cc::new(ContextInternal {
+			captures: self.captures.clone(),
+			locals,
+			sup_this: self.sup_this,
+		}));
+		build(&val.0.locals, &val);
+		val
+	}
+}
 
-impl ContextBuilder {
-	pub fn new() -> Self {
-		Self {
+impl Context {
+	#[inline]
+	pub fn slot(&self, slot: LSlot) -> Thunk<Val> {
+		match slot {
+			LSlot::Local(i) => self.local(i),
+			LSlot::Capture(i) => self.capture(i),
+		}
+	}
+	/// Read a local slot from the shared locals frame.
+	///
+	/// # Panics
+	/// If the slot has not yet been filled. The analyzer guarantees
+	/// that slot indices are in range and that letrec setup completes
+	/// before the first read. A panic indicates an analyzer/runtime
+	/// invariant violation, not a user error.
+	#[inline]
+	pub fn local(&self, slot: LocalSlot) -> Thunk<Val> {
+		match &*self.0.locals {
+			LocalsFrame::Once1(cell) => {
+				debug_assert_eq!(slot.0, 0, "Once1 only holds slot 0");
+				cell.get().expect("local read before letrec init").clone()
+			}
+			LocalsFrame::Once(cells) => cells[slot.0 as usize]
+				.get()
+				.expect("local read before letrec init")
+				.clone(),
+			LocalsFrame::Iter(cells) => cells.slots[slot.0 as usize]
+				.borrow()
+				.as_ref()
+				.expect("iter local read before iteration filled it")
+				.clone(),
+		}
+	}
+
+	/// Read a captured slot from this closure's capture pack.
+	#[inline]
+	pub fn capture(&self, slot: CaptureSlot) -> Thunk<Val> {
+		(*self.0.captures)[slot.0 as usize].clone()
+	}
+
+	pub fn sup_this(&self) -> Option<&SupThis> {
+		self.0.sup_this.as_ref()
+	}
+
+	pub fn try_sup_this(&self) -> Result<SupThis> {
+		self.0
+			.sup_this
+			.clone()
+			.ok_or_else(|| error!(CantUseSelfSupOutsideOfObject))
+	}
+
+	/// Build a root context: empty captures, externals filled into a
+	/// fresh Once locals frame in declaration order. Used once at
+	/// program entry to construct the context the analyzed root LIR
+	/// runs against.
+	pub(crate) fn root(externals: Vec<Thunk<Val>>) -> Self {
+		let n: u16 = externals
+			.len()
+			.try_into()
+			.expect("more than u16::MAX externals");
+		let cells: Vec<OnceCell<Thunk<Val>>> = externals
+			.into_iter()
+			.map(|t| {
+				let cell = OnceCell::new();
+				cell.set(t).map_err(|_| ()).expect("fresh cell");
+				cell
+			})
+			.collect();
+		debug_assert_eq!(cells.len(), n as usize);
+		let locals = Cc::new(LocalsFrame::Once(cells));
+		Self(Cc::new(ContextInternal {
+			captures: Cc::new(Vec::new()),
+			locals,
 			sup_this: None,
-			bindings: Vec::new(),
-			offset: 0,
-			parent: None,
-		}
+		}))
 	}
 
-	pub(crate) fn extend(parent: Context, capacity: usize) -> Self {
-		let offset = parent.0.offset + parent.0.bindings.len() as u32;
-		Self {
-			sup_this: parent.0.sup_this.clone(),
-			bindings: Vec::with_capacity(capacity),
-			offset,
-			parent: Some(parent),
+	pub(crate) fn pack_captures(&self, shape: &ClosureShape) -> PackedContext {
+		PackedContext {
+			captures: Cc::new(pack_captures(self, &shape.captures)),
+			n_locals: shape.n_locals,
 		}
 	}
-
-	pub(crate) fn bind(&mut self, id: LocalId, value: Thunk<Val>) {
-		debug_assert!(
-			id.0 >= self.offset,
-			"cannot bind {id:?} below offset {}",
-			self.offset,
-		);
-		let local_idx = (id.0 - self.offset) as usize;
-		self.bindings.reserve(local_idx);
-		while self.bindings.len() <= local_idx {
-			self.bindings.push(None);
+	pub(crate) fn pack_captures_sup_this(&self, shape: &ClosureShape) -> PackedContextSupThis {
+		PackedContextSupThis {
+			captures: Cc::new(pack_captures(self, &shape.captures)),
+			n_locals: shape.n_locals,
+			sup_this: self.0.sup_this.clone(),
 		}
-		self.bindings[local_idx] = Some(value);
 	}
 
-	pub(crate) fn build(self) -> Context {
-		Context(Cc::new(ContextInternal {
-			sup_this: self.sup_this,
-			bindings: self.bindings,
-			offset: self.offset,
-			parent: self.parent,
-		}))
+	pub(crate) fn enter_iter(
+		parent: &Context,
+		shape: &ClosureShape,
+		cb: impl FnOnce(IterContext) -> Result<()>,
+	) -> Result<()> {
+		let captures = Cc::new(pack_captures(parent, &shape.captures));
+		let locals = IterFrame::new(shape.n_locals);
+		cb(IterContext {
+			context: Self(Cc::new(ContextInternal {
+				captures,
+				locals: Cc::new(LocalsFrame::Iter(locals)),
+				sup_this: parent.0.sup_this.clone(),
+			})),
+		})
 	}
 
-	pub(crate) fn build_sup_this(mut self, st: SupThis) -> Context {
-		self.sup_this = Some(st);
-		self.build()
+	pub(crate) fn enter_using(parent: &Context, shape: &ClosureShape) -> Self {
+		debug_assert_eq!(shape.n_locals, 0);
+		if shape.captures.is_empty() {
+			if let LocalsFrame::Iter(i) = &*parent.0.locals {
+				i.captured.set(true);
+			}
+			// Value never uses captures, thus evaluating it against the parent gives the same result
+			return parent.clone();
+		}
+		let captures = Cc::new(pack_captures(parent, &shape.captures));
+		Self(Cc::new(ContextInternal {
+			captures,
+			locals: parent.0.locals.clone(),
+			sup_this: parent.0.sup_this.clone(),
+		}))
 	}
 }
 
-impl Default for ContextBuilder {
-	fn default() -> Self {
-		Self::new()
-	}
+fn pack_captures(parent: &Context, sources: &[LSlot]) -> Vec<Thunk<Val>> {
+	sources.iter().map(|src| parent.slot(*src)).collect()
 }
 
 pub struct InitialContextBuilder {
-	builder: ContextBuilder,
 	externals: Vec<(IStr, LocalId)>,
+	values: Vec<Thunk<Val>>,
 	next_id: u32,
 }
 
 impl InitialContextBuilder {
 	pub(crate) fn new() -> Self {
 		Self {
-			builder: ContextBuilder::new(),
 			externals: Vec::new(),
+			values: Vec::new(),
 			next_id: 0,
 		}
 	}
@@ -155,11 +300,11 @@
 		let id = LocalId(self.next_id);
 		self.next_id += 1;
 		self.externals.push((name, id));
-		self.builder.bind(id, value);
+		self.values.push(value);
 	}
 
-	pub(crate) fn build(self) -> (ContextBuilder, Vec<(IStr, LocalId)>) {
-		(self.builder, self.externals)
+	pub(crate) fn build(self) -> (Vec<(IStr, LocalId)>, Vec<Thunk<Val>>) {
+		(self.externals, self.values)
 	}
 }
 
modifiedcrates/jrsonnet-evaluator/src/dynamic.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/dynamic.rs
+++ b/crates/jrsonnet-evaluator/src/dynamic.rs
@@ -1,59 +1,6 @@
-use std::{cell::OnceCell, hash::Hasher, ptr::addr_of};
-
-use educe::Educe;
-use jrsonnet_gcmodule::{Cc, Trace};
-
-use crate::{Result, bail, error::ErrorKind::InfiniteRecursionDetected, val::ThunkValue};
-
-#[derive(Trace, Educe)]
-#[educe(Clone)]
-pub struct Pending<V: Trace + 'static>(pub Cc<OnceCell<V>>);
-impl<T: Trace + 'static> Pending<T> {
-	pub fn new() -> Self {
-		Self(Cc::new(OnceCell::new()))
-	}
-	pub fn new_filled(v: T) -> Self {
-		let cell = OnceCell::new();
-		let _ = cell.set(v);
-		Self(Cc::new(cell))
-	}
-	/// # Panics
-	/// If wrapper is filled already
-	pub fn fill(self, value: T) {
-		self.0
-			.set(value)
-			.map_err(|_| ())
-			.expect("wrapper is filled already");
-	}
-}
-impl<T: Trace + 'static + Clone> Pending<T> {
-	/// # Panics
-	/// If wrapper is not yet filled
-	pub fn unwrap(&self) -> T {
-		self.0.get().cloned().expect("pending was not filled")
-	}
-	pub fn try_get(&self) -> Option<T> {
-		self.0.get().cloned()
-	}
-}
+use std::{hash::Hasher, ptr::addr_of};
 
-impl<T: Trace + Clone> ThunkValue for Pending<T> {
-	type Output = T;
-
-	fn get(&self) -> Result<Self::Output> {
-		let Some(value) = self.0.get() else {
-			// TODO: Other error?
-			bail!(InfiniteRecursionDetected);
-		};
-		Ok(value.clone())
-	}
-}
-
-impl<T: Trace + 'static> Default for Pending<T> {
-	fn default() -> Self {
-		Self::new()
-	}
-}
+use jrsonnet_gcmodule::Cc;
 
 pub fn identity_hash<T, H: Hasher>(v: &Cc<T>, hasher: &mut H) {
 	hasher.write_usize(addr_of!(**v) as usize);
modifiedcrates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -10,6 +10,7 @@
 
 use crate::{
 	ObjValue, ResolvePathOwned,
+	analyze::Diagnostic,
 	function::{CallLocation, FunctionSignature, ParamName},
 	stdlib::format::FormatError,
 	typed::TypeLocError,
@@ -112,12 +113,14 @@
 	CantUseSelfSupOutsideOfObject,
 
 	#[error("static analysis errors: {}", .0.iter().map(|d| d.message.as_str()).collect::<Vec<_>>().join("; "))]
-	StaticAnalysisError(Vec<crate::analyze::Diagnostic>),
+	StaticAnalysisError(Vec<Diagnostic>),
 	#[error("no super found")]
 	NoSuperFound,
 
 	#[error("for loop can only iterate over arrays")]
 	InComprehensionCanOnlyIterateOverArray,
+	#[error("(should not be visible) eager compspec evaluation failed due to captured context")]
+	EagerCompspecCaptured,
 
 	#[error("array out of bounds: {0} is not within [0,{1})")]
 	ArrayBoundsError(isize, u32),
@@ -394,12 +397,5 @@
 	};
 	($l:literal$(, $($tt:tt)*)?) => {
 		<$crate::error::Error as From<$crate::error::ErrorKind>>::from($crate::error::ErrorKind::RuntimeError($crate::jrsonnet_macros::format_istr!($l$(, $($tt)*)?)).into())
-	};
-}
-
-#[macro_export]
-macro_rules! runtime_error {
-	($l:literal$(, $($tt:tt)*)?) => {
-		$crate::error::Error::from($crate::error::ErrorKind::RuntimeError($crate::jrsonnet_macros::format_istr!($l$(, $($tt)*)?)))
 	};
 }
modifiedcrates/jrsonnet-evaluator/src/evaluate/compspec.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/compspec.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/compspec.rs
@@ -3,16 +3,19 @@
 use jrsonnet_types::ValType;
 
 use super::{
-	destructure::{self, evaluate_locals, evaluate_locals_unbound},
+	destructure::{destruct, evaluate_locals_unbound, fill_letrec_binds},
 	evaluate_field_member_static, evaluate_field_member_unbound,
 };
 use crate::{
-	Context, ContextBuilder, ObjValue, ObjValueBuilder, Pending, Result, Thunk, Val,
-	analyze::{LArrComp, LBind, LCompSpec, LDestruct, LExpr, LFieldMember, LObjComp, LocalId},
+	Context, ObjValue, ObjValueBuilder, Result, Thunk, Val,
+	analyze::{
+		ClosureShape, LArrComp, LBind, LCompSpec, LDestruct, LExpr, LFieldMember, LObjComp,
+		LocalSlot,
+	},
 	arr::ArrValue,
 	bail,
 	error::ErrorKind::*,
-	evaluate::evaluate,
+	evaluate::{evaluate, evaluate_trivial},
 };
 
 trait CompCollector {
@@ -22,6 +25,7 @@
 
 struct EagerArrCollector<'a> {
 	out: &'a mut Vec<Val>,
+	value_shape: &'a ClosureShape,
 	value: &'a LExpr,
 }
 impl CompCollector for EagerArrCollector<'_> {
@@ -29,13 +33,23 @@
 		self.out.reserve(size_hint);
 	}
 	fn collect(&mut self, ctx: Context) -> Result<()> {
-		self.out.push(evaluate(ctx, self.value)?);
+		if let Some(v) = evaluate_trivial(self.value) {
+			self.out.push(v);
+			return Ok(());
+		}
+		if let LExpr::Slot(slot) = self.value {
+			self.out.push(ctx.slot(*slot).evaluate()?);
+			return Ok(());
+		}
+		let env = Context::enter_using(&ctx, self.value_shape);
+		self.out.push(evaluate(env, self.value)?);
 		Ok(())
 	}
 }
 
 struct LazyArrCollector<'a> {
 	out: &'a mut Vec<Thunk<Val>>,
+	value_shape: &'a ClosureShape,
 	value: &'a Rc<LExpr>,
 }
 impl CompCollector for LazyArrCollector<'_> {
@@ -43,14 +57,24 @@
 		self.out.reserve(size_hint);
 	}
 	fn collect(&mut self, ctx: Context) -> Result<()> {
+		if let Some(v) = evaluate_trivial(self.value) {
+			self.out.push(Thunk::evaluated(v));
+			return Ok(());
+		}
+		if let LExpr::Slot(slot) = self.value.as_ref() {
+			self.out.push(ctx.slot(*slot));
+			return Ok(());
+		}
+		let env = Context::enter_using(&ctx, self.value_shape);
 		let value_expr = self.value.clone();
-		self.out.push(Thunk!(move || evaluate(ctx, &value_expr)));
+		self.out.push(Thunk!(move || evaluate(env, &value_expr)));
 		Ok(())
 	}
 }
 
 struct ObjCompCollectorStatic<'a> {
 	builder: &'a mut ObjValueBuilder,
+	frame_shape: &'a ClosureShape,
 	locals: &'a [LBind],
 	field: &'a LFieldMember,
 }
@@ -59,15 +83,23 @@
 		self.builder.reserve_fields(guaranteed);
 	}
 	fn collect(&mut self, inner_ctx: Context) -> Result<()> {
-		let value_ctx = evaluate_locals(inner_ctx.clone(), self.locals);
+		// Build the object's A-frame fresh per iteration: captures from
+		// the comp's iter ctx, locals = `this` (slot 0, unfilled in the
+		// static path) + member-locals via letrec.
+		let value_ctx = inner_ctx
+			.pack_captures_sup_this(self.frame_shape)
+			.enter(|fill, ctx| {
+				fill_letrec_binds(fill, &ctx, self.locals);
+			});
 		evaluate_field_member_static(self.builder, inner_ctx, value_ctx, self.field)
 	}
 }
 
 struct ObjCompCollectorUnbound<'a> {
 	builder: &'a mut ObjValueBuilder,
+	frame_shape: Rc<ClosureShape>,
 	locals: Rc<Vec<LBind>>,
-	this_id: Option<LocalId>,
+	this_slot: Option<LocalSlot>,
 	field: &'a LFieldMember,
 }
 impl CompCollector for ObjCompCollectorUnbound<'_> {
@@ -75,7 +107,12 @@
 		self.builder.reserve_fields(guaranteed);
 	}
 	fn collect(&mut self, inner_ctx: Context) -> Result<()> {
-		let uctx = evaluate_locals_unbound(inner_ctx.clone(), self.locals.clone(), self.this_id);
+		let uctx = evaluate_locals_unbound(
+			&inner_ctx,
+			&self.frame_shape,
+			self.this_slot,
+			self.locals.clone(),
+		);
 		evaluate_field_member_unbound(self.builder, inner_ctx, uctx, self.field)
 	}
 }
@@ -100,8 +137,9 @@
 			0,
 			&mut ObjCompCollectorUnbound {
 				builder: &mut builder,
+				frame_shape: comp.frame_shape.clone(),
 				locals: comp.locals.clone(),
-				this_id: comp.this,
+				this_slot: comp.this,
 				field: &comp.field,
 			},
 		)?;
@@ -114,6 +152,7 @@
 			0,
 			&mut ObjCompCollectorStatic {
 				builder: &mut builder,
+				frame_shape: &comp.frame_shape,
 				locals: &comp.locals,
 				field: &comp.field,
 			},
@@ -126,7 +165,9 @@
 pub fn evaluate_arr_comp(ctx: Context, comp: &LArrComp) -> Result<Val> {
 	let cached_overs = cache_overs(&ctx, &comp.compspecs)?;
 
-	// In eager evaluation, Context is not captured, thus updates in CoW fashion will likely to success
+	// Eager fast-path: when the comp has only `if` and `for { destruct: Full(_) }`
+	// specs, allocate one Iter A-frame per for-spec and re-set the slot
+	// per iteration as long as the frame's refcount stays at 1.
 	'eager: {
 		let mut out = Vec::new();
 
@@ -147,6 +188,7 @@
 			0,
 			&mut EagerArrCollector {
 				out: &mut out,
+				value_shape: &comp.value_shape,
 				value: &comp.value,
 			},
 		)
@@ -166,6 +208,7 @@
 		0,
 		&mut LazyArrCollector {
 			out: &mut items,
+			value_shape: &comp.value_shape,
 			value: &comp.value,
 		},
 	)?;
@@ -220,7 +263,12 @@
 				evaluate_compspecs_eager(ctx, specs, cached_overs, idx + 1, 0, collector)?;
 			}
 		}
-		LCompSpec::For { destruct, over, .. } => {
+		LCompSpec::For {
+			frame_shape,
+			destruct,
+			over,
+			..
+		} => {
 			let arr = if let Some(cached) = &cached_overs[idx] {
 				cached.clone()
 			} else {
@@ -232,21 +280,24 @@
 			};
 			let inner_reserve = guaranteed_reserve.max(1) * arr.len() as usize;
 			match destruct {
-				LDestruct::Full(id) => {
-					let id = *id;
-					let mut inner_ctx = ContextBuilder::extend(ctx, 1).build();
-					for (i, item) in arr.iter().enumerate() {
-						// TODO: reuse one ContextBuilder for full evaluate_compspecs pipeline
-						inner_ctx.cow_fill_binding(id, Thunk::evaluated(item?));
-						evaluate_compspecs_eager(
-							inner_ctx.clone(),
-							specs,
-							cached_overs,
-							idx + 1,
-							if i == 0 { inner_reserve } else { 0 },
-							collector,
-						)?;
-					}
+				LDestruct::Full(slot) => {
+					Context::enter_iter(&ctx, frame_shape, |it| {
+						for (i, item) in arr.iter().enumerate() {
+							let item = item?;
+							let ctx = it.create(|f| {
+								f.set(*slot, Thunk::evaluated(item));
+							})?;
+							evaluate_compspecs_eager(
+								ctx,
+								specs,
+								cached_overs,
+								idx + 1,
+								if i == 0 { inner_reserve } else { 0 },
+								collector,
+							)?;
+						}
+						Ok(())
+					})?;
 				}
 				// TODO: Should not be eager? CoW won't work here
 				#[cfg(feature = "exp-destruct")]
@@ -283,7 +334,12 @@
 				evaluate_compspecs(ctx, specs, cached_overs, idx + 1, 0, collector)?;
 			}
 		}
-		LCompSpec::For { destruct, over, .. } => {
+		LCompSpec::For {
+			frame_shape,
+			destruct: dst,
+			over,
+			..
+		} => {
 			let arr = if let Some(cached) = &cached_overs[idx] {
 				cached.clone()
 			} else {
@@ -295,16 +351,10 @@
 			};
 			let inner_reserve = guaranteed_reserve.max(1) * arr.len() as usize;
 			for (i, item) in arr.iter().enumerate() {
-				let item_val = item?;
-				let mut inner_builder = ContextBuilder::extend(ctx.clone(), 1);
-				let fctx = Pending::new();
-				destructure::destruct(
-					destruct,
-					Thunk::evaluated(item_val),
-					fctx.clone(),
-					&mut inner_builder,
-				);
-				let inner_ctx = inner_builder.build().into_future(fctx);
+				let item = item?;
+				let inner_ctx = ctx.pack_captures_sup_this(frame_shape).enter(|fill, ctx| {
+					destruct(dst, fill, Thunk::evaluated(item), &ctx);
+				});
 				evaluate_compspecs(
 					inner_ctx,
 					specs,
modifiedcrates/jrsonnet-evaluator/src/evaluate/destructure.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/destructure.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/destructure.rs
@@ -3,8 +3,10 @@
 use jrsonnet_gcmodule::Trace;
 
 use crate::{
-	Context, ContextBuilder, Pending, Result, SupThis, Thunk, Unbound, Val,
-	analyze::{LBind, LDestruct, LDestructField, LDestructRest, LExpr, LocalId},
+	Context, LocalsFrame, PackedContext, Result, SupThis, Thunk, Unbound, Val,
+	analyze::{
+		ClosureShape, LBind, LDestruct, LDestructField, LDestructRest, LExpr, LLocalExpr, LocalSlot,
+	},
 	bail,
 	evaluate::evaluate,
 };
@@ -15,9 +17,9 @@
 	rest: Option<&LDestructRest>,
 	end: &[LDestruct],
 
+	fill: &LocalsFrame,
 	value: Thunk<Val>,
-	fctx: Pending<Context>,
-	builder: &mut ContextBuilder,
+	a_ctx: &Context,
 ) {
 	let min_len = start.len() + end.len();
 	let has_rest = rest.is_some();
@@ -44,19 +46,19 @@
 		let full = full.clone();
 		destruct(
 			d,
+			fill,
 			Thunk!(move || Ok(full.evaluate()?.get(i as u32)?.expect("length is checked"))),
-			fctx.clone(),
-			builder,
+			a_ctx,
 		);
 	}
 
 	let start_len = start.len() as u32;
 	let end_len = end.len() as u32;
 
-	if let Some(crate::analyze::LDestructRest::Keep(id)) = rest {
+	if let Some(LDestructRest::Keep(slot)) = rest {
 		let full = full.clone();
-		builder.bind(
-			*id,
+		fill.set(
+			*slot,
 			Thunk!(move || {
 				let full = full.evaluate()?;
 				let to = full.len() - end_len;
@@ -73,14 +75,14 @@
 		let full = full.clone();
 		destruct(
 			d,
+			fill,
 			Thunk!(move || {
 				let full = full.evaluate()?;
 				Ok(full
 					.get(full.len() - end_len + i as u32)?
 					.expect("length is checked"))
 			}),
-			fctx.clone(),
-			builder,
+			a_ctx,
 		);
 	}
 }
@@ -90,9 +92,9 @@
 	fields: &[LDestructField],
 	rest: Option<&LDestructRest>,
 
+	fill: &LocalsFrame,
 	value: Thunk<Val>,
-	fctx: Pending<Context>,
-	builder: &mut ContextBuilder,
+	a_ctx: &Context,
 ) {
 	use jrsonnet_interner::IStr;
 	use rustc_hash::FxHashSet;
@@ -124,10 +126,10 @@
 		Ok(obj)
 	});
 
-	if let Some(crate::analyze::LDestructRest::Keep(id)) = rest {
+	if let Some(LDestructRest::Keep(slot)) = rest {
 		let full = full.clone();
-		builder.bind(
-			*id,
+		fill.set(
+			*slot,
 			Thunk!(move || {
 				let full = full.evaluate()?;
 				let mut out = ObjValueBuilder::new();
@@ -140,121 +142,100 @@
 
 	for field in fields {
 		let field_name = field.name.clone();
-		let default: Option<(Pending<Context>, Rc<LExpr>)> =
-			field.default.as_ref().map(|e| (fctx.clone(), e.clone()));
+		let default_thunk: Option<Thunk<Val>> = field
+			.default
+			.as_ref()
+			.map(|(shape, expr)| build_b_thunk(a_ctx, shape, expr.clone()));
+
 		let field_full = full.clone();
 		let value_thunk = Thunk!(move || {
 			let obj = field_full.evaluate()?;
 			obj.get(field_name)?.map_or_else(
-				|| {
-					let (fctx, expr) = default.as_ref().expect("shape is checked");
-					evaluate(fctx.unwrap(), expr)
-				},
+				|| default_thunk.as_ref().expect("shape is checked").evaluate(),
 				Ok,
 			)
 		});
 
 		if let Some(into) = &field.into {
-			destruct(into, value_thunk, fctx.clone(), builder);
+			destruct(into, fill, value_thunk, a_ctx);
 		} else {
 			unreachable!("analyzer lowers object-destruct shorthands into `into`");
 		}
 	}
 }
 
-/// Bind a pre-built thunk to an [`LDestruct`] pattern, inserting one
-/// binding per [`LocalId`] the pattern introduces.
-///
-/// `fctx` is needed for object-destruct defaults (feature `exp-destruct`).
 #[allow(unused_variables)]
-pub fn destruct(
-	d: &LDestruct,
-	value: Thunk<Val>,
-	fctx: Pending<Context>,
-	builder: &mut ContextBuilder,
-) {
+pub fn destruct(d: &LDestruct, fill: &LocalsFrame, value: Thunk<Val>, a_ctx: &Context) {
 	match d {
-		LDestruct::Full(id) => builder.bind(*id, value),
+		LDestruct::Full(slot) => fill.set(*slot, value),
 		#[cfg(feature = "exp-destruct")]
 		LDestruct::Skip => {}
 		#[cfg(feature = "exp-destruct")]
 		LDestruct::Array { start, rest, end } => {
-			destruct_array(start, rest.as_ref(), end, value, fctx, builder)
+			destruct_array(start, rest.as_ref(), end, fill, value, a_ctx)
 		}
 		#[cfg(feature = "exp-destruct")]
-		LDestruct::Object { fields, rest } => {
-			destruct_object(fields, rest.as_ref(), value, fctx, builder)
-		}
+		LDestruct::Object { fields, rest } => destruct_object(fields, rest.as_ref(), fill, value, a_ctx),
 	}
 }
 
-/// Bind one [`LBind`] as a lazy thunk that evaluates in the given
-/// future context. Mirrors the old `evaluate_dest` — one entry per
-/// binding in a `local … ;` frame.
-pub fn evaluate_dest(bind: &LBind, fctx: Pending<Context>, builder: &mut ContextBuilder) {
-	let value = bind.value.clone();
-	let fctx_clone = fctx.clone();
-	let thunk = Thunk!(move || {
-		let ctx = fctx_clone.unwrap();
-		evaluate(ctx, &value)
-	});
-	destruct(&bind.destruct, thunk, fctx, builder);
+pub fn build_b_thunk(a_ctx: &Context, shape: &ClosureShape, expr: Rc<LExpr>) -> Thunk<Val> {
+	let env = Context::enter_using(a_ctx, shape);
+	Thunk!(move || evaluate(env, &expr))
+}
+pub fn build_b_thunk_uno(a_ctx: &Context, shape: Rc<(ClosureShape, LExpr)>) -> Thunk<Val> {
+	let env = Context::enter_using(a_ctx, &shape.0);
+	Thunk!(move || evaluate(env, &shape.1))
 }
 
-/// Bind each LBind's value as a lazy thunk. Mutually recursive locals
-/// resolve lazily through the shared Pending<Context>.
-pub fn evaluate_locals(parent: Context, binds: &[LBind]) -> Context {
-	if binds.is_empty() {
-		return parent;
-	}
-	let fctx = Context::new_future();
-	let mut builder =
-		ContextBuilder::extend(parent, binds.iter().map(|b| b.destruct.ids().len()).sum());
+pub fn fill_letrec_binds(fill: &LocalsFrame, ctx: &Context, binds: &[LBind]) {
 	for bind in binds {
-		evaluate_dest(bind, fctx.clone(), &mut builder);
+		let value_thunk = build_b_thunk(ctx, &bind.value_shape, bind.value.clone());
+		destruct(&bind.destruct, fill, value_thunk, ctx);
 	}
-	builder.build().into_future(fctx)
 }
 
+pub fn evaluate_local_expr(parent: Context, l: &LLocalExpr) -> Result<Val> {
+	let ctx = parent
+		.pack_captures_sup_this(&l.frame_shape)
+		.enter(|fill, ctx| {
+			fill_letrec_binds(fill, ctx, &l.binds);
+		});
+	evaluate(ctx, &l.body)
+}
+
 pub trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}
 impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}
 
 pub fn evaluate_locals_unbound(
-	fctx: Context,
+	outer: &Context,
+	frame_shape: &ClosureShape,
+	this_slot: Option<LocalSlot>,
 	locals: Rc<Vec<LBind>>,
-	this_id: Option<LocalId>,
 ) -> impl CloneableUnbound<Context> {
 	#[derive(Trace, Clone)]
 	struct UnboundLocals {
-		fctx: Context,
+		captures: PackedContext,
+		this_slot: Option<LocalSlot>,
 		locals: Rc<Vec<LBind>>,
-		this_id: Option<LocalId>,
 	}
 	impl Unbound for UnboundLocals {
 		type Bound = Context;
 
 		fn bind(&self, sup_this: SupThis) -> Result<Context> {
-			let parent = self.fctx.clone();
-
-			let fctx = Context::new_future();
-			let mut builder = ContextBuilder::extend(
-				parent,
-				self.locals.iter().map(|b| b.destruct.ids().len()).sum(),
-			);
-			for b in self.locals.iter() {
-				evaluate_dest(b, fctx.clone(), &mut builder);
-			}
-			if let Some(this_id) = self.this_id {
-				builder.bind(this_id, Thunk::evaluated(Val::Obj(sup_this.this().clone())));
-			}
-			let ctx = builder.build_sup_this(sup_this).into_future(fctx);
-			Ok(ctx)
+			Ok(self.captures.clone().enter(sup_this, |fill, ctx| {
+				if let Some(slot) = self.this_slot {
+					let this_obj = ctx.sup_this().expect("sup_this set above").this().clone();
+					fill.set(slot, Thunk::evaluated(Val::Obj(this_obj)));
+				}
+				fill_letrec_binds(fill, ctx, &self.locals);
+			}))
 		}
 	}
 
 	UnboundLocals {
-		fctx,
+		captures: outer.pack_captures(frame_shape),
+		this_slot,
 		locals,
-		this_id,
 	}
 }
modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/evaluate/mod.rs
1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_ir::ImportKind;6use jrsonnet_types::ValType;78use self::{9	compspec::{evaluate_arr_comp, evaluate_obj_comp},10	destructure::{evaluate_locals, evaluate_locals_unbound},11	operator::evaluate_binary_op_special,12};13use crate::{14	Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Result, ResultExt as _, SupThis,15	Unbound, Val,16	analyze::{17		LArgsDesc, LAssertStmt, LExpr, LFieldMember, LFieldName, LFunction, LIndexPart, LObjBody,18		LObjMembers,19	},20	bail,21	error::{ErrorKind::*, suggest_object_fields},22	evaluate::operator::evaluate_unary_op,23	function::{CallLocation, FuncDesc, FuncVal, prepared::PreparedFuncVal},24	in_frame, runtime_error,25	typed::FromUntyped as _,26	val::{CachedUnbound, Thunk},27	with_state,28};2930pub mod compspec;31pub mod destructure;32pub mod operator;3334// This is the amount of bytes that need to be left on the stack before increasing the size.35// It must be at least as large as the stack required by any code that does not call36// `ensure_sufficient_stack`.37const RED_ZONE: usize = 100 * 1024;3839// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then40// on. This flag has performance relevant characteristics. Don't set it too high.41const STACK_PER_RECURSION: usize = 1024 * 1024;4243/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations44/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit45/// from this.46///47/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.48#[inline]49pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {50	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)51}5253pub fn evaluate_trivial(expr: &LExpr) -> Option<Val> {54	// TODO: Eager trivial array55	Some(match expr {56		LExpr::Str(s) => Val::string(s.clone()),57		LExpr::Num(n) => Val::Num(*n),58		LExpr::Bool(false) => Val::Bool(false),59		LExpr::Bool(true) => Val::Bool(true),60		LExpr::Null => Val::Null,61		_ => return None,62	})63}6465/// Evaluate a method definition.66pub fn evaluate_method(ctx: Context, name: IStr, func: &Rc<LFunction>) -> Val {67	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {68		name,69		ctx,70		func: func.clone(),71	})))72}7374pub fn evaluate_field_name(ctx: Context, field_name: &LFieldName) -> Result<Option<IStr>> {75	Ok(match field_name {76		LFieldName::Fixed(n) => Some(n.clone()),77		LFieldName::Dyn(expr) => in_frame(78			// TODO: Spanned<LFieldName>79			CallLocation::native(),80			|| "evaluating field name".to_string(),81			|| {82				let v = evaluate(ctx.clone(), expr)?;83				Ok(if matches!(v, Val::Null) {84					None85				} else {86					Some(IStr::from_untyped(v)?)87				})88			},89		)?,90	})91}9293pub fn evaluate_thunk(ctx: Context, expr: Rc<LExpr>, tailstrict: bool) -> Result<Thunk<Val>> {94	Ok(if tailstrict {95		Thunk::evaluated(evaluate(ctx, &expr)?)96	} else {97		Thunk!(move || { evaluate(ctx, &expr) })98	})99}100101mod names {102	use crate::names;103104	names! {105		anonymous: "anonymous",106	}107}108109pub fn evaluate_named(name: &IStr, ctx: Context, expr: &LExpr) -> Result<Val> {110	if let LExpr::Function(f) = &expr {111		return Ok(evaluate_method(112			ctx,113			f.name.clone().unwrap_or_else(|| name.clone()),114			f,115		));116	}117	evaluate(ctx, expr)118}119120pub fn evaluate(ctx: Context, expr: &LExpr) -> Result<Val> {121	Ok(match expr {122		LExpr::Null => Val::Null,123		LExpr::Bool(b) => Val::Bool(*b),124		LExpr::Str(s) => Val::string(s.clone()),125		LExpr::Num(n) => Val::Num(*n),126		LExpr::Local(id) => {127			let Some(thunk) = ctx.binding(*id) else {128				bail!("should not happen: unbound local {id:?}");129			};130			thunk.evaluate()?131		}132		LExpr::BadLocal(name) => panic!("unresolvable reference: {name}"),133		LExpr::Arr(items) => Val::Arr(crate::arr::ArrValue::expr(ctx, items.clone())),134		LExpr::UnaryOp(op, value) => {135			let value = evaluate(ctx, value)?;136			evaluate_unary_op(*op, &value)?137		}138		LExpr::BinaryOp { lhs, op, rhs } => evaluate_binary_op_special(ctx, lhs, *op, rhs)?,139		LExpr::LocalExpr { binds, body } => {140			let ctx = evaluate_locals(ctx, binds);141			evaluate(ctx, body)?142		}143		LExpr::IfElse {144			cond,145			cond_then,146			cond_else,147		} => {148			let cond_val = evaluate(ctx.clone(), cond)?;149			let Val::Bool(b) = cond_val else {150				bail!(TypeMismatch(151					"if condition",152					vec![ValType::Bool],153					cond_val.value_type()154				))155			};156			if b {157				evaluate(ctx, cond_then)?158			} else if let Some(e) = cond_else {159				evaluate(ctx, e)?160			} else {161				Val::Null162			}163		}164		LExpr::Error(s, e) => in_frame(165			CallLocation::new(s),166			|| "error statement".to_owned(),167			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),168		)?,169		LExpr::AssertExpr { assert, rest } => {170			evaluate_assert(ctx.clone(), assert)?;171			evaluate(ctx, rest)?172		}173174		LExpr::Function(func) => evaluate_method(175			ctx,176			func.name.clone().unwrap_or_else(names::anonymous),177			func,178		),179		LExpr::Apply {180			applicable,181			args,182			tailstrict,183		} => evaluate_apply(184			ctx,185			applicable,186			args,187			CallLocation::new(&args.span),188			*tailstrict,189		)?,190		LExpr::Index { indexable, parts } => evaluate_index(ctx, indexable, parts)?,191		LExpr::Obj(body) => evaluate_obj_body(None, ctx, body)?,192		LExpr::ObjExtend(lhs, body) => {193			let lhs_val = evaluate(ctx.clone(), lhs)?;194			let Val::Obj(lhs_obj) = lhs_val else {195				bail!(TypeMismatch(196					"object extend lhs",197					vec![ValType::Obj],198					lhs_val.value_type(),199				))200			};201			evaluate_obj_body(Some(lhs_obj), ctx, body)?202		}203		LExpr::ArrComp(comp) => evaluate_arr_comp(ctx, comp)?,204		LExpr::Slice(slice) => {205			use crate::typed::BoundedUsize;206			let val = evaluate(ctx.clone(), &slice.value)?;207			let indexable = val.into_indexable()?;208			let start = slice209				.start210				.as_ref()211				.map(|e| evaluate(ctx.clone(), e))212				.transpose()?213				.map(|v| -> Result<i32> {214					v.as_num()215						.ok_or_else(|| {216							TypeMismatch("slice start", vec![ValType::Num], v.value_type()).into()217						})218						.map(|n| n as i32)219				})220				.transpose()?;221			let end = slice222				.end223				.as_ref()224				.map(|e| evaluate(ctx.clone(), e))225				.transpose()?226				.map(|v| -> Result<i32> {227					v.as_num()228						.ok_or_else(|| {229							TypeMismatch("slice end", vec![ValType::Num], v.value_type()).into()230						})231						.map(|n| n as i32)232				})233				.transpose()?;234			let step = slice235				.step236				.as_ref()237				.map(|e| evaluate(ctx, e))238				.transpose()?239				.map(|v| -> Result<BoundedUsize<1, { i32::MAX as usize }>> {240					let n = v.as_num().ok_or_else(|| -> crate::Error {241						TypeMismatch("slice step", vec![ValType::Num], v.value_type()).into()242					})?;243					BoundedUsize::new(n as usize)244						.ok_or_else(|| runtime_error!("slice step must be >= 1"))245				})246				.transpose()?;247			Val::from(indexable.slice(start, end, step)?)248		}249		LExpr::Super => Val::Obj(ctx.try_sup_this()?.standalone_super()?),250		LExpr::Import {251			kind,252			kind_span,253			path,254		} => with_state(|state| {255			let resolved = state.resolve_from(kind_span.0.source_path(), &path.clone())?;256			Ok::<_, Error>(match kind.value {257				ImportKind::Normal => in_frame(258					CallLocation::new(&kind.span),259					|| "import".to_string(),260					|| state.import_resolved(resolved),261				)?,262				ImportKind::Str => Val::string(state.import_resolved_str(resolved)?),263				ImportKind::Bin => Val::arr(state.import_resolved_bin(resolved)?),264			})265		})?,266	})267}268269fn evaluate_apply(270	ctx: Context,271	applicable: &LExpr,272	args: &LArgsDesc,273	loc: CallLocation<'_>,274	tailstrict: bool,275) -> Result<Val> {276	let func_val = evaluate(ctx.clone(), applicable)?;277	let Val::Func(func) = func_val else {278		bail!(OnlyFunctionsCanBeCalledGot(func_val.value_type()))279	};280281	let name = func.name();282	let unnamed = args283		.unnamed284		.iter()285		.cloned()286		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))287		.collect::<Result<Vec<_>>>()?;288289	let named = args290		.values291		.iter()292		.cloned()293		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))294		.collect::<Result<Vec<_>>>()?;295	let prepare = PreparedFuncVal::new(func, unnamed.len(), &args.names)296		.with_description_src(loc, || format!("function <{name}> preparation"))?;297	in_frame(298		loc,299		|| format!("function <{name}> call"),300		|| prepare.call(CallLocation::native(), &unnamed, &named),301	)302}303304fn evaluate_index(ctx: Context, indexable: &LExpr, parts: &[LIndexPart]) -> Result<Val> {305	let mut value = if let LExpr::Super = indexable {306		let sup_this = ctx.try_sup_this()?;307		// First part must be evaluated to get the super field name308		if parts.is_empty() {309			bail!(RuntimeError("super requires an index".into()))310		}311		let key_val = evaluate(ctx.clone(), &parts[0].value)?;312		let Val::Str(key) = &key_val else {313			bail!(ValueIndexMustBeTypeGot(314				ValType::Obj,315				ValType::Str,316				key_val.value_type(),317			))318		};319		let field = key.clone().into_flat();320		if let Some(v) = sup_this.get_super(field.clone())? {321			// Continue with remaining parts322			let mut value = v;323			for part in &parts[1..] {324				value = index_val(ctx.clone(), CallLocation::new(&part.span), value, part)?;325			}326			return Ok(value);327		}328		let suggestions = suggest_object_fields(sup_this.this(), field.clone());329		bail!(NoSuchField(field, suggestions))330	} else {331		evaluate(ctx.clone(), indexable)?332	};333334	for part in parts {335		value = index_val(ctx.clone(), CallLocation::new(&part.span), value, part)?;336	}337	Ok(value)338}339340fn index_val(ctx: Context, loc: CallLocation<'_>, value: Val, part: &LIndexPart) -> Result<Val> {341	let key_val = evaluate(ctx, &part.value)?;342	Ok(match (&value, &key_val) {343		(Val::Obj(obj), Val::Str(key)) => {344			let field = key.clone().into_flat();345			if let Some(v) = obj346				.get(field.clone())347				.with_description_src(loc, || format!("field <{field}> access"))?348			{349				v350			} else {351				bail!(NoSuchField(352					field.clone(),353					suggest_object_fields(obj, field)354				))355			}356		}357		(Val::Arr(arr), Val::Num(idx)) => {358			let n = idx.get();359			if n.fract() > f64::EPSILON {360				bail!(FractionalIndex)361			}362			if n < 0.0 {363				bail!(ArrayBoundsError(364					n as isize, // truncation is fine for error display365					arr.len()366				));367			}368			#[expect(369				clippy::cast_possible_truncation,370				clippy::cast_sign_loss,371				reason = "n is checked positive"372			)]373			let i = n as u32;374			arr.get(i)375				.with_description_src(loc, || format!("element <{i}> access"))?376				.ok_or_else(|| ArrayBoundsError(i as isize, arr.len()))?377		}378		(Val::Str(s), Val::Num(idx)) => {379			let n = idx.get();380			if n.fract() > f64::EPSILON {381				bail!(FractionalIndex)382			}383			let flat = s.clone().into_flat();384			if n < 0.0 {385				bail!(ArrayBoundsError(386					n as isize, // truncation is fine for error display387					flat.chars().count() as u32388				));389			}390			#[expect(391				clippy::cast_possible_truncation,392				clippy::cast_sign_loss,393				reason = "n is checked positive, overflow will truncate as expected"394			)]395			let i = n as usize;396			let Some(char) = flat.chars().nth(i) else {397				bail!(StringBoundsError(i, flat.chars().count()))398			};399			Val::string(char)400		}401		_ => bail!(ValueIndexMustBeTypeGot(402			value.value_type(),403			ValType::Str,404			key_val.value_type()405		)),406	})407}408409fn evaluate_obj_body(super_obj: Option<ObjValue>, ctx: Context, body: &LObjBody) -> Result<Val> {410	match body {411		LObjBody::MemberList(members) => evaluate_obj_members(super_obj, ctx, members),412		LObjBody::ObjComp(comp) => evaluate_obj_comp(super_obj, ctx, comp),413	}414}415416pub fn evaluate_field_member_unbound<B: Unbound<Bound = Context> + Clone>(417	builder: &mut ObjValueBuilder,418	ctx: Context,419	uctx: B,420	field: &LFieldMember,421) -> Result<()> {422	#[derive(Trace)]423	struct UnboundValue<B: Trace> {424		uctx: B,425		value: Rc<LExpr>,426		name: IStr,427	}428	impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {429		type Bound = Val;430		fn bind(&self, sup_this: SupThis) -> Result<Val> {431			evaluate(self.uctx.bind(sup_this)?, &self.value)432		}433	}434435	let LFieldMember {436		name,437		plus,438		visibility,439		value,440	} = field;441	let Some(name) = evaluate_field_name(ctx, name)? else {442		return Ok(());443	};444445	builder446		.field(name.clone())447		.with_add(*plus)448		.with_visibility(*visibility)449		.bindable(UnboundValue {450			uctx,451			value: value.clone(),452			name,453		})454}455pub fn evaluate_field_member_static(456	builder: &mut ObjValueBuilder,457	field_ctx: Context,458	value_ctx: Context,459	field: &LFieldMember,460) -> Result<()> {461	let LFieldMember {462		name,463		plus,464		visibility,465		value,466	} = field;467	let Some(name) = evaluate_field_name(field_ctx, name)? else {468		return Ok(());469	};470471	let value = value.clone();472	builder473		.field(name)474		.with_add(*plus)475		.with_visibility(*visibility)476		.try_thunk(Thunk!(move || { evaluate(value_ctx, &value) }))?;477	Ok(())478}479480fn evaluate_obj_members(481	super_obj: Option<ObjValue>,482	ctx: Context,483	members: &LObjMembers,484) -> Result<Val> {485	let mut builder = ObjValueBuilder::with_capacity(members.fields.len());486	if let Some(sup) = super_obj {487		builder.with_super(sup);488	}489490	let needs_unbound = members.this.is_some() || members.uses_super;491492	if needs_unbound {493		let uctx = CachedUnbound::new(evaluate_locals_unbound(494			ctx.clone(),495			members.locals.clone(),496			members.this,497		));498		for field in &members.fields {499			evaluate_field_member_unbound(&mut builder, ctx.clone(), uctx.clone(), field)?;500		}501		if !members.asserts.is_empty() {502			builder.assert(evaluate_object_assertions_unbound(503				uctx,504				members.asserts.clone(),505			));506		}507	} else {508		let field_ctx = ctx;509		let value_ctx = evaluate_locals(field_ctx.clone(), &members.locals);510		for field in &members.fields {511			evaluate_field_member_static(512				&mut builder,513				field_ctx.clone(),514				value_ctx.clone(),515				field,516			)?;517		}518		if !members.asserts.is_empty() {519			builder.assert(evaluate_object_assertions_static(520				value_ctx,521				members.asserts.clone(),522			));523		}524	}525526	Ok(Val::Obj(builder.build()))527}528529pub fn evaluate_assert(ctx: Context, assertion: &LAssertStmt) -> Result<()> {530	let LAssertStmt { cond, message } = assertion;531	let assertion_result = in_frame(532		CallLocation::native(),533		|| "assertion condition".to_owned(),534		|| bool::from_untyped(evaluate(ctx.clone(), cond)?),535	)?;536	if !assertion_result {537		in_frame(538			CallLocation::new(&cond.span),539			|| "assertion failure".to_owned(),540			|| {541				if let Some(msg) = message {542					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));543				}544				bail!(AssertionFailed(Val::Null.to_string()?));545			},546		)?;547	}548	Ok(())549}550551fn evaluate_object_assertions_unbound<B: Unbound<Bound = Context>>(552	uctx: B,553	asserts: Rc<Vec<LAssertStmt>>,554) -> impl ObjectAssertion {555	#[derive(Trace)]556	struct ObjectAssert<B: Trace> {557		uctx: B,558		asserts: Rc<Vec<LAssertStmt>>,559	}560	impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {561		fn run(&self, sup_this: SupThis) -> Result<()> {562			let ctx = self.uctx.bind(sup_this)?;563			for assert in &*self.asserts {564				evaluate_assert(ctx.clone(), assert)?;565			}566			Ok(())567		}568	}569	ObjectAssert { uctx, asserts }570}571fn evaluate_object_assertions_static(572	ctx: Context,573	asserts: Rc<Vec<LAssertStmt>>,574) -> impl ObjectAssertion {575	#[derive(Trace)]576	struct ObjectAssert {577		ctx: Context,578		asserts: Rc<Vec<LAssertStmt>>,579	}580	impl ObjectAssertion for ObjectAssert {581		fn run(&self, _sup_this: SupThis) -> Result<()> {582			for assert in &*self.asserts {583				evaluate_assert(self.ctx.clone(), assert)?;584			}585			Ok(())586		}587	}588	ObjectAssert { ctx, asserts }589}
after · crates/jrsonnet-evaluator/src/evaluate/mod.rs
1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_ir::ImportKind;6use jrsonnet_types::ValType;78use self::{9	compspec::{evaluate_arr_comp, evaluate_obj_comp},10	destructure::{build_b_thunk_uno, evaluate_local_expr, evaluate_locals_unbound},11	operator::evaluate_binary_op_special,12};13use crate::{14	Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Result, ResultExt as _, SupThis,15	Unbound, Val,16	analyze::{17		ClosureShape, LArgsDesc, LAssertStmt, LExpr, LFieldMember, LFieldName, LFunction,18		LIndexPart, LObjAsserts, LObjBody, LObjMembers, LSlot,19	},20	arr::ArrValue,21	bail, error,22	error::{ErrorKind::*, suggest_object_fields},23	evaluate::{destructure::fill_letrec_binds, operator::evaluate_unary_op},24	function::{CallLocation, FuncDesc, FuncVal, prepared::PreparedFuncVal},25	in_frame,26	typed::FromUntyped as _,27	val::{CachedUnbound, Thunk},28	with_state,29};3031pub mod compspec;32pub mod destructure;33pub mod operator;3435// This is the amount of bytes that need to be left on the stack before increasing the size.36// It must be at least as large as the stack required by any code that does not call37// `ensure_sufficient_stack`.38const RED_ZONE: usize = 100 * 1024;3940// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then41// on. This flag has performance relevant characteristics. Don't set it too high.42const STACK_PER_RECURSION: usize = 1024 * 1024;4344/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations45/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit46/// from this.47///48/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.49#[inline]50pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {51	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)52}5354pub fn evaluate_trivial(expr: &LExpr) -> Option<Val> {55	// TODO: Eager trivial array56	Some(match expr {57		LExpr::Str(s) => Val::string(s.clone()),58		LExpr::Num(n) => Val::Num(*n),59		LExpr::Bool(false) => Val::Bool(false),60		LExpr::Bool(true) => Val::Bool(true),61		LExpr::Null => Val::Null,62		_ => return None,63	})64}6566pub fn evaluate_method(ctx: Context, name: IStr, func: &Rc<LFunction>) -> Val {67	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {68		name,69		body_captures: ctx.pack_captures_sup_this(&func.body_shape),70		func: func.clone(),71	})))72}7374pub fn evaluate_field_name(ctx: Context, field_name: &LFieldName) -> Result<Option<IStr>> {75	Ok(match field_name {76		LFieldName::Fixed(n) => Some(n.clone()),77		LFieldName::Dyn(expr) => in_frame(78			// TODO: Spanned<LFieldName>79			CallLocation::native(),80			|| "evaluating field name".to_string(),81			|| {82				let v = evaluate(ctx.clone(), expr)?;83				Ok(if matches!(v, Val::Null) {84					None85				} else {86					Some(IStr::from_untyped(v)?)87				})88			},89		)?,90	})91}9293pub fn evaluate_thunk(ctx: Context, expr: Rc<LExpr>, tailstrict: bool) -> Result<Thunk<Val>> {94	match &*expr {95		LExpr::Slot(LSlot::Local(i)) => return Ok(ctx.local(*i)),96		LExpr::Slot(LSlot::Capture(i)) => return Ok(ctx.capture(*i)),97		_ => {98			if let Some(v) = evaluate_trivial(&expr) {99				return Ok(Thunk::evaluated(v));100			}101		}102	}103	Ok(if tailstrict {104		Thunk::evaluated(evaluate(ctx, &expr)?)105	} else {106		Thunk!(move || { evaluate(ctx, &expr) })107	})108}109110mod names {111	use crate::names;112113	names! {114		anonymous: "anonymous",115	}116}117118pub fn evaluate(ctx: Context, expr: &LExpr) -> Result<Val> {119	Ok(match expr {120		LExpr::Null => Val::Null,121		LExpr::Bool(b) => Val::Bool(*b),122		LExpr::Str(s) => Val::string(s.clone()),123		LExpr::Num(n) => Val::Num(*n),124		LExpr::Slot(slot) => ctx.slot(*slot).evaluate()?,125		LExpr::BadLocal(name) => panic!("unresolvable reference: {name}"),126		LExpr::Arr { shape, items } => Val::Arr(ArrValue::expr(ctx, shape, items.clone())),127		LExpr::UnaryOp(op, value) => {128			let value = evaluate(ctx, value)?;129			evaluate_unary_op(*op, &value)?130		}131		LExpr::BinaryOp { lhs, op, rhs } => evaluate_binary_op_special(ctx, lhs, *op, rhs)?,132		LExpr::LocalExpr(local_expr) => evaluate_local_expr(ctx, local_expr)?,133		LExpr::IfElse {134			cond,135			cond_then,136			cond_else,137		} => {138			let cond_val = evaluate(ctx.clone(), cond)?;139			let Val::Bool(b) = cond_val else {140				bail!(TypeMismatch(141					"if condition",142					vec![ValType::Bool],143					cond_val.value_type()144				))145			};146			if b {147				evaluate(ctx, cond_then)?148			} else if let Some(e) = cond_else {149				evaluate(ctx, e)?150			} else {151				Val::Null152			}153		}154		LExpr::Error(s, e) => in_frame(155			CallLocation::new(s),156			|| "error statement".to_owned(),157			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),158		)?,159		LExpr::AssertExpr { assert, rest } => {160			evaluate_assert(ctx.clone(), assert)?;161			evaluate(ctx, rest)?162		}163164		LExpr::Function(func) => evaluate_method(165			ctx,166			func.name.clone().unwrap_or_else(names::anonymous),167			func,168		),169		LExpr::IdentityFunction => Val::Func(FuncVal::identity()),170		LExpr::Apply {171			applicable,172			args,173			tailstrict,174		} => evaluate_apply(175			ctx,176			applicable,177			args,178			CallLocation::new(&args.span),179			*tailstrict,180		)?,181		LExpr::Index { indexable, parts } => evaluate_index(ctx, indexable, parts)?,182		LExpr::Obj(body) => evaluate_obj_body(None, ctx, body)?,183		LExpr::ObjExtend(lhs, body) => {184			let lhs_val = evaluate(ctx.clone(), lhs)?;185			let Val::Obj(lhs_obj) = lhs_val else {186				bail!(TypeMismatch(187					"object extend lhs",188					vec![ValType::Obj],189					lhs_val.value_type(),190				))191			};192			evaluate_obj_body(Some(lhs_obj), ctx, body)?193		}194		LExpr::ArrComp(comp) => evaluate_arr_comp(ctx, comp)?,195		LExpr::Slice(slice) => {196			use crate::typed::BoundedUsize;197			let val = evaluate(ctx.clone(), &slice.value)?;198			let indexable = val.into_indexable()?;199			let start = slice200				.start201				.as_ref()202				.map(|e| evaluate(ctx.clone(), e))203				.transpose()?204				.map(|v| -> Result<i32> {205					v.as_num()206						.ok_or_else(|| {207							TypeMismatch("slice start", vec![ValType::Num], v.value_type()).into()208						})209						.map(|n| n as i32)210				})211				.transpose()?;212			let end = slice213				.end214				.as_ref()215				.map(|e| evaluate(ctx.clone(), e))216				.transpose()?217				.map(|v| -> Result<i32> {218					v.as_num()219						.ok_or_else(|| {220							TypeMismatch("slice end", vec![ValType::Num], v.value_type()).into()221						})222						.map(|n| n as i32)223				})224				.transpose()?;225			let step = slice226				.step227				.as_ref()228				.map(|e| evaluate(ctx, e))229				.transpose()?230				.map(|v| -> Result<BoundedUsize<1, { i32::MAX as usize }>> {231					let n = v.as_num().ok_or_else(|| -> crate::Error {232						TypeMismatch("slice step", vec![ValType::Num], v.value_type()).into()233					})?;234					BoundedUsize::new(n as usize).ok_or_else(|| error!("slice step must be >= 1"))235				})236				.transpose()?;237			Val::from(indexable.slice(start, end, step)?)238		}239		LExpr::Super => Val::Obj(ctx.try_sup_this()?.standalone_super()?),240		LExpr::Import {241			kind,242			kind_span,243			path,244		} => with_state(|state| {245			let resolved = state.resolve_from(kind_span.0.source_path(), &path.clone())?;246			Ok::<_, Error>(match kind.value {247				ImportKind::Normal => in_frame(248					CallLocation::new(&kind.span),249					|| "import".to_string(),250					|| state.import_resolved(resolved),251				)?,252				ImportKind::Str => Val::string(state.import_resolved_str(resolved)?),253				ImportKind::Bin => Val::arr(state.import_resolved_bin(resolved)?),254			})255		})?,256	})257}258259fn evaluate_apply(260	ctx: Context,261	applicable: &LExpr,262	args: &LArgsDesc,263	loc: CallLocation<'_>,264	tailstrict: bool,265) -> Result<Val> {266	let func_val = evaluate(ctx.clone(), applicable)?;267	let Val::Func(func) = func_val else {268		bail!(OnlyFunctionsCanBeCalledGot(func_val.value_type()))269	};270271	if func.is_identity() && args.names.is_empty() && args.unnamed.len() == 1 {272		return evaluate_thunk(ctx, args.unnamed[0].clone(), tailstrict)?.evaluate();273	}274275	let name = func.name();276277	if args.names.is_empty() && args.unnamed.len() == 1 && func.params().len() == 1 {278		use crate::function::prepared::PreparedCall;279		let prepared_inline = PreparedCall::empty();280		let arg = evaluate_thunk(ctx, args.unnamed[0].clone(), tailstrict)?;281		let arg_slice = std::slice::from_ref(&arg);282		return in_frame(283			loc,284			|| format!("function <{name}> call"),285			|| {286				func.evaluate_prepared(287					&prepared_inline,288					CallLocation::native(),289					arg_slice,290					&[],291					tailstrict,292				)293			},294		);295	}296297	let unnamed = args298		.unnamed299		.iter()300		.cloned()301		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))302		.collect::<Result<Vec<_>>>()?;303304	// Fast path: positional-only multi-arg call fully covering the305	// params, no defaults.306	if args.names.is_empty() && unnamed.len() == func.params().len() {307		use crate::function::prepared::PreparedCall;308		let prepared_inline = PreparedCall::empty();309		return in_frame(310			loc,311			|| format!("function <{name}> call"),312			|| {313				func.evaluate_prepared(314					&prepared_inline,315					CallLocation::native(),316					&unnamed,317					&[],318					tailstrict,319				)320			},321		);322	}323324	let named = args325		.values326		.iter()327		.cloned()328		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))329		.collect::<Result<Vec<_>>>()?;330	let prepare = PreparedFuncVal::new(func, unnamed.len(), &args.names)331		.with_description_src(loc, || format!("function <{name}> preparation"))?;332	in_frame(333		loc,334		|| format!("function <{name}> call"),335		|| prepare.call(CallLocation::native(), &unnamed, &named),336	)337}338339fn evaluate_index(ctx: Context, indexable: &LExpr, parts: &[LIndexPart]) -> Result<Val> {340	let mut value = if matches!(indexable, LExpr::Super) {341		let sup_this = ctx.try_sup_this()?;342		// First part must be evaluated to get the super field name343		if parts.is_empty() {344			bail!(RuntimeError("super requires an index".into()))345		}346		let key_val = evaluate(ctx.clone(), &parts[0].value)?;347		let Val::Str(key) = &key_val else {348			bail!(ValueIndexMustBeTypeGot(349				ValType::Obj,350				ValType::Str,351				key_val.value_type(),352			))353		};354		let field = key.clone().into_flat();355		if let Some(v) = sup_this.get_super(field.clone())? {356			// Continue with remaining parts357			let mut value = v;358			for part in &parts[1..] {359				value = index_val(ctx.clone(), CallLocation::new(&part.span), value, part)?;360			}361			return Ok(value);362		}363		let suggestions = suggest_object_fields(sup_this.this(), field.clone());364		bail!(NoSuchField(field, suggestions))365	} else {366		evaluate(ctx.clone(), indexable)?367	};368369	for part in parts {370		value = index_val(ctx.clone(), CallLocation::new(&part.span), value, part)?;371	}372	Ok(value)373}374375fn index_val(ctx: Context, loc: CallLocation<'_>, value: Val, part: &LIndexPart) -> Result<Val> {376	let key_val = evaluate(ctx, &part.value)?;377	Ok(match (&value, &key_val) {378		(Val::Obj(obj), Val::Str(key)) => {379			let field = key.clone().into_flat();380			if let Some(v) = obj381				.get(field.clone())382				.with_description_src(loc, || format!("field <{field}> access"))?383			{384				v385			} else {386				bail!(NoSuchField(387					field.clone(),388					suggest_object_fields(obj, field)389				))390			}391		}392		(Val::Arr(arr), Val::Num(idx)) => {393			let n = idx.get();394			if n.fract() > f64::EPSILON {395				bail!(FractionalIndex)396			}397			if n < 0.0 {398				bail!(ArrayBoundsError(399					n as isize, // truncation is fine for error display400					arr.len()401				));402			}403			#[expect(404				clippy::cast_possible_truncation,405				clippy::cast_sign_loss,406				reason = "n is checked positive"407			)]408			let i = n as u32;409			arr.get(i)410				.with_description_src(loc, || format!("element <{i}> access"))?411				.ok_or_else(|| ArrayBoundsError(i as isize, arr.len()))?412		}413		(Val::Str(s), Val::Num(idx)) => {414			let n = idx.get();415			if n.fract() > f64::EPSILON {416				bail!(FractionalIndex)417			}418			let flat = s.clone().into_flat();419			if n < 0.0 {420				bail!(ArrayBoundsError(421					n as isize, // truncation is fine for error display422					flat.chars().count() as u32423				));424			}425			#[expect(426				clippy::cast_possible_truncation,427				clippy::cast_sign_loss,428				reason = "n is checked positive, overflow will truncate as expected"429			)]430			let i = n as usize;431			let Some(char) = flat.chars().nth(i) else {432				bail!(StringBoundsError(i, flat.chars().count()))433			};434			Val::string(char)435		}436		_ => bail!(ValueIndexMustBeTypeGot(437			value.value_type(),438			ValType::Str,439			key_val.value_type()440		)),441	})442}443444fn evaluate_obj_body(super_obj: Option<ObjValue>, ctx: Context, body: &LObjBody) -> Result<Val> {445	match body {446		LObjBody::MemberList(members) => evaluate_obj_members(super_obj, ctx, members),447		LObjBody::ObjComp(comp) => evaluate_obj_comp(super_obj, ctx, comp),448	}449}450451pub fn evaluate_field_member_unbound<B: Unbound<Bound = Context> + Clone>(452	builder: &mut ObjValueBuilder,453	ctx: Context,454	uctx: B,455	field: &LFieldMember,456) -> Result<()> {457	#[derive(Trace)]458	struct UnboundValue<B: Trace> {459		uctx: B,460		value: Rc<(ClosureShape, LExpr)>,461		name: IStr,462	}463	impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {464		type Bound = Val;465		fn bind(&self, sup_this: SupThis) -> Result<Val> {466			let a_ctx = self.uctx.bind(sup_this)?;467			let b_ctx = Context::enter_using(&a_ctx, &self.value.0);468			evaluate(b_ctx, &self.value.1)469		}470	}471472	let LFieldMember {473		name,474		plus,475		visibility,476		value,477	} = field;478	let Some(name) = evaluate_field_name(ctx, name)? else {479		return Ok(());480	};481482	builder483		.field(name.clone())484		.with_add(*plus)485		.with_visibility(*visibility)486		.bindable(UnboundValue {487			uctx,488			value: value.clone(),489			name,490		})491}492pub fn evaluate_field_member_static(493	builder: &mut ObjValueBuilder,494	field_ctx: Context,495	value_ctx: Context,496	field: &LFieldMember,497) -> Result<()> {498	let LFieldMember {499		name,500		plus,501		visibility,502		value,503	} = field;504	let Some(name) = evaluate_field_name(field_ctx, name)? else {505		return Ok(());506	};507508	let thunk = build_b_thunk_uno(&value_ctx, value.clone());509	builder510		.field(name)511		.with_add(*plus)512		.with_visibility(*visibility)513		.try_thunk(thunk)?;514	Ok(())515}516517fn evaluate_obj_members(518	super_obj: Option<ObjValue>,519	ctx: Context,520	members: &LObjMembers,521) -> Result<Val> {522	let mut builder = ObjValueBuilder::with_capacity(members.fields.len());523	if let Some(sup) = super_obj {524		builder.with_super(sup);525	}526527	let needs_unbound = members.this.is_some() || members.uses_super;528529	if needs_unbound {530		let uctx = CachedUnbound::new(evaluate_locals_unbound(531			&ctx,532			&members.frame_shape,533			members.this,534			members.locals.clone(),535		));536		for field in &members.fields {537			evaluate_field_member_unbound(&mut builder, ctx.clone(), uctx.clone(), field)?;538		}539		if let Some(asserts_block) = &members.asserts {540			builder.assert(evaluate_object_assertions_unbound(541				uctx,542				asserts_block.clone(),543			));544		}545	} else {546		let a_ctx = ctx547			.pack_captures_sup_this(&members.frame_shape)548			.enter(|fill, ctx| {549				fill_letrec_binds(fill, &ctx, &members.locals);550			});551		for field in &members.fields {552			evaluate_field_member_static(&mut builder, ctx.clone(), a_ctx.clone(), field)?;553		}554		if let Some(asserts_block) = &members.asserts {555			builder.assert(evaluate_object_assertions_static(556				a_ctx,557				asserts_block.clone(),558			));559		}560	}561562	Ok(Val::Obj(builder.build()))563}564565pub fn evaluate_assert(ctx: Context, assertion: &LAssertStmt) -> Result<()> {566	let LAssertStmt { cond, message } = assertion;567	let assertion_result = in_frame(568		CallLocation::new(&cond.span),569		|| "assertion condition".to_owned(),570		|| bool::from_untyped(evaluate(ctx.clone(), cond)?),571	)?;572	if !assertion_result {573		in_frame(574			CallLocation::new(&cond.span),575			|| "assertion failure".to_owned(),576			|| {577				if let Some(msg) = message {578					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));579				}580				bail!(AssertionFailed(Val::Null.to_string()?));581			},582		)?;583	}584	Ok(())585}586587fn evaluate_object_assertions_unbound<B: Unbound<Bound = Context>>(588	uctx: B,589	asserts: Rc<LObjAsserts>,590) -> impl ObjectAssertion {591	#[derive(Trace)]592	struct ObjectAssert<B: Trace> {593		uctx: B,594		asserts: Rc<LObjAsserts>,595	}596	impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {597		fn run(&self, sup_this: SupThis) -> Result<()> {598			let a_ctx = self.uctx.bind(sup_this)?;599			let assert_env = Context::enter_using(&a_ctx, &self.asserts.shape);600			for assert in &self.asserts.asserts {601				evaluate_assert(assert_env.clone(), assert)?;602			}603			Ok(())604		}605	}606	ObjectAssert { uctx, asserts }607}608fn evaluate_object_assertions_static(609	a_ctx: Context,610	asserts: Rc<LObjAsserts>,611) -> impl ObjectAssertion {612	#[derive(Trace)]613	struct ObjectAssert {614		assert_env: Context,615		asserts: Rc<LObjAsserts>,616	}617	impl ObjectAssertion for ObjectAssert {618		fn run(&self, _sup_this: SupThis) -> Result<()> {619			for assert in &self.asserts.asserts {620				evaluate_assert(self.assert_env.clone(), assert)?;621			}622			Ok(())623		}624	}625	let assert_env = Context::enter_using(&a_ctx, &asserts.shape);626	ObjectAssert {627		assert_env,628		asserts,629	}630}
modifiedcrates/jrsonnet-evaluator/src/function/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/mod.rs
+++ b/crates/jrsonnet-evaluator/src/function/mod.rs
@@ -11,9 +11,12 @@
 	prepared::{PreparedCall, parse_prepared_builtin_call},
 };
 use crate::{
-	Context, ContextBuilder, Result, Thunk, Val,
-	analyze::{LDestruct, LExpr, LFunction},
-	evaluate::{destructure::destruct, ensure_sufficient_stack, evaluate, evaluate_trivial},
+	PackedContextSupThis, Result, Thunk, Val,
+	analyze::LFunction,
+	evaluate::{
+		destructure::{build_b_thunk, destruct},
+		ensure_sufficient_stack, evaluate, evaluate_trivial,
+	},
 	function::builtin::BuiltinFunc,
 };
 
@@ -56,16 +59,7 @@
 	/// { a() = ... }
 	/// ```
 	pub name: IStr,
-	/// Context, in which this function was evaluated.
-	///
-	/// # Example
-	/// In
-	/// ```jsonnet
-	/// local a = 2;
-	/// function() ...
-	/// ```
-	/// context will contain `a`.
-	pub ctx: Context,
+	pub(crate) body_captures: PackedContextSupThis,
 
 	#[educe(PartialEq(method = Rc::ptr_eq))]
 	pub func: Rc<LFunction>,
@@ -82,44 +76,34 @@
 		named: &[Thunk<Val>],
 		prepared: &PreparedCall,
 	) -> Result<Val> {
-		let has_defaults = !prepared.defaults().is_empty();
-		let mut builder = ContextBuilder::extend(self.ctx.clone(), self.func.params.len());
-
-		let fctx = Context::new_future();
-		for (param_idx, thunk) in unnamed.iter().enumerate() {
-			destruct(
-				&self.func.params[param_idx].destruct,
-				thunk.clone(),
-				fctx.clone(),
-				&mut builder,
-			);
-		}
-
-		for &(param_idx, arg_idx) in prepared.named() {
-			destruct(
-				&self.func.params[param_idx].destruct,
-				named[arg_idx].clone(),
-				fctx.clone(),
-				&mut builder,
-			);
-		}
+		let body_ctx = self.body_captures.clone().enter(|fill, ctx| {
+			// Place each provided arg-thunk into its destructured slots.
+			for (param_idx, thunk) in unnamed.iter().enumerate() {
+				destruct(
+					&self.func.params[param_idx].destruct,
+					fill,
+					thunk.clone(),
+					&ctx,
+				);
+			}
+			for &(param_idx, arg_idx) in prepared.named() {
+				destruct(
+					&self.func.params[param_idx].destruct,
+					fill,
+					named[arg_idx].clone(),
+					&ctx,
+				);
+			}
 
-		if has_defaults {
 			for &param_idx in prepared.defaults() {
 				let param = &self.func.params[param_idx];
-				if let Some(default_expr) = &param.default {
-					let default_expr = default_expr.clone();
-					let fctxc = fctx.clone();
-					let thunk = Thunk!(move || {
-						let ctx = fctxc.unwrap();
-						evaluate(ctx, &default_expr)
-					});
-					destruct(&param.destruct, thunk, fctx.clone(), &mut builder);
-				}
+				let (shape, expr) = param.default.as_ref().expect("default exists");
+				let thunk = build_b_thunk(&ctx, shape, expr.clone());
+				destruct(&param.destruct, fill, thunk, &ctx);
 			}
-		};
-		let ctx = builder.build().into_future(fctx);
-		ensure_sufficient_stack(|| evaluate(ctx, &self.func.body))
+		});
+
+		ensure_sufficient_stack(|| evaluate(body_ctx, &self.func.body))
 	}
 
 	pub fn evaluate_trivial(&self) -> Option<Val> {
@@ -157,6 +141,10 @@
 		Self::Builtin(BuiltinFunc::new(builtin))
 	}
 
+	pub fn identity() -> Self {
+		Self::builtin(builtin_id {})
+	}
+
 	pub fn params(&self) -> FunctionSignature {
 		match self {
 			Self::Builtin(i) => i.params(),
@@ -193,27 +181,12 @@
 	}
 
 	/// Is this function an identity function.
-	///
-	/// Currently only works for builtin `std.id`, aka `Self::Id` value, and `function(x) x`.
 	///
 	/// This function should only be used for optimization, not for the conditional logic, i.e code should work with syntetic identity function too
 	pub fn is_identity(&self) -> bool {
 		match self {
 			Self::Builtin(b) => b.as_any().downcast_ref::<builtin_id>().is_some(),
-			Self::Normal(desc) => {
-				if desc.func.params.len() != 1 {
-					return false;
-				}
-				let param = &desc.func.params[0];
-				if param.default.is_some() {
-					return false;
-				}
-				#[allow(irrefutable_let_patterns, reason = "refutable with exp-destruct")]
-				let LDestruct::Full(id) = &param.destruct else {
-					return false;
-				};
-				matches!(&*desc.func.body, LExpr::Local(v) if v == id)
-			}
+			Self::Normal(_) => false,
 		}
 	}
 
modifiedcrates/jrsonnet-evaluator/src/function/prepared.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/prepared.rs
+++ b/crates/jrsonnet-evaluator/src/function/prepared.rs
@@ -46,6 +46,12 @@
 	pub fn defaults(&self) -> &[usize] {
 		&self.defaults
 	}
+	pub const fn empty() -> Self {
+		Self {
+			named: Vec::new(),
+			defaults: Vec::new(),
+		}
+	}
 }
 
 pub fn prepare_call(
modifiedcrates/jrsonnet-evaluator/src/integrations/serde.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/integrations/serde.rs
+++ b/crates/jrsonnet-evaluator/src/integrations/serde.rs
@@ -12,7 +12,7 @@
 };
 
 use crate::{
-	Error as JrError, ObjValue, ObjValueBuilder, Result, Val, in_description_frame, runtime_error,
+	Error as JrError, ObjValue, ObjValueBuilder, Result, Val, error, in_description_frame,
 };
 
 impl<'de> Deserialize<'de> for Val {
@@ -629,6 +629,6 @@
 	where
 		T: std::fmt::Display,
 	{
-		runtime_error!("serde: {msg}")
+		error!("serde: {msg}")
 	}
 }
modifiedcrates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/lib.rs
+++ b/crates/jrsonnet-evaluator/src/lib.rs
@@ -58,6 +58,7 @@
 pub use val::{Thunk, Val};
 
 pub mod analyze;
+use self::analyze::{LExpr, analyze_root};
 use crate::gc::WithCapacityExt as _;
 
 #[allow(clippy::needless_return)]
@@ -408,12 +409,15 @@
 		file.evaluating = true;
 		// Dropping file cache guard here, as evaluation may use this map too
 		drop(file_cache);
-		let (ctx, externals) = self.create_default_context(file_name.clone()).build();
-		let report = analyze::analyze_root(&parsed, externals);
+		let (externals, thunks) = self.create_default_context(file_name).build();
+		let report = analyze_root(&parsed, externals);
 		if report.errored {
 			return Err(StaticAnalysisError(report.diagnostics_list).into());
 		}
-		let res = evaluate::evaluate(ctx.build(), &report.lir);
+		debug_assert_eq!(report.root_shape.n_locals as usize, thunks.len());
+		debug_assert!(report.root_shape.captures.is_empty());
+		let ctx = Context::root(thunks);
+		let res = evaluate::evaluate(ctx, &report.lir);
 
 		let mut file_cache = self.file_cache();
 		let mut file = file_cache.entry(path);
@@ -501,33 +505,66 @@
 	}
 }
 
+pub struct PreparedSnippet {
+	lir: LExpr,
+	thunks: Vec<Thunk<Val>>,
+}
+
 /// Raw methods evaluate passed values but don't perform TLA execution
 impl State {
-	/// Parses and evaluates the given snippet
-	pub fn evaluate_snippet(&self, name: impl Into<IStr>, code: impl Into<IStr>) -> Result<Val> {
-		self.evaluate_snippet_with(name, code, &())
-	}
-	/// Parses and evaluates the given snippet with custom context modifier
-	pub fn evaluate_snippet_with(
+	/// Parses and analyses the given snippet with a custom context
+	/// modifier.
+	pub fn prepare_snippet_with(
 		&self,
 		name: impl Into<IStr>,
 		code: impl Into<IStr>,
 		context_initializer: &dyn ContextInitializer,
-	) -> Result<Val> {
+	) -> Result<PreparedSnippet> {
 		let code = code.into();
 		let source = Source::new_virtual(name.into(), code.clone());
 		let parsed = parse_jsonnet(&code, source.clone()).map_err(|e| ImportSyntaxError {
 			path: source.clone(),
 			error: Box::new(e),
 		})?;
-		let (ctx, externals) = self
-			.create_default_context_with(source.clone(), context_initializer)
+		let (externals, thunks) = self
+			.create_default_context_with(source, context_initializer)
 			.build();
-		let report = analyze::analyze_root(&parsed, externals);
+		let report = analyze_root(&parsed, externals);
 		if report.errored {
 			return Err(StaticAnalysisError(report.diagnostics_list).into());
 		}
-		evaluate::evaluate(ctx.build(), &report.lir)
+		debug_assert_eq!(report.root_shape.n_locals as usize, thunks.len());
+		debug_assert!(report.root_shape.captures.is_empty());
+		Ok(PreparedSnippet {
+			lir: report.lir,
+			thunks,
+		})
+	}
+	/// Parses and analyses the given snippet
+	pub fn prepare_snippet(
+		&self,
+		name: impl Into<IStr>,
+		code: impl Into<IStr>,
+	) -> Result<PreparedSnippet> {
+		self.prepare_snippet_with(name, code, &())
+	}
+	pub fn evaluate_prepared_snippet(&self, prepared: &PreparedSnippet) -> Result<Val> {
+		let ctx = Context::root(prepared.thunks.clone());
+		evaluate::evaluate(ctx, &prepared.lir)
+	}
+	/// Parses and evaluates the given snippet with custom context modifier
+	pub fn evaluate_snippet_with(
+		&self,
+		name: impl Into<IStr>,
+		code: impl Into<IStr>,
+		context_initializer: &dyn ContextInitializer,
+	) -> Result<Val> {
+		let prepared = self.prepare_snippet_with(name, code, context_initializer)?;
+		self.evaluate_prepared_snippet(&prepared)
+	}
+	/// Parses and evaluates the given snippet
+	pub fn evaluate_snippet(&self, name: impl Into<IStr>, code: impl Into<IStr>) -> Result<Val> {
+		self.evaluate_snippet_with(name, code, &())
 	}
 }
 
modifiedtests/benches/cpp_test_suite.rsdiffbeforeafterboth
--- a/tests/benches/cpp_test_suite.rs
+++ b/tests/benches/cpp_test_suite.rs
@@ -1,10 +1,8 @@
-use std::{collections::HashMap, fs::read_dir, hint::black_box, path::Path};
+use std::{collections::HashMap, fs, fs::read_dir, hint::black_box, path::Path};
 
 use criterion::{Criterion, criterion_group, criterion_main};
 use jrsonnet_evaluator::{
-	FileImportResolver, State, apply_tla,
-	manifest::{BlackBoxFormat, JsonFormat},
-	stack::limit_stack_depth,
+	FileImportResolver, State, apply_tla, manifest::JsonFormat, stack::limit_stack_depth,
 	trace::PathResolver,
 };
 
@@ -12,31 +10,37 @@
 static GLOBAL: mimallocator::Mimalloc = mimallocator::Mimalloc;
 
 fn bench_entry(c: &mut Criterion, path: &Path) {
-	c.bench_function(
-		path.file_name()
-			.expect("file path")
-			.to_str()
-			.expect("name is utf-8"),
-		|b| {
-			let _stack = limit_stack_depth(200_000);
+	let name = path
+		.file_name()
+		.expect("file path")
+		.to_str()
+		.expect("name is utf-8")
+		.to_owned();
+	let code = fs::read_to_string(path).expect("read bench source");
 
-			let mut s = State::builder();
+	c.bench_function(&name, |b| {
+		let _stack = limit_stack_depth(200_000);
 
-			s.context_initializer(jrsonnet_stdlib::ContextInitializer::new(
-				PathResolver::Absolute,
-			))
-			.import_resolver(FileImportResolver::new(vec![]));
+		let mut s = State::builder();
+		s.context_initializer(jrsonnet_stdlib::ContextInitializer::new(
+			PathResolver::Absolute,
+		))
+		.import_resolver(FileImportResolver::new(vec![]));
+		let s = s.build();
+		let _entered = s.enter();
 
-			let s = s.build();
-			let _s = s.enter();
+		// Parse + analysis happen once; each iter only measures
+		// evaluation + manifestation.
+		let prepared = s
+			.prepare_snippet(name.clone(), code.clone())
+			.expect("prepared");
 
-			b.iter(|| {
-				let imported = s.import(path).expect("evaluated");
-				let res = apply_tla(&HashMap::new(), imported).expect("tla applied");
-				black_box(res.manifest(JsonFormat::cli(3)).expect("manifested"));
-			});
-		},
-	);
+		b.iter(|| {
+			let imported = s.evaluate_prepared_snippet(&prepared).expect("evaluated");
+			let res = apply_tla(&HashMap::new(), imported).expect("tla applied");
+			black_box(res.manifest(JsonFormat::cli(3)).expect("manifested"));
+		});
+	});
 }
 fn criterion_benchmark(c: &mut Criterion) {
 	for entry in read_dir("go_builtin_benchmarks").expect("dir exists") {