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
--- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs
@@ -2,39 +2,42 @@
 
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::IStr;
-use jrsonnet_ir::{
-	ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, ExprParams, FieldMember,
-	FieldName, ForSpecData, IfSpecData, ImportKind, LiteralType, ObjBody, ObjMembers, Spanned,
-	function::ParamName,
-};
+use jrsonnet_ir::ImportKind;
 use jrsonnet_types::ValType;
 
-use self::destructure::destruct;
+use self::{
+	compspec::{evaluate_arr_comp, evaluate_obj_comp},
+	destructure::{evaluate_locals, evaluate_locals_unbound},
+	operator::evaluate_binary_op_special,
+};
 use crate::{
-	Context, ContextBuilder, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,
-	ResultExt, SupThis, Unbound, Val,
-	arr::ArrValue,
+	analyze::{
+		LArgsDesc, LAssertStmt, LExpr, LFieldMember, LFieldName, LFunction, LIndexPart, LObjBody,
+		LObjMembers,
+	},
 	bail,
-	destructure::evaluate_dest,
-	error::{ErrorKind::*, suggest_object_fields},
-	evaluate::operator::{evaluate_binary_op_special, evaluate_unary_op},
-	function::{CallLocation, FuncDesc, FuncVal, PreparedFuncVal},
-	in_frame,
-	typed::{FromUntyped, IntoUntyped as _, Typed},
-	val::{CachedUnbound, IndexableVal, StrValue, Thunk},
-	with_state,
+	error::{suggest_object_fields, ErrorKind::*},
+	evaluate::operator::evaluate_unary_op,
+	function::{prepared::PreparedFuncVal, CallLocation, FuncDesc, FuncVal},
+	in_frame, runtime_error,
+	typed::FromUntyped as _,
+	val::{CachedUnbound, Thunk},
+	with_state, Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Result, ResultExt as _,
+	SupThis, Unbound, Val,
 };
+
+pub mod compspec;
 pub mod destructure;
 pub mod operator;
 
 // This is the amount of bytes that need to be left on the stack before increasing the size.
 // It must be at least as large as the stack required by any code that does not call
 // `ensure_sufficient_stack`.
-const RED_ZONE: usize = 100 * 1024; // 100k
+const RED_ZONE: usize = 100 * 1024;
 
 // Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then
 // on. This flag has performance relevant characteristics. Don't set it too high.
-const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB
+const STACK_PER_RECURSION: usize = 1024 * 1024;
 
 /// Grows the stack on demand to prevent stack overflow. Call this in strategic locations
 /// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit
@@ -46,54 +49,36 @@
 	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)
 }
 
-pub fn evaluate_trivial(expr: &Expr) -> Option<Val> {
-	fn is_trivial(expr: &Expr) -> bool {
-		match expr {
-			Expr::Str(_)
-			| Expr::Num(_)
-			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,
-			Expr::Arr(a) => a.iter().all(is_trivial),
-			_ => false,
-		}
-	}
+pub fn evaluate_trivial(expr: &LExpr) -> Option<Val> {
+	// TODO: Eager trivial array
 	Some(match expr {
-		Expr::Str(s) => Val::string(s.clone()),
-		Expr::Num(n) => Val::Num(*n),
-		Expr::Literal(LiteralType::False) => Val::Bool(false),
-		Expr::Literal(LiteralType::True) => Val::Bool(true),
-		Expr::Literal(LiteralType::Null) => Val::Null,
-		Expr::Arr(n) => {
-			if n.iter().any(|e| !is_trivial(e)) {
-				return None;
-			}
-			Val::Arr(
-				n.iter()
-					.map(evaluate_trivial)
-					.map(|e| e.expect("checked trivial"))
-					.collect(),
-			)
-		}
+		LExpr::Str(s) => Val::string(s.clone()),
+		LExpr::Num(n) => Val::Num(*n),
+		LExpr::Bool(false) => Val::Bool(false),
+		LExpr::Bool(true) => Val::Bool(true),
+		LExpr::Null => Val::Null,
 		_ => return None,
 	})
 }
 
-pub fn evaluate_method(ctx: Context, name: IStr, params: ExprParams, body: Rc<Expr>) -> Val {
+/// Evaluate a method definition.
+pub fn evaluate_method(ctx: Context, name: IStr, func: &Rc<LFunction>) -> Val {
 	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {
 		name,
 		ctx,
-		params,
-		body,
+		func: func.clone(),
 	})))
 }
 
-pub fn evaluate_field_name(ctx: Context, field_name: &Spanned<FieldName>) -> Result<Option<IStr>> {
-	Ok(match &field_name.value {
-		FieldName::Fixed(n) => Some(n.clone()),
-		FieldName::Dyn(expr) => in_frame(
-			CallLocation::new(&field_name.span),
+pub fn evaluate_field_name(ctx: Context, field_name: &LFieldName) -> Result<Option<IStr>> {
+	Ok(match field_name {
+		LFieldName::Fixed(n) => Some(n.clone()),
+		LFieldName::Dyn(expr) => in_frame(
+			// TODO: Spanned<LFieldName>
+			CallLocation::native(),
 			|| "evaluating field name".to_string(),
 			|| {
-				let v = evaluate(ctx, expr)?;
+				let v = evaluate(ctx.clone(), expr)?;
 				Ok(if matches!(v, Val::Null) {
 					None
 				} else {
@@ -104,371 +89,452 @@
 	})
 }
 
-pub fn evaluate_comp(
-	ctx: Context,
-	specs: &[CompSpec],
-	mut guaranteed_reserve: usize,
-	callback: &mut impl FnMut(Context, usize) -> Result<()>,
-) -> Result<()> {
-	match specs.first() {
-		None => callback(ctx, guaranteed_reserve)?,
-		Some(CompSpec::IfSpec(IfSpecData { cond, span: _ })) => {
-			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {
-				evaluate_comp(ctx, &specs[1..], 0, callback)?;
-			}
-		}
-		Some(CompSpec::ForSpec(ForSpecData {
-			destruct: into,
-			over,
-		})) => {
-			match evaluate(ctx.clone(), over)? {
-				Val::Arr(list) => {
-					guaranteed_reserve = guaranteed_reserve.max(1) * list.len();
-					for (i, item) in list.iter_lazy().enumerate() {
-						let fctx = Pending::new();
-						let mut ctx = ContextBuilder::extend_fast(ctx.clone());
-						destruct(into, item, fctx.clone(), &mut ctx)?;
-						let ctx = ctx.build().into_future(fctx);
+pub fn evaluate_thunk(ctx: Context, expr: Rc<LExpr>, tailstrict: bool) -> Result<Thunk<Val>> {
+	Ok(if tailstrict {
+		Thunk::evaluated(evaluate(ctx, &expr)?)
+	} else {
+		Thunk!(move || { evaluate(ctx, &expr) })
+	})
+}
 
-						let specs = &specs[1..];
-						evaluate_comp(
-							ctx,
-							specs,
-							if i == 0 || !specs.is_empty() {
-								guaranteed_reserve
-							} else {
-								0
-							},
-							callback,
-						)?;
-					}
-				}
-				Val::Obj(obj) if cfg!(feature = "exp-object-iteration") => {
-					let fields = obj.fields(
-						// TODO: Should there be ability to preserve iteration order?
-						#[cfg(feature = "exp-preserve-order")]
-						false,
-					);
-					guaranteed_reserve = guaranteed_reserve.max(1) * fields.len();
-					for (i, field) in fields.into_iter().enumerate() {
-						let fctx = Pending::new();
-						let mut ctx = ContextBuilder::extend_fast(ctx.clone());
-						let obj = obj.clone();
-						let value = Thunk::evaluated(Val::arr(vec![
-							Thunk::evaluated(Val::string(field.clone())),
-							obj.get_lazy(field).expect(
-								"field exists, as field name was obtained from object.fields()",
-							),
-						]));
-						destruct(into, value, fctx.clone(), &mut ctx)?;
-						let ctx = ctx.build().into_future(fctx);
+mod names {
+	use crate::names;
 
-						evaluate_comp(
-							ctx,
-							&specs[1..],
-							if i == 0 || !specs.is_empty() {
-								guaranteed_reserve
-							} else {
-								0
-							},
-							callback,
-						)?;
-					}
-				}
-				_ => bail!(InComprehensionCanOnlyIterateOverArray),
-			}
-		}
+	names! {
+		anonymous: "anonymous",
 	}
-	Ok(())
 }
 
-fn evaluate_arr_comp(ctx: Context, expr: &Rc<Expr>, comp_specs: &[CompSpec]) -> Result<ArrValue> {
-	let ctx = ctx.branch_point();
-	'eager: {
-		let mut out = Vec::new();
+pub fn evaluate_named(name: &IStr, ctx: Context, expr: &LExpr) -> Result<Val> {
+	if let LExpr::Function(f) = &expr {
+		return Ok(evaluate_method(
+			ctx,
+			f.name.clone().unwrap_or_else(|| name.clone()),
+			f,
+		));
+	}
+	evaluate(ctx, expr)
+}
 
-		if evaluate_comp(ctx.clone(), comp_specs, 0, &mut |ctx, reserve| {
-			if reserve != 0 {
-				out.reserve(reserve);
+pub fn evaluate(ctx: Context, expr: &LExpr) -> Result<Val> {
+	Ok(match expr {
+		LExpr::Null => Val::Null,
+		LExpr::Bool(b) => Val::Bool(*b),
+		LExpr::Str(s) => Val::string(s.clone()),
+		LExpr::Num(n) => Val::Num(*n),
+		LExpr::Local(id) => {
+			let Some(thunk) = ctx.binding(*id) else {
+				bail!("should not happen: unbound local {id:?}");
+			};
+			thunk.evaluate()?
+		}
+		LExpr::BadLocal(name) => panic!("unresolvable reference: {name}"),
+		LExpr::Arr(items) => Val::Arr(crate::arr::ArrValue::expr(ctx, items.clone())),
+		LExpr::UnaryOp(op, value) => {
+			let value = evaluate(ctx, value)?;
+			evaluate_unary_op(*op, &value)?
+		}
+		LExpr::BinaryOp { lhs, op, rhs } => evaluate_binary_op_special(ctx, lhs, *op, rhs)?,
+		LExpr::LocalExpr { binds, body } => {
+			let ctx = evaluate_locals(ctx, binds);
+			evaluate(ctx, body)?
+		}
+		LExpr::IfElse {
+			cond,
+			cond_then,
+			cond_else,
+		} => {
+			let cond_val = evaluate(ctx.clone(), cond)?;
+			let Val::Bool(b) = cond_val else {
+				bail!(TypeMismatch(
+					"if condition",
+					vec![ValType::Bool],
+					cond_val.value_type()
+				))
+			};
+			if b {
+				evaluate(ctx, cond_then)?
+			} else if let Some(e) = cond_else {
+				evaluate(ctx, e)?
+			} else {
+				Val::Null
 			}
-			out.push(evaluate(ctx, expr)?);
-			Ok(())
-		})
-		.is_err()
-		{
-			break 'eager;
 		}
+		LExpr::Error(s, e) => in_frame(
+			CallLocation::new(s),
+			|| "error statement".to_owned(),
+			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),
+		)?,
+		LExpr::AssertExpr { assert, rest } => {
+			evaluate_assert(ctx.clone(), assert)?;
+			evaluate(ctx, rest)?
+		}
 
-		return Ok(ArrValue::new(out));
-	};
-	let mut out = Vec::new();
-	evaluate_comp(ctx, comp_specs, 0, &mut |ctx, reserve| {
-		if reserve != 0 {
-			out.reserve(reserve);
+		LExpr::Function(func) => evaluate_method(
+			ctx,
+			func.name.clone().unwrap_or_else(names::anonymous),
+			func,
+		),
+		LExpr::Apply {
+			applicable,
+			args,
+			tailstrict,
+		} => evaluate_apply(
+			ctx,
+			applicable,
+			args,
+			CallLocation::new(&args.span),
+			*tailstrict,
+		)?,
+		LExpr::Index { indexable, parts } => evaluate_index(ctx, indexable, parts)?,
+		LExpr::Obj(body) => evaluate_obj_body(None, ctx, body)?,
+		LExpr::ObjExtend(lhs, body) => {
+			let lhs_val = evaluate(ctx.clone(), lhs)?;
+			let Val::Obj(lhs_obj) = lhs_val else {
+				bail!(TypeMismatch(
+					"object extend lhs",
+					vec![ValType::Obj],
+					lhs_val.value_type(),
+				))
+			};
+			evaluate_obj_body(Some(lhs_obj), ctx, body)?
+		}
+		LExpr::ArrComp(comp) => evaluate_arr_comp(ctx, comp)?,
+		LExpr::Slice(slice) => {
+			use crate::typed::BoundedUsize;
+			let val = evaluate(ctx.clone(), &slice.value)?;
+			let indexable = val.into_indexable()?;
+			let start = slice
+				.start
+				.as_ref()
+				.map(|e| evaluate(ctx.clone(), e))
+				.transpose()?
+				.map(|v| -> Result<i32> {
+					v.as_num()
+						.ok_or_else(|| {
+							TypeMismatch("slice start", vec![ValType::Num], v.value_type()).into()
+						})
+						.map(|n| n as i32)
+				})
+				.transpose()?;
+			let end = slice
+				.end
+				.as_ref()
+				.map(|e| evaluate(ctx.clone(), e))
+				.transpose()?
+				.map(|v| -> Result<i32> {
+					v.as_num()
+						.ok_or_else(|| {
+							TypeMismatch("slice end", vec![ValType::Num], v.value_type()).into()
+						})
+						.map(|n| n as i32)
+				})
+				.transpose()?;
+			let step = slice
+				.step
+				.as_ref()
+				.map(|e| evaluate(ctx, e))
+				.transpose()?
+				.map(|v| -> Result<BoundedUsize<1, { i32::MAX as usize }>> {
+					let n = v.as_num().ok_or_else(|| -> crate::Error {
+						TypeMismatch("slice step", vec![ValType::Num], v.value_type()).into()
+					})?;
+					BoundedUsize::new(n as usize)
+						.ok_or_else(|| runtime_error!("slice step must be >= 1"))
+				})
+				.transpose()?;
+			Val::from(indexable.slice(start, end, step)?)
 		}
-		let expr = expr.clone();
-		out.push(Thunk!(move || evaluate(ctx, &expr)));
-		Ok(())
-	})?;
-	Ok(ArrValue::new(out))
+		LExpr::Super => Val::Obj(ctx.try_sup_this()?.standalone_super()?),
+		LExpr::Import {
+			kind,
+			kind_span,
+			path,
+		} => with_state(|state| {
+			let resolved = state.resolve_from(kind_span.0.source_path(), &path.clone())?;
+			Ok::<_, Error>(match kind.value {
+				ImportKind::Normal => in_frame(
+					CallLocation::new(&kind.span),
+					|| "import".to_string(),
+					|| state.import_resolved(resolved),
+				)?,
+				ImportKind::Str => Val::string(state.import_resolved_str(resolved)?),
+				ImportKind::Bin => Val::arr(state.import_resolved_bin(resolved)?),
+			})
+		})?,
+	})
 }
 
-trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}
-impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}
+fn evaluate_apply(
+	ctx: Context,
+	applicable: &LExpr,
+	args: &LArgsDesc,
+	loc: CallLocation<'_>,
+	tailstrict: bool,
+) -> Result<Val> {
+	let func_val = evaluate(ctx.clone(), applicable)?;
+	let Val::Func(func) = func_val else {
+		bail!(OnlyFunctionsCanBeCalledGot(func_val.value_type()))
+	};
 
-fn evaluate_object_locals(
-	fctx: Context,
-	locals: Rc<Vec<BindSpec>>,
-) -> impl CloneableUnbound<Context> {
-	#[derive(Trace, Clone)]
-	struct UnboundLocals {
-		fctx: Context,
-		locals: Rc<Vec<BindSpec>>,
-	}
-	impl Unbound for UnboundLocals {
-		type Bound = Context;
+	let name = func.name();
+	let unnamed = args
+		.unnamed
+		.iter()
+		.cloned()
+		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))
+		.collect::<Result<Vec<_>>>()?;
 
-		fn bind(&self, sup_this: SupThis) -> Result<Context> {
-			let fctx = Context::new_future();
-			let ctx = self.fctx.clone();
-			let mut ctx = ContextBuilder::extend(ctx);
-			for b in self.locals.iter() {
-				evaluate_dest(b, fctx.clone(), &mut ctx)?;
-			}
-
-			let ctx = ctx.build_sup_this(sup_this).into_future(fctx);
+	let named = args
+		.values
+		.iter()
+		.cloned()
+		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))
+		.collect::<Result<Vec<_>>>()?;
+	let prepare = PreparedFuncVal::new(func, unnamed.len(), &args.names)
+		.with_description_src(loc, || format!("function <{name}> preparation"))?;
+	in_frame(
+		loc,
+		|| format!("function <{name}> call"),
+		|| prepare.call(CallLocation::native(), &unnamed, &named),
+	)
+}
 
-			Ok(ctx)
+fn evaluate_index(ctx: Context, indexable: &LExpr, parts: &[LIndexPart]) -> Result<Val> {
+	let mut value = if let LExpr::Super = indexable {
+		let sup_this = ctx.try_sup_this()?;
+		// First part must be evaluated to get the super field name
+		if parts.is_empty() {
+			bail!(RuntimeError("super requires an index".into()))
 		}
-	}
+		let key_val = evaluate(ctx.clone(), &parts[0].value)?;
+		let Val::Str(key) = &key_val else {
+			bail!(ValueIndexMustBeTypeGot(
+				ValType::Obj,
+				ValType::Str,
+				key_val.value_type(),
+			))
+		};
+		let field = key.clone().into_flat();
+		if let Some(v) = sup_this.get_super(field.clone())? {
+			// Continue with remaining parts
+			let mut value = v;
+			for part in &parts[1..] {
+				value = index_val(ctx.clone(), CallLocation::new(&part.span), value, part)?;
+			}
+			return Ok(value);
+		}
+		let suggestions = suggest_object_fields(sup_this.this(), field.clone());
+		bail!(NoSuchField(field, suggestions))
+	} else {
+		evaluate(ctx.clone(), indexable)?
+	};
 
-	UnboundLocals { fctx, locals }
+	for part in parts {
+		value = index_val(ctx.clone(), CallLocation::new(&part.span), value, part)?;
+	}
+	Ok(value)
 }
 
-pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(
-	builder: &mut ObjValueBuilder,
-	ctx: Context,
-	uctx: B,
-	field: &FieldMember,
-) -> Result<()> {
-	let name = evaluate_field_name(ctx, &field.name)?;
-	let Some(name) = name else {
-		return Ok(());
-	};
-
-	match field {
-		FieldMember {
-			plus,
-			params: None,
-			visibility,
-			value,
-			..
-		} => {
-			#[derive(Trace)]
-			struct UnboundValue<B: Trace> {
-				uctx: B,
-				value: Rc<Expr>,
-				name: IStr,
+fn index_val(ctx: Context, loc: CallLocation<'_>, value: Val, part: &LIndexPart) -> Result<Val> {
+	let key_val = evaluate(ctx, &part.value)?;
+	Ok(match (&value, &key_val) {
+		(Val::Obj(obj), Val::Str(key)) => {
+			let field = key.clone().into_flat();
+			if let Some(v) = obj
+				.get(field.clone())
+				.with_description_src(loc, || format!("field <{field}> access"))?
+			{
+				v
+			} else {
+				bail!(NoSuchField(
+					field.clone(),
+					suggest_object_fields(obj, field)
+				))
 			}
-			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {
-				type Bound = Val;
-				fn bind(&self, sup_this: SupThis) -> Result<Val> {
-					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())
-				}
+		}
+		(Val::Arr(arr), Val::Num(idx)) => {
+			let n = idx.get();
+			if n.fract() > f64::EPSILON {
+				bail!(FractionalIndex)
 			}
-
-			builder
-				.field(name.clone())
-				.with_add(*plus)
-				.with_visibility(*visibility)
-				.with_location(field.name.span.clone())
-				.bindable(UnboundValue {
-					uctx,
-					value: value.clone(),
-					name,
-				})?;
+			if n < 0.0 {
+				bail!(ArrayBoundsError(
+					n as isize, // truncation is fine for error display
+					arr.len()
+				));
+			}
+			#[expect(
+				clippy::cast_possible_truncation,
+				clippy::cast_sign_loss,
+				reason = "n is checked positive"
+			)]
+			let i = n as u32;
+			arr.get(i)
+				.with_description_src(loc, || format!("element <{i}> access"))?
+				.ok_or_else(|| ArrayBoundsError(i as isize, arr.len()))?
 		}
-		FieldMember {
-			params: Some(params),
-			visibility,
-			value,
-			..
-		} => {
-			#[derive(Trace)]
-			struct UnboundMethod<B: Trace> {
-				uctx: B,
-				value: Rc<Expr>,
-				params: ExprParams,
-				name: IStr,
+		(Val::Str(s), Val::Num(idx)) => {
+			let n = idx.get();
+			if n.fract() > f64::EPSILON {
+				bail!(FractionalIndex)
 			}
-			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {
-				type Bound = Val;
-				fn bind(&self, sup_this: SupThis) -> Result<Val> {
-					Ok(evaluate_method(
-						self.uctx.bind(sup_this)?,
-						self.name.clone(),
-						self.params.clone(),
-						self.value.clone(),
-					))
-				}
+			let flat = s.clone().into_flat();
+			if n < 0.0 {
+				bail!(ArrayBoundsError(
+					n as isize, // truncation is fine for error display
+					flat.chars().count() as u32
+				));
 			}
-
-			builder
-				.field(name.clone())
-				.with_visibility(*visibility)
-				// .with_location(value.span())
-				.bindable(UnboundMethod {
-					uctx,
-					value: value.clone(),
-					params: params.clone(),
-					name,
-				})?;
+			#[expect(
+				clippy::cast_possible_truncation,
+				clippy::cast_sign_loss,
+				reason = "n is checked positive, overflow will truncate as expected"
+			)]
+			let i = n as usize;
+			let Some(char) = flat.chars().nth(i) else {
+				bail!(StringBoundsError(i, flat.chars().count()))
+			};
+			Val::string(char)
 		}
-	}
-	Ok(())
+		_ => bail!(ValueIndexMustBeTypeGot(
+			value.value_type(),
+			ValType::Str,
+			key_val.value_type()
+		)),
+	})
 }
 
-#[derive(Trace, Clone)]
-struct DirectUnbound(Context);
-impl Unbound for DirectUnbound {
-	type Bound = Context;
-	fn bind(&self, sup_this: SupThis) -> Result<Context> {
-		Ok(ContextBuilder::extend(self.0.clone()).build_sup_this(sup_this))
+fn evaluate_obj_body(super_obj: Option<ObjValue>, ctx: Context, body: &LObjBody) -> Result<Val> {
+	match body {
+		LObjBody::MemberList(members) => evaluate_obj_members(super_obj, ctx, members),
+		LObjBody::ObjComp(comp) => evaluate_obj_comp(super_obj, ctx, comp),
 	}
 }
 
-#[allow(clippy::too_many_lines)]
-pub fn evaluate_member_list_object(
-	super_obj: Option<ObjValue>,
+pub fn evaluate_field_member_unbound<B: Unbound<Bound = Context> + Clone>(
+	builder: &mut ObjValueBuilder,
 	ctx: Context,
-	members: &ObjMembers,
-) -> Result<ObjValue> {
+	uctx: B,
+	field: &LFieldMember,
+) -> Result<()> {
 	#[derive(Trace)]
-	struct ObjectAssert<B: Trace> {
+	struct UnboundValue<B: Trace> {
 		uctx: B,
-		asserts: Rc<Vec<AssertStmt>>,
+		value: Rc<LExpr>,
+		name: IStr,
 	}
-	impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {
-		fn run(&self, sup_this: SupThis) -> Result<()> {
-			let ctx = self.uctx.bind(sup_this)?;
-			for assert in &*self.asserts {
-				evaluate_assert(ctx.clone(), assert)?;
-			}
-			Ok(())
+	impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {
+		type Bound = Val;
+		fn bind(&self, sup_this: SupThis) -> Result<Val> {
+			evaluate(self.uctx.bind(sup_this)?, &self.value)
 		}
 	}
 
-	let mut builder = ObjValueBuilder::new();
-	if let Some(super_obj) = super_obj {
-		builder.with_super(super_obj);
+	let LFieldMember {
+		name,
+		plus,
+		visibility,
+		value,
+	} = field;
+	let Some(name) = evaluate_field_name(ctx, name)? else {
+		return Ok(());
+	};
+
+	builder
+		.field(name.clone())
+		.with_add(*plus)
+		.with_visibility(*visibility)
+		.bindable(UnboundValue {
+			uctx,
+			value: value.clone(),
+			name,
+		})
+}
+pub fn evaluate_field_member_static(
+	builder: &mut ObjValueBuilder,
+	field_ctx: Context,
+	value_ctx: Context,
+	field: &LFieldMember,
+) -> Result<()> {
+	let LFieldMember {
+		name,
+		plus,
+		visibility,
+		value,
+	} = field;
+	let Some(name) = evaluate_field_name(field_ctx, name)? else {
+		return Ok(());
+	};
+
+	let value = value.clone();
+	builder
+		.field(name)
+		.with_add(*plus)
+		.with_visibility(*visibility)
+		.try_thunk(Thunk!(move || { evaluate(value_ctx, &value) }))?;
+	Ok(())
+}
+
+fn evaluate_obj_members(
+	super_obj: Option<ObjValue>,
+	ctx: Context,
+	members: &LObjMembers,
+) -> Result<Val> {
+	let mut builder = ObjValueBuilder::with_capacity(members.fields.len());
+	if let Some(sup) = super_obj {
+		builder.with_super(sup);
 	}
 
-	if members.locals.is_empty() {
-		// We can use the same context for all field evaluation, it doesn't depends on locals, only on this/super
-		let uctx = DirectUnbound(ctx.clone());
+	let needs_unbound = members.this.is_some() || members.uses_super;
+
+	if needs_unbound {
+		let uctx = CachedUnbound::new(evaluate_locals_unbound(
+			ctx.clone(),
+			members.locals.clone(),
+			members.this,
+		));
 		for field in &members.fields {
-			evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;
+			evaluate_field_member_unbound(&mut builder, ctx.clone(), uctx.clone(), field)?;
 		}
 		if !members.asserts.is_empty() {
-			builder.assert(ObjectAssert {
+			builder.assert(evaluate_object_assertions_unbound(
 				uctx,
-				asserts: members.asserts.clone(),
-			});
+				members.asserts.clone(),
+			));
 		}
 	} else {
-		let locals = members.locals.clone();
-		// We have single context for all fields, so we can cache them together
-		let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));
+		let field_ctx = ctx;
+		let value_ctx = evaluate_locals(field_ctx.clone(), &members.locals);
 		for field in &members.fields {
-			evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;
+			evaluate_field_member_static(
+				&mut builder,
+				field_ctx.clone(),
+				value_ctx.clone(),
+				field,
+			)?;
 		}
 		if !members.asserts.is_empty() {
-			builder.assert(ObjectAssert {
-				uctx,
-				asserts: members.asserts.clone(),
-			});
+			builder.assert(evaluate_object_assertions_static(
+				value_ctx,
+				members.asserts.clone(),
+			));
 		}
 	}
 
-	Ok(builder.build())
+	Ok(Val::Obj(builder.build()))
 }
 
-pub fn evaluate_object(
-	super_obj: Option<ObjValue>,
-	ctx: Context,
-	object: &ObjBody,
-) -> Result<ObjValue> {
-	Ok(match object {
-		ObjBody::MemberList(members) => evaluate_member_list_object(super_obj, ctx, members)?,
-		ObjBody::ObjComp(obj) => {
-			let mut builder = ObjValueBuilder::new();
-			if let Some(super_obj) = super_obj {
-				builder.with_super(super_obj);
-			}
-			let locals = obj.locals.clone();
-			evaluate_comp(
-				ctx.branch_point(),
-				&obj.compspecs,
-				0,
-				&mut |ctx, reserve| {
-					let uctx = evaluate_object_locals(ctx.clone(), locals.clone());
-					builder.reserve_fields(reserve);
-
-					evaluate_field_member(&mut builder, ctx, uctx, &obj.field)
-				},
-			)?;
-
-			builder.build()
-		}
-	})
-}
-
-pub fn evaluate_apply(
-	ctx: Context,
-	value: &Expr,
-	args: &ArgsDesc,
-	loc: CallLocation<'_>,
-	tailstrict: bool,
-) -> Result<Val> {
-	let value = evaluate(ctx.clone(), value)?;
-	Ok(match value {
-		Val::Func(f) => {
-			let name = f.name();
-			let unnamed = args
-				.unnamed
-				.iter()
-				.cloned()
-				.map(|un| evaluate_thunk(ctx.clone(), un, tailstrict))
-				.collect::<Result<Vec<_>>>()?;
-			let named = args
-				.values
-				.iter()
-				.cloned()
-				.map(|un| evaluate_thunk(ctx.clone(), un, tailstrict))
-				.collect::<Result<Vec<_>>>()?;
-			let prepare = PreparedFuncVal::new(f, args.unnamed.len(), &args.names)
-				.with_description_src(loc, || format!("function <{name}> call"))?;
-			let body = || prepare.call(loc, &unnamed, &named);
-			if tailstrict {
-				body()?
-			} else {
-				in_frame(loc, || format!("function <{name}> call"), body)?
-			}
-		}
-		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),
-	})
-}
-
-pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {
-	let AssertStmt { assertion, message } = assertion;
+pub fn evaluate_assert(ctx: Context, assertion: &LAssertStmt) -> Result<()> {
+	let LAssertStmt { cond, message } = assertion;
 	let assertion_result = in_frame(
-		CallLocation::new(&assertion.span),
+		CallLocation::native(),
 		|| "assertion condition".to_owned(),
-		|| bool::from_untyped(evaluate(ctx.clone(), assertion)?),
+		|| bool::from_untyped(evaluate(ctx.clone(), cond)?),
 	)?;
 	if !assertion_result {
 		in_frame(
-			CallLocation::new(&assertion.span),
+			CallLocation::new(&cond.span),
 			|| "assertion failure".to_owned(),
 			|| {
 				if let Some(msg) = message {
@@ -481,306 +547,42 @@
 	Ok(())
 }
 
-pub fn evaluate_named_param(ctx: Context, expr: &Expr, name: ParamName) -> Result<Val> {
-	match name {
-		ParamName::Named(name) => evaluate_named(ctx, expr, name),
-		ParamName::Unnamed => evaluate(ctx, expr),
+fn evaluate_object_assertions_unbound<B: Unbound<Bound = Context>>(
+	uctx: B,
+	asserts: Rc<Vec<LAssertStmt>>,
+) -> impl ObjectAssertion {
+	#[derive(Trace)]
+	struct ObjectAssert<B: Trace> {
+		uctx: B,
+		asserts: Rc<Vec<LAssertStmt>>,
 	}
-}
-
-pub fn evaluate_named(ctx: Context, expr: &Expr, name: IStr) -> Result<Val> {
-	use Expr::*;
-	Ok(match expr {
-		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),
-		_ => evaluate(ctx, expr)?,
-	})
-}
-
-pub fn evaluate_thunk(ctx: Context, expr: Rc<Expr>, tailstrict: bool) -> Result<Thunk<Val>> {
-	Ok(if tailstrict {
-		Thunk::evaluated(evaluate(ctx, &expr)?)
-	} else {
-		Thunk!(move || { evaluate(ctx, &expr) })
-	})
-}
-#[allow(clippy::too_many_lines)]
-pub fn evaluate(ctx: Context, expr: &Expr) -> Result<Val> {
-	use Expr::*;
-
-	Ok(match expr {
-		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),
-		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),
-		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),
-		Literal(LiteralType::True) => Val::Bool(true),
-		Literal(LiteralType::False) => Val::Bool(false),
-		Literal(LiteralType::Null) => Val::Null,
-		Str(v) => Val::string(v.clone()),
-		Num(v) => Val::try_num(*v)?,
-		// I have tried to remove special behavior from super by implementing standalone-super
-		// expresion, but looks like this case still needs special treatment.
-		//
-		// Note that other jsonnet implementations will fail on `if value in (super)` expression,
-		// because the standalone super literal is not supported, that is because in other
-		// implementations `in super` treated differently from `in smth_else`.
-		BinaryOp(bin)
-			if matches!(&bin.rhs, Expr::Literal(LiteralType::Super))
-				&& bin.op == BinaryOpType::In =>
-		{
-			let sup_this = ctx.try_sup_this()?;
-			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.
-			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.
-			if !sup_this.has_super() {
-				return Ok(Val::Bool(false));
-			}
-			let field = evaluate(ctx, &bin.lhs)?;
-			Val::Bool(sup_this.field_in_super(field.to_string()?))
-		}
-		BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?,
-		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,
-		Var(name) => in_frame(
-			CallLocation::new(&name.span),
-			|| format!("local <{}> access", &**name),
-			|| ctx.binding((**name).clone())?.evaluate(),
-		)?,
-		Index { indexable, parts } => ensure_sufficient_stack(|| {
-			let mut parts = parts.iter();
-			let mut indexable = if matches!(&**indexable, Expr::Literal(LiteralType::Super)) {
-				let part = parts.next().expect("at least part should exist");
-				// sup_this existence check might also be skipped here for null-coalesce...
-				// But I believe this might cause errors.
-				let sup_this = ctx.try_sup_this()?;
-				if !sup_this.has_super() {
-					#[cfg(feature = "exp-null-coaelse")]
-					if part.null_coaelse {
-						return Ok(Val::Null);
-					}
-					bail!(NoSuperFound)
-				}
-				let name = evaluate(ctx.clone(), &part.value)?;
-
-				let Val::Str(name) = name else {
-					bail!(ValueIndexMustBeTypeGot(
-						ValType::Obj,
-						ValType::Str,
-						name.value_type(),
-					))
-				};
-
-				let name = name.into_flat();
-				match sup_this
-					.get_super(name.clone())
-					.with_description_src(&part.span, || format!("field <{name}> access"))?
-				{
-					Some(v) => v,
-					#[cfg(feature = "exp-null-coaelse")]
-					None if part.null_coaelse => return Ok(Val::Null),
-					None => {
-						let suggestions = suggest_object_fields(
-							&sup_this.standalone_super().expect("super exists"),
-							name.clone(),
-						);
-
-						bail!(NoSuchField(name, suggestions))
-					}
-				}
-			} else {
-				evaluate(ctx.clone(), indexable)?
-			};
-
-			for part in parts {
-				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {
-					(Val::Obj(v), Val::Str(key)) => match v
-						.get(key.clone().into_flat())
-						.with_description_src(&part.span, || format!("field <{key}> access"))?
-					{
-						Some(v) => v,
-						#[cfg(feature = "exp-null-coaelse")]
-						None if part.null_coaelse => return Ok(Val::Null),
-						None => {
-							let suggestions = suggest_object_fields(&v, key.into_flat());
-
-							return Err(Error::from(NoSuchField(
-								key.clone().into_flat(),
-								suggestions,
-							)))
-							.with_description_src(&part.span, || format!("field <{key}> access"));
-						}
-					},
-					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(
-						ValType::Obj,
-						ValType::Str,
-						n.value_type(),
-					)),
-					(Val::Arr(v), Val::Num(n)) => {
-						let n = n.get();
-						if n.fract() > f64::EPSILON {
-							bail!(FractionalIndex)
-						}
-						if n < 0.0 {
-							#[expect(
-								clippy::cast_possible_truncation,
-								reason = "it would be truncated anyway"
-							)]
-							let n = n as isize;
-							bail!(ArrayBoundsError(n, v.len()));
-						}
-						#[expect(
-							clippy::cast_possible_truncation,
-							clippy::cast_sign_loss,
-							reason = "n is checked postive"
-						)]
-						v.get(n as usize)?
-							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?
-					}
-					(Val::Arr(_), Val::Str(n)) => {
-						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))
-					}
-					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(
-						ValType::Arr,
-						ValType::Num,
-						n.value_type(),
-					)),
-
-					(Val::Str(s), Val::Num(n)) => Val::Str({
-						let n = n.get();
-						if n.fract() > f64::EPSILON {
-							bail!(FractionalIndex)
-						}
-						if n < 0.0 {
-							#[expect(
-								clippy::cast_possible_truncation,
-								reason = "it would be truncated anyway"
-							)]
-							let n = n as isize;
-							bail!(ArrayBoundsError(n, s.into_flat().chars().count()));
-						}
-						#[expect(
-							clippy::cast_sign_loss,
-							clippy::cast_possible_truncation,
-							reason = "n is positive, overflow will truncate as expected"
-						)]
-						let n = n as usize;
-						let v: IStr = s
-							.clone()
-							.into_flat()
-							.chars()
-							.skip(n)
-							.take(1)
-							.collect::<String>()
-							.into();
-						if v.is_empty() {
-							bail!(StringBoundsError(n, s.into_flat().chars().count()))
-						}
-						StrValue::Flat(v)
-					}),
-					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(
-						ValType::Str,
-						ValType::Num,
-						n.value_type(),
-					)),
-					#[cfg(feature = "exp-null-coaelse")]
-					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),
-					(v, _) => bail!(CantIndexInto(v.value_type())),
-				};
-			}
-			Ok(indexable)
-		})?,
-		LocalExpr(bindings, returned) => {
-			let fctx = Context::new_future();
-			let mut ctx = ContextBuilder::extend(ctx);
-			for b in bindings {
-				evaluate_dest(b, fctx.clone(), &mut ctx)?;
-			}
-			let ctx = ctx.build().into_future(fctx);
-			evaluate(ctx, returned)?
-		}
-		Arr(items) => {
-			if items.is_empty() {
-				Val::arr(())
-			} else {
-				Val::Arr(ArrValue::expr(ctx, items.clone()))
-			}
-		}
-		ArrComp(expr, comp_specs) => Val::Arr(evaluate_arr_comp(ctx, expr, comp_specs)?),
-		Obj(body) => Val::Obj(evaluate_object(None, ctx, body)?),
-		ObjExtend(a, b) => {
-			let base = evaluate(ctx.clone(), a)?;
-			match base {
-				Val::Obj(base_obj) => Val::Obj(evaluate_object(Some(base_obj), ctx, b)?),
-				_ => bail!("ObjExtend lhs should be an object value"),
-			}
-		}
-		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {
-			evaluate_apply(ctx, value, args, CallLocation::new(&args.span), *tailstrict)
-		})?,
-		Function(params, body) => {
-			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())
-		}
-		AssertExpr(assert) => {
-			evaluate_assert(ctx.clone(), &assert.assert)?;
-			evaluate(ctx, &assert.rest)?
-		}
-		ErrorStmt(s, e) => in_frame(
-			CallLocation::new(s),
-			|| "error statement".to_owned(),
-			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),
-		)?,
-		IfElse(if_else) => {
-			if in_frame(
-				CallLocation::new(&if_else.cond.span),
-				|| "if condition".to_owned(),
-				|| bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.cond)?),
-			)? {
-				evaluate(ctx, &if_else.cond_then)?
-			} else {
-				match &if_else.cond_else {
-					Some(v) => evaluate(ctx, v)?,
-					None => Val::Null,
-				}
+	impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {
+		fn run(&self, sup_this: SupThis) -> Result<()> {
+			let ctx = self.uctx.bind(sup_this)?;
+			for assert in &*self.asserts {
+				evaluate_assert(ctx.clone(), assert)?;
 			}
+			Ok(())
 		}
-		Slice(slice) => {
-			fn parse_idx<T: Typed + FromUntyped>(
-				ctx: Context,
-				expr: Option<&Spanned<Expr>>,
-				desc: &'static str,
-			) -> Result<Option<T>> {
-				if let Some(value) = expr {
-					Ok(in_frame(
-						CallLocation::new(&value.span),
-						|| format!("slice {desc}"),
-						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),
-					)?)
-				} else {
-					Ok(None)
-				}
+	}
+	ObjectAssert { uctx, asserts }
+}
+fn evaluate_object_assertions_static(
+	ctx: Context,
+	asserts: Rc<Vec<LAssertStmt>>,
+) -> impl ObjectAssertion {
+	#[derive(Trace)]
+	struct ObjectAssert {
+		ctx: Context,
+		asserts: Rc<Vec<LAssertStmt>>,
+	}
+	impl ObjectAssertion for ObjectAssert {
+		fn run(&self, _sup_this: SupThis) -> Result<()> {
+			for assert in &*self.asserts {
+				evaluate_assert(self.ctx.clone(), assert)?;
 			}
-
-			let indexable = evaluate(ctx.clone(), &slice.value)?;
-
-			let start = parse_idx(ctx.clone(), slice.slice.start.as_ref(), "start")?;
-			let end = parse_idx(ctx.clone(), slice.slice.end.as_ref(), "end")?;
-			let step = parse_idx(ctx, slice.slice.step.as_ref(), "step")?;
-
-			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?
+			Ok(())
 		}
-		Import(kind, path) => {
-			let Expr::Str(path) = &**path else {
-				bail!("computed imports are not supported")
-			};
-			with_state(|s| {
-				let span = &kind.span;
-				let resolved_path = s.resolve_from(span.0.source_path(), path)?;
-				Ok(match &**kind {
-					ImportKind::Normal => in_frame(
-						CallLocation::new(span),
-						|| format!("import {:?}", path.clone()),
-						|| s.import_resolved(resolved_path),
-					)?,
-					ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?),
-					ImportKind::Bin => Val::arr(s.import_resolved_bin(resolved_path)?),
-				}) as Result<Val>
-			})?
-		}
-	})
+	}
+	ObjectAssert { ctx, asserts }
 }
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
after · tests/tests/snapshots/golden__golden@issue23.jsonnet.snap
1---2source: tests/tests/golden.rs3expression: result4input_file: tests/golden/issue23.jsonnet5---6infinite recursion detected7    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