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

difftreelog

refactor(evaluator) use static analysis

qpqzvqtlYaroslav Bolyukin2026-04-25parent: #1979dbd.patch.diff
in: master

31 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -815,9 +815,11 @@
 dependencies = [
  "annotate-snippets",
  "anyhow",
+ "drop_bomb",
  "educe",
  "hi-doc",
  "im-rc",
+ "insta",
  "jrsonnet-gcmodule",
  "jrsonnet-interner",
  "jrsonnet-ir",
@@ -830,8 +832,10 @@
  "rustc-hash 2.1.2",
  "rustversion",
  "serde",
+ "smallvec 1.15.1",
  "stacker",
  "static_assertions",
+ "strip-ansi-escapes",
  "strsim",
  "thiserror",
 ]
@@ -899,6 +903,7 @@
  "jrsonnet-interner",
  "peg",
  "static_assertions",
+ "thiserror",
 ]
 
 [[package]]
@@ -960,7 +965,6 @@
  "base64",
  "jrsonnet-evaluator",
  "jrsonnet-gcmodule",
- "jrsonnet-ir",
  "jrsonnet-macros",
  "lru",
  "md5",
modifiedbindings/jsonnet/src/val_modify.rsdiffbeforeafterboth
--- a/bindings/jsonnet/src/val_modify.rs
+++ b/bindings/jsonnet/src/val_modify.rs
@@ -4,7 +4,7 @@
 
 use std::{ffi::CStr, os::raw::c_char};
 
-use jrsonnet_evaluator::{Thunk, Val, val::ArrValue};
+use jrsonnet_evaluator::{Thunk, Val};
 
 use crate::VM;
 
modifiedcrates/jrsonnet-evaluator/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/Cargo.toml
+++ b/crates/jrsonnet-evaluator/Cargo.toml
@@ -77,6 +77,12 @@
   "PartialEq",
 ] }
 im-rc = { version = "15.1.0", features = ["pool"] }
+smallvec = "1.15.1"
+drop_bomb.workspace = true
 
 [build-dependencies]
 rustversion = "1.0.22"
+
+[dev-dependencies]
+insta.workspace = true
+strip-ansi-escapes = "0.2.1"
modifiedcrates/jrsonnet-evaluator/src/arr/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/mod.rs
+++ b/crates/jrsonnet-evaluator/src/arr/mod.rs
@@ -5,10 +5,9 @@
 	rc::Rc,
 };
 
-use jrsonnet_gcmodule::{Cc, cc_dyn};
-use jrsonnet_ir::Expr;
+use jrsonnet_gcmodule::{cc_dyn, Cc};
 
-use crate::{Context, Result, Thunk, Val, function::NativeFn, typed::IntoUntyped};
+use crate::{analyze::LExpr, function::NativeFn, typed::IntoUntyped, Context, Result, Thunk, Val};
 
 mod spec;
 pub use spec::{ArrayLike, *};
@@ -37,14 +36,18 @@
 		Self::new(())
 	}
 
-	pub fn expr(ctx: Context, exprs: Rc<Vec<Expr>>) -> Self {
+	pub fn expr(ctx: Context, exprs: Rc<Vec<LExpr>>) -> Self {
 		Self::new(ExprArray::new(ctx, exprs))
 	}
 
-	pub fn repeated(data: Self, repeats: usize) -> Option<Self> {
+	pub fn repeated(data: Self, repeats: u32) -> Option<Self> {
 		Some(Self::new(RepeatedArray::new(data, repeats)?))
 	}
 
+	pub fn make(len: u32, cb: NativeFn!((u32,)->Val)) -> Self {
+		Self::new(MakeArray::new(len, cb))
+	}
+
 	#[must_use]
 	pub fn map(self, mapper: NativeFn!((Val) -> Val)) -> Self {
 		Self::new(<MappedArray>::new(self, ArrayMapper::Plain(mapper)))
@@ -79,14 +82,14 @@
 		Ok(Self::new(out))
 	}
 
-	pub fn extended(a: Self, b: Self) -> Self {
-		if a.is_empty() {
+	pub fn extended(a: Self, b: Self) -> Option<Self> {
+		Some(if a.is_empty() {
 			b
 		} else if b.is_empty() {
 			a
 		} else {
-			Self::new(ExtendedArray::new(a, b))
-		}
+			Self::new(ExtendedArray::new(a, b)?)
+		})
 	}
 
 	pub fn range_exclusive(a: i32, b: i32) -> Self {
@@ -98,14 +101,14 @@
 
 	#[must_use]
 	pub fn slice(self, index: Option<i32>, end: Option<i32>, step: Option<NonZeroU32>) -> Self {
-		let get_idx = |pos: Option<i32>, len: usize, default| match pos {
+		let get_idx = |pos: Option<i32>, len: u32, default| match pos {
 			#[expect(
 				clippy::cast_sign_loss,
 				reason = "abs value is used, len is limited to u31"
 			)]
-			Some(v) if v < 0 => len.saturating_sub((-v) as usize),
+			Some(v) if v < 0 => len.saturating_add_signed(v),
 			#[expect(clippy::cast_sign_loss, reason = "abs value is used")]
-			Some(v) => (v as usize).min(len),
+			Some(v) => (v as u32).min(len),
 			None => default,
 		};
 		let index = get_idx(index, self.len(), 0);
@@ -127,7 +130,7 @@
 	}
 
 	/// Array length.
-	pub fn len(&self) -> usize {
+	pub fn len(&self) -> u32 {
 		self.0.len()
 	}
 
@@ -143,14 +146,14 @@
 	/// Get array element by index, evaluating it, if it is lazy.
 	///
 	/// Returns `None` on out-of-bounds condition.
-	pub fn get(&self, index: usize) -> Result<Option<Val>> {
+	pub fn get(&self, index: u32) -> Result<Option<Val>> {
 		self.0.get(index)
 	}
 
 	/// Get array element by index, without evaluation.
 	///
 	/// Returns `None` on out-of-bounds condition.
-	pub fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
+	pub fn get_lazy(&self, index: u32) -> Option<Thunk<Val>> {
 		self.0.get_lazy(index)
 	}
 
modifiedcrates/jrsonnet-evaluator/src/arr/spec.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/spec.rs
+++ b/crates/jrsonnet-evaluator/src/arr/spec.rs
@@ -8,47 +8,47 @@
 
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::{IBytes, IStr};
-use jrsonnet_ir::Expr;
 
 use super::ArrValue;
 use crate::{
-	Context, Error, ObjValue, Result, Thunk, Val,
+	analyze::LExpr,
 	error::ErrorKind::InfiniteRecursionDetected,
-	evaluate,
+	evaluate::evaluate,
 	function::NativeFn,
 	typed::{IntoUntyped, Typed},
 	val::ThunkValue,
+	Context, Error, ObjValue, Result, Thunk, Val,
 };
 
 pub trait ArrayLike: Any + Trace + Debug {
-	fn len(&self) -> usize;
+	fn len(&self) -> u32;
 	fn is_empty(&self) -> bool {
 		self.len() == 0
 	}
-	fn get(&self, index: usize) -> Result<Option<Val>>;
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>>;
+	fn get(&self, index: u32) -> Result<Option<Val>>;
+	fn get_lazy(&self, index: u32) -> Option<Thunk<Val>>;
 
 	fn is_cheap(&self) -> bool {
 		false
 	}
 }
 trait ArrayCheap {
-	fn get(&self, index: usize) -> Option<Val>;
-	fn len(&self) -> usize;
+	fn get(&self, index: u32) -> Option<Val>;
+	fn len(&self) -> u32;
 }
 impl<T> ArrayLike for T
 where
 	T: Any + Trace + Debug + ArrayCheap,
 {
-	fn len(&self) -> usize {
+	fn len(&self) -> u32 {
 		<T as ArrayCheap>::len(self)
 	}
 
-	fn get(&self, index: usize) -> Result<Option<Val>> {
+	fn get(&self, index: u32) -> Result<Option<Val>> {
 		Ok(<T as ArrayCheap>::get(self, index))
 	}
 
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
+	fn get_lazy(&self, index: u32) -> Option<Thunk<Val>> {
 		<T as ArrayCheap>::get(self, index).map(Thunk::evaluated)
 	}
 
@@ -58,10 +58,10 @@
 }
 
 impl ArrayCheap for () {
-	fn len(&self) -> usize {
+	fn len(&self) -> u32 {
 		0
 	}
-	fn get(&self, _index: usize) -> Option<Val> {
+	fn get(&self, _index: u32) -> Option<Val> {
 		None
 	}
 }
@@ -75,20 +75,20 @@
 }
 
 impl SliceArray {
-	fn map_idx(&self, index: usize) -> usize {
-		self.from as usize + self.step as usize * index
+	fn map_idx(&self, index: u32) -> u32 {
+		self.from + self.step * index
 	}
 }
 impl ArrayLike for SliceArray {
-	fn len(&self) -> usize {
-		(self.to - self.from).div_ceil(self.step) as usize
+	fn len(&self) -> u32 {
+		(self.to - self.from).div_ceil(self.step)
 	}
 
-	fn get(&self, index: usize) -> Result<Option<Val>> {
+	fn get(&self, index: u32) -> Result<Option<Val>> {
 		self.inner.get(self.map_idx(index))
 	}
 
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
+	fn get_lazy(&self, index: u32) -> Option<Thunk<Val>> {
 		self.inner.get_lazy(self.map_idx(index))
 	}
 
@@ -98,11 +98,13 @@
 }
 
 impl ArrayCheap for IBytes {
-	fn len(&self) -> usize {
-		self.as_slice().len()
+	fn len(&self) -> u32 {
+		self.as_slice().len() as u32
 	}
-	fn get(&self, index: usize) -> Option<Val> {
-		self.as_slice().get(index).map(|v| Val::Num((*v).into()))
+	fn get(&self, index: u32) -> Option<Val> {
+		self.as_slice()
+			.get(index as usize)
+			.map(|v| Val::Num((*v).into()))
 	}
 }
 
@@ -117,11 +119,11 @@
 #[derive(Debug, Trace, Clone)]
 pub struct ExprArray {
 	ctx: Context,
-	src: Rc<Vec<Expr>>,
+	src: Rc<Vec<LExpr>>,
 	cached: Cc<RefCell<Vec<ArrayThunk>>>,
 }
 impl ExprArray {
-	pub fn new(ctx: Context, src: Rc<Vec<Expr>>) -> Self {
+	pub fn new(ctx: Context, src: Rc<Vec<LExpr>>) -> Self {
 		Self {
 			ctx,
 			cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; src.len()])),
@@ -130,41 +132,36 @@
 	}
 }
 impl ArrayLike for ExprArray {
-	fn len(&self) -> usize {
-		self.cached.borrow().len()
+	fn len(&self) -> u32 {
+		self.cached.borrow().len() as u32
 	}
-	fn get(&self, index: usize) -> Result<Option<Val>> {
+	fn get(&self, index: u32) -> Result<Option<Val>> {
 		if index >= self.len() {
 			return Ok(None);
 		}
-		match &self.cached.borrow()[index] {
+		match &self.cached.borrow()[index as usize] {
 			ArrayThunk::Computed(c) => return Ok(Some(c.clone())),
 			ArrayThunk::Errored(e) => return Err(e.clone()),
 			ArrayThunk::Pending => return Err(InfiniteRecursionDetected.into()),
 			ArrayThunk::Waiting => {}
 		}
 
-		let ArrayThunk::Waiting =
-			replace(&mut self.cached.borrow_mut()[index], ArrayThunk::Pending)
-		else {
+		let ArrayThunk::Waiting = replace(
+			&mut self.cached.borrow_mut()[index as usize],
+			ArrayThunk::Pending,
+		) else {
 			unreachable!()
 		};
 
-		let new_value = match evaluate(self.ctx.clone(), &self.src[index]) {
-			Ok(v) => v,
-			Err(e) => {
-				self.cached.borrow_mut()[index] = ArrayThunk::Errored(e.clone());
-				return Err(e);
-			}
-		};
-		self.cached.borrow_mut()[index] = ArrayThunk::Computed(new_value.clone());
+		let new_value: Val = evaluate(self.ctx.clone(), &self.src[index as usize])?;
+		self.cached.borrow_mut()[index as usize] = ArrayThunk::Computed(new_value.clone());
 		Ok(Some(new_value))
 	}
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
+	fn get_lazy(&self, index: u32) -> Option<Thunk<Val>> {
 		#[derive(Trace)]
 		struct ExprArrThunk {
 			expr: ExprArray,
-			index: usize,
+			index: u32,
 		}
 		impl ThunkValue for ExprArrThunk {
 			type Output = Val;
@@ -180,7 +177,7 @@
 		if index >= self.len() {
 			return None;
 		}
-		match &self.cached.borrow()[index] {
+		match &self.cached.borrow()[index as usize] {
 			ArrayThunk::Computed(c) => return Some(Thunk::evaluated(c.clone())),
 			ArrayThunk::Errored(e) => return Some(Thunk::errored(e.clone())),
 			ArrayThunk::Waiting | ArrayThunk::Pending => {}
@@ -200,19 +197,20 @@
 pub struct ExtendedArray {
 	pub a: ArrValue,
 	pub b: ArrValue,
-	split: usize,
-	len: usize,
+	split: u32,
+	len: u32,
 }
 impl ExtendedArray {
-	pub fn new(a: ArrValue, b: ArrValue) -> Self {
+	pub fn new(a: ArrValue, b: ArrValue) -> Option<Self> {
 		let a_len = a.len();
 		let b_len = b.len();
-		Self {
+		let len = a_len.checked_add(b_len)?;
+		Some(Self {
 			a,
 			b,
 			split: a_len,
-			len: a_len.checked_add(b_len).expect("too large array value"),
-		}
+			len,
+		})
 	}
 }
 
@@ -253,14 +251,14 @@
 	}
 }
 impl ArrayLike for ExtendedArray {
-	fn get(&self, index: usize) -> Result<Option<Val>> {
+	fn get(&self, index: u32) -> Result<Option<Val>> {
 		if self.split > index {
 			self.a.get(index)
 		} else {
 			self.b.get(index - self.split)
 		}
 	}
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
+	fn get_lazy(&self, index: u32) -> Option<Thunk<Val>> {
 		if self.split > index {
 			self.a.get_lazy(index)
 		} else {
@@ -268,7 +266,7 @@
 		}
 	}
 
-	fn len(&self) -> usize {
+	fn len(&self) -> u32 {
 		self.len
 	}
 
@@ -282,19 +280,19 @@
 	T: IntoUntyped + Trace + fmt::Debug,
 	for<'a> &'a T: IntoUntyped,
 {
-	fn len(&self) -> usize {
-		self.as_slice().len()
+	fn len(&self) -> u32 {
+		self.as_slice().len().try_into().unwrap_or(u32::MAX)
 	}
 
-	fn get(&self, index: usize) -> Result<Option<Val>> {
-		let Some(elem) = self.as_slice().get(index) else {
+	fn get(&self, index: u32) -> Result<Option<Val>> {
+		let Some(elem) = self.as_slice().get(index as usize) else {
 			return Ok(None);
 		};
 		IntoUntyped::into_untyped(elem).map(Some)
 	}
 
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
-		let elem = self.as_slice().get(index)?;
+	fn get_lazy(&self, index: u32) -> Option<Thunk<Val>> {
+		let elem = self.as_slice().get(index as usize)?;
 		Some(IntoUntyped::into_lazy_untyped(elem))
 	}
 
@@ -324,20 +322,20 @@
 		clippy::cast_sign_loss,
 		reason = "the math is valid with wrapping, sign loss works as intended"
 	)]
-	fn size(&self) -> usize {
-		(self.end as usize)
-			.wrapping_sub(self.start as usize)
+	fn size(&self) -> u32 {
+		(self.end as u32)
+			.wrapping_sub(self.start as u32)
 			.wrapping_add(1)
 	}
 	fn range(&self) -> impl ExactSizeIterator<Item = i32> + DoubleEndedIterator {
-		WithExactSize(self.start..=self.end, self.size())
+		WithExactSize(self.start..=self.end, self.size() as usize)
 	}
 }
 impl ArrayCheap for RangeArray {
-	fn get(&self, index: usize) -> Option<Val> {
-		self.range().nth(index).map(|i| Val::Num(i.into()))
+	fn get(&self, index: u32) -> Option<Val> {
+		self.range().nth(index as usize).map(|i| Val::Num(i.into()))
 	}
-	fn len(&self) -> usize {
+	fn len(&self) -> u32 {
 		self.size()
 	}
 }
@@ -345,15 +343,15 @@
 #[derive(Debug, Trace)]
 pub struct ReverseArray(pub ArrValue);
 impl ArrayLike for ReverseArray {
-	fn len(&self) -> usize {
+	fn len(&self) -> u32 {
 		self.0.len()
 	}
 
-	fn get(&self, index: usize) -> Result<Option<Val>> {
+	fn get(&self, index: u32) -> Result<Option<Val>> {
 		self.0.get(self.0.len() - index - 1)
 	}
 
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
+	fn get_lazy(&self, index: u32) -> Option<Thunk<Val>> {
 		self.0.get_lazy(self.0.len() - index - 1)
 	}
 
@@ -379,40 +377,37 @@
 		let len = inner.len();
 		Self {
 			inner,
-			cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; len])),
+			cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; len as usize])),
 			mapper,
 		}
 	}
-	fn evaluate(&self, index: usize, value: Val) -> Result<Val> {
+	fn evaluate(&self, index: u32, value: Val) -> Result<Val> {
 		match &self.mapper {
 			ArrayMapper::Plain(f) => f.call(value),
-			#[expect(
-				clippy::cast_possible_truncation,
-				reason = "array len is limited to u31"
-			)]
-			ArrayMapper::WithIndex(f) => f.call(index as u32, value),
+			ArrayMapper::WithIndex(f) => f.call(index, value),
 		}
 	}
 }
 impl ArrayLike for MappedArray {
-	fn len(&self) -> usize {
-		self.cached.borrow().len()
+	fn len(&self) -> u32 {
+		self.cached.borrow().len() as u32
 	}
 
-	fn get(&self, index: usize) -> Result<Option<Val>> {
+	fn get(&self, index: u32) -> Result<Option<Val>> {
 		if index >= self.len() {
 			return Ok(None);
 		}
-		match &self.cached.borrow()[index] {
+		match &self.cached.borrow()[index as usize] {
 			ArrayThunk::Computed(c) => return Ok(Some(c.clone())),
 			ArrayThunk::Errored(e) => return Err(e.clone()),
 			ArrayThunk::Pending => return Err(InfiniteRecursionDetected.into()),
 			ArrayThunk::Waiting => {}
 		}
 
-		let ArrayThunk::Waiting =
-			replace(&mut self.cached.borrow_mut()[index], ArrayThunk::Pending)
-		else {
+		let ArrayThunk::Waiting = replace(
+			&mut self.cached.borrow_mut()[index as usize],
+			ArrayThunk::Pending,
+		) else {
 			unreachable!()
 		};
 
@@ -426,18 +421,18 @@
 		let new_value = match val {
 			Ok(v) => v,
 			Err(e) => {
-				self.cached.borrow_mut()[index] = ArrayThunk::Errored(e.clone());
+				self.cached.borrow_mut()[index as usize] = ArrayThunk::Errored(e.clone());
 				return Err(e);
 			}
 		};
-		self.cached.borrow_mut()[index] = ArrayThunk::Computed(new_value.clone());
+		self.cached.borrow_mut()[index as usize] = ArrayThunk::Computed(new_value.clone());
 		Ok(Some(new_value))
 	}
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
+	fn get_lazy(&self, index: u32) -> Option<Thunk<Val>> {
 		#[derive(Trace)]
 		struct MappedArrayThunk {
 			arr: MappedArray,
-			index: usize,
+			index: u32,
 		}
 		impl ThunkValue for MappedArrayThunk {
 			type Output = Val;
@@ -450,7 +445,7 @@
 		if index >= self.len() {
 			return None;
 		}
-		match &self.cached.borrow()[index] {
+		match &self.cached.borrow()[index as usize] {
 			ArrayThunk::Computed(c) => return Some(Thunk::evaluated(c.clone())),
 			ArrayThunk::Errored(e) => return Some(Thunk::errored(e.clone())),
 			ArrayThunk::Waiting | ArrayThunk::Pending => {}
@@ -462,15 +457,92 @@
 		}))
 	}
 }
+#[derive(Trace, Debug, Clone)]
+pub struct MakeArray {
+	cached: Cc<RefCell<Vec<ArrayThunk>>>,
+	mapper: NativeFn!((u32,)->Val),
+}
+impl MakeArray {
+	pub fn new(len: u32, mapper: NativeFn!((u32)->Val)) -> Self {
+		Self {
+			cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; len as usize])),
+			mapper,
+		}
+	}
+}
+impl ArrayLike for MakeArray {
+	fn len(&self) -> u32 {
+		self.cached.borrow().len() as u32
+	}
+
+	fn get(&self, index: u32) -> Result<Option<Val>> {
+		if index >= self.len() {
+			return Ok(None);
+		}
+		match &self.cached.borrow()[index as usize] {
+			ArrayThunk::Computed(c) => return Ok(Some(c.clone())),
+			ArrayThunk::Errored(e) => return Err(e.clone()),
+			ArrayThunk::Pending => return Err(InfiniteRecursionDetected.into()),
+			ArrayThunk::Waiting => {}
+		}
+
+		let ArrayThunk::Waiting = replace(
+			&mut self.cached.borrow_mut()[index as usize],
+			ArrayThunk::Pending,
+		) else {
+			unreachable!()
+		};
+
+		let val = self.mapper.call(index as u32);
+
+		let new_value = match val {
+			Ok(v) => v,
+			Err(e) => {
+				self.cached.borrow_mut()[index as usize] = ArrayThunk::Errored(e.clone());
+				return Err(e);
+			}
+		};
+		self.cached.borrow_mut()[index as usize] = ArrayThunk::Computed(new_value.clone());
+		Ok(Some(new_value))
+	}
+	fn get_lazy(&self, index: u32) -> Option<Thunk<Val>> {
+		#[derive(Trace)]
+		struct MakeArrayThunk {
+			arr: MakeArray,
+			index: u32,
+		}
+		impl ThunkValue for MakeArrayThunk {
+			type Output = Val;
+
+			fn get(&self) -> Result<Self::Output> {
+				self.arr.get(self.index).transpose().expect("index checked")
+			}
+		}
+
+		if index >= self.len() {
+			return None;
+		}
+		match &self.cached.borrow()[index as usize] {
+			ArrayThunk::Computed(c) => return Some(Thunk::evaluated(c.clone())),
+			ArrayThunk::Errored(e) => return Some(Thunk::errored(e.clone())),
+			ArrayThunk::Waiting | ArrayThunk::Pending => {}
+		}
+
+		Some(Thunk::new(MakeArrayThunk {
+			arr: self.clone(),
+			index,
+		}))
+	}
+}
 
 #[derive(Trace, Debug)]
 pub struct RepeatedArray {
 	data: ArrValue,
-	repeats: usize,
-	total_len: usize,
+	repeats: u32,
+	total_len: u32,
 }
 impl RepeatedArray {
-	pub fn new(data: ArrValue, repeats: usize) -> Option<Self> {
+	pub fn new(data: ArrValue, repeats: u32) -> Option<Self> {
 		let total_len = data.len().checked_mul(repeats)?;
 		Some(Self {
 			data,
@@ -478,7 +550,7 @@
 			total_len,
 		})
 	}
-	fn map_idx(&self, index: usize) -> Option<usize> {
+	fn map_idx(&self, index: u32) -> Option<u32> {
 		if index > self.total_len {
 			return None;
 		}
@@ -487,18 +559,18 @@
 }
 
 impl ArrayLike for RepeatedArray {
-	fn len(&self) -> usize {
+	fn len(&self) -> u32 {
 		self.total_len
 	}
 
-	fn get(&self, index: usize) -> Result<Option<Val>> {
+	fn get(&self, index: u32) -> Result<Option<Val>> {
 		let Some(idx) = self.map_idx(index) else {
 			return Ok(None);
 		};
 		self.data.get(idx)
 	}
 
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
+	fn get_lazy(&self, index: u32) -> Option<Thunk<Val>> {
 		let idx = self.map_idx(index)?;
 		self.data.get_lazy(idx)
 	}
@@ -521,19 +593,19 @@
 }
 
 impl ArrayLike for PickObjectValues {
-	fn len(&self) -> usize {
-		self.keys.len()
+	fn len(&self) -> u32 {
+		self.keys.len() as u32
 	}
 
-	fn get(&self, index: usize) -> Result<Option<Val>> {
-		let Some(key) = self.keys.as_slice().get(index) else {
+	fn get(&self, index: u32) -> Result<Option<Val>> {
+		let Some(key) = self.keys.as_slice().get(index as usize) else {
 			return Ok(None);
 		};
 		Ok(Some(self.obj.get_or_bail(key.clone())?))
 	}
 
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
-		let key = self.keys.as_slice().get(index)?;
+	fn get_lazy(&self, index: u32) -> Option<Thunk<Val>> {
+		let key = self.keys.as_slice().get(index as usize)?;
 		Some(self.obj.get_lazy_or_bail(key.clone()))
 	}
 
@@ -561,12 +633,12 @@
 }
 
 impl ArrayLike for PickObjectKeyValues {
-	fn len(&self) -> usize {
-		self.keys.len()
+	fn len(&self) -> u32 {
+		self.keys.len() as u32
 	}
 
-	fn get(&self, index: usize) -> Result<Option<Val>> {
-		let Some(key) = self.keys.as_slice().get(index) else {
+	fn get(&self, index: u32) -> Result<Option<Val>> {
+		let Some(key) = self.keys.as_slice().get(index as usize) else {
 			return Ok(None);
 		};
 		Ok(Some(
@@ -578,8 +650,8 @@
 		))
 	}
 
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
-		let key = self.keys.as_slice().get(index)?;
+	fn get_lazy(&self, index: u32) -> Option<Thunk<Val>> {
+		let key = self.keys.as_slice().get(index as usize)?;
 		// Nothing can fail in the key part, yet value is still
 		// lazy-evaluated
 		Some(Thunk::evaluated(
modifiedcrates/jrsonnet-evaluator/src/ctx.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/ctx.rs
+++ b/crates/jrsonnet-evaluator/src/ctx.rs
@@ -1,58 +1,29 @@
-use std::fmt::Debug;
+use std::{clone::Clone, fmt::Debug};
 
 use educe::Educe;
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::IStr;
-use rustc_hash::{FxHashMap, FxHashSet};
 
-use crate::{
-	ObjValue, Pending, Result, SupThis, Thunk, Val, bail, error::ErrorKind::*,
-	gc::WithCapacityExt as _,
-};
-/// Context keeps information about current lexical code location
-///
-/// This information includes local variables, top-level object (`$`), current object (`this`), and super object (`super`)
+use crate::{analyze::LocalId, error, error::ErrorKind::*, Pending, Result, SupThis, Thunk, Val};
+
 #[derive(Debug, Trace, Clone, Educe)]
 #[educe(PartialEq)]
 pub struct Context(#[educe(PartialEq(method = Cc::ptr_eq))] Cc<ContextInternal>);
 
-#[derive(Debug, Trace)]
+#[derive(Debug, Trace, Clone)]
 struct ContextInternal {
-	dollar: Option<ObjValue>,
 	sup_this: Option<SupThis>,
-	bindings: FxHashMap<IStr, Thunk<Val>>,
-
-	branch_point: Option<Context>,
+	/// `bindings[i]` corresponds to `LocalId(offset + i)`.
+	bindings: Vec<Option<Thunk<Val>>>,
+	offset: u32,
+	parent: Option<Context>,
 }
+
 impl Context {
 	pub fn new_future() -> Pending<Self> {
 		Pending::new()
-	}
-
-	pub fn dollar(&self) -> Option<&ObjValue> {
-		self.0.dollar.as_ref()
 	}
 
-	pub fn try_dollar(&self) -> Result<ObjValue> {
-		self.0
-			.dollar
-			.clone()
-			.ok_or_else(|| CantUseSelfSupOutsideOfObject.into())
-	}
-
-	pub fn this(&self) -> Option<&ObjValue> {
-		self.0.sup_this.as_ref().map(SupThis::this)
-	}
-
-	pub fn try_this(&self) -> Result<ObjValue> {
-		self.0
-			.sup_this
-			.as_ref()
-			.ok_or_else(|| CantUseSelfSupOutsideOfObject.into())
-			.map(SupThis::this)
-			.cloned()
-	}
-
 	pub fn sup_this(&self) -> Option<&SupThis> {
 		self.0.sup_this.as_ref()
 	}
@@ -61,127 +32,98 @@
 		self.0
 			.sup_this
 			.clone()
-			.ok_or_else(|| CantUseSelfSupOutsideOfObject.into())
+			.ok_or_else(|| error!(CantUseSelfSupOutsideOfObject))
 	}
 
-	pub fn binding(&self, name: IStr) -> Result<Thunk<Val>> {
-		use std::cmp::Ordering;
-
-		use crate::bail;
+	/// 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));
 
-		if let Some(val) = self.0.bindings.get(&name).cloned() {
-			return Ok(val);
-		}
-
-		if let Some(branch_point) = &self.0.branch_point {
-			return branch_point.binding(name);
-		}
+		self.0.update_with(|inner| {
+			let local_idx = (id.0 - inner.offset) as usize;
+			while inner.bindings.len() <= local_idx {
+				inner.bindings.push(None);
+			}
+			inner.bindings[local_idx] = value.take().expect("called once");
+		});
+	}
 
-		let mut heap = Vec::new();
-		for k in self.0.bindings.keys() {
-			let conf = strsim::jaro_winkler(k as &str, &name as &str);
-			if conf < 0.8 {
-				continue;
+	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());
 			}
-			heap.push((conf, k.clone()));
 		}
-		heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
+		if let Some(parent) = &self.0.parent {
+			return parent.binding(id);
+		}
+		None
+	}
 
-		bail!(VariableIsNotDefined(
-			name,
-			heap.into_iter().map(|(_, k)| k).collect()
-		))
-	}
-	pub fn contains_binding(&self, name: IStr) -> bool {
-		self.0.bindings.contains_key(&name)
-	}
 	#[must_use]
 	pub fn into_future(self, ctx: Pending<Self>) -> Self {
 		{
 			ctx.clone().fill(self);
 		}
 		ctx.unwrap()
-	}
-
-	#[must_use]
-	pub fn branch_point(self) -> Self {
-		if self.0.bindings.is_empty() {
-			self
-		} else {
-			ContextBuilder::extend(self).build()
-		}
 	}
 }
 
 #[derive(Clone)]
 pub struct ContextBuilder {
-	dollar: Option<ObjValue>,
 	sup_this: Option<SupThis>,
-	bindings: FxHashMap<IStr, Thunk<Val>>,
-	filled: FxHashSet<IStr>,
-	branch_point: Option<Context>,
+	bindings: Vec<Option<Thunk<Val>>>,
+	offset: u32,
+	parent: Option<Context>,
 }
 
 impl ContextBuilder {
 	pub fn new() -> Self {
 		Self {
-			dollar: None,
 			sup_this: None,
-			bindings: FxHashMap::new(),
-			filled: FxHashSet::new(),
-			branch_point: None,
+			bindings: Vec::new(),
+			offset: 0,
+			parent: None,
 		}
 	}
 
-	pub fn extend_fast(parent: Context) -> Self {
+	pub(crate) fn extend(parent: Context, capacity: usize) -> Self {
+		let offset = parent.0.offset + parent.0.bindings.len() as u32;
 		Self {
-			dollar: parent.0.dollar.clone(),
 			sup_this: parent.0.sup_this.clone(),
-			bindings: parent.0.bindings.clone(),
-			filled: FxHashSet::new(),
-			branch_point: parent.0.branch_point.clone(),
+			bindings: Vec::with_capacity(capacity),
+			offset,
+			parent: Some(parent),
 		}
 	}
 
-	pub fn extend(parent: Context) -> Self {
-		Self {
-			dollar: parent.0.dollar.clone(),
-			sup_this: parent.0.sup_this.clone(),
-			bindings: FxHashMap::new(),
-			filled: FxHashSet::new(),
-			branch_point: Some(parent.clone()),
+	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);
 		}
+		self.bindings[local_idx] = Some(value);
 	}
 
-	pub fn bind(&mut self, name: impl Into<IStr>, value: Thunk<Val>) {
-		let _ = self.bindings.insert(name.into(), value);
-	}
-	/// After commit, binds would shadow the previous declarations
-	#[must_use]
-	pub fn commit(mut self) -> Self {
-		self.filled.clear();
-		self
-	}
-	pub fn try_bind(&mut self, name: impl Into<IStr>, value: Thunk<Val>) -> Result<()> {
-		let name = name.into();
-		if !self.filled.insert(name.clone()) {
-			bail!(DuplicateLocalVar(name))
-		}
-		self.bind(name, value);
-		Ok(())
-	}
-	pub fn build(self) -> Context {
+	pub(crate) fn build(self) -> Context {
 		Context(Cc::new(ContextInternal {
-			dollar: self.dollar,
 			sup_this: self.sup_this,
 			bindings: self.bindings,
-			branch_point: self.branch_point,
+			offset: self.offset,
+			parent: self.parent,
 		}))
 	}
-	pub fn build_sup_this(mut self, st: SupThis) -> Context {
-		if self.dollar.is_none() {
-			self.dollar = Some(st.this().clone());
-		}
+
+	pub(crate) fn build_sup_this(mut self, st: SupThis) -> Context {
 		self.sup_this = Some(st);
 		self.build()
 	}
@@ -192,3 +134,37 @@
 		Self::new()
 	}
 }
+
+pub struct InitialContextBuilder {
+	builder: ContextBuilder,
+	externals: Vec<(IStr, LocalId)>,
+	next_id: u32,
+}
+
+impl InitialContextBuilder {
+	pub(crate) fn new() -> Self {
+		Self {
+			builder: ContextBuilder::new(),
+			externals: Vec::new(),
+			next_id: 0,
+		}
+	}
+
+	pub fn bind(&mut self, name: impl Into<IStr>, value: Thunk<Val>) {
+		let name = name.into();
+		let id = LocalId(self.next_id);
+		self.next_id += 1;
+		self.externals.push((name, id));
+		self.builder.bind(id, value);
+	}
+
+	pub(crate) fn build(self) -> (ContextBuilder, Vec<(IStr, LocalId)>) {
+		(self.builder, self.externals)
+	}
+}
+
+impl Default for InitialContextBuilder {
+	fn default() -> Self {
+		Self::new()
+	}
+}
modifiedcrates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -9,16 +9,16 @@
 use thiserror::Error;
 
 use crate::{
-	ObjValue, ResolvePathOwned,
 	function::{CallLocation, FunctionSignature, ParamName},
 	stdlib::format::FormatError,
 	typed::TypeLocError,
+	ObjValue, ResolvePathOwned,
 };
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Acyclic)]
 pub struct SyntaxError {
 	pub message: String,
-	pub location: (u32, u32),
+	pub location: Span,
 }
 impl fmt::Display for SyntaxError {
 	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -63,26 +63,36 @@
 	}
 }
 
+pub(crate) fn suggest_names<'a, 'b>(
+	name: &'a IStr,
+	names: impl IntoIterator<Item = &'b IStr>,
+) -> Vec<IStr> {
+	let mut heap: Vec<(f64, IStr)> = names
+		.into_iter()
+		.filter_map(|def| {
+			let conf = strsim::jaro_winkler(def.as_str(), name.as_str());
+			if conf < 0.8 {
+				return None;
+			}
+			debug_assert!(
+				def.as_str() != name.as_str(),
+				"string pooling failure: look for DOC(string-pooling) comment in jrsonnet-interner"
+			);
+
+			Some((conf, def.clone()))
+		})
+		.collect();
+	heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
+	heap.into_iter().map(|v| v.1).collect()
+}
+
 pub(crate) fn suggest_object_fields(v: &ObjValue, key: IStr) -> Vec<IStr> {
-	let mut heap = Vec::new();
-	for field in v.fields_ex(
+	let fields = v.fields_ex(
 		true,
 		#[cfg(feature = "exp-preserve-order")]
 		false,
-	) {
-		let conf = strsim::jaro_winkler(field.as_str(), key.as_str());
-		if conf < 0.8 {
-			continue;
-		}
-		assert!(
-			field.as_str() != key.as_str(),
-			"looks like string pooling failure, please write any info regarding this crash to https://github.com/CertainLach/jrsonnet/issues/113, thanks!"
-		);
-
-		heap.push((conf, field));
-	}
-	heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
-	heap.into_iter().map(|v| v.1).collect()
+	);
+	suggest_names(&key, &fields)
 }
 
 /// Possible errors
@@ -100,6 +110,9 @@
 
 	#[error("self/super/$ are only usable inside objects")]
 	CantUseSelfSupOutsideOfObject,
+
+	#[error("static analysis errors: {}", .0.iter().map(|d| d.message.as_str()).collect::<Vec<_>>().join("; "))]
+	StaticAnalysisError(Vec<crate::analyze::Diagnostic>),
 	#[error("no super found")]
 	NoSuperFound,
 
@@ -107,17 +120,12 @@
 	InComprehensionCanOnlyIterateOverArray,
 
 	#[error("array out of bounds: {0} is not within [0,{1})")]
-	ArrayBoundsError(isize, usize),
+	ArrayBoundsError(isize, u32),
 	#[error("string out of bounds: {0} is not within [0,{1})")]
 	StringBoundsError(usize, usize),
 
 	#[error("assert failed: {}", format_empty_str(.0))]
 	AssertionFailed(IStr),
-
-	#[error("local is not defined: {0}{found}", found = format_found(.1, "local"))]
-	VariableIsNotDefined(IStr, Vec<IStr>),
-	#[error("duplicate local var: {0}")]
-	DuplicateLocalVar(IStr),
 
 	#[error("type mismatch: expected {expected}, got {2} {0}", expected = .1.iter().map(|e| format!("{e}")).collect::<Vec<_>>().join(", "))]
 	TypeMismatch(&'static str, Vec<ValType>, ValType),
@@ -172,7 +180,6 @@
 	#[error("syntax error: {error}")]
 	ImportSyntaxError {
 		path: Source,
-		#[trace(skip)]
 		error: Box<SyntaxError>,
 	},
 
@@ -279,11 +286,11 @@
 }
 impl fmt::Display for Error {
 	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		writeln!(f, "{}", self.0.0)?;
-		for el in &self.0.1.0 {
+		writeln!(f, "{}", self.0 .0)?;
+		for el in &self.0 .1 .0 {
 			write!(f, "\t{}", el.desc)?;
 			if let Some(loc) = &el.location {
-				write!(f, "at {}", loc.0.0.0)?;
+				write!(f, "at {}", loc.0 .0 .0)?;
 				loc.0.map_source_locations(&[loc.1, loc.2]);
 			}
 			writeln!(f)?;
@@ -377,6 +384,18 @@
 		return Err($crate::error::ErrorKind::RuntimeError($crate::jrsonnet_macros::format_istr!($l$(, $($tt)*)?)).into())
 	};
 }
+#[macro_export]
+macro_rules! error {
+	($w:ident$(::$i:ident)*$(($($tt:tt)*))?) => {
+		$crate::error::Error::from($w$(::$i)*$(($($tt)*))?)
+	};
+	($w:ident$(::$i:ident)*$({$($tt:tt)*})?) => {
+		$crate::error::Error::from($w$(::$i)*$({$($tt)*})?)
+	};
+	($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 {
addedcrates/jrsonnet-evaluator/src/evaluate/compspec.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-evaluator/src/evaluate/compspec.rs
@@ -0,0 +1,330 @@
+use std::rc::Rc;
+
+use jrsonnet_types::ValType;
+
+use super::{
+	destructure::{self, evaluate_locals, evaluate_locals_unbound},
+	evaluate_field_member_static, evaluate_field_member_unbound,
+};
+use crate::{
+	analyze::{LArrComp, LBind, LCompSpec, LDestruct, LExpr, LFieldMember, LObjComp, LocalId},
+	arr::ArrValue,
+	bail,
+	error::ErrorKind::*,
+	evaluate::evaluate,
+	Context, ContextBuilder, ObjValue, ObjValueBuilder, Pending, Result, Thunk, Val,
+};
+
+trait CompCollector {
+	fn reserve(&mut self, _guaranteed: usize) {}
+	fn collect(&mut self, ctx: Context) -> Result<()>;
+}
+
+struct EagerArrCollector<'a> {
+	out: &'a mut Vec<Val>,
+	value: &'a LExpr,
+}
+impl CompCollector for EagerArrCollector<'_> {
+	fn reserve(&mut self, size_hint: usize) {
+		self.out.reserve(size_hint);
+	}
+	fn collect(&mut self, ctx: Context) -> Result<()> {
+		self.out.push(evaluate(ctx, self.value)?);
+		Ok(())
+	}
+}
+
+struct LazyArrCollector<'a> {
+	out: &'a mut Vec<Thunk<Val>>,
+	value: &'a Rc<LExpr>,
+}
+impl CompCollector for LazyArrCollector<'_> {
+	fn reserve(&mut self, size_hint: usize) {
+		self.out.reserve(size_hint);
+	}
+	fn collect(&mut self, ctx: Context) -> Result<()> {
+		let value_expr = self.value.clone();
+		self.out.push(Thunk!(move || evaluate(ctx, &value_expr)));
+		Ok(())
+	}
+}
+
+struct ObjCompCollectorStatic<'a> {
+	builder: &'a mut ObjValueBuilder,
+	locals: &'a [LBind],
+	field: &'a LFieldMember,
+}
+impl CompCollector for ObjCompCollectorStatic<'_> {
+	fn reserve(&mut self, guaranteed: usize) {
+		self.builder.reserve_fields(guaranteed);
+	}
+	fn collect(&mut self, inner_ctx: Context) -> Result<()> {
+		let value_ctx = evaluate_locals(inner_ctx.clone(), self.locals);
+		evaluate_field_member_static(self.builder, inner_ctx, value_ctx, self.field)
+	}
+}
+
+struct ObjCompCollectorUnbound<'a> {
+	builder: &'a mut ObjValueBuilder,
+	locals: Rc<Vec<LBind>>,
+	this_id: Option<LocalId>,
+	field: &'a LFieldMember,
+}
+impl CompCollector for ObjCompCollectorUnbound<'_> {
+	fn reserve(&mut self, guaranteed: usize) {
+		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);
+		evaluate_field_member_unbound(self.builder, inner_ctx, uctx, self.field)
+	}
+}
+
+pub fn evaluate_obj_comp(
+	super_obj: Option<ObjValue>,
+	ctx: Context,
+	comp: &LObjComp,
+) -> Result<Val> {
+	let mut builder = ObjValueBuilder::new();
+	if let Some(super_obj) = super_obj {
+		builder.with_super(super_obj);
+	}
+
+	let cached_overs = cache_overs(&ctx, &comp.compspecs)?;
+	if comp.this.is_some() || comp.uses_super {
+		evaluate_compspecs(
+			ctx,
+			&comp.compspecs,
+			&cached_overs,
+			0,
+			0,
+			&mut ObjCompCollectorUnbound {
+				builder: &mut builder,
+				locals: comp.locals.clone(),
+				this_id: comp.this,
+				field: &comp.field,
+			},
+		)?;
+	} else {
+		evaluate_compspecs(
+			ctx,
+			&comp.compspecs,
+			&cached_overs,
+			0,
+			0,
+			&mut ObjCompCollectorStatic {
+				builder: &mut builder,
+				locals: &comp.locals,
+				field: &comp.field,
+			},
+		)?;
+	}
+
+	Ok(Val::Obj(builder.build()))
+}
+
+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: {
+		let mut out = Vec::new();
+
+		if evaluate_compspecs_eager(
+			ctx.clone(),
+			&comp.compspecs,
+			&cached_overs,
+			0,
+			0,
+			&mut EagerArrCollector {
+				out: &mut out,
+				value: &comp.value,
+			},
+		)
+		.is_err()
+		{
+			break 'eager;
+		}
+		return Ok(Val::arr(out));
+	}
+
+	let mut items: Vec<Thunk<Val>> = Vec::new();
+	evaluate_compspecs(
+		ctx,
+		&comp.compspecs,
+		&cached_overs,
+		0,
+		0,
+		&mut LazyArrCollector {
+			out: &mut items,
+			value: &comp.value,
+		},
+	)?;
+	Ok(Val::arr(items))
+}
+
+fn cache_overs(ctx: &Context, specs: &[LCompSpec]) -> Result<Vec<Option<ArrValue>>> {
+	specs
+		.iter()
+		.map(|spec| {
+			Ok(match spec {
+				LCompSpec::For {
+					over,
+					loop_invariant: true,
+					..
+				} => {
+					let val = evaluate(ctx.clone(), over)?;
+					let Val::Arr(arr) = val else {
+						bail!(InComprehensionCanOnlyIterateOverArray)
+					};
+					Some(arr)
+				}
+				_ => None,
+			})
+		})
+		.collect::<Result<_>>()
+}
+
+fn evaluate_compspecs_eager(
+	ctx: Context,
+	specs: &[LCompSpec],
+	cached_overs: &[Option<ArrValue>],
+	idx: usize,
+	guaranteed_reserve: usize,
+	collector: &mut dyn CompCollector,
+) -> Result<()> {
+	if idx >= specs.len() {
+		collector.reserve(guaranteed_reserve);
+		return collector.collect(ctx.clone());
+	}
+	match &specs[idx] {
+		LCompSpec::If(cond) => {
+			let val = evaluate(ctx.clone(), cond)?;
+			let Val::Bool(b) = val else {
+				bail!(TypeMismatch(
+					"if spec condition",
+					vec![ValType::Bool],
+					val.value_type()
+				))
+			};
+			if b {
+				evaluate_compspecs_eager(ctx, specs, cached_overs, idx + 1, 0, collector)?;
+			}
+		}
+		LCompSpec::For { destruct, over, .. } => {
+			let arr = if let Some(cached) = &cached_overs[idx] {
+				cached.clone()
+			} else {
+				let arr_val = evaluate(ctx.clone(), over)?;
+				let Val::Arr(arr) = arr_val else {
+					bail!(InComprehensionCanOnlyIterateOverArray)
+				};
+				arr
+			};
+			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,
+						)?;
+					}
+				}
+				#[cfg(feature = "exp-destruct")]
+				_ => {
+					for (i, item) in arr.iter().enumerate() {
+						let item_val = item?;
+						let mut inner_builder = ContextBuilder::extend(ctx.clone(), 1);
+						destructure::destruct(
+							destruct,
+							Thunk::evaluated(item_val),
+							None,
+							&mut inner_builder,
+						);
+						let inner_ctx = inner_builder.build();
+						evaluate_compspecs_eager(
+							inner_ctx,
+							specs,
+							cached_overs,
+							idx + 1,
+							if i == 0 { inner_reserve } else { 0 },
+							collector,
+						)?;
+					}
+				}
+			}
+		}
+	}
+	Ok(())
+}
+
+fn evaluate_compspecs(
+	ctx: Context,
+	specs: &[LCompSpec],
+	cached_overs: &[Option<ArrValue>],
+	idx: usize,
+	guaranteed_reserve: usize,
+	collector: &mut dyn CompCollector,
+) -> Result<()> {
+	if idx >= specs.len() {
+		collector.reserve(guaranteed_reserve);
+		return collector.collect(ctx);
+	}
+	match &specs[idx] {
+		LCompSpec::If(cond) => {
+			let val = evaluate(ctx.clone(), cond)?;
+			let Val::Bool(b) = val else {
+				bail!(TypeMismatch(
+					"if spec condition",
+					vec![ValType::Bool],
+					val.value_type()
+				))
+			};
+			if b {
+				evaluate_compspecs(ctx, specs, cached_overs, idx + 1, 0, collector)?;
+			}
+		}
+		LCompSpec::For { destruct, over, .. } => {
+			let arr = if let Some(cached) = &cached_overs[idx] {
+				cached.clone()
+			} else {
+				let arr_val = evaluate(ctx.clone(), over)?;
+				let Val::Arr(arr) = arr_val else {
+					bail!(InComprehensionCanOnlyIterateOverArray)
+				};
+				arr
+			};
+			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);
+				evaluate_compspecs(
+					inner_ctx,
+					specs,
+					cached_overs,
+					idx + 1,
+					if i == 0 { inner_reserve } else { 0 },
+					collector,
+				)?;
+			}
+		}
+	}
+	Ok(())
+}
modifiedcrates/jrsonnet-evaluator/src/evaluate/destructure.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/destructure.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/destructure.rs
@@ -1,213 +1,256 @@
-use jrsonnet_ir::{BindSpec, Destruct};
+use std::rc::Rc;
+
+use jrsonnet_gcmodule::Trace;
 
 use crate::{
-	Context, ContextBuilder, Pending, Thunk, Val, error::Result, evaluate_method,
-	evaluate_named_param,
+	analyze::{LBind, LDestruct, LDestructField, LDestructRest, LExpr, LocalId},
+	bail,
+	evaluate::evaluate,
+	Context, ContextBuilder, Pending, Result, SupThis, Thunk, Unbound, Val,
 };
 
-#[allow(clippy::too_many_lines)]
-#[allow(unused_variables)]
-pub fn destruct(
-	d: &Destruct,
-	parent: Thunk<Val>,
+#[allow(dead_code, reason = "not dead in exp-destruct")]
+fn destruct_array(
+	start: &[LDestruct],
+	rest: Option<LDestructRest>,
+	end: &[LDestruct],
+
+	value: Thunk<Val>,
 	fctx: Pending<Context>,
-	new_bindings: &mut ContextBuilder,
-) -> Result<()> {
-	match d {
-		Destruct::Full(v) => {
-			new_bindings.try_bind(v.clone(), parent)?;
+	builder: &mut ContextBuilder,
+) {
+	let min_len = start.len() + end.len();
+	let has_rest = rest.is_some();
+	let full = Thunk!(move || {
+		let v = value.evaluate()?;
+		let Val::Arr(arr) = v else {
+			bail!("expected array");
+		};
+		if !has_rest {
+			if arr.len() as usize != min_len {
+				bail!("expected {} elements, got {}", min_len, arr.len())
+			}
+		} else if (arr.len() as usize) < min_len {
+			bail!(
+				"expected at least {} elements, but array was only {}",
+				min_len,
+				arr.len()
+			)
 		}
-		#[cfg(feature = "exp-destruct")]
-		Destruct::Skip => {}
-		#[cfg(feature = "exp-destruct")]
-		Destruct::Array { start, rest, end } => {
-			use jrsonnet_ir::DestructRest;
+		Ok(arr)
+	});
 
-			use crate::bail;
+	for (i, d) in start.iter().enumerate() {
+		let full = full.clone();
+		destruct(
+			d,
+			Thunk!(move || Ok(full.evaluate()?.get(i as u32)?.expect("length is checked"))),
+			fctx.clone(),
+			builder,
+		);
+	}
 
-			let min_len = start.len() + end.len();
-			let has_rest = rest.is_some();
-			let full = Thunk!(move || {
-				let v = parent.evaluate()?;
-				let Val::Arr(arr) = v else {
-					bail!("expected array");
-				};
-				if !has_rest {
-					if arr.len() != min_len {
-						bail!("expected {} elements, got {}", min_len, arr.len())
-					}
-				} else if arr.len() < min_len {
-					bail!(
-						"expected at least {} elements, but array was only {}",
-						min_len,
-						arr.len()
-					)
-				}
-				Ok(arr)
-			});
+	let start_len = start.len() as u32;
+	let end_len = end.len() as u32;
 
-			{
-				for (i, d) in start.iter().enumerate() {
-					let full = full.clone();
-					destruct(
-						d,
-						Thunk!(move || Ok(full.evaluate()?.get(i)?.expect("length is checked"))),
-						fctx.clone(),
-						new_bindings,
-					)?;
-				}
-			}
+	if let Some(crate::analyze::LDestructRest::Keep(id)) = rest {
+		let full = full.clone();
+		builder.bind(
+			id,
+			Thunk!(move || {
+				let full = full.evaluate()?;
+				let to = full.len() - end_len;
+				Ok(Val::Arr(full.slice(
+					Some(start_len as i32),
+					Some(to as i32),
+					None,
+				)))
+			}),
+		);
+	}
 
-			match rest {
-				Some(DestructRest::Keep(v)) => {
-					let start = start.len();
-					let end = end.len();
-					let full = full.clone();
-					destruct(
-						&Destruct::Full(v.clone()),
-						Thunk!(move || {
-							let full = full.evaluate()?;
-							let to = full.len() - end;
-							Ok(Val::Arr(full.slice(
-								Some(start as i32),
-								Some(to as i32),
-								None,
-							)))
-						}),
-						fctx.clone(),
-						new_bindings,
-					)?;
-				}
-				Some(DestructRest::Drop) | None => {}
-			}
+	for (i, d) in end.iter().enumerate() {
+		let full = full.clone();
+		destruct(
+			d,
+			Thunk!(move || {
+				let full = full.evaluate()?;
+				Ok(full
+					.get(full.len() - end_len + i as u32)?
+					.expect("length is checked"))
+			}),
+			fctx.clone(),
+			builder,
+		);
+	}
+}
 
-			{
-				for (i, d) in end.iter().enumerate() {
-					let full = full.clone();
-					let end = end.len();
-					destruct(
-						d,
-						Thunk!(move || {
-							let full = full.evaluate()?;
-							Ok(full.get(full.len() - end + i)?.expect("length is checked"))
-						}),
-						fctx.clone(),
-						new_bindings,
-					)?;
-				}
-			}
-		}
-		#[cfg(feature = "exp-destruct")]
-		Destruct::Object { fields, rest } => {
-			use jrsonnet_ir::DestructRest;
-			use rustc_hash::FxHashSet;
+#[allow(dead_code, reason = "not dead in exp-destruct")]
+fn destruct_object(
+	fields: &[LDestructField],
+	rest: Option<LDestructRest>,
 
-			use crate::{ObjValueBuilder, bail};
+	value: Thunk<Val>,
+	fctx: Pending<Context>,
+	builder: &mut ContextBuilder,
+) {
+	use jrsonnet_interner::IStr;
+	use rustc_hash::FxHashSet;
 
-			let captured_fields: FxHashSet<_> = fields.iter().map(|f| f.0.clone()).collect();
-			let field_names: Vec<_> = fields
-				.iter()
-				.map(|f| (f.0.clone(), f.2.is_some()))
-				.collect();
-			let has_rest = rest.is_some();
-			let full = Thunk!(move || {
-				let v = parent.evaluate()?;
-				let Val::Obj(obj) = v else {
-					bail!("expected object");
-				};
-				for (field, has_default) in &field_names {
-					if !has_default && !obj.has_field_ex(field.clone(), true) {
-						bail!("missing field: {field}");
-					}
-				}
-				if !has_rest {
-					let len = obj.len();
-					if len > field_names.len() {
-						bail!("too many fields, and rest not found");
-					}
-				}
-				Ok(obj)
-			});
+	use crate::{bail, ObjValueBuilder};
 
-			match rest {
-				Some(DestructRest::Keep(v)) => {
-					let full = full.clone();
-					destruct(
-						&Destruct::Full(v.clone()),
-						Thunk!(move || {
-							let full = full.evaluate()?;
-							let mut builder = ObjValueBuilder::new();
-							builder.extend_with_core(full.as_standalone());
-							builder.with_fields_omitted(captured_fields);
-							Ok(Val::Obj(builder.build()))
-						}),
-						fctx.clone(),
-						new_bindings,
-					)?;
-				}
-				Some(DestructRest::Drop) | None => {}
+	let captured_fields: FxHashSet<IStr> = fields.iter().map(|f| f.name.clone()).collect();
+	let field_names: Vec<(IStr, bool)> = fields
+		.iter()
+		.map(|f| (f.name.clone(), f.default.is_some()))
+		.collect();
+	let has_rest = rest.is_some();
+	let full = Thunk!(move || {
+		let v = value.evaluate()?;
+		let Val::Obj(obj) = v else {
+			bail!("expected object");
+		};
+		for (field, has_default) in &field_names {
+			if !has_default && !obj.has_field_ex(field.clone(), true) {
+				bail!("missing field: {field}");
+			}
+		}
+		if !has_rest {
+			let len = obj.len();
+			if len as usize > field_names.len() {
+				bail!("too many fields, and rest not found");
 			}
+		}
+		Ok(obj)
+	});
+
+	if let Some(crate::analyze::LDestructRest::Keep(id)) = rest {
+		let full = full.clone();
+		builder.bind(
+			id,
+			Thunk!(move || {
+				let full = full.evaluate()?;
+				let mut out = ObjValueBuilder::new();
+				out.extend_with_core(full.as_standalone());
+				out.with_fields_omitted(captured_fields);
+				Ok(Val::Obj(out.build()))
+			}),
+		);
+	}
 
-			for (field, d, default) in fields {
-				let default = default.clone().map(|e| (fctx.clone(), e));
-				let value = {
-					let field = field.clone();
-					let full = full.clone();
-					Thunk!(move || {
-						let full = full.evaluate()?;
-						if let Some(field) = full.get(field)? {
-							Ok(field)
-						} else {
-							let (fctx, expr) = default.as_ref().expect("shape is checked");
-							Ok(crate::evaluate(fctx.clone().unwrap(), expr)?)
-						}
-					})
-				};
+	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 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)
+				},
+				Ok,
+			)
+		});
 
-				if let Some(d) = d {
-					destruct(d, value, fctx.clone(), new_bindings)?;
-				} else {
-					destruct(
-						&Destruct::Full(field.clone()),
-						value,
-						fctx.clone(),
-						new_bindings,
-					)?;
-				}
-			}
+		if let Some(into) = &field.into {
+			destruct(into, value_thunk, fctx.clone(), builder);
+		} else {
+			unreachable!("analyzer lowers object-destruct shorthands into `into`");
 		}
 	}
-	Ok(())
 }
 
-pub fn evaluate_dest(
-	d: &BindSpec,
+/// 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>,
-	new_bindings: &mut ContextBuilder,
-) -> Result<()> {
+	builder: &mut ContextBuilder,
+) {
 	match d {
-		BindSpec::Field { into, value } => {
-			let name = into.name();
-			let value = value.clone();
-			let data = {
-				let fctx = fctx.clone();
-				Thunk!(move || evaluate_named_param(fctx.unwrap(), &value, name))
-			};
-			destruct(into, data, fctx, new_bindings)?;
+		LDestruct::Full(id) => builder.bind(*id, value),
+		#[cfg(feature = "exp-destruct")]
+		LDestruct::Skip => {}
+		#[cfg(feature = "exp-destruct")]
+		LDestruct::Array { start, rest, end } => destruct_array(start, rest, end, value, fctx, builder),
+		#[cfg(feature = "exp-destruct")]
+		LDestruct::Object { fields, rest } => destruct_object(fields, rest, value, fctx, builder),
+	}
+}
+
+/// 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);
+}
+
+/// 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());
+	for bind in binds {
+		evaluate_dest(bind, fctx.clone(), &mut builder);
+	}
+	builder.build().into_future(fctx)
+}
+
+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,
+	locals: Rc<Vec<LBind>>,
+	this_id: Option<LocalId>,
+) -> impl CloneableUnbound<Context> {
+	#[derive(Trace, Clone)]
+	struct UnboundLocals {
+		fctx: Context,
+		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)
 		}
-		BindSpec::Function {
-			name,
-			params,
-			value,
-		} => {
-			let params = params.clone();
-			let name = name.clone();
-			let value = value.clone();
-			new_bindings.try_bind(
-				name.clone(),
-				Thunk!(move || Ok(evaluate_method(fctx.unwrap(), name, params, value))),
-			)?;
-		}
 	}
-	Ok(())
+
+	UnboundLocals {
+		fctx,
+		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::{6	ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, ExprParams, FieldMember,7	FieldName, ForSpecData, IfSpecData, ImportKind, LiteralType, ObjBody, ObjMembers, Spanned,8	function::ParamName,9};10use jrsonnet_types::ValType;1112use self::destructure::destruct;13use crate::{14	Context, ContextBuilder, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,15	ResultExt, SupThis, Unbound, Val,16	arr::ArrValue,17	bail,18	destructure::evaluate_dest,19	error::{ErrorKind::*, suggest_object_fields},20	evaluate::operator::{evaluate_binary_op_special, evaluate_unary_op},21	function::{CallLocation, FuncDesc, FuncVal, PreparedFuncVal},22	in_frame,23	typed::{FromUntyped, IntoUntyped as _, Typed},24	val::{CachedUnbound, IndexableVal, StrValue, Thunk},25	with_state,26};27pub mod destructure;28pub mod operator;2930// This is the amount of bytes that need to be left on the stack before increasing the size.31// It must be at least as large as the stack required by any code that does not call32// `ensure_sufficient_stack`.33const RED_ZONE: usize = 100 * 1024; // 100k3435// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then36// on. This flag has performance relevant characteristics. Don't set it too high.37const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB3839/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations40/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit41/// from this.42///43/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.44#[inline]45pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {46	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)47}4849pub fn evaluate_trivial(expr: &Expr) -> Option<Val> {50	fn is_trivial(expr: &Expr) -> bool {51		match expr {52			Expr::Str(_)53			| Expr::Num(_)54			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,55			Expr::Arr(a) => a.iter().all(is_trivial),56			_ => false,57		}58	}59	Some(match expr {60		Expr::Str(s) => Val::string(s.clone()),61		Expr::Num(n) => Val::Num(*n),62		Expr::Literal(LiteralType::False) => Val::Bool(false),63		Expr::Literal(LiteralType::True) => Val::Bool(true),64		Expr::Literal(LiteralType::Null) => Val::Null,65		Expr::Arr(n) => {66			if n.iter().any(|e| !is_trivial(e)) {67				return None;68			}69			Val::Arr(70				n.iter()71					.map(evaluate_trivial)72					.map(|e| e.expect("checked trivial"))73					.collect(),74			)75		}76		_ => return None,77	})78}7980pub fn evaluate_method(ctx: Context, name: IStr, params: ExprParams, body: Rc<Expr>) -> Val {81	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {82		name,83		ctx,84		params,85		body,86	})))87}8889pub fn evaluate_field_name(ctx: Context, field_name: &Spanned<FieldName>) -> Result<Option<IStr>> {90	Ok(match &field_name.value {91		FieldName::Fixed(n) => Some(n.clone()),92		FieldName::Dyn(expr) => in_frame(93			CallLocation::new(&field_name.span),94			|| "evaluating field name".to_string(),95			|| {96				let v = evaluate(ctx, expr)?;97				Ok(if matches!(v, Val::Null) {98					None99				} else {100					Some(IStr::from_untyped(v)?)101				})102			},103		)?,104	})105}106107pub fn evaluate_comp(108	ctx: Context,109	specs: &[CompSpec],110	mut guaranteed_reserve: usize,111	callback: &mut impl FnMut(Context, usize) -> Result<()>,112) -> Result<()> {113	match specs.first() {114		None => callback(ctx, guaranteed_reserve)?,115		Some(CompSpec::IfSpec(IfSpecData { cond, span: _ })) => {116			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {117				evaluate_comp(ctx, &specs[1..], 0, callback)?;118			}119		}120		Some(CompSpec::ForSpec(ForSpecData {121			destruct: into,122			over,123		})) => {124			match evaluate(ctx.clone(), over)? {125				Val::Arr(list) => {126					guaranteed_reserve = guaranteed_reserve.max(1) * list.len();127					for (i, item) in list.iter_lazy().enumerate() {128						let fctx = Pending::new();129						let mut ctx = ContextBuilder::extend_fast(ctx.clone());130						destruct(into, item, fctx.clone(), &mut ctx)?;131						let ctx = ctx.build().into_future(fctx);132133						let specs = &specs[1..];134						evaluate_comp(135							ctx,136							specs,137							if i == 0 || !specs.is_empty() {138								guaranteed_reserve139							} else {140								0141							},142							callback,143						)?;144					}145				}146				Val::Obj(obj) if cfg!(feature = "exp-object-iteration") => {147					let fields = obj.fields(148						// TODO: Should there be ability to preserve iteration order?149						#[cfg(feature = "exp-preserve-order")]150						false,151					);152					guaranteed_reserve = guaranteed_reserve.max(1) * fields.len();153					for (i, field) in fields.into_iter().enumerate() {154						let fctx = Pending::new();155						let mut ctx = ContextBuilder::extend_fast(ctx.clone());156						let obj = obj.clone();157						let value = Thunk::evaluated(Val::arr(vec![158							Thunk::evaluated(Val::string(field.clone())),159							obj.get_lazy(field).expect(160								"field exists, as field name was obtained from object.fields()",161							),162						]));163						destruct(into, value, fctx.clone(), &mut ctx)?;164						let ctx = ctx.build().into_future(fctx);165166						evaluate_comp(167							ctx,168							&specs[1..],169							if i == 0 || !specs.is_empty() {170								guaranteed_reserve171							} else {172								0173							},174							callback,175						)?;176					}177				}178				_ => bail!(InComprehensionCanOnlyIterateOverArray),179			}180		}181	}182	Ok(())183}184185fn evaluate_arr_comp(ctx: Context, expr: &Rc<Expr>, comp_specs: &[CompSpec]) -> Result<ArrValue> {186	let ctx = ctx.branch_point();187	'eager: {188		let mut out = Vec::new();189190		if evaluate_comp(ctx.clone(), comp_specs, 0, &mut |ctx, reserve| {191			if reserve != 0 {192				out.reserve(reserve);193			}194			out.push(evaluate(ctx, expr)?);195			Ok(())196		})197		.is_err()198		{199			break 'eager;200		}201202		return Ok(ArrValue::new(out));203	};204	let mut out = Vec::new();205	evaluate_comp(ctx, comp_specs, 0, &mut |ctx, reserve| {206		if reserve != 0 {207			out.reserve(reserve);208		}209		let expr = expr.clone();210		out.push(Thunk!(move || evaluate(ctx, &expr)));211		Ok(())212	})?;213	Ok(ArrValue::new(out))214}215216trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}217impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}218219fn evaluate_object_locals(220	fctx: Context,221	locals: Rc<Vec<BindSpec>>,222) -> impl CloneableUnbound<Context> {223	#[derive(Trace, Clone)]224	struct UnboundLocals {225		fctx: Context,226		locals: Rc<Vec<BindSpec>>,227	}228	impl Unbound for UnboundLocals {229		type Bound = Context;230231		fn bind(&self, sup_this: SupThis) -> Result<Context> {232			let fctx = Context::new_future();233			let ctx = self.fctx.clone();234			let mut ctx = ContextBuilder::extend(ctx);235			for b in self.locals.iter() {236				evaluate_dest(b, fctx.clone(), &mut ctx)?;237			}238239			let ctx = ctx.build_sup_this(sup_this).into_future(fctx);240241			Ok(ctx)242		}243	}244245	UnboundLocals { fctx, locals }246}247248pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(249	builder: &mut ObjValueBuilder,250	ctx: Context,251	uctx: B,252	field: &FieldMember,253) -> Result<()> {254	let name = evaluate_field_name(ctx, &field.name)?;255	let Some(name) = name else {256		return Ok(());257	};258259	match field {260		FieldMember {261			plus,262			params: None,263			visibility,264			value,265			..266		} => {267			#[derive(Trace)]268			struct UnboundValue<B: Trace> {269				uctx: B,270				value: Rc<Expr>,271				name: IStr,272			}273			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {274				type Bound = Val;275				fn bind(&self, sup_this: SupThis) -> Result<Val> {276					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())277				}278			}279280			builder281				.field(name.clone())282				.with_add(*plus)283				.with_visibility(*visibility)284				.with_location(field.name.span.clone())285				.bindable(UnboundValue {286					uctx,287					value: value.clone(),288					name,289				})?;290		}291		FieldMember {292			params: Some(params),293			visibility,294			value,295			..296		} => {297			#[derive(Trace)]298			struct UnboundMethod<B: Trace> {299				uctx: B,300				value: Rc<Expr>,301				params: ExprParams,302				name: IStr,303			}304			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {305				type Bound = Val;306				fn bind(&self, sup_this: SupThis) -> Result<Val> {307					Ok(evaluate_method(308						self.uctx.bind(sup_this)?,309						self.name.clone(),310						self.params.clone(),311						self.value.clone(),312					))313				}314			}315316			builder317				.field(name.clone())318				.with_visibility(*visibility)319				// .with_location(value.span())320				.bindable(UnboundMethod {321					uctx,322					value: value.clone(),323					params: params.clone(),324					name,325				})?;326		}327	}328	Ok(())329}330331#[derive(Trace, Clone)]332struct DirectUnbound(Context);333impl Unbound for DirectUnbound {334	type Bound = Context;335	fn bind(&self, sup_this: SupThis) -> Result<Context> {336		Ok(ContextBuilder::extend(self.0.clone()).build_sup_this(sup_this))337	}338}339340#[allow(clippy::too_many_lines)]341pub fn evaluate_member_list_object(342	super_obj: Option<ObjValue>,343	ctx: Context,344	members: &ObjMembers,345) -> Result<ObjValue> {346	#[derive(Trace)]347	struct ObjectAssert<B: Trace> {348		uctx: B,349		asserts: Rc<Vec<AssertStmt>>,350	}351	impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {352		fn run(&self, sup_this: SupThis) -> Result<()> {353			let ctx = self.uctx.bind(sup_this)?;354			for assert in &*self.asserts {355				evaluate_assert(ctx.clone(), assert)?;356			}357			Ok(())358		}359	}360361	let mut builder = ObjValueBuilder::new();362	if let Some(super_obj) = super_obj {363		builder.with_super(super_obj);364	}365366	if members.locals.is_empty() {367		// We can use the same context for all field evaluation, it doesn't depends on locals, only on this/super368		let uctx = DirectUnbound(ctx.clone());369		for field in &members.fields {370			evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;371		}372		if !members.asserts.is_empty() {373			builder.assert(ObjectAssert {374				uctx,375				asserts: members.asserts.clone(),376			});377		}378	} else {379		let locals = members.locals.clone();380		// We have single context for all fields, so we can cache them together381		let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));382		for field in &members.fields {383			evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;384		}385		if !members.asserts.is_empty() {386			builder.assert(ObjectAssert {387				uctx,388				asserts: members.asserts.clone(),389			});390		}391	}392393	Ok(builder.build())394}395396pub fn evaluate_object(397	super_obj: Option<ObjValue>,398	ctx: Context,399	object: &ObjBody,400) -> Result<ObjValue> {401	Ok(match object {402		ObjBody::MemberList(members) => evaluate_member_list_object(super_obj, ctx, members)?,403		ObjBody::ObjComp(obj) => {404			let mut builder = ObjValueBuilder::new();405			if let Some(super_obj) = super_obj {406				builder.with_super(super_obj);407			}408			let locals = obj.locals.clone();409			evaluate_comp(410				ctx.branch_point(),411				&obj.compspecs,412				0,413				&mut |ctx, reserve| {414					let uctx = evaluate_object_locals(ctx.clone(), locals.clone());415					builder.reserve_fields(reserve);416417					evaluate_field_member(&mut builder, ctx, uctx, &obj.field)418				},419			)?;420421			builder.build()422		}423	})424}425426pub fn evaluate_apply(427	ctx: Context,428	value: &Expr,429	args: &ArgsDesc,430	loc: CallLocation<'_>,431	tailstrict: bool,432) -> Result<Val> {433	let value = evaluate(ctx.clone(), value)?;434	Ok(match value {435		Val::Func(f) => {436			let name = f.name();437			let unnamed = args438				.unnamed439				.iter()440				.cloned()441				.map(|un| evaluate_thunk(ctx.clone(), un, tailstrict))442				.collect::<Result<Vec<_>>>()?;443			let named = args444				.values445				.iter()446				.cloned()447				.map(|un| evaluate_thunk(ctx.clone(), un, tailstrict))448				.collect::<Result<Vec<_>>>()?;449			let prepare = PreparedFuncVal::new(f, args.unnamed.len(), &args.names)450				.with_description_src(loc, || format!("function <{name}> call"))?;451			let body = || prepare.call(loc, &unnamed, &named);452			if tailstrict {453				body()?454			} else {455				in_frame(loc, || format!("function <{name}> call"), body)?456			}457		}458		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),459	})460}461462pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {463	let AssertStmt { assertion, message } = assertion;464	let assertion_result = in_frame(465		CallLocation::new(&assertion.span),466		|| "assertion condition".to_owned(),467		|| bool::from_untyped(evaluate(ctx.clone(), assertion)?),468	)?;469	if !assertion_result {470		in_frame(471			CallLocation::new(&assertion.span),472			|| "assertion failure".to_owned(),473			|| {474				if let Some(msg) = message {475					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));476				}477				bail!(AssertionFailed(Val::Null.to_string()?));478			},479		)?;480	}481	Ok(())482}483484pub fn evaluate_named_param(ctx: Context, expr: &Expr, name: ParamName) -> Result<Val> {485	match name {486		ParamName::Named(name) => evaluate_named(ctx, expr, name),487		ParamName::Unnamed => evaluate(ctx, expr),488	}489}490491pub fn evaluate_named(ctx: Context, expr: &Expr, name: IStr) -> Result<Val> {492	use Expr::*;493	Ok(match expr {494		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),495		_ => evaluate(ctx, expr)?,496	})497}498499pub fn evaluate_thunk(ctx: Context, expr: Rc<Expr>, tailstrict: bool) -> Result<Thunk<Val>> {500	Ok(if tailstrict {501		Thunk::evaluated(evaluate(ctx, &expr)?)502	} else {503		Thunk!(move || { evaluate(ctx, &expr) })504	})505}506#[allow(clippy::too_many_lines)]507pub fn evaluate(ctx: Context, expr: &Expr) -> Result<Val> {508	use Expr::*;509510	Ok(match expr {511		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),512		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),513		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),514		Literal(LiteralType::True) => Val::Bool(true),515		Literal(LiteralType::False) => Val::Bool(false),516		Literal(LiteralType::Null) => Val::Null,517		Str(v) => Val::string(v.clone()),518		Num(v) => Val::try_num(*v)?,519		// I have tried to remove special behavior from super by implementing standalone-super520		// expresion, but looks like this case still needs special treatment.521		//522		// Note that other jsonnet implementations will fail on `if value in (super)` expression,523		// because the standalone super literal is not supported, that is because in other524		// implementations `in super` treated differently from `in smth_else`.525		BinaryOp(bin)526			if matches!(&bin.rhs, Expr::Literal(LiteralType::Super))527				&& bin.op == BinaryOpType::In =>528		{529			let sup_this = ctx.try_sup_this()?;530			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.531			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.532			if !sup_this.has_super() {533				return Ok(Val::Bool(false));534			}535			let field = evaluate(ctx, &bin.lhs)?;536			Val::Bool(sup_this.field_in_super(field.to_string()?))537		}538		BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?,539		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,540		Var(name) => in_frame(541			CallLocation::new(&name.span),542			|| format!("local <{}> access", &**name),543			|| ctx.binding((**name).clone())?.evaluate(),544		)?,545		Index { indexable, parts } => ensure_sufficient_stack(|| {546			let mut parts = parts.iter();547			let mut indexable = if matches!(&**indexable, Expr::Literal(LiteralType::Super)) {548				let part = parts.next().expect("at least part should exist");549				// sup_this existence check might also be skipped here for null-coalesce...550				// But I believe this might cause errors.551				let sup_this = ctx.try_sup_this()?;552				if !sup_this.has_super() {553					#[cfg(feature = "exp-null-coaelse")]554					if part.null_coaelse {555						return Ok(Val::Null);556					}557					bail!(NoSuperFound)558				}559				let name = evaluate(ctx.clone(), &part.value)?;560561				let Val::Str(name) = name else {562					bail!(ValueIndexMustBeTypeGot(563						ValType::Obj,564						ValType::Str,565						name.value_type(),566					))567				};568569				let name = name.into_flat();570				match sup_this571					.get_super(name.clone())572					.with_description_src(&part.span, || format!("field <{name}> access"))?573				{574					Some(v) => v,575					#[cfg(feature = "exp-null-coaelse")]576					None if part.null_coaelse => return Ok(Val::Null),577					None => {578						let suggestions = suggest_object_fields(579							&sup_this.standalone_super().expect("super exists"),580							name.clone(),581						);582583						bail!(NoSuchField(name, suggestions))584					}585				}586			} else {587				evaluate(ctx.clone(), indexable)?588			};589590			for part in parts {591				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {592					(Val::Obj(v), Val::Str(key)) => match v593						.get(key.clone().into_flat())594						.with_description_src(&part.span, || format!("field <{key}> access"))?595					{596						Some(v) => v,597						#[cfg(feature = "exp-null-coaelse")]598						None if part.null_coaelse => return Ok(Val::Null),599						None => {600							let suggestions = suggest_object_fields(&v, key.into_flat());601602							return Err(Error::from(NoSuchField(603								key.clone().into_flat(),604								suggestions,605							)))606							.with_description_src(&part.span, || format!("field <{key}> access"));607						}608					},609					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(610						ValType::Obj,611						ValType::Str,612						n.value_type(),613					)),614					(Val::Arr(v), Val::Num(n)) => {615						let n = n.get();616						if n.fract() > f64::EPSILON {617							bail!(FractionalIndex)618						}619						if n < 0.0 {620							#[expect(621								clippy::cast_possible_truncation,622								reason = "it would be truncated anyway"623							)]624							let n = n as isize;625							bail!(ArrayBoundsError(n, v.len()));626						}627						#[expect(628							clippy::cast_possible_truncation,629							clippy::cast_sign_loss,630							reason = "n is checked postive"631						)]632						v.get(n as usize)?633							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?634					}635					(Val::Arr(_), Val::Str(n)) => {636						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))637					}638					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(639						ValType::Arr,640						ValType::Num,641						n.value_type(),642					)),643644					(Val::Str(s), Val::Num(n)) => Val::Str({645						let n = n.get();646						if n.fract() > f64::EPSILON {647							bail!(FractionalIndex)648						}649						if n < 0.0 {650							#[expect(651								clippy::cast_possible_truncation,652								reason = "it would be truncated anyway"653							)]654							let n = n as isize;655							bail!(ArrayBoundsError(n, s.into_flat().chars().count()));656						}657						#[expect(658							clippy::cast_sign_loss,659							clippy::cast_possible_truncation,660							reason = "n is positive, overflow will truncate as expected"661						)]662						let n = n as usize;663						let v: IStr = s664							.clone()665							.into_flat()666							.chars()667							.skip(n)668							.take(1)669							.collect::<String>()670							.into();671						if v.is_empty() {672							bail!(StringBoundsError(n, s.into_flat().chars().count()))673						}674						StrValue::Flat(v)675					}),676					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(677						ValType::Str,678						ValType::Num,679						n.value_type(),680					)),681					#[cfg(feature = "exp-null-coaelse")]682					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),683					(v, _) => bail!(CantIndexInto(v.value_type())),684				};685			}686			Ok(indexable)687		})?,688		LocalExpr(bindings, returned) => {689			let fctx = Context::new_future();690			let mut ctx = ContextBuilder::extend(ctx);691			for b in bindings {692				evaluate_dest(b, fctx.clone(), &mut ctx)?;693			}694			let ctx = ctx.build().into_future(fctx);695			evaluate(ctx, returned)?696		}697		Arr(items) => {698			if items.is_empty() {699				Val::arr(())700			} else {701				Val::Arr(ArrValue::expr(ctx, items.clone()))702			}703		}704		ArrComp(expr, comp_specs) => Val::Arr(evaluate_arr_comp(ctx, expr, comp_specs)?),705		Obj(body) => Val::Obj(evaluate_object(None, ctx, body)?),706		ObjExtend(a, b) => {707			let base = evaluate(ctx.clone(), a)?;708			match base {709				Val::Obj(base_obj) => Val::Obj(evaluate_object(Some(base_obj), ctx, b)?),710				_ => bail!("ObjExtend lhs should be an object value"),711			}712		}713		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {714			evaluate_apply(ctx, value, args, CallLocation::new(&args.span), *tailstrict)715		})?,716		Function(params, body) => {717			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())718		}719		AssertExpr(assert) => {720			evaluate_assert(ctx.clone(), &assert.assert)?;721			evaluate(ctx, &assert.rest)?722		}723		ErrorStmt(s, e) => in_frame(724			CallLocation::new(s),725			|| "error statement".to_owned(),726			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),727		)?,728		IfElse(if_else) => {729			if in_frame(730				CallLocation::new(&if_else.cond.span),731				|| "if condition".to_owned(),732				|| bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.cond)?),733			)? {734				evaluate(ctx, &if_else.cond_then)?735			} else {736				match &if_else.cond_else {737					Some(v) => evaluate(ctx, v)?,738					None => Val::Null,739				}740			}741		}742		Slice(slice) => {743			fn parse_idx<T: Typed + FromUntyped>(744				ctx: Context,745				expr: Option<&Spanned<Expr>>,746				desc: &'static str,747			) -> Result<Option<T>> {748				if let Some(value) = expr {749					Ok(in_frame(750						CallLocation::new(&value.span),751						|| format!("slice {desc}"),752						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),753					)?)754				} else {755					Ok(None)756				}757			}758759			let indexable = evaluate(ctx.clone(), &slice.value)?;760761			let start = parse_idx(ctx.clone(), slice.slice.start.as_ref(), "start")?;762			let end = parse_idx(ctx.clone(), slice.slice.end.as_ref(), "end")?;763			let step = parse_idx(ctx, slice.slice.step.as_ref(), "step")?;764765			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?766		}767		Import(kind, path) => {768			let Expr::Str(path) = &**path else {769				bail!("computed imports are not supported")770			};771			with_state(|s| {772				let span = &kind.span;773				let resolved_path = s.resolve_from(span.0.source_path(), path)?;774				Ok(match &**kind {775					ImportKind::Normal => in_frame(776						CallLocation::new(span),777						|| format!("import {:?}", path.clone()),778						|| s.import_resolved(resolved_path),779					)?,780					ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?),781					ImportKind::Bin => Val::arr(s.import_resolved_bin(resolved_path)?),782				}) as Result<Val>783			})?784		}785	})786}
modifiedcrates/jrsonnet-evaluator/src/evaluate/operator.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/operator.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/operator.rs
@@ -1,16 +1,17 @@
 use std::cmp::Ordering;
 
-use jrsonnet_ir::{BinaryOpType, Expr, UnaryOpType};
+use jrsonnet_ir::{BinaryOpType, UnaryOpType};
 
 use crate::{
-	Context, Result, Val,
+	analyze::LExpr,
 	arr::ArrValue,
-	bail,
+	bail, error,
 	error::ErrorKind::*,
-	evaluate,
+	evaluate::evaluate,
 	stdlib::std_format,
 	typed::IntoUntyped as _,
-	val::{StrValue, equals},
+	val::{equals, StrValue},
+	Context, Result, Val,
 };
 
 pub fn evaluate_unary_op(op: UnaryOpType, b: &Val) -> Result<Val> {
@@ -39,7 +40,9 @@
 		(o, Str(a)) => Val::string(format!("{}{a}", o.clone().to_string()?)),
 
 		(Obj(v1), Obj(v2)) => Obj(v2.extend_from(v1.clone())),
-		(Arr(a), Arr(b)) => Val::Arr(ArrValue::extended(a.clone(), b.clone())),
+		(Arr(a), Arr(b)) => Val::Arr(
+			ArrValue::extended(a.clone(), b.clone()).ok_or_else(|| error!("array is too large"))?,
+		),
 
 		(Num(v1), Num(v2)) => Val::try_num(v1.get() + v2.get())?,
 
@@ -158,19 +161,27 @@
 
 pub fn evaluate_binary_op_special(
 	ctx: Context,
-	a: &Expr,
+	a: &LExpr,
 	op: BinaryOpType,
-	b: &Expr,
+	b: &LExpr,
 ) -> Result<Val> {
 	use BinaryOpType::*;
 	use Val::*;
+
 	Ok(match (evaluate(ctx.clone(), a)?, op, b) {
-		(Bool(true), Or, _o) => Val::Bool(true),
-		(Bool(false), And, _o) => Val::Bool(false),
+		(Bool(true), Or, _) => Val::Bool(true),
+		(Bool(false), And, _) => Val::Bool(false),
 		#[cfg(feature = "exp-null-coaelse")]
 		(Null, NullCoaelse, eb) => evaluate(ctx, eb)?,
 		#[cfg(feature = "exp-null-coaelse")]
-		(a, NullCoaelse, _o) => a,
+		(a, NullCoaelse, _) => a,
+		(a, In, LExpr::Super) => {
+			let sup_this = ctx.try_sup_this()?;
+			if !sup_this.has_super() {
+				return Ok(Val::Bool(false));
+			}
+			return Ok(Val::Bool(sup_this.field_in_super(a.to_string()?)));
+		}
 		(a, op, eb) => evaluate_binary_op_normal(&a, op, &evaluate(ctx, eb)?)?,
 	})
 }
modifiedcrates/jrsonnet-evaluator/src/function/builtin.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/builtin.rs
+++ b/crates/jrsonnet-evaluator/src/function/builtin.rs
@@ -19,6 +19,23 @@
 	};
 }
 
+#[macro_export]
+macro_rules! names {
+	($($name:ident: $val:literal),* $(,)?) => {
+		struct Names {
+			$($name: $crate::IStr,)*
+		}
+		thread_local! {
+			static NAMES: Names = Names {
+				$($name: $crate::IStr::from($val)),*
+			};
+		}
+		$(pub fn $name() -> $crate::IStr {
+			NAMES.with(|n| n.$name.clone())
+		})*
+	}
+}
+
 cc_dyn!(
 	#[derive(Clone)]
 	BuiltinFunc,
modifiedcrates/jrsonnet-evaluator/src/function/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/mod.rs
+++ b/crates/jrsonnet-evaluator/src/function/mod.rs
@@ -3,22 +3,24 @@
 use educe::Educe;
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::IStr;
-use jrsonnet_ir::{Destruct, Expr, ExprParams, Span};
+use jrsonnet_ir::Span;
 pub use jrsonnet_macros::builtin;
 
 use self::{
 	builtin::Builtin,
-	parse::parse_default_function_call,
-	prepared::{PreparedCall, parse_prepared_builtin_call, parse_prepared_function_call},
+	prepared::{parse_prepared_builtin_call, PreparedCall},
 };
 use crate::{
-	Context, Result, Thunk, Val, evaluate, evaluate_trivial, function::builtin::BuiltinFunc,
+	analyze::{LDestruct, LExpr, LFunction},
+	evaluate::{destructure::destruct, ensure_sufficient_stack, evaluate, evaluate_trivial},
+	function::builtin::BuiltinFunc,
+	Context, ContextBuilder, Result, Thunk, Val,
 };
 
 pub mod builtin;
 mod native;
 mod parse;
-mod prepared;
+pub(crate) mod prepared;
 
 pub use jrsonnet_ir::function::*;
 pub use native::NativeFn;
@@ -66,19 +68,63 @@
 	/// context will contain `a`.
 	pub ctx: Context,
 
-	/// Function parameter definition
-	pub params: ExprParams,
-	/// Function body
-	pub body: Rc<Expr>,
+	#[educe(PartialEq(method = Rc::ptr_eq))]
+	pub func: Rc<LFunction>,
 }
+
 impl FuncDesc {
-	/// Create body context, but fill arguments without defaults with lazy error
-	pub fn default_body_context(&self) -> Result<Context> {
-		parse_default_function_call(self.ctx.clone(), &self.params)
+	pub fn signature(&self) -> FunctionSignature {
+		self.func.signature.clone()
 	}
 
+	pub fn call(
+		&self,
+		unnamed: &[Thunk<Val>],
+		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,
+			);
+		}
+
+		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 ctx = builder.build().into_future(fctx);
+		ensure_sufficient_stack(|| evaluate(ctx, &self.func.body))
+	}
+
 	pub fn evaluate_trivial(&self) -> Option<Val> {
-		evaluate_trivial(&self.body)
+		evaluate_trivial(&self.func.body)
 	}
 }
 
@@ -115,12 +161,12 @@
 	pub fn params(&self) -> FunctionSignature {
 		match self {
 			Self::Builtin(i) => i.params(),
-			Self::Normal(p) => p.params.signature.clone(),
+			Self::Normal(p) => p.signature(),
 		}
 	}
 	/// Amount of non-default required arguments
-	pub fn params_len(&self) -> usize {
-		self.params().iter().filter(|p| !p.has_default()).count()
+	pub fn params_len(&self) -> u32 {
+		self.params().iter().filter(|p| !p.has_default()).count() as u32
 	}
 	/// Function name, as defined in code.
 	pub fn name(&self) -> IStr {
@@ -139,16 +185,7 @@
 		_tailstrict: bool,
 	) -> Result<Val> {
 		match self {
-			FuncVal::Normal(func) => {
-				let body_ctx = parse_prepared_function_call(
-					func.ctx.clone(),
-					prepared,
-					&func.params,
-					unnamed,
-					named,
-				)?;
-				evaluate(body_ctx, &func.body)
-			}
+			FuncVal::Normal(func) => func.call(unnamed, named, prepared),
 			FuncVal::Builtin(b) => {
 				let args = parse_prepared_builtin_call(prepared, b.params(), unnamed, named);
 				b.call(loc, &args)
@@ -156,7 +193,7 @@
 		}
 	}
 
-	/// Is this function an indentity function.
+	/// Is this function an identity function.
 	///
 	/// Currently only works for builtin `std.id`, aka `Self::Id` value, and `function(x) x`.
 	///
@@ -165,21 +202,19 @@
 		match self {
 			Self::Builtin(b) => b.as_any().downcast_ref::<builtin_id>().is_some(),
 			Self::Normal(desc) => {
-				if desc.params.len() != 1 {
+				if desc.func.params.len() != 1 {
 					return false;
 				}
-				let param = &desc.params.exprs[0];
+				let param = &desc.func.params[0];
 				if param.default.is_some() {
 					return false;
 				}
-
-				#[allow(clippy::infallible_destructuring_match)]
-				let id = match &param.destruct {
-					Destruct::Full(id) => id,
-					#[cfg(feature = "exp-destruct")]
-					_ => return false,
+				#[allow(irrefutable_let_patterns, reason = "refutable with exp-destruct")]
+				let LDestruct::Full(id) = &param.destruct
+				else {
+					return false;
 				};
-				matches!(&*desc.body, Expr::Var(v) if &**v == id)
+				matches!(&*desc.func.body, LExpr::Local(v) if v == id)
 			}
 		}
 	}
modifiedcrates/jrsonnet-evaluator/src/function/parse.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/parse.rs
+++ b/crates/jrsonnet-evaluator/src/function/parse.rs
@@ -1,49 +1,39 @@
-use jrsonnet_ir::ExprParams;
+use std::rc::Rc;
 
 use crate::{
-	Context, ContextBuilder, Thunk,
-	destructure::destruct,
-	error::{ErrorKind::*, Result},
-	evaluate_named_param,
+	analyze::LFunction,
+	evaluate::{destructure::destruct, evaluate},
+	Context, ContextBuilder, Result, Thunk,
 };
 
-/// 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: &ExprParams) -> Result<Context> {
+/// Creates Context with all argument default values applied
+/// and with unbound values causing error to be returned.
+pub fn parse_default_function_call(body_ctx: Context, func: &Rc<LFunction>) -> Result<Context> {
 	let fctx = Context::new_future();
-
-	let mut ctx = ContextBuilder::extend(body_ctx);
+	let mut builder = ContextBuilder::extend(body_ctx, func.params.len());
 
-	for param in params.exprs.iter() {
-		if let Some(v) = &param.default {
-			destruct(
-				&param.destruct.clone(),
-				{
-					let ctx = fctx.clone();
-					let name = param.destruct.name();
-					let value = v.clone();
-					Thunk!(move || evaluate_named_param(ctx.unwrap(), &value, name))
-				},
-				fctx.clone(),
-				&mut ctx,
-			)?;
+	for param in &func.params {
+		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);
 		} else {
-			destruct(
-				&param.destruct,
-				{
-					let param_name = param.destruct.name();
-					let params = params.clone();
-					Thunk!(move || Err(FunctionParameterNotBoundInCall(
-						param_name,
-						params.signature
-					)
-					.into()))
-				},
-				fctx.clone(),
-				&mut ctx,
-			)?;
+			let name = param.name.clone().unwrap_or_else(|| "<param>".into());
+			let thunk = Thunk::errored(
+				crate::error::ErrorKind::FunctionParameterNotBoundInCall(
+					jrsonnet_ir::function::ParamName::Named(name),
+					jrsonnet_ir::function::FunctionSignature::empty(),
+				)
+				.into(),
+			);
+			destruct(&param.destruct, thunk, fctx.clone(), &mut builder);
 		}
 	}
 
-	Ok(ctx.build().into_future(fctx))
+	let ctx = builder.build().into_future(fctx);
+	Ok(ctx)
 }
modifiedcrates/jrsonnet-evaluator/src/function/prepared.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/prepared.rs
+++ b/crates/jrsonnet-evaluator/src/function/prepared.rs
@@ -1,13 +1,13 @@
 use std::rc::Rc;
 
 use jrsonnet_gcmodule::{Acyclic, Trace};
-use jrsonnet_ir::{ExprParams, IStr, function::FunctionSignature};
+use jrsonnet_ir::{IStr, function::FunctionSignature};
 use rustc_hash::FxHashSet;
 
 use super::{CallLocation, FuncVal};
 use crate::{
-	Context, ContextBuilder, Pending, Result, Thunk, Val, bail, destructure::destruct,
-	error::ErrorKind::*, evaluate_named_param,
+	Result, Thunk, Val, bail,
+	error::ErrorKind::*,
 };
 
 #[derive(Debug, Trace, Clone)]
@@ -42,6 +42,15 @@
 	defaults: Vec<usize>,
 }
 
+impl PreparedCall {
+	pub fn named(&self) -> &[(usize, usize)] {
+		&self.named
+	}
+	pub fn defaults(&self) -> &[usize] {
+		&self.defaults
+	}
+}
+
 pub fn prepare_call(
 	params: FunctionSignature,
 	unnamed: usize,
@@ -51,6 +60,25 @@
 		bail!(TooManyArgsFunctionHas(params.len(), params))
 	}
 
+	// Fast path: positional-only (no named args). Avoids HashMap entirely.
+	if named.is_empty() {
+		let mut defaults = Vec::new();
+		for (param_id, param) in params.iter().enumerate().skip(unnamed) {
+			if param.has_default() {
+				defaults.push(param_id);
+			} else {
+				bail!(FunctionParameterNotBoundInCall(
+					param.name().clone(),
+					params.clone(),
+				))
+			}
+		}
+		return Ok(PreparedCall {
+			named: Vec::new(),
+			defaults,
+		});
+	}
+
 	let expected_defaults = (params.len() - unnamed).saturating_sub(named.len());
 	let mut ops = PreparedCall {
 		named: Vec::with_capacity(named.len()),
@@ -110,63 +138,6 @@
 	}
 
 	Ok(ops)
-}
-pub fn parse_prepared_function_call(
-	body_ctx: Context,
-	prepared: &PreparedCall,
-	params: &ExprParams,
-	unnamed: &[Thunk<Val>],
-	named: &[Thunk<Val>],
-) -> Result<Context> {
-	let mut ctx = ContextBuilder::extend(body_ctx);
-
-	let destruct_ctx = Pending::new();
-
-	for (param_idx, unnamed) in unnamed.iter().enumerate() {
-		destruct(
-			&params.exprs[param_idx].destruct,
-			unnamed.clone(),
-			destruct_ctx.clone(),
-			&mut ctx,
-		)?;
-	}
-
-	for (param_idx, arg_idx) in prepared.named.iter().copied() {
-		destruct(
-			&params.exprs[param_idx].destruct,
-			named[arg_idx].clone(),
-			destruct_ctx.clone(),
-			&mut ctx,
-		)?;
-	}
-
-	if prepared.defaults.is_empty() {
-		let body_ctx = ctx.build().into_future(destruct_ctx);
-		Ok(body_ctx)
-	} else {
-		let fctx = Context::new_future();
-		let mut ctx = ctx.commit();
-		for param_idx in prepared.defaults.iter().copied() {
-			// let param = params.0.rc_idx(param_idx);
-			destruct(
-				&params.exprs[param_idx].destruct,
-				{
-					let ctx = fctx.clone();
-					let params = params.clone();
-					Thunk!(move || {
-						let param = &params.exprs[param_idx];
-						let name = param.destruct.name();
-						let value = param.default.as_ref().expect("default exists");
-						evaluate_named_param(ctx.unwrap(), value, name)
-					})
-				},
-				fctx.clone(),
-				&mut ctx,
-			)?;
-		}
-
-		Ok(ctx.build().into_future(fctx).into_future(destruct_ctx))
-	}
 }
 pub fn parse_prepared_builtin_call(
 	prepared: &PreparedCall,
modifiedcrates/jrsonnet-evaluator/src/integrations/serde.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/integrations/serde.rs
+++ b/crates/jrsonnet-evaluator/src/integrations/serde.rs
@@ -3,16 +3,16 @@
 use jrsonnet_interner::{IBytes, IStr};
 use jrsonnet_ir::NumValue;
 use serde::{
-	Deserialize, Serialize, Serializer,
 	de::{self, Visitor},
 	ser::{
 		Error, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple,
 		SerializeTupleStruct, SerializeTupleVariant,
 	},
+	Deserialize, Serialize, Serializer,
 };
 
 use crate::{
-	Error as JrError, ObjValue, ObjValueBuilder, Result, Val, in_description_frame, runtime_error,
+	in_description_frame, runtime_error, Error as JrError, ObjValue, ObjValueBuilder, Result, Val,
 };
 
 impl<'de> Deserialize<'de> for Val {
@@ -182,7 +182,7 @@
 			#[cfg(feature = "exp-bigint")]
 			Self::BigInt(b) => b.serialize(serializer),
 			Self::Arr(arr) => {
-				let mut seq = serializer.serialize_seq(Some(arr.len()))?;
+				let mut seq = serializer.serialize_seq(Some(arr.len() as usize))?;
 				for (i, element) in arr.iter().enumerate() {
 					let mut serde_error = None;
 					in_description_frame(
@@ -203,7 +203,7 @@
 				seq.end()
 			}
 			Self::Obj(obj) => {
-				let mut map = serializer.serialize_map(Some(obj.len()))?;
+				let mut map = serializer.serialize_map(Some(obj.len() as usize))?;
 				for (field, value) in obj.iter(
 					#[cfg(feature = "exp-preserve-order")]
 					true,
modifiedcrates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/lib.rs
+++ b/crates/jrsonnet-evaluator/src/lib.rs
@@ -36,14 +36,13 @@
 pub use ctx::*;
 pub use dynamic::*;
 pub use error::{Error, ErrorKind::*, Result, ResultExt};
-pub use evaluate::*;
+pub use evaluate::ensure_sufficient_stack;
 use function::CallLocation;
 pub use import::*;
-use jrsonnet_gcmodule::{Cc, Trace, cc_dyn};
+use jrsonnet_gcmodule::{cc_dyn, Cc, Trace};
 pub use jrsonnet_interner::{IBytes, IStr};
-pub use jrsonnet_ir as parser;
-pub use jrsonnet_ir::NumValue;
-use jrsonnet_ir::{Expr, Source, SourcePath};
+use jrsonnet_ir::Expr;
+pub use jrsonnet_ir::{NumValue, Source, SourcePath, Span};
 #[doc(hidden)]
 pub use jrsonnet_macros;
 
@@ -58,6 +57,7 @@
 pub use tla::apply_tla;
 pub use val::{Thunk, Val};
 
+pub mod analyze;
 use crate::gc::WithCapacityExt as _;
 
 #[allow(clippy::needless_return)]
@@ -87,7 +87,7 @@
 	jrsonnet_ir_parser::parse(code, &jrsonnet_ir_parser::ParserSettings { source }).map_err(|e| {
 		SyntaxError {
 			message: e.message,
-			location: (e.location.0, e.location.1),
+			location: e.location,
 		}
 	})
 }
@@ -165,7 +165,7 @@
 pub trait ContextInitializer {
 	/// For composability: extend builder. May panic if this initialization is not supported,
 	/// and the context may only be created via `initialize`.
-	fn populate(&self, for_file: Source, builder: &mut ContextBuilder);
+	fn populate(&self, for_file: Source, builder: &mut InitialContextBuilder);
 	/// Allows upcasting from abstract to concrete context initializer.
 	/// jrsonnet by itself doesn't use this method, it is allowed for it to panic.
 	fn as_any(&self) -> &dyn Any;
@@ -174,7 +174,7 @@
 where
 	T: ContextInitializer,
 {
-	fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {
+	fn populate(&self, for_file: Source, builder: &mut InitialContextBuilder) {
 		(*self).populate(for_file, builder);
 	}
 
@@ -185,7 +185,7 @@
 
 /// Context initializer which adds nothing.
 impl ContextInitializer for () {
-	fn populate(&self, _for_file: Source, _builder: &mut ContextBuilder) {}
+	fn populate(&self, _for_file: Source, _builder: &mut InitialContextBuilder) {}
 	fn as_any(&self) -> &dyn Any {
 		self
 	}
@@ -195,7 +195,7 @@
 where
 	T: ContextInitializer + 'static,
 {
-	fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {
+	fn populate(&self, for_file: Source, builder: &mut InitialContextBuilder) {
 		if let Some(ctx) = self {
 			ctx.populate(for_file, builder);
 		}
@@ -210,7 +210,7 @@
 	($($gen:ident)*) => {
 		#[allow(non_snake_case)]
 		impl<$($gen: ContextInitializer + Trace,)*> ContextInitializer for ($($gen,)*) {
-			fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {
+			fn populate(&self, for_file: Source, builder: &mut InitialContextBuilder) {
 				let ($($gen,)*) = self;
 				$($gen.populate(for_file.clone(), builder);)*
 			}
@@ -408,7 +408,12 @@
 		file.evaluating = true;
 		// Dropping file cache guard here, as evaluation may use this map too
 		drop(file_cache);
-		let res = evaluate(self.create_default_context(file_name), &parsed);
+		let (ctx, externals) = self.create_default_context(file_name.clone()).build();
+		let report = analyze::analyze_root(&parsed, externals);
+		if report.errored {
+			return Err(StaticAnalysisError(report.diagnostics_list).into());
+		}
+		let res = evaluate::evaluate(ctx.build(), &report.lir);
 
 		let mut file_cache = self.file_cache();
 		let mut file = file_cache.entry(path);
@@ -438,7 +443,7 @@
 	}
 
 	/// Creates context with all passed global variables
-	pub fn create_default_context(&self, source: Source) -> Context {
+	pub fn create_default_context(&self, source: Source) -> InitialContextBuilder {
 		self.create_default_context_with(source, &())
 	}
 
@@ -447,13 +452,13 @@
 		&self,
 		source: Source,
 		context_initializer: &dyn ContextInitializer,
-	) -> Context {
+	) -> InitialContextBuilder {
 		let default_initializer = self.context_initializer();
-		let mut builder = ContextBuilder::new();
+		let mut builder = InitialContextBuilder::new();
 		default_initializer.populate(source.clone(), &mut builder);
 		context_initializer.populate(source, &mut builder);
 
-		builder.build()
+		builder
 	}
 }
 
@@ -487,7 +492,7 @@
 #[derive(Trace)]
 pub struct InitialUnderscore(pub Thunk<Val>);
 impl ContextInitializer for InitialUnderscore {
-	fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {
+	fn populate(&self, _for_file: Source, builder: &mut InitialContextBuilder) {
 		builder.bind("_", self.0.clone());
 	}
 
@@ -515,10 +520,14 @@
 			path: source.clone(),
 			error: Box::new(e),
 		})?;
-		evaluate(
-			self.create_default_context_with(source, context_initializer),
-			&parsed,
-		)
+		let (ctx, externals) = self
+			.create_default_context_with(source.clone(), context_initializer)
+			.build();
+		let report = analyze::analyze_root(&parsed, externals);
+		if report.errored {
+			return Err(StaticAnalysisError(report.diagnostics_list).into());
+		}
+		evaluate::evaluate(ctx.build(), &report.lir)
 	}
 }
 
modifiedcrates/jrsonnet-evaluator/src/obj/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/obj/mod.rs
+++ b/crates/jrsonnet-evaluator/src/obj/mod.rs
@@ -11,8 +11,8 @@
 };
 
 use educe::Educe;
-use im_rc::{Vector, vector};
-use jrsonnet_gcmodule::{Acyclic, Cc, Trace, Weak, cc_dyn};
+use im_rc::{vector, Vector};
+use jrsonnet_gcmodule::{cc_dyn, Acyclic, Cc, Trace, Weak};
 use jrsonnet_interner::IStr;
 use jrsonnet_ir::Span;
 use rustc_hash::{FxHashMap, FxHashSet};
@@ -23,13 +23,13 @@
 pub use oop::ObjValueBuilder;
 
 use crate::{
-	CcUnbound, MaybeUnbound, Result, Thunk, Unbound, Val,
 	arr::{PickObjectKeyValues, PickObjectValues},
 	bail,
-	error::{ErrorKind::*, suggest_object_fields},
+	error::{suggest_object_fields, ErrorKind::*},
+	evaluate::operator::evaluate_add_op,
 	identity_hash,
-	operator::evaluate_add_op,
 	val::{ArrValue, ThunkValue},
+	CcUnbound, MaybeUnbound, Result, Thunk, Unbound, Val,
 };
 
 #[cfg(not(feature = "exp-preserve-order"))]
@@ -400,6 +400,15 @@
 	this: ObjValue,
 }
 impl SupThis {
+	/// Create a `SupThis` for a freshly constructed object (no super).
+	pub fn new(this: ObjValue) -> Self {
+		Self {
+			sup: CoreIdx {
+				idx: this.0.cores.len(),
+			},
+			this,
+		}
+	}
 	pub fn has_super(&self) -> bool {
 		self.sup.super_exists()
 	}
@@ -501,11 +510,11 @@
 	// }
 	/// Returns amount of visible object fields
 	/// If object only contains hidden fields - may return zero.
-	pub fn len(&self) -> usize {
+	pub fn len(&self) -> u32 {
 		self.fields_visibility()
 			.values()
 			.filter(|d| d.visible())
-			.count()
+			.count() as u32
 	}
 	/// For each field, calls callback.
 	/// If callback returns false - ends iteration prematurely.
modifiedcrates/jrsonnet-evaluator/src/trace/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/trace/mod.rs
+++ b/crates/jrsonnet-evaluator/src/trace/mod.rs
@@ -10,7 +10,7 @@
 #[cfg(feature = "explaining-traces")]
 use jrsonnet_ir::Span;
 
-use crate::{Error, error::ErrorKind};
+use crate::{error::ErrorKind, Error};
 
 /// The way paths should be displayed
 #[derive(Clone, Trace)]
@@ -122,7 +122,7 @@
 				|| path.source_path().to_string(),
 				|r| self.resolver.resolve(r),
 			);
-			let mut offset = error.location.0 as usize;
+			let mut offset = error.location.1 as usize;
 			let is_eof = if offset >= path.code().len() {
 				offset = path.code().len().saturating_sub(1);
 				true
@@ -259,25 +259,64 @@
 		struct ResetData {
 			loc: Span,
 		}
-		use hi_doc::{Formatting, SnippetBuilder, Text, source_to_ansi};
+		use hi_doc::{source_to_ansi, Formatting, SnippetBuilder, Text};
 
 		write!(out, "{}", error.error())?;
 		if let ErrorKind::ImportSyntaxError { path, error } = error.error() {
 			writeln!(out)?;
-			let mut offset = error.location;
-			// To inclusive range
-			if offset.1 > offset.0 {
-				offset.1 -= 1;
-			}
 			let mut builder = SnippetBuilder::new(path.code());
 			builder
 				.error(Text::fragment("syntax error", Formatting::default()))
-				.range(offset.0 as usize..=offset.1 as usize)
+				.range(error.location.range())
 				.build();
 			let source = builder.build();
 			let ansi = source_to_ansi(&source);
 			write!(out, "{ansi}")?;
 		}
+		if let ErrorKind::StaticAnalysisError(diagnostics) = error.error() {
+			use crate::analyze::DiagLevel;
+			let mut builder: Option<SnippetBuilder> = None;
+			let mut current_src: Option<&str> = None;
+			let flush =
+				|builder: Option<SnippetBuilder>, out: &mut dyn std::fmt::Write| -> Result<(), std::fmt::Error> {
+					if let Some(b) = builder {
+						let ansi = source_to_ansi(&b.build());
+						write!(out, "\n{}", ansi.trim_end())?;
+					}
+					Ok(())
+				};
+			for diag in diagnostics {
+				if let Some(span) = &diag.span {
+					let src = span.0.code();
+					if current_src != Some(src) {
+						flush(builder.take(), out)?;
+						builder = Some(SnippetBuilder::new(src));
+						current_src = Some(src);
+					}
+					let b = builder.as_mut().unwrap();
+					let ab = match diag.level {
+						DiagLevel::Error => b.error(Text::fragment(
+							diag.message.clone(),
+							Formatting::default(),
+						)),
+						DiagLevel::Warning => b.warning(Text::fragment(
+							diag.message.clone(),
+							Formatting::default(),
+						)),
+					};
+					ab.range(span.range()).build();
+				} else {
+					flush(builder.take(), out)?;
+					current_src = None;
+					let prefix = match diag.level {
+						DiagLevel::Error => "error",
+						DiagLevel::Warning => "warning",
+					};
+					write!(out, "\n{prefix}: {}", diag.message)?;
+				}
+			}
+			flush(builder, out)?;
+		}
 		let trace = &error.trace();
 		let snippet_builder: RefCell<Option<SnippetBuilder>> = RefCell::new(None);
 		let mut last_location: Option<Span> = None;
modifiedcrates/jrsonnet-evaluator/src/typed/conversions.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/typed/conversions.rs
+++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs
@@ -637,7 +637,7 @@
 		}
 		<Self as Typed>::TYPE.check(&value)?;
 		// Any::downcast_ref::<ByteArray>(&a);
-		let mut out = Vec::with_capacity(a.len());
+		let mut out = Vec::with_capacity(a.len() as usize);
 		for e in a.iter() {
 			let r = e?;
 			out.push(u8::from_untyped(r)?);
modifiedcrates/jrsonnet-interner/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-interner/src/lib.rs
+++ b/crates/jrsonnet-interner/src/lib.rs
@@ -171,6 +171,7 @@
 			let mut pool = pool.borrow_mut();
 
 			if pool.remove(inner).is_none() {
+				// DOC(string-pooling)
 				// On some platforms (i.e i686-windows), try_with will not fail after TLS
 				// destructor is called, but instead re-initialize the TLS with the empty pool.
 				// Allow non-pooled Drop in this case.
modifiedcrates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-macros/src/lib.rs
+++ b/crates/jrsonnet-macros/src/lib.rs
@@ -3,13 +3,14 @@
 use proc_macro2::TokenStream;
 use quote::{quote, quote_spanned};
 use syn::{
-	Attribute, DeriveInput, Error, Expr, ExprClosure, FnArg, GenericArgument, Ident, ItemFn,
-	LitStr, Meta, Pat, Path, PathArguments, Result, ReturnType, Token, Type, parenthesized,
+	parenthesized,
 	parse::{Parse, ParseStream},
 	parse_macro_input,
 	punctuated::Punctuated,
 	spanned::Spanned,
 	token::Comma,
+	Attribute, DeriveInput, Error, Expr, ExprClosure, FnArg, GenericArgument, Ident, ItemFn,
+	LitStr, Meta, Pat, Path, PathArguments, Result, ReturnType, Token, Type,
 };
 
 use self::typed::{derive_from_untyped_inner, derive_into_untyped_inner, derive_typed_inner};
@@ -402,7 +403,7 @@
 				State, Val,
 				function::{builtin::Builtin, FunctionSignature, ParamParse, ParamName, ParamDefault, CallLocation},
 				Result, Context, typed::{Typed, FromUntyped, IntoUntypedResult},
-				parser::Span, params, Thunk,
+				Span, params, Thunk,
 			};
 			params!(
 				#(#params_desc)*
modifiedcrates/jrsonnet-stdlib/src/arrays.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/arrays.rs
+++ b/crates/jrsonnet-stdlib/src/arrays.rs
@@ -1,11 +1,12 @@
 #![allow(non_snake_case)]
 
 use jrsonnet_evaluator::{
-	Either, IStr, ObjValue, ObjValueBuilder, Result, ResultExt, Thunk, Val, bail,
-	function::{FuncVal, NativeFn, builtin},
+	bail, error,
+	function::{builtin, NativeFn},
 	runtime_error,
-	typed::{BoundedI32, BoundedUsize, Either2, FromUntyped},
-	val::{ArrValue, IndexableVal, equals},
+	typed::{BoundedUsize, Either2, FromUntyped},
+	val::{equals, ArrValue, IndexableVal},
+	Either, IStr, ObjValue, ObjValueBuilder, Result, ResultExt, Thunk, Val,
 };
 
 pub fn eval_on_empty(on_empty: Option<Thunk<Val>>) -> Result<Val> {
@@ -17,32 +18,28 @@
 }
 
 #[builtin]
-pub fn builtin_make_array(
-	// Can't use usize because range_exclusive is over i32
-	sz: BoundedI32<0, { i32::MAX }>,
-	func: FuncVal,
-) -> Result<ArrValue> {
-	if *sz == 0 {
+pub fn builtin_make_array(sz: u32, func: NativeFn!((u32,) -> Val)) -> Result<ArrValue> {
+	if sz == 0 {
 		return Ok(ArrValue::empty());
 	}
-	func.evaluate_trivial().map_or_else(
-		// TODO: Different mapped array impl avoiding allocating unnecessary vals
-		|| Ok(ArrValue::range_exclusive(0, *sz).map(FromUntyped::from_untyped(Val::Func(func))?)),
-		|trivial| {
-			#[expect(clippy::cast_sign_loss, reason = "sz is bounded to be larger than 0")]
-			let mut out = Vec::with_capacity(*sz as usize);
-			for _ in 0..*sz {
-				out.push(trivial.clone());
+	// Try eager evaluation: call func(i) immediately for each element.
+	'eager: {
+		let mut out = Vec::with_capacity(sz as usize);
+		for i in 0..sz {
+			match func.call(i) {
+				Ok(v) => out.push(v),
+				Err(_) => break 'eager,
 			}
-			Ok(ArrValue::new(out))
-		},
-	)
+		}
+		return Ok(ArrValue::new(out));
+	}
+	Ok(ArrValue::make(sz, func))
 }
 
 #[builtin]
-pub fn builtin_repeat(what: Either![IStr, ArrValue], count: usize) -> Result<Val> {
+pub fn builtin_repeat(what: Either![IStr, ArrValue], count: u32) -> Result<Val> {
 	Ok(match what {
-		Either2::A(s) => Val::string(s.repeat(count)),
+		Either2::A(s) => Val::string(s.repeat(count as usize)),
 		Either2::B(arr) => Val::Arr(
 			ArrValue::repeated(arr, count)
 				.ok_or_else(|| runtime_error!("repeated length overflow"))?,
@@ -210,14 +207,14 @@
 				let item = item?.clone();
 				if let Val::Arr(items) = item {
 					if !first {
-						out.reserve(joiner_items.len());
+						out.reserve(joiner_items.len() as usize);
 						// TODO: extend
 						for item in joiner_items.iter() {
 							out.push(item?);
 						}
 					}
 					first = false;
-					out.reserve(items.len());
+					out.reserve(items.len() as usize);
 					for item in items.iter() {
 						out.push(item?);
 					}
@@ -256,7 +253,8 @@
 pub fn builtin_lines(arr: ArrValue) -> Result<IndexableVal> {
 	builtin_join(
 		IndexableVal::Str("\n".into()),
-		ArrValue::extended(arr, ArrValue::new(vec![Val::string("")])),
+		ArrValue::extended(arr, ArrValue::new(vec![Val::string("")]))
+			.ok_or_else(|| error!("array is too large"))?,
 	)
 }
 
@@ -380,7 +378,7 @@
 	let newArrLeft = arr.clone().slice(None, Some(at), None);
 	let newArrRight = arr.slice(Some(at + 1), None, None);
 
-	Ok(ArrValue::extended(newArrLeft, newArrRight))
+	Ok(ArrValue::extended(newArrLeft, newArrRight).ok_or_else(|| error!("array is too large"))?)
 }
 
 #[builtin]
@@ -399,20 +397,22 @@
 }
 
 #[builtin]
-pub fn builtin_flatten_arrays(arrs: Vec<ArrValue>) -> ArrValue {
-	pub fn flatten_inner(values: &[ArrValue]) -> ArrValue {
+pub fn builtin_flatten_arrays(arrs: Vec<ArrValue>) -> Result<ArrValue> {
+	pub fn flatten_inner(values: &[ArrValue]) -> Result<ArrValue> {
 		if values.len() == 1 {
-			return values[0].clone();
+			return Ok(values[0].clone());
 		} else if values.len() == 2 {
-			return ArrValue::extended(values[0].clone(), values[1].clone());
+			return ArrValue::extended(values[0].clone(), values[1].clone())
+				.ok_or_else(|| error!("array is too large"));
 		}
 		let (a, b) = values.split_at(values.len() / 2);
-		ArrValue::extended(flatten_inner(a), flatten_inner(b))
+		ArrValue::extended(flatten_inner(a)?, flatten_inner(b)?)
+			.ok_or_else(|| error!("array is too large"))
 	}
 	if arrs.is_empty() {
-		return ArrValue::empty();
+		return Ok(ArrValue::empty());
 	} else if arrs.len() == 1 {
-		return arrs.into_iter().next().expect("single");
+		return Ok(arrs.into_iter().next().expect("single"));
 	}
 	flatten_inner(&arrs)
 }
modifiedcrates/jrsonnet-stdlib/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/lib.rs
+++ b/crates/jrsonnet-stdlib/src/lib.rs
@@ -12,15 +12,9 @@
 pub use encoding::*;
 pub use hash::*;
 use jrsonnet_evaluator::{
-	ContextBuilder, IStr, NumValue, ObjValue, ObjValueBuilder, Thunk, Val,
-	error::Result,
-	function::{CallLocation, FuncVal, builtin_id},
-	tla::TlaArg,
-	trace::PathResolver,
-	typed::SerializeTypedObj as _,
+	IStr, InitialContextBuilder, NumValue, ObjValue, ObjValueBuilder, Source, Thunk, Val, error::Result, function::{CallLocation, FuncVal, builtin_id}, tla::TlaArg, trace::PathResolver, typed::SerializeTypedObj as _
 };
 use jrsonnet_gcmodule::{Acyclic, Cc, Trace};
-use jrsonnet_ir::Source;
 use jrsonnet_macros::{IntoUntyped, Typed};
 pub use manifest::*;
 pub use math::*;
@@ -544,7 +538,7 @@
 	}
 }
 impl jrsonnet_evaluator::ContextInitializer for ContextInitializer {
-	fn populate(&self, source: Source, builder: &mut ContextBuilder) {
+	fn populate(&self, source: Source, builder: &mut InitialContextBuilder) {
 		let mut std = ObjValueBuilder::new();
 		std.with_super(self.stdlib_obj.clone());
 		std.field("thisFile").hide().value({
modifiedcrates/jrsonnet-stdlib/src/misc.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/misc.rs
+++ b/crates/jrsonnet-stdlib/src/misc.rs
@@ -1,22 +1,23 @@
 use std::{cell::RefCell, collections::BTreeSet};
 
 use jrsonnet_evaluator::{
-	Either, IStr, ObjValue, ObjValueBuilder, ResultExt, Thunk, Val, bail,
+	bail,
 	error::{ErrorKind::*, Result},
-	function::{CallLocation, FuncVal, builtin},
+	function::{builtin, CallLocation, FuncVal},
 	manifest::JsonFormat,
 	typed::{Either2, Either4},
-	val::{ArrValue, equals},
+	val::{equals, ArrValue},
+	Either, IStr, ObjValue, ObjValueBuilder, ResultExt, Thunk, Val,
 };
 use jrsonnet_gcmodule::Cc;
 
 use crate::Settings;
 
 #[builtin]
-pub fn builtin_length(x: Either![IStr, ArrValue, ObjValue, FuncVal]) -> usize {
+pub fn builtin_length(x: Either![IStr, ArrValue, ObjValue, FuncVal]) -> u32 {
 	use Either4::*;
 	match x {
-		A(x) => x.chars().count(),
+		A(x) => x.chars().count() as u32,
 		B(x) => x.len(),
 		C(x) => x.len(),
 		D(f) => f.params_len(),
@@ -102,7 +103,7 @@
 			} else if b.len() == a.len() {
 				return equals(&Val::Arr(a), &Val::Arr(b));
 			}
-			for (a, b) in a.iter().take(b.len()).zip(b.iter()) {
+			for (a, b) in a.iter().take(b.len() as usize).zip(b.iter()) {
 				let a = a?;
 				let b = b?;
 				if !equals(&a, &b)? {
@@ -127,7 +128,7 @@
 				return equals(&Val::Arr(a), &Val::Arr(b));
 			}
 			let a_len = a.len();
-			for (a, b) in a.iter().skip(a_len - b.len()).zip(b.iter()) {
+			for (a, b) in a.iter().skip((a_len - b.len()) as usize).zip(b.iter()) {
 				let a = a?;
 				let b = b?;
 				if !equals(&a, &b)? {
modifiedtests/tests/builtin.rsdiffbeforeafterboth
--- a/tests/tests/builtin.rs
+++ b/tests/tests/builtin.rs
@@ -1,11 +1,7 @@
 mod common;
 
 use jrsonnet_evaluator::{
-	ContextBuilder, ContextInitializer, FileImportResolver, Result, State, Thunk, Val,
-	function::{CallLocation, FuncVal, builtin, builtin::Builtin},
-	parser::Source,
-	trace::PathResolver,
-	typed::FromUntyped,
+	ContextInitializer, FileImportResolver, InitialContextBuilder, Result, Source, State, Thunk, Val, function::{CallLocation, FuncVal, builtin, builtin::{Builtin}}, trace::PathResolver, typed::FromUntyped
 };
 use jrsonnet_gcmodule::Trace;
 use jrsonnet_stdlib::ContextInitializer as StdContextInitializer;
@@ -31,7 +27,7 @@
 #[derive(Trace)]
 struct NativeAddContextInitializer;
 impl ContextInitializer for NativeAddContextInitializer {
-	fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {
+	fn populate(&self, _for_file: Source, builder: &mut InitialContextBuilder) {
 		builder.bind("nativeAdd", Thunk::evaluated(Val::function(native_add {})));
 	}
 
@@ -76,7 +72,7 @@
 #[derive(Trace)]
 struct CurryAddContextInitializer;
 impl ContextInitializer for CurryAddContextInitializer {
-	fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {
+	fn populate(&self, _for_file: Source, builder: &mut InitialContextBuilder) {
 		builder.bind("curryAdd", Thunk::evaluated(Val::function(curry_add {})));
 	}
 
modifiedtests/tests/common.rsdiffbeforeafterboth
--- a/tests/tests/common.rs
+++ b/tests/tests/common.rs
@@ -1,8 +1,5 @@
 use jrsonnet_evaluator::{
-	ContextBuilder, ContextInitializer as ContextInitializerT, ObjValueBuilder, Result, Thunk, Val,
-	bail,
-	function::{FuncVal, builtin},
-	parser::Source,
+	ContextBuilder, ContextInitializer as ContextInitializerT, InitialContextBuilder, ObjValueBuilder, Result, Thunk, Val, bail, function::{FuncVal, builtin}, Source
 };
 use jrsonnet_gcmodule::Trace;
 
@@ -68,7 +65,7 @@
 #[allow(dead_code)]
 pub struct ContextInitializer;
 impl ContextInitializerT for ContextInitializer {
-	fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {
+	fn populate(&self, _for_file: Source, builder: &mut InitialContextBuilder) {
 		let mut bobj = ObjValueBuilder::new();
 		bobj.method("assertThrow", assert_throw {});
 		bobj.method("paramNames", param_names {});
modifiedtests/tests/cpp_test_suite.rsdiffbeforeafterboth
--- a/tests/tests/cpp_test_suite.rs
+++ b/tests/tests/cpp_test_suite.rs
@@ -9,9 +9,11 @@
 	gc::WithCapacityExt as _,
 	manifest::JsonFormat,
 	rustc_hash::FxHashMap,
+	stack::limit_stack_depth,
 	tla::TlaArg,
 	trace::{CompactFormat, PathResolver, TraceFormat},
 };
+use jrsonnet_gcmodule::ObjectSpace;
 use jrsonnet_stdlib::ContextInitializer;
 mod common;
 use common::ContextInitializer as TestContextInitializer;
@@ -179,6 +181,12 @@
 			continue;
 		}
 
+		let _stack = if entry.path().file_stem().is_some_and(|e| e == "recursive_function" || e == "tailstrict"|| e == "tailstrict5") {
+			Some(limit_stack_depth(100_000))
+		} else {
+			None
+		};
+
 		if entry
 			.path()
 			.file_name()
@@ -188,7 +196,7 @@
 			continue;
 		}
 
-		println!("test: {}", entry.path().display());
+		eprintln!("test: {}", entry.path().display());
 
 		let result = run(&entry.path(), &root);
 
@@ -212,25 +220,10 @@
 		if let Some(golden_path) = read_file(&golden_override)? {
 			golden = Some(golden_path);
 		}
-
-		// ir-parser has its own override layer
-		#[cfg(feature = "ir-parser")]
-		let ir_parser_override_path = {
-			let p = root_tests
-				.join(format!("{root_dir}_golden_override_ir_parser"))
-				.join(golden_path.file_name().expect("file has basename"));
-			if let Some(golden_path) = read_file(&p)? {
-				golden = Some(golden_path);
-			}
-			p
-		};
 
 		// Otherwise assume test should just not fail and return true.
 		let golden = golden.unwrap_or_else(|| "true".to_owned());
 
-		#[cfg(feature = "ir-parser")]
-		let update_golden_path = &ir_parser_override_path;
-		#[cfg(not(feature = "ir-parser"))]
 		let update_golden_path = &golden_override;
 
 		match (serde_json::from_str::<serde_json::Value>(&result), serde_json::from_str::<serde_json::Value>(&golden)) {
@@ -270,8 +263,11 @@
 				}
 			}
 		}
+		println!("done!");
 	}
 	}
 
+	jrsonnet_gcmodule::with_thread_object_space(ObjectSpace::leak);
+
 	Ok(())
 }
modifiedtests/tests/snapshots/golden__golden@issue172.jsonnet.snapdiffbeforeafterboth
--- a/tests/tests/snapshots/golden__golden@issue172.jsonnet.snap
+++ b/tests/tests/snapshots/golden__golden@issue172.jsonnet.snap
@@ -3,7 +3,4 @@
 expression: result
 input_file: tests/golden/issue172.jsonnet
 ---
-local is not defined: b
-    issue172.jsonnet:1:45-47: local <b> access
-    issue172.jsonnet:1:4-10:  field <value> access
-    elem <0> evaluation
+static analysis errors: undefined local: b
modifiedtests/tests/snapshots/golden__golden@issue23.jsonnet.snapdiffbeforeafterboth
--- a/tests/tests/snapshots/golden__golden@issue23.jsonnet.snap
+++ b/tests/tests/snapshots/golden__golden@issue23.jsonnet.snap
@@ -4,4 +4,4 @@
 input_file: tests/golden/issue23.jsonnet
 ---
 infinite recursion detected
-    issue23.jsonnet:1:1-8: import "issue23.jsonnet"
+    issue23.jsonnet:1:1-8: import
modifiedtests/tests/snapshots/golden__golden@missing_binding.jsonnet.snapdiffbeforeafterboth
--- a/tests/tests/snapshots/golden__golden@missing_binding.jsonnet.snap
+++ b/tests/tests/snapshots/golden__golden@missing_binding.jsonnet.snap
@@ -3,6 +3,5 @@
 expression: result
 input_file: tests/golden/missing_binding.jsonnet
 ---
-local is not defined: sta
+static analysis errors: undefined local: sta
 There is a local with similar name present: std
-    missing_binding.jsonnet:1:1-5: local <sta> access