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
22
3use jrsonnet_gcmodule::{Cc, Trace};3use jrsonnet_gcmodule::{Cc, Trace};
4use jrsonnet_interner::IStr;4use jrsonnet_interner::IStr;
5use jrsonnet_ir::{5use jrsonnet_ir::ImportKind;
6 ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, ExprParams, FieldMember,
7 FieldName, ForSpecData, IfSpecData, ImportKind, LiteralType, ObjBody, ObjMembers, Spanned,
8 function::ParamName,
9};
10use jrsonnet_types::ValType;6use jrsonnet_types::ValType;
117
12use self::destructure::destruct;8use self::{
9 compspec::{evaluate_arr_comp, evaluate_obj_comp},
10 destructure::{evaluate_locals, evaluate_locals_unbound},
11 operator::evaluate_binary_op_special,
12};
13use crate::{13use crate::{
14 Context, ContextBuilder, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,14 analyze::{
15 LArgsDesc, LAssertStmt, LExpr, LFieldMember, LFieldName, LFunction, LIndexPart, LObjBody,
15 ResultExt, SupThis, Unbound, Val,16 LObjMembers,
16 arr::ArrValue,17 },
17 bail,18 bail,
18 destructure::evaluate_dest,19 error::{suggest_object_fields, ErrorKind::*},
19 error::{ErrorKind::*, suggest_object_fields},
20 evaluate::operator::{evaluate_binary_op_special, evaluate_unary_op},20 evaluate::operator::evaluate_unary_op,
21 function::{CallLocation, FuncDesc, FuncVal, PreparedFuncVal},21 function::{prepared::PreparedFuncVal, CallLocation, FuncDesc, FuncVal},
22 in_frame,22 in_frame, runtime_error,
23 typed::{FromUntyped, IntoUntyped as _, Typed},23 typed::FromUntyped as _,
24 val::{CachedUnbound, IndexableVal, StrValue, Thunk},24 val::{CachedUnbound, Thunk},
25 with_state, Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Result, ResultExt as _,
25 with_state,26 SupThis, Unbound, Val,
26};27};
28
29pub mod compspec;
27pub mod destructure;30pub mod destructure;
28pub mod operator;31pub mod operator;
2932
30// This is the amount of bytes that need to be left on the stack before increasing the size.33// This is the amount of bytes that need to be left on the stack before increasing the size.
31// It must be at least as large as the stack required by any code that does not call34// It must be at least as large as the stack required by any code that does not call
32// `ensure_sufficient_stack`.35// `ensure_sufficient_stack`.
33const RED_ZONE: usize = 100 * 1024; // 100k36const RED_ZONE: usize = 100 * 1024;
3437
35// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then38// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then
36// on. This flag has performance relevant characteristics. Don't set it too high.39// on. This flag has performance relevant characteristics. Don't set it too high.
37const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB40const STACK_PER_RECURSION: usize = 1024 * 1024;
3841
39/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations42/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations
40/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit43/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit
46 stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)49 stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)
47}50}
4851
49pub fn evaluate_trivial(expr: &Expr) -> Option<Val> {52pub fn evaluate_trivial(expr: &LExpr) -> Option<Val> {
50 fn is_trivial(expr: &Expr) -> bool {53 // TODO: Eager trivial array
51 match expr {
52 Expr::Str(_)
53 | Expr::Num(_)
54 | Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,
55 Expr::Arr(a) => a.iter().all(is_trivial),
56 _ => false,
57 }
58 }
59 Some(match expr {54 Some(match expr {
60 Expr::Str(s) => Val::string(s.clone()),55 LExpr::Str(s) => Val::string(s.clone()),
61 Expr::Num(n) => Val::Num(*n),56 LExpr::Num(n) => Val::Num(*n),
62 Expr::Literal(LiteralType::False) => Val::Bool(false),57 LExpr::Bool(false) => Val::Bool(false),
63 Expr::Literal(LiteralType::True) => Val::Bool(true),58 LExpr::Bool(true) => Val::Bool(true),
64 Expr::Literal(LiteralType::Null) => Val::Null,59 LExpr::Null => Val::Null,
65 Expr::Arr(n) => {
66 if n.iter().any(|e| !is_trivial(e)) {
67 return None;
68 }
69 Val::Arr(
70 n.iter()
71 .map(evaluate_trivial)
72 .map(|e| e.expect("checked trivial"))
73 .collect(),
74 )
75 }
76 _ => return None,60 _ => return None,
77 })61 })
78}62}
7963
64/// Evaluate a method definition.
80pub fn evaluate_method(ctx: Context, name: IStr, params: ExprParams, body: Rc<Expr>) -> Val {65pub fn evaluate_method(ctx: Context, name: IStr, func: &Rc<LFunction>) -> Val {
81 Val::Func(FuncVal::Normal(Cc::new(FuncDesc {66 Val::Func(FuncVal::Normal(Cc::new(FuncDesc {
82 name,67 name,
83 ctx,68 ctx,
84 params,69 func: func.clone(),
85 body,
86 })))70 })))
87}71}
8872
89pub fn evaluate_field_name(ctx: Context, field_name: &Spanned<FieldName>) -> Result<Option<IStr>> {73pub fn evaluate_field_name(ctx: Context, field_name: &LFieldName) -> Result<Option<IStr>> {
90 Ok(match &field_name.value {74 Ok(match field_name {
91 FieldName::Fixed(n) => Some(n.clone()),75 LFieldName::Fixed(n) => Some(n.clone()),
92 FieldName::Dyn(expr) => in_frame(76 LFieldName::Dyn(expr) => in_frame(
93 CallLocation::new(&field_name.span),77 // TODO: Spanned<LFieldName>
78 CallLocation::native(),
94 || "evaluating field name".to_string(),79 || "evaluating field name".to_string(),
95 || {80 || {
96 let v = evaluate(ctx, expr)?;81 let v = evaluate(ctx.clone(), expr)?;
97 Ok(if matches!(v, Val::Null) {82 Ok(if matches!(v, Val::Null) {
98 None83 None
99 } else {84 } else {
104 })89 })
105}90}
10691
107pub fn evaluate_comp(92pub fn evaluate_thunk(ctx: Context, expr: Rc<LExpr>, tailstrict: bool) -> Result<Thunk<Val>> {
108 ctx: Context,
109 specs: &[CompSpec],
110 mut guaranteed_reserve: usize,
111 callback: &mut impl FnMut(Context, usize) -> Result<()>,
112) -> Result<()> {
113 match specs.first() {93 Ok(if tailstrict {
114 None => callback(ctx, guaranteed_reserve)?,
115 Some(CompSpec::IfSpec(IfSpecData { cond, span: _ })) => {
116 if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {94 Thunk::evaluated(evaluate(ctx, &expr)?)
117 evaluate_comp(ctx, &specs[1..], 0, callback)?;95 } else {
118 }
119 }
120 Some(CompSpec::ForSpec(ForSpecData {
121 destruct: into,
122 over,
123 })) => {
124 match evaluate(ctx.clone(), over)? {96 Thunk!(move || { evaluate(ctx, &expr) })
125 Val::Arr(list) => {
126 guaranteed_reserve = guaranteed_reserve.max(1) * list.len();
127 for (i, item) in list.iter_lazy().enumerate() {
128 let fctx = Pending::new();
129 let mut ctx = ContextBuilder::extend_fast(ctx.clone());
130 destruct(into, item, fctx.clone(), &mut ctx)?;
131 let ctx = ctx.build().into_future(fctx);97 })
98}
13299
133 let specs = &specs[1..];100mod names {
134 evaluate_comp(
135 ctx,
136 specs,
137 if i == 0 || !specs.is_empty() {
138 guaranteed_reserve101 use crate::names;
139 } else {
140 0
141 },
142 callback,
143 )?;
144 }
145 }
146 Val::Obj(obj) if cfg!(feature = "exp-object-iteration") => {
147 let fields = obj.fields(
148 // TODO: Should there be ability to preserve iteration order?
149 #[cfg(feature = "exp-preserve-order")]
150 false,
151 );
152 guaranteed_reserve = guaranteed_reserve.max(1) * fields.len();
153 for (i, field) in fields.into_iter().enumerate() {
154 let fctx = Pending::new();
155 let mut ctx = ContextBuilder::extend_fast(ctx.clone());
156 let obj = obj.clone();
157 let value = Thunk::evaluated(Val::arr(vec![
158 Thunk::evaluated(Val::string(field.clone())),
159 obj.get_lazy(field).expect(
160 "field exists, as field name was obtained from object.fields()",
161 ),
162 ]));
163 destruct(into, value, fctx.clone(), &mut ctx)?;
164 let ctx = ctx.build().into_future(fctx);
165102
166 evaluate_comp(103 names! {
167 ctx,
168 &specs[1..],
169 if i == 0 || !specs.is_empty() {
170 guaranteed_reserve104 anonymous: "anonymous",
171 } else {
172 0
173 },
174 callback,
175 )?;
176 }
177 }
178 _ => bail!(InComprehensionCanOnlyIterateOverArray),
179 }
180 }
181 }105 }
182 Ok(())
183}106}
184107
185fn evaluate_arr_comp(ctx: Context, expr: &Rc<Expr>, comp_specs: &[CompSpec]) -> Result<ArrValue> {108pub fn evaluate_named(name: &IStr, ctx: Context, expr: &LExpr) -> Result<Val> {
186 let ctx = ctx.branch_point();109 if let LExpr::Function(f) = &expr {
110 return Ok(evaluate_method(
111 ctx,
112 f.name.clone().unwrap_or_else(|| name.clone()),
113 f,
114 ));
187 'eager: {115 }
188 let mut out = Vec::new();116 evaluate(ctx, expr)
117}
189118
119pub fn evaluate(ctx: Context, expr: &LExpr) -> Result<Val> {
190 if evaluate_comp(ctx.clone(), comp_specs, 0, &mut |ctx, reserve| {120 Ok(match expr {
121 LExpr::Null => Val::Null,
122 LExpr::Bool(b) => Val::Bool(*b),
123 LExpr::Str(s) => Val::string(s.clone()),
124 LExpr::Num(n) => Val::Num(*n),
125 LExpr::Local(id) => {
126 let Some(thunk) = ctx.binding(*id) else {
127 bail!("should not happen: unbound local {id:?}");
128 };
129 thunk.evaluate()?
130 }
131 LExpr::BadLocal(name) => panic!("unresolvable reference: {name}"),
132 LExpr::Arr(items) => Val::Arr(crate::arr::ArrValue::expr(ctx, items.clone())),
133 LExpr::UnaryOp(op, value) => {
134 let value = evaluate(ctx, value)?;
135 evaluate_unary_op(*op, &value)?
136 }
137 LExpr::BinaryOp { lhs, op, rhs } => evaluate_binary_op_special(ctx, lhs, *op, rhs)?,
138 LExpr::LocalExpr { binds, body } => {
191 if reserve != 0 {139 let ctx = evaluate_locals(ctx, binds);
140 evaluate(ctx, body)?
141 }
142 LExpr::IfElse {
192 out.reserve(reserve);143 cond,
144 cond_then,
145 cond_else,
146 } => {
147 let cond_val = evaluate(ctx.clone(), cond)?;
148 let Val::Bool(b) = cond_val else {
149 bail!(TypeMismatch(
150 "if condition",
151 vec![ValType::Bool],
152 cond_val.value_type()
153 ))
154 };
155 if b {
156 evaluate(ctx, cond_then)?
157 } else if let Some(e) = cond_else {
158 evaluate(ctx, e)?
159 } else {
160 Val::Null
193 }161 }
194 out.push(evaluate(ctx, expr)?);
195 Ok(())
196 })
197 .is_err()
198 {
199 break 'eager;
200 }162 }
163 LExpr::Error(s, e) => in_frame(
164 CallLocation::new(s),
165 || "error statement".to_owned(),
166 || bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),
167 )?,
168 LExpr::AssertExpr { assert, rest } => {
169 evaluate_assert(ctx.clone(), assert)?;
170 evaluate(ctx, rest)?
171 }
201172
202 return Ok(ArrValue::new(out));173 LExpr::Function(func) => evaluate_method(
174 ctx,
175 func.name.clone().unwrap_or_else(names::anonymous),
176 func,
177 ),
203 };178 LExpr::Apply {
204 let mut out = Vec::new();179 applicable,
180 args,
181 tailstrict,
182 } => evaluate_apply(
183 ctx,
184 applicable,
185 args,
186 CallLocation::new(&args.span),
205 evaluate_comp(ctx, comp_specs, 0, &mut |ctx, reserve| {187 *tailstrict,
188 )?,
189 LExpr::Index { indexable, parts } => evaluate_index(ctx, indexable, parts)?,
190 LExpr::Obj(body) => evaluate_obj_body(None, ctx, body)?,
191 LExpr::ObjExtend(lhs, body) => {
206 if reserve != 0 {192 let lhs_val = evaluate(ctx.clone(), lhs)?;
193 let Val::Obj(lhs_obj) = lhs_val else {
207 out.reserve(reserve);194 bail!(TypeMismatch(
195 "object extend lhs",
196 vec![ValType::Obj],
197 lhs_val.value_type(),
198 ))
199 };
200 evaluate_obj_body(Some(lhs_obj), ctx, body)?
208 }201 }
209 let expr = expr.clone();202 LExpr::ArrComp(comp) => evaluate_arr_comp(ctx, comp)?,
210 out.push(Thunk!(move || evaluate(ctx, &expr)));203 LExpr::Slice(slice) => {
211 Ok(())204 use crate::typed::BoundedUsize;
212 })?;205 let val = evaluate(ctx.clone(), &slice.value)?;
213 Ok(ArrValue::new(out))206 let indexable = val.into_indexable()?;
207 let start = slice
208 .start
209 .as_ref()
210 .map(|e| evaluate(ctx.clone(), e))
211 .transpose()?
212 .map(|v| -> Result<i32> {
213 v.as_num()
214 .ok_or_else(|| {
215 TypeMismatch("slice start", vec![ValType::Num], v.value_type()).into()
216 })
217 .map(|n| n as i32)
218 })
219 .transpose()?;
220 let end = slice
221 .end
222 .as_ref()
223 .map(|e| evaluate(ctx.clone(), e))
224 .transpose()?
225 .map(|v| -> Result<i32> {
226 v.as_num()
227 .ok_or_else(|| {
228 TypeMismatch("slice end", vec![ValType::Num], v.value_type()).into()
229 })
230 .map(|n| n as i32)
231 })
232 .transpose()?;
233 let step = slice
234 .step
235 .as_ref()
236 .map(|e| evaluate(ctx, e))
237 .transpose()?
238 .map(|v| -> Result<BoundedUsize<1, { i32::MAX as usize }>> {
239 let n = v.as_num().ok_or_else(|| -> crate::Error {
240 TypeMismatch("slice step", vec![ValType::Num], v.value_type()).into()
241 })?;
242 BoundedUsize::new(n as usize)
243 .ok_or_else(|| runtime_error!("slice step must be >= 1"))
244 })
245 .transpose()?;
246 Val::from(indexable.slice(start, end, step)?)
247 }
248 LExpr::Super => Val::Obj(ctx.try_sup_this()?.standalone_super()?),
249 LExpr::Import {
250 kind,
251 kind_span,
252 path,
253 } => with_state(|state| {
254 let resolved = state.resolve_from(kind_span.0.source_path(), &path.clone())?;
255 Ok::<_, Error>(match kind.value {
256 ImportKind::Normal => in_frame(
257 CallLocation::new(&kind.span),
258 || "import".to_string(),
259 || state.import_resolved(resolved),
260 )?,
261 ImportKind::Str => Val::string(state.import_resolved_str(resolved)?),
262 ImportKind::Bin => Val::arr(state.import_resolved_bin(resolved)?),
263 })
264 })?,
265 })
214}266}
215267
216trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}268fn evaluate_apply(
269 ctx: Context,
270 applicable: &LExpr,
271 args: &LArgsDesc,
217impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}272 loc: CallLocation<'_>,
273 tailstrict: bool,
274) -> Result<Val> {
275 let func_val = evaluate(ctx.clone(), applicable)?;
276 let Val::Func(func) = func_val else {
277 bail!(OnlyFunctionsCanBeCalledGot(func_val.value_type()))
278 };
218279
219fn evaluate_object_locals(280 let name = func.name();
220 fctx: Context,281 let unnamed = args
221 locals: Rc<Vec<BindSpec>>,282 .unnamed
222) -> impl CloneableUnbound<Context> {283 .iter()
223 #[derive(Trace, Clone)]284 .cloned()
224 struct UnboundLocals {285 .map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))
225 fctx: Context,
226 locals: Rc<Vec<BindSpec>>,286 .collect::<Result<Vec<_>>>()?;
227 }
228 impl Unbound for UnboundLocals {
229 type Bound = Context;
230287
231 fn bind(&self, sup_this: SupThis) -> Result<Context> {288 let named = args
232 let fctx = Context::new_future();289 .values
290 .iter()
233 let ctx = self.fctx.clone();291 .cloned()
292 .map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))
293 .collect::<Result<Vec<_>>>()?;
234 let mut ctx = ContextBuilder::extend(ctx);294 let prepare = PreparedFuncVal::new(func, unnamed.len(), &args.names)
235 for b in self.locals.iter() {295 .with_description_src(loc, || format!("function <{name}> preparation"))?;
236 evaluate_dest(b, fctx.clone(), &mut ctx)?;296 in_frame(
297 loc,
298 || format!("function <{name}> call"),
299 || prepare.call(CallLocation::native(), &unnamed, &named),
237 }300 )
301}
238302
303fn evaluate_index(ctx: Context, indexable: &LExpr, parts: &[LIndexPart]) -> Result<Val> {
239 let ctx = ctx.build_sup_this(sup_this).into_future(fctx);304 let mut value = if let LExpr::Super = indexable {
240305 let sup_this = ctx.try_sup_this()?;
306 // First part must be evaluated to get the super field name
307 if parts.is_empty() {
241 Ok(ctx)308 bail!(RuntimeError("super requires an index".into()))
242 }309 }
243 }310 let key_val = evaluate(ctx.clone(), &parts[0].value)?;
311 let Val::Str(key) = &key_val else {
312 bail!(ValueIndexMustBeTypeGot(
313 ValType::Obj,
314 ValType::Str,
315 key_val.value_type(),
316 ))
317 };
318 let field = key.clone().into_flat();
319 if let Some(v) = sup_this.get_super(field.clone())? {
320 // Continue with remaining parts
321 let mut value = v;
322 for part in &parts[1..] {
323 value = index_val(ctx.clone(), CallLocation::new(&part.span), value, part)?;
324 }
325 return Ok(value);
326 }
327 let suggestions = suggest_object_fields(sup_this.this(), field.clone());
328 bail!(NoSuchField(field, suggestions))
329 } else {
330 evaluate(ctx.clone(), indexable)?
331 };
244332
245 UnboundLocals { fctx, locals }333 for part in parts {
334 value = index_val(ctx.clone(), CallLocation::new(&part.span), value, part)?;
335 }
336 Ok(value)
246}337}
247338
248pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(339fn index_val(ctx: Context, loc: CallLocation<'_>, value: Val, part: &LIndexPart) -> Result<Val> {
249 builder: &mut ObjValueBuilder,
250 ctx: Context,
251 uctx: B,
252 field: &FieldMember,
253) -> Result<()> {
254 let name = evaluate_field_name(ctx, &field.name)?;340 let key_val = evaluate(ctx, &part.value)?;
255 let Some(name) = name else {341 Ok(match (&value, &key_val) {
342 (Val::Obj(obj), Val::Str(key)) => {
256 return Ok(());343 let field = key.clone().into_flat();
257 };344 if let Some(v) = obj
258
259 match field {
260 FieldMember {345 .get(field.clone())
261 plus,
262 params: None,346 .with_description_src(loc, || format!("field <{field}> access"))?
263 visibility,347 {
264 value,348 v
265 ..349 } else {
266 } => {
267 #[derive(Trace)]350 bail!(NoSuchField(
268 struct UnboundValue<B: Trace> {351 field.clone(),
269 uctx: B,
270 value: Rc<Expr>,352 suggest_object_fields(obj, field)
271 name: IStr,353 ))
272 }354 }
273 impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {355 }
356 (Val::Arr(arr), Val::Num(idx)) => {
274 type Bound = Val;357 let n = idx.get();
275 fn bind(&self, sup_this: SupThis) -> Result<Val> {358 if n.fract() > f64::EPSILON {
276 evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())359 bail!(FractionalIndex)
277 }
278 }360 }
279
280 builder361 if n < 0.0 {
281 .field(name.clone())362 bail!(ArrayBoundsError(
282 .with_add(*plus)363 n as isize, // truncation is fine for error display
283 .with_visibility(*visibility)364 arr.len()
284 .with_location(field.name.span.clone())365 ));
285 .bindable(UnboundValue {366 }
367 #[expect(
286 uctx,368 clippy::cast_possible_truncation,
369 clippy::cast_sign_loss,
287 value: value.clone(),370 reason = "n is checked positive"
371 )]
372 let i = n as u32;
373 arr.get(i)
288 name,374 .with_description_src(loc, || format!("element <{i}> access"))?
289 })?;375 .ok_or_else(|| ArrayBoundsError(i as isize, arr.len()))?
290 }376 }
291 FieldMember {377 (Val::Str(s), Val::Num(idx)) => {
292 params: Some(params),
293 visibility,
294 value,
295 ..
296 } => {
297 #[derive(Trace)]378 let n = idx.get();
298 struct UnboundMethod<B: Trace> {379 if n.fract() > f64::EPSILON {
299 uctx: B,
300 value: Rc<Expr>,
301 params: ExprParams,380 bail!(FractionalIndex)
302 name: IStr,
303 }381 }
304 impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {382 let flat = s.clone().into_flat();
305 type Bound = Val;
306 fn bind(&self, sup_this: SupThis) -> Result<Val> {383 if n < 0.0 {
307 Ok(evaluate_method(384 bail!(ArrayBoundsError(
308 self.uctx.bind(sup_this)?,385 n as isize, // truncation is fine for error display
309 self.name.clone(),386 flat.chars().count() as u32
310 self.params.clone(),
311 self.value.clone(),387 ));
312 ))
313 }
314 }388 }
315
316 builder389 #[expect(
317 .field(name.clone())390 clippy::cast_possible_truncation,
318 .with_visibility(*visibility)391 clippy::cast_sign_loss,
319 // .with_location(value.span())392 reason = "n is checked positive, overflow will truncate as expected"
320 .bindable(UnboundMethod {393 )]
321 uctx,394 let i = n as usize;
322 value: value.clone(),395 let Some(char) = flat.chars().nth(i) else {
323 params: params.clone(),396 bail!(StringBoundsError(i, flat.chars().count()))
324 name,397 };
325 })?;398 Val::string(char)
326 }399 }
327 }400 _ => bail!(ValueIndexMustBeTypeGot(
328 Ok(())401 value.value_type(),
402 ValType::Str,
403 key_val.value_type()
404 )),
405 })
329}406}
330407
331#[derive(Trace, Clone)]408fn evaluate_obj_body(super_obj: Option<ObjValue>, ctx: Context, body: &LObjBody) -> Result<Val> {
332struct DirectUnbound(Context);
333impl Unbound for DirectUnbound {
334 type Bound = Context;409 match body {
335 fn bind(&self, sup_this: SupThis) -> Result<Context> {410 LObjBody::MemberList(members) => evaluate_obj_members(super_obj, ctx, members),
336 Ok(ContextBuilder::extend(self.0.clone()).build_sup_this(sup_this))411 LObjBody::ObjComp(comp) => evaluate_obj_comp(super_obj, ctx, comp),
337 }412 }
338}413}
339414
340#[allow(clippy::too_many_lines)]
341pub fn evaluate_member_list_object(415pub fn evaluate_field_member_unbound<B: Unbound<Bound = Context> + Clone>(
342 super_obj: Option<ObjValue>,416 builder: &mut ObjValueBuilder,
343 ctx: Context,417 ctx: Context,
344 members: &ObjMembers,418 uctx: B,
419 field: &LFieldMember,
345) -> Result<ObjValue> {420) -> Result<()> {
346 #[derive(Trace)]421 #[derive(Trace)]
347 struct ObjectAssert<B: Trace> {422 struct UnboundValue<B: Trace> {
348 uctx: B,423 uctx: B,
349 asserts: Rc<Vec<AssertStmt>>,424 value: Rc<LExpr>,
425 name: IStr,
350 }426 }
351 impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {427 impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {
352 fn run(&self, sup_this: SupThis) -> Result<()> {428 type Bound = Val;
429 fn bind(&self, sup_this: SupThis) -> Result<Val> {
353 let ctx = self.uctx.bind(sup_this)?;430 evaluate(self.uctx.bind(sup_this)?, &self.value)
354 for assert in &*self.asserts {
355 evaluate_assert(ctx.clone(), assert)?;
356 }
357 Ok(())
358 }431 }
359 }432 }
360433
361 let mut builder = ObjValueBuilder::new();434 let LFieldMember {
435 name,
436 plus,
437 visibility,
438 value,
439 } = field;
440 let Some(name) = evaluate_field_name(ctx, name)? else {
441 return Ok(());
442 };
443
444 builder
445 .field(name.clone())
446 .with_add(*plus)
447 .with_visibility(*visibility)
448 .bindable(UnboundValue {
449 uctx,
450 value: value.clone(),
451 name,
452 })
453}
454pub fn evaluate_field_member_static(
455 builder: &mut ObjValueBuilder,
456 field_ctx: Context,
457 value_ctx: Context,
458 field: &LFieldMember,
459) -> Result<()> {
460 let LFieldMember {
461 name,
462 plus,
463 visibility,
464 value,
465 } = field;
466 let Some(name) = evaluate_field_name(field_ctx, name)? else {
467 return Ok(());
468 };
469
470 let value = value.clone();
471 builder
472 .field(name)
473 .with_add(*plus)
474 .with_visibility(*visibility)
475 .try_thunk(Thunk!(move || { evaluate(value_ctx, &value) }))?;
476 Ok(())
477}
478
479fn evaluate_obj_members(
480 super_obj: Option<ObjValue>,
481 ctx: Context,
482 members: &LObjMembers,
483) -> Result<Val> {
484 let mut builder = ObjValueBuilder::with_capacity(members.fields.len());
362 if let Some(super_obj) = super_obj {485 if let Some(sup) = super_obj {
363 builder.with_super(super_obj);486 builder.with_super(sup);
364 }487 }
365488
366 if members.locals.is_empty() {489 let needs_unbound = members.this.is_some() || members.uses_super;
490
367 // We can use the same context for all field evaluation, it doesn't depends on locals, only on this/super491 if needs_unbound {
368 let uctx = DirectUnbound(ctx.clone());492 let uctx = CachedUnbound::new(evaluate_locals_unbound(
493 ctx.clone(),
494 members.locals.clone(),
495 members.this,
496 ));
369 for field in &members.fields {497 for field in &members.fields {
370 evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;498 evaluate_field_member_unbound(&mut builder, ctx.clone(), uctx.clone(), field)?;
371 }499 }
372 if !members.asserts.is_empty() {500 if !members.asserts.is_empty() {
373 builder.assert(ObjectAssert {501 builder.assert(evaluate_object_assertions_unbound(
374 uctx,502 uctx,
375 asserts: members.asserts.clone(),503 members.asserts.clone(),
376 });504 ));
377 }505 }
378 } else {506 } else {
379 let locals = members.locals.clone();507 let field_ctx = ctx;
380 // We have single context for all fields, so we can cache them together508 let value_ctx = evaluate_locals(field_ctx.clone(), &members.locals);
381 let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));
382 for field in &members.fields {509 for field in &members.fields {
383 evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;510 evaluate_field_member_static(
511 &mut builder,
512 field_ctx.clone(),
513 value_ctx.clone(),
514 field,
515 )?;
384 }516 }
385 if !members.asserts.is_empty() {517 if !members.asserts.is_empty() {
386 builder.assert(ObjectAssert {518 builder.assert(evaluate_object_assertions_static(
387 uctx,519 value_ctx,
388 asserts: members.asserts.clone(),520 members.asserts.clone(),
389 });521 ));
390 }522 }
391 }523 }
392524
393 Ok(builder.build())525 Ok(Val::Obj(builder.build()))
394}526}
395527
396pub fn evaluate_object(528pub fn evaluate_assert(ctx: Context, assertion: &LAssertStmt) -> Result<()> {
397 super_obj: Option<ObjValue>,
398 ctx: Context,
399 object: &ObjBody,
400) -> Result<ObjValue> {
401 Ok(match object {
402 ObjBody::MemberList(members) => evaluate_member_list_object(super_obj, ctx, members)?,
403 ObjBody::ObjComp(obj) => {
404 let mut builder = ObjValueBuilder::new();
405 if let Some(super_obj) = super_obj {
406 builder.with_super(super_obj);
407 }
408 let locals = obj.locals.clone();
409 evaluate_comp(
410 ctx.branch_point(),
411 &obj.compspecs,
412 0,
413 &mut |ctx, reserve| {
414 let uctx = evaluate_object_locals(ctx.clone(), locals.clone());
415 builder.reserve_fields(reserve);
416
417 evaluate_field_member(&mut builder, ctx, uctx, &obj.field)
418 },
419 )?;
420
421 builder.build()
422 }
423 })
424}
425
426pub fn evaluate_apply(
427 ctx: Context,
428 value: &Expr,
429 args: &ArgsDesc,
430 loc: CallLocation<'_>,
431 tailstrict: bool,
432) -> Result<Val> {
433 let value = evaluate(ctx.clone(), value)?;
434 Ok(match value {
435 Val::Func(f) => {
436 let name = f.name();
437 let unnamed = args
438 .unnamed
439 .iter()
440 .cloned()
441 .map(|un| evaluate_thunk(ctx.clone(), un, tailstrict))
442 .collect::<Result<Vec<_>>>()?;
443 let named = args
444 .values
445 .iter()
446 .cloned()
447 .map(|un| evaluate_thunk(ctx.clone(), un, tailstrict))
448 .collect::<Result<Vec<_>>>()?;
449 let prepare = PreparedFuncVal::new(f, args.unnamed.len(), &args.names)
450 .with_description_src(loc, || format!("function <{name}> call"))?;
451 let body = || prepare.call(loc, &unnamed, &named);
452 if tailstrict {
453 body()?
454 } else {
455 in_frame(loc, || format!("function <{name}> call"), body)?
456 }
457 }
458 v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),
459 })
460}
461
462pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {
463 let AssertStmt { assertion, message } = assertion;529 let LAssertStmt { cond, message } = assertion;
464 let assertion_result = in_frame(530 let assertion_result = in_frame(
465 CallLocation::new(&assertion.span),531 CallLocation::native(),
466 || "assertion condition".to_owned(),532 || "assertion condition".to_owned(),
467 || bool::from_untyped(evaluate(ctx.clone(), assertion)?),533 || bool::from_untyped(evaluate(ctx.clone(), cond)?),
468 )?;534 )?;
469 if !assertion_result {535 if !assertion_result {
470 in_frame(536 in_frame(
471 CallLocation::new(&assertion.span),537 CallLocation::new(&cond.span),
472 || "assertion failure".to_owned(),538 || "assertion failure".to_owned(),
473 || {539 || {
474 if let Some(msg) = message {540 if let Some(msg) = message {
481 Ok(())547 Ok(())
482}548}
483549
484pub fn evaluate_named_param(ctx: Context, expr: &Expr, name: ParamName) -> Result<Val> {550fn evaluate_object_assertions_unbound<B: Unbound<Bound = Context>>(
551 uctx: B,
552 asserts: Rc<Vec<LAssertStmt>>,
553) -> impl ObjectAssertion {
485 match name {554 #[derive(Trace)]
555 struct ObjectAssert<B: Trace> {
486 ParamName::Named(name) => evaluate_named(ctx, expr, name),556 uctx: B,
487 ParamName::Unnamed => evaluate(ctx, expr),557 asserts: Rc<Vec<LAssertStmt>>,
488 }558 }
489}
490
491pub fn evaluate_named(ctx: Context, expr: &Expr, name: IStr) -> Result<Val> {559 impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {
492 use Expr::*;
493 Ok(match expr {
494 Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),
495 _ => evaluate(ctx, expr)?,
496 })
497}
498
499pub fn evaluate_thunk(ctx: Context, expr: Rc<Expr>, tailstrict: bool) -> Result<Thunk<Val>> {
500 Ok(if tailstrict {560 fn run(&self, sup_this: SupThis) -> Result<()> {
501 Thunk::evaluated(evaluate(ctx, &expr)?)
502 } else {
503 Thunk!(move || { evaluate(ctx, &expr) })
504 })
505}
506#[allow(clippy::too_many_lines)]
507pub fn evaluate(ctx: Context, expr: &Expr) -> Result<Val> {
508 use Expr::*;561 let ctx = self.uctx.bind(sup_this)?;
509
510 Ok(match expr {
511 Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),
512 Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),
513 Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),562 for assert in &*self.asserts {
514 Literal(LiteralType::True) => Val::Bool(true),
515 Literal(LiteralType::False) => Val::Bool(false),
516 Literal(LiteralType::Null) => Val::Null,
517 Str(v) => Val::string(v.clone()),
518 Num(v) => Val::try_num(*v)?,
519 // I have tried to remove special behavior from super by implementing standalone-super
520 // expresion, but looks like this case still needs special treatment.
521 //
522 // Note that other jsonnet implementations will fail on `if value in (super)` expression,
523 // because the standalone super literal is not supported, that is because in other
524 // implementations `in super` treated differently from `in smth_else`.
525 BinaryOp(bin)
526 if matches!(&bin.rhs, Expr::Literal(LiteralType::Super))
527 && bin.op == BinaryOpType::In =>
528 {
529 let sup_this = ctx.try_sup_this()?;563 evaluate_assert(ctx.clone(), assert)?;
530 // In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.
531 // In jrsonnet, however, this wasn't true, this was kept here for compatibility.
532 if !sup_this.has_super() {
533 return Ok(Val::Bool(false));
534 }564 }
535 let field = evaluate(ctx, &bin.lhs)?;565 Ok(())
536 Val::Bool(sup_this.field_in_super(field.to_string()?))
537 }566 }
538 BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?,567 }
539 UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,568 ObjectAssert { uctx, asserts }
540 Var(name) => in_frame(569}
541 CallLocation::new(&name.span),570fn evaluate_object_assertions_static(
542 || format!("local <{}> access", &**name),571 ctx: Context,
543 || ctx.binding((**name).clone())?.evaluate(),572 asserts: Rc<Vec<LAssertStmt>>,
544 )?,573) -> impl ObjectAssertion {
545 Index { indexable, parts } => ensure_sufficient_stack(|| {574 #[derive(Trace)]
546 let mut parts = parts.iter();575 struct ObjectAssert {
547 let mut indexable = if matches!(&**indexable, Expr::Literal(LiteralType::Super)) {576 ctx: Context,
548 let part = parts.next().expect("at least part should exist");577 asserts: Rc<Vec<LAssertStmt>>,
549 // sup_this existence check might also be skipped here for null-coalesce...578 }
550 // But I believe this might cause errors.579 impl ObjectAssertion for ObjectAssert {
551 let sup_this = ctx.try_sup_this()?;580 fn run(&self, _sup_this: SupThis) -> Result<()> {
552 if !sup_this.has_super() {581 for assert in &*self.asserts {
553 #[cfg(feature = "exp-null-coaelse")]582 evaluate_assert(self.ctx.clone(), assert)?;
554 if part.null_coaelse {
555 return Ok(Val::Null);
556 }
557 bail!(NoSuperFound)
558 }
559 let name = evaluate(ctx.clone(), &part.value)?;
560
561 let Val::Str(name) = name else {
562 bail!(ValueIndexMustBeTypeGot(
563 ValType::Obj,
564 ValType::Str,
565 name.value_type(),
566 ))
567 };
568
569 let name = name.into_flat();
570 match sup_this
571 .get_super(name.clone())
572 .with_description_src(&part.span, || format!("field <{name}> access"))?
573 {
574 Some(v) => v,
575 #[cfg(feature = "exp-null-coaelse")]
576 None if part.null_coaelse => return Ok(Val::Null),
577 None => {
578 let suggestions = suggest_object_fields(
579 &sup_this.standalone_super().expect("super exists"),
580 name.clone(),
581 );
582
583 bail!(NoSuchField(name, suggestions))
584 }
585 }
586 } else {
587 evaluate(ctx.clone(), indexable)?
588 };
589
590 for part in parts {
591 indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {
592 (Val::Obj(v), Val::Str(key)) => match v
593 .get(key.clone().into_flat())
594 .with_description_src(&part.span, || format!("field <{key}> access"))?
595 {
596 Some(v) => v,
597 #[cfg(feature = "exp-null-coaelse")]
598 None if part.null_coaelse => return Ok(Val::Null),
599 None => {
600 let suggestions = suggest_object_fields(&v, key.into_flat());
601
602 return Err(Error::from(NoSuchField(
603 key.clone().into_flat(),
604 suggestions,
605 )))
606 .with_description_src(&part.span, || format!("field <{key}> access"));
607 }
608 },
609 (Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(
610 ValType::Obj,
611 ValType::Str,
612 n.value_type(),
613 )),
614 (Val::Arr(v), Val::Num(n)) => {
615 let n = n.get();
616 if n.fract() > f64::EPSILON {
617 bail!(FractionalIndex)
618 }
619 if n < 0.0 {
620 #[expect(
621 clippy::cast_possible_truncation,
622 reason = "it would be truncated anyway"
623 )]
624 let n = n as isize;
625 bail!(ArrayBoundsError(n, v.len()));
626 }
627 #[expect(
628 clippy::cast_possible_truncation,
629 clippy::cast_sign_loss,
630 reason = "n is checked postive"
631 )]
632 v.get(n as usize)?
633 .ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?
634 }
635 (Val::Arr(_), Val::Str(n)) => {
636 bail!(AttemptedIndexAnArrayWithString(n.into_flat()))
637 }
638 (Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(
639 ValType::Arr,
640 ValType::Num,
641 n.value_type(),
642 )),
643
644 (Val::Str(s), Val::Num(n)) => Val::Str({
645 let n = n.get();
646 if n.fract() > f64::EPSILON {
647 bail!(FractionalIndex)
648 }
649 if n < 0.0 {
650 #[expect(
651 clippy::cast_possible_truncation,
652 reason = "it would be truncated anyway"
653 )]
654 let n = n as isize;
655 bail!(ArrayBoundsError(n, s.into_flat().chars().count()));
656 }
657 #[expect(
658 clippy::cast_sign_loss,
659 clippy::cast_possible_truncation,
660 reason = "n is positive, overflow will truncate as expected"
661 )]
662 let n = n as usize;
663 let v: IStr = s
664 .clone()
665 .into_flat()
666 .chars()
667 .skip(n)
668 .take(1)
669 .collect::<String>()
670 .into();
671 if v.is_empty() {
672 bail!(StringBoundsError(n, s.into_flat().chars().count()))
673 }
674 StrValue::Flat(v)
675 }),
676 (Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(
677 ValType::Str,
678 ValType::Num,
679 n.value_type(),
680 )),
681 #[cfg(feature = "exp-null-coaelse")]
682 (Val::Null, _) if part.null_coaelse => return Ok(Val::Null),
683 (v, _) => bail!(CantIndexInto(v.value_type())),
684 };
685 }583 }
686 Ok(indexable)584 Ok(())
687 })?,
688 LocalExpr(bindings, returned) => {
689 let fctx = Context::new_future();
690 let mut ctx = ContextBuilder::extend(ctx);
691 for b in bindings {
692 evaluate_dest(b, fctx.clone(), &mut ctx)?;
693 }
694 let ctx = ctx.build().into_future(fctx);
695 evaluate(ctx, returned)?
696 }585 }
697 Arr(items) => {586 }
698 if items.is_empty() {587 ObjectAssert { ctx, asserts }
699 Val::arr(())
700 } else {
701 Val::Arr(ArrValue::expr(ctx, items.clone()))
702 }
703 }
704 ArrComp(expr, comp_specs) => Val::Arr(evaluate_arr_comp(ctx, expr, comp_specs)?),
705 Obj(body) => Val::Obj(evaluate_object(None, ctx, body)?),
706 ObjExtend(a, b) => {
707 let base = evaluate(ctx.clone(), a)?;
708 match base {
709 Val::Obj(base_obj) => Val::Obj(evaluate_object(Some(base_obj), ctx, b)?),
710 _ => bail!("ObjExtend lhs should be an object value"),
711 }
712 }
713 Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {
714 evaluate_apply(ctx, value, args, CallLocation::new(&args.span), *tailstrict)
715 })?,
716 Function(params, body) => {
717 evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())
718 }
719 AssertExpr(assert) => {
720 evaluate_assert(ctx.clone(), &assert.assert)?;
721 evaluate(ctx, &assert.rest)?
722 }
723 ErrorStmt(s, e) => in_frame(
724 CallLocation::new(s),
725 || "error statement".to_owned(),
726 || bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),
727 )?,
728 IfElse(if_else) => {
729 if in_frame(
730 CallLocation::new(&if_else.cond.span),
731 || "if condition".to_owned(),
732 || bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.cond)?),
733 )? {
734 evaluate(ctx, &if_else.cond_then)?
735 } else {
736 match &if_else.cond_else {
737 Some(v) => evaluate(ctx, v)?,
738 None => Val::Null,
739 }
740 }
741 }
742 Slice(slice) => {
743 fn parse_idx<T: Typed + FromUntyped>(
744 ctx: Context,
745 expr: Option<&Spanned<Expr>>,
746 desc: &'static str,
747 ) -> Result<Option<T>> {
748 if let Some(value) = expr {
749 Ok(in_frame(
750 CallLocation::new(&value.span),
751 || format!("slice {desc}"),
752 || <Option<T>>::from_untyped(evaluate(ctx, value)?),
753 )?)
754 } else {
755 Ok(None)
756 }
757 }
758
759 let indexable = evaluate(ctx.clone(), &slice.value)?;
760
761 let start = parse_idx(ctx.clone(), slice.slice.start.as_ref(), "start")?;
762 let end = parse_idx(ctx.clone(), slice.slice.end.as_ref(), "end")?;
763 let step = parse_idx(ctx, slice.slice.step.as_ref(), "step")?;
764
765 IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?
766 }
767 Import(kind, path) => {
768 let Expr::Str(path) = &**path else {
769 bail!("computed imports are not supported")
770 };
771 with_state(|s| {
772 let span = &kind.span;
773 let resolved_path = s.resolve_from(span.0.source_path(), path)?;
774 Ok(match &**kind {
775 ImportKind::Normal => in_frame(
776 CallLocation::new(span),
777 || format!("import {:?}", path.clone()),
778 || s.import_resolved(resolved_path),
779 )?,
780 ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?),
781 ImportKind::Bin => Val::arr(s.import_resolved_bin(resolved_path)?),
782 }) as Result<Val>
783 })?
784 }
785 })
786}588}
787589
modifiedcrates/jrsonnet-evaluator/src/evaluate/operator.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/operator.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/operator.rs
@@ -1,16 +1,17 @@
 use std::cmp::Ordering;
 
-use jrsonnet_ir::{BinaryOpType, Expr, UnaryOpType};
+use jrsonnet_ir::{BinaryOpType, UnaryOpType};
 
 use crate::{
-	Context, Result, Val,
+	analyze::LExpr,
 	arr::ArrValue,
-	bail,
+	bail, error,
 	error::ErrorKind::*,
-	evaluate,
+	evaluate::evaluate,
 	stdlib::std_format,
 	typed::IntoUntyped as _,
-	val::{StrValue, equals},
+	val::{equals, StrValue},
+	Context, Result, Val,
 };
 
 pub fn evaluate_unary_op(op: UnaryOpType, b: &Val) -> Result<Val> {
@@ -39,7 +40,9 @@
 		(o, Str(a)) => Val::string(format!("{}{a}", o.clone().to_string()?)),
 
 		(Obj(v1), Obj(v2)) => Obj(v2.extend_from(v1.clone())),
-		(Arr(a), Arr(b)) => Val::Arr(ArrValue::extended(a.clone(), b.clone())),
+		(Arr(a), Arr(b)) => Val::Arr(
+			ArrValue::extended(a.clone(), b.clone()).ok_or_else(|| error!("array is too large"))?,
+		),
 
 		(Num(v1), Num(v2)) => Val::try_num(v1.get() + v2.get())?,
 
@@ -158,19 +161,27 @@
 
 pub fn evaluate_binary_op_special(
 	ctx: Context,
-	a: &Expr,
+	a: &LExpr,
 	op: BinaryOpType,
-	b: &Expr,
+	b: &LExpr,
 ) -> Result<Val> {
 	use BinaryOpType::*;
 	use Val::*;
+
 	Ok(match (evaluate(ctx.clone(), a)?, op, b) {
-		(Bool(true), Or, _o) => Val::Bool(true),
-		(Bool(false), And, _o) => Val::Bool(false),
+		(Bool(true), Or, _) => Val::Bool(true),
+		(Bool(false), And, _) => Val::Bool(false),
 		#[cfg(feature = "exp-null-coaelse")]
 		(Null, NullCoaelse, eb) => evaluate(ctx, eb)?,
 		#[cfg(feature = "exp-null-coaelse")]
-		(a, NullCoaelse, _o) => a,
+		(a, NullCoaelse, _) => a,
+		(a, In, LExpr::Super) => {
+			let sup_this = ctx.try_sup_this()?;
+			if !sup_this.has_super() {
+				return Ok(Val::Bool(false));
+			}
+			return Ok(Val::Bool(sup_this.field_in_super(a.to_string()?)));
+		}
 		(a, op, eb) => evaluate_binary_op_normal(&a, op, &evaluate(ctx, eb)?)?,
 	})
 }
modifiedcrates/jrsonnet-evaluator/src/function/builtin.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/builtin.rs
+++ b/crates/jrsonnet-evaluator/src/function/builtin.rs
@@ -19,6 +19,23 @@
 	};
 }
 
+#[macro_export]
+macro_rules! names {
+	($($name:ident: $val:literal),* $(,)?) => {
+		struct Names {
+			$($name: $crate::IStr,)*
+		}
+		thread_local! {
+			static NAMES: Names = Names {
+				$($name: $crate::IStr::from($val)),*
+			};
+		}
+		$(pub fn $name() -> $crate::IStr {
+			NAMES.with(|n| n.$name.clone())
+		})*
+	}
+}
+
 cc_dyn!(
 	#[derive(Clone)]
 	BuiltinFunc,
modifiedcrates/jrsonnet-evaluator/src/function/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/mod.rs
+++ b/crates/jrsonnet-evaluator/src/function/mod.rs
@@ -3,22 +3,24 @@
 use educe::Educe;
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::IStr;
-use jrsonnet_ir::{Destruct, Expr, ExprParams, Span};
+use jrsonnet_ir::Span;
 pub use jrsonnet_macros::builtin;
 
 use self::{
 	builtin::Builtin,
-	parse::parse_default_function_call,
-	prepared::{PreparedCall, parse_prepared_builtin_call, parse_prepared_function_call},
+	prepared::{parse_prepared_builtin_call, PreparedCall},
 };
 use crate::{
-	Context, Result, Thunk, Val, evaluate, evaluate_trivial, function::builtin::BuiltinFunc,
+	analyze::{LDestruct, LExpr, LFunction},
+	evaluate::{destructure::destruct, ensure_sufficient_stack, evaluate, evaluate_trivial},
+	function::builtin::BuiltinFunc,
+	Context, ContextBuilder, Result, Thunk, Val,
 };
 
 pub mod builtin;
 mod native;
 mod parse;
-mod prepared;
+pub(crate) mod prepared;
 
 pub use jrsonnet_ir::function::*;
 pub use native::NativeFn;
@@ -66,19 +68,63 @@
 	/// context will contain `a`.
 	pub ctx: Context,
 
-	/// Function parameter definition
-	pub params: ExprParams,
-	/// Function body
-	pub body: Rc<Expr>,
+	#[educe(PartialEq(method = Rc::ptr_eq))]
+	pub func: Rc<LFunction>,
 }
+
 impl FuncDesc {
-	/// Create body context, but fill arguments without defaults with lazy error
-	pub fn default_body_context(&self) -> Result<Context> {
-		parse_default_function_call(self.ctx.clone(), &self.params)
+	pub fn signature(&self) -> FunctionSignature {
+		self.func.signature.clone()
 	}
 
+	pub fn call(
+		&self,
+		unnamed: &[Thunk<Val>],
+		named: &[Thunk<Val>],
+		prepared: &PreparedCall,
+	) -> Result<Val> {
+		let has_defaults = !prepared.defaults().is_empty();
+		let mut builder = ContextBuilder::extend(self.ctx.clone(), self.func.params.len());
+
+		let fctx = Context::new_future();
+		for (param_idx, thunk) in unnamed.iter().enumerate() {
+			destruct(
+				&self.func.params[param_idx].destruct,
+				thunk.clone(),
+				fctx.clone(),
+				&mut builder,
+			);
+		}
+
+		for &(param_idx, arg_idx) in prepared.named() {
+			destruct(
+				&self.func.params[param_idx].destruct,
+				named[arg_idx].clone(),
+				fctx.clone(),
+				&mut builder,
+			);
+		}
+
+		if has_defaults {
+			for &param_idx in prepared.defaults() {
+				let param = &self.func.params[param_idx];
+				if let Some(default_expr) = &param.default {
+					let default_expr = default_expr.clone();
+					let fctxc = fctx.clone();
+					let thunk = Thunk!(move || {
+						let ctx = fctxc.unwrap();
+						evaluate(ctx, &default_expr)
+					});
+					destruct(&param.destruct, thunk, fctx.clone(), &mut builder);
+				}
+			}
+		};
+		let ctx = builder.build().into_future(fctx);
+		ensure_sufficient_stack(|| evaluate(ctx, &self.func.body))
+	}
+
 	pub fn evaluate_trivial(&self) -> Option<Val> {
-		evaluate_trivial(&self.body)
+		evaluate_trivial(&self.func.body)
 	}
 }
 
@@ -115,12 +161,12 @@
 	pub fn params(&self) -> FunctionSignature {
 		match self {
 			Self::Builtin(i) => i.params(),
-			Self::Normal(p) => p.params.signature.clone(),
+			Self::Normal(p) => p.signature(),
 		}
 	}
 	/// Amount of non-default required arguments
-	pub fn params_len(&self) -> usize {
-		self.params().iter().filter(|p| !p.has_default()).count()
+	pub fn params_len(&self) -> u32 {
+		self.params().iter().filter(|p| !p.has_default()).count() as u32
 	}
 	/// Function name, as defined in code.
 	pub fn name(&self) -> IStr {
@@ -139,16 +185,7 @@
 		_tailstrict: bool,
 	) -> Result<Val> {
 		match self {
-			FuncVal::Normal(func) => {
-				let body_ctx = parse_prepared_function_call(
-					func.ctx.clone(),
-					prepared,
-					&func.params,
-					unnamed,
-					named,
-				)?;
-				evaluate(body_ctx, &func.body)
-			}
+			FuncVal::Normal(func) => func.call(unnamed, named, prepared),
 			FuncVal::Builtin(b) => {
 				let args = parse_prepared_builtin_call(prepared, b.params(), unnamed, named);
 				b.call(loc, &args)
@@ -156,7 +193,7 @@
 		}
 	}
 
-	/// Is this function an indentity function.
+	/// Is this function an identity function.
 	///
 	/// Currently only works for builtin `std.id`, aka `Self::Id` value, and `function(x) x`.
 	///
@@ -165,21 +202,19 @@
 		match self {
 			Self::Builtin(b) => b.as_any().downcast_ref::<builtin_id>().is_some(),
 			Self::Normal(desc) => {
-				if desc.params.len() != 1 {
+				if desc.func.params.len() != 1 {
 					return false;
 				}
-				let param = &desc.params.exprs[0];
+				let param = &desc.func.params[0];
 				if param.default.is_some() {
 					return false;
 				}
-
-				#[allow(clippy::infallible_destructuring_match)]
-				let id = match &param.destruct {
-					Destruct::Full(id) => id,
-					#[cfg(feature = "exp-destruct")]
-					_ => return false,
+				#[allow(irrefutable_let_patterns, reason = "refutable with exp-destruct")]
+				let LDestruct::Full(id) = &param.destruct
+				else {
+					return false;
 				};
-				matches!(&*desc.body, Expr::Var(v) if &**v == id)
+				matches!(&*desc.func.body, LExpr::Local(v) if v == id)
 			}
 		}
 	}
modifiedcrates/jrsonnet-evaluator/src/function/parse.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/parse.rs
+++ b/crates/jrsonnet-evaluator/src/function/parse.rs
@@ -1,49 +1,39 @@
-use jrsonnet_ir::ExprParams;
+use std::rc::Rc;
 
 use crate::{
-	Context, ContextBuilder, Thunk,
-	destructure::destruct,
-	error::{ErrorKind::*, Result},
-	evaluate_named_param,
+	analyze::LFunction,
+	evaluate::{destructure::destruct, evaluate},
+	Context, ContextBuilder, Result, Thunk,
 };
 
-/// Creates Context, which has all argument default values applied
-/// and with unbound values causing error to be returned
-pub fn parse_default_function_call(body_ctx: Context, params: &ExprParams) -> Result<Context> {
+/// Creates Context with all argument default values applied
+/// and with unbound values causing error to be returned.
+pub fn parse_default_function_call(body_ctx: Context, func: &Rc<LFunction>) -> Result<Context> {
 	let fctx = Context::new_future();
-
-	let mut ctx = ContextBuilder::extend(body_ctx);
+	let mut builder = ContextBuilder::extend(body_ctx, func.params.len());
 
-	for param in params.exprs.iter() {
-		if let Some(v) = &param.default {
-			destruct(
-				&param.destruct.clone(),
-				{
-					let ctx = fctx.clone();
-					let name = param.destruct.name();
-					let value = v.clone();
-					Thunk!(move || evaluate_named_param(ctx.unwrap(), &value, name))
-				},
-				fctx.clone(),
-				&mut ctx,
-			)?;
+	for param in &func.params {
+		if let Some(default_expr) = &param.default {
+			let default_expr = default_expr.clone();
+			let fctxc = fctx.clone();
+			let thunk = Thunk!(move || {
+				let ctx = fctxc.unwrap();
+				evaluate(ctx, &default_expr)
+			});
+			destruct(&param.destruct, thunk, fctx.clone(), &mut builder);
 		} else {
-			destruct(
-				&param.destruct,
-				{
-					let param_name = param.destruct.name();
-					let params = params.clone();
-					Thunk!(move || Err(FunctionParameterNotBoundInCall(
-						param_name,
-						params.signature
-					)
-					.into()))
-				},
-				fctx.clone(),
-				&mut ctx,
-			)?;
+			let name = param.name.clone().unwrap_or_else(|| "<param>".into());
+			let thunk = Thunk::errored(
+				crate::error::ErrorKind::FunctionParameterNotBoundInCall(
+					jrsonnet_ir::function::ParamName::Named(name),
+					jrsonnet_ir::function::FunctionSignature::empty(),
+				)
+				.into(),
+			);
+			destruct(&param.destruct, thunk, fctx.clone(), &mut builder);
 		}
 	}
 
-	Ok(ctx.build().into_future(fctx))
+	let ctx = builder.build().into_future(fctx);
+	Ok(ctx)
 }
modifiedcrates/jrsonnet-evaluator/src/function/prepared.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/prepared.rs
+++ b/crates/jrsonnet-evaluator/src/function/prepared.rs
@@ -1,13 +1,13 @@
 use std::rc::Rc;
 
 use jrsonnet_gcmodule::{Acyclic, Trace};
-use jrsonnet_ir::{ExprParams, IStr, function::FunctionSignature};
+use jrsonnet_ir::{IStr, function::FunctionSignature};
 use rustc_hash::FxHashSet;
 
 use super::{CallLocation, FuncVal};
 use crate::{
-	Context, ContextBuilder, Pending, Result, Thunk, Val, bail, destructure::destruct,
-	error::ErrorKind::*, evaluate_named_param,
+	Result, Thunk, Val, bail,
+	error::ErrorKind::*,
 };
 
 #[derive(Debug, Trace, Clone)]
@@ -42,6 +42,15 @@
 	defaults: Vec<usize>,
 }
 
+impl PreparedCall {
+	pub fn named(&self) -> &[(usize, usize)] {
+		&self.named
+	}
+	pub fn defaults(&self) -> &[usize] {
+		&self.defaults
+	}
+}
+
 pub fn prepare_call(
 	params: FunctionSignature,
 	unnamed: usize,
@@ -51,6 +60,25 @@
 		bail!(TooManyArgsFunctionHas(params.len(), params))
 	}
 
+	// Fast path: positional-only (no named args). Avoids HashMap entirely.
+	if named.is_empty() {
+		let mut defaults = Vec::new();
+		for (param_id, param) in params.iter().enumerate().skip(unnamed) {
+			if param.has_default() {
+				defaults.push(param_id);
+			} else {
+				bail!(FunctionParameterNotBoundInCall(
+					param.name().clone(),
+					params.clone(),
+				))
+			}
+		}
+		return Ok(PreparedCall {
+			named: Vec::new(),
+			defaults,
+		});
+	}
+
 	let expected_defaults = (params.len() - unnamed).saturating_sub(named.len());
 	let mut ops = PreparedCall {
 		named: Vec::with_capacity(named.len()),
@@ -110,63 +138,6 @@
 	}
 
 	Ok(ops)
-}
-pub fn parse_prepared_function_call(
-	body_ctx: Context,
-	prepared: &PreparedCall,
-	params: &ExprParams,
-	unnamed: &[Thunk<Val>],
-	named: &[Thunk<Val>],
-) -> Result<Context> {
-	let mut ctx = ContextBuilder::extend(body_ctx);
-
-	let destruct_ctx = Pending::new();
-
-	for (param_idx, unnamed) in unnamed.iter().enumerate() {
-		destruct(
-			&params.exprs[param_idx].destruct,
-			unnamed.clone(),
-			destruct_ctx.clone(),
-			&mut ctx,
-		)?;
-	}
-
-	for (param_idx, arg_idx) in prepared.named.iter().copied() {
-		destruct(
-			&params.exprs[param_idx].destruct,
-			named[arg_idx].clone(),
-			destruct_ctx.clone(),
-			&mut ctx,
-		)?;
-	}
-
-	if prepared.defaults.is_empty() {
-		let body_ctx = ctx.build().into_future(destruct_ctx);
-		Ok(body_ctx)
-	} else {
-		let fctx = Context::new_future();
-		let mut ctx = ctx.commit();
-		for param_idx in prepared.defaults.iter().copied() {
-			// let param = params.0.rc_idx(param_idx);
-			destruct(
-				&params.exprs[param_idx].destruct,
-				{
-					let ctx = fctx.clone();
-					let params = params.clone();
-					Thunk!(move || {
-						let param = &params.exprs[param_idx];
-						let name = param.destruct.name();
-						let value = param.default.as_ref().expect("default exists");
-						evaluate_named_param(ctx.unwrap(), value, name)
-					})
-				},
-				fctx.clone(),
-				&mut ctx,
-			)?;
-		}
-
-		Ok(ctx.build().into_future(fctx).into_future(destruct_ctx))
-	}
 }
 pub fn parse_prepared_builtin_call(
 	prepared: &PreparedCall,
modifiedcrates/jrsonnet-evaluator/src/integrations/serde.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/integrations/serde.rs
+++ b/crates/jrsonnet-evaluator/src/integrations/serde.rs
@@ -3,16 +3,16 @@
 use jrsonnet_interner::{IBytes, IStr};
 use jrsonnet_ir::NumValue;
 use serde::{
-	Deserialize, Serialize, Serializer,
 	de::{self, Visitor},
 	ser::{
 		Error, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple,
 		SerializeTupleStruct, SerializeTupleVariant,
 	},
+	Deserialize, Serialize, Serializer,
 };
 
 use crate::{
-	Error as JrError, ObjValue, ObjValueBuilder, Result, Val, in_description_frame, runtime_error,
+	in_description_frame, runtime_error, Error as JrError, ObjValue, ObjValueBuilder, Result, Val,
 };
 
 impl<'de> Deserialize<'de> for Val {
@@ -182,7 +182,7 @@
 			#[cfg(feature = "exp-bigint")]
 			Self::BigInt(b) => b.serialize(serializer),
 			Self::Arr(arr) => {
-				let mut seq = serializer.serialize_seq(Some(arr.len()))?;
+				let mut seq = serializer.serialize_seq(Some(arr.len() as usize))?;
 				for (i, element) in arr.iter().enumerate() {
 					let mut serde_error = None;
 					in_description_frame(
@@ -203,7 +203,7 @@
 				seq.end()
 			}
 			Self::Obj(obj) => {
-				let mut map = serializer.serialize_map(Some(obj.len()))?;
+				let mut map = serializer.serialize_map(Some(obj.len() as usize))?;
 				for (field, value) in obj.iter(
 					#[cfg(feature = "exp-preserve-order")]
 					true,
modifiedcrates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/lib.rs
+++ b/crates/jrsonnet-evaluator/src/lib.rs
@@ -36,14 +36,13 @@
 pub use ctx::*;
 pub use dynamic::*;
 pub use error::{Error, ErrorKind::*, Result, ResultExt};
-pub use evaluate::*;
+pub use evaluate::ensure_sufficient_stack;
 use function::CallLocation;
 pub use import::*;
-use jrsonnet_gcmodule::{Cc, Trace, cc_dyn};
+use jrsonnet_gcmodule::{cc_dyn, Cc, Trace};
 pub use jrsonnet_interner::{IBytes, IStr};
-pub use jrsonnet_ir as parser;
-pub use jrsonnet_ir::NumValue;
-use jrsonnet_ir::{Expr, Source, SourcePath};
+use jrsonnet_ir::Expr;
+pub use jrsonnet_ir::{NumValue, Source, SourcePath, Span};
 #[doc(hidden)]
 pub use jrsonnet_macros;
 
@@ -58,6 +57,7 @@
 pub use tla::apply_tla;
 pub use val::{Thunk, Val};
 
+pub mod analyze;
 use crate::gc::WithCapacityExt as _;
 
 #[allow(clippy::needless_return)]
@@ -87,7 +87,7 @@
 	jrsonnet_ir_parser::parse(code, &jrsonnet_ir_parser::ParserSettings { source }).map_err(|e| {
 		SyntaxError {
 			message: e.message,
-			location: (e.location.0, e.location.1),
+			location: e.location,
 		}
 	})
 }
@@ -165,7 +165,7 @@
 pub trait ContextInitializer {
 	/// For composability: extend builder. May panic if this initialization is not supported,
 	/// and the context may only be created via `initialize`.
-	fn populate(&self, for_file: Source, builder: &mut ContextBuilder);
+	fn populate(&self, for_file: Source, builder: &mut InitialContextBuilder);
 	/// Allows upcasting from abstract to concrete context initializer.
 	/// jrsonnet by itself doesn't use this method, it is allowed for it to panic.
 	fn as_any(&self) -> &dyn Any;
@@ -174,7 +174,7 @@
 where
 	T: ContextInitializer,
 {
-	fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {
+	fn populate(&self, for_file: Source, builder: &mut InitialContextBuilder) {
 		(*self).populate(for_file, builder);
 	}
 
@@ -185,7 +185,7 @@
 
 /// Context initializer which adds nothing.
 impl ContextInitializer for () {
-	fn populate(&self, _for_file: Source, _builder: &mut ContextBuilder) {}
+	fn populate(&self, _for_file: Source, _builder: &mut InitialContextBuilder) {}
 	fn as_any(&self) -> &dyn Any {
 		self
 	}
@@ -195,7 +195,7 @@
 where
 	T: ContextInitializer + 'static,
 {
-	fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {
+	fn populate(&self, for_file: Source, builder: &mut InitialContextBuilder) {
 		if let Some(ctx) = self {
 			ctx.populate(for_file, builder);
 		}
@@ -210,7 +210,7 @@
 	($($gen:ident)*) => {
 		#[allow(non_snake_case)]
 		impl<$($gen: ContextInitializer + Trace,)*> ContextInitializer for ($($gen,)*) {
-			fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {
+			fn populate(&self, for_file: Source, builder: &mut InitialContextBuilder) {
 				let ($($gen,)*) = self;
 				$($gen.populate(for_file.clone(), builder);)*
 			}
@@ -408,7 +408,12 @@
 		file.evaluating = true;
 		// Dropping file cache guard here, as evaluation may use this map too
 		drop(file_cache);
-		let res = evaluate(self.create_default_context(file_name), &parsed);
+		let (ctx, externals) = self.create_default_context(file_name.clone()).build();
+		let report = analyze::analyze_root(&parsed, externals);
+		if report.errored {
+			return Err(StaticAnalysisError(report.diagnostics_list).into());
+		}
+		let res = evaluate::evaluate(ctx.build(), &report.lir);
 
 		let mut file_cache = self.file_cache();
 		let mut file = file_cache.entry(path);
@@ -438,7 +443,7 @@
 	}
 
 	/// Creates context with all passed global variables
-	pub fn create_default_context(&self, source: Source) -> Context {
+	pub fn create_default_context(&self, source: Source) -> InitialContextBuilder {
 		self.create_default_context_with(source, &())
 	}
 
@@ -447,13 +452,13 @@
 		&self,
 		source: Source,
 		context_initializer: &dyn ContextInitializer,
-	) -> Context {
+	) -> InitialContextBuilder {
 		let default_initializer = self.context_initializer();
-		let mut builder = ContextBuilder::new();
+		let mut builder = InitialContextBuilder::new();
 		default_initializer.populate(source.clone(), &mut builder);
 		context_initializer.populate(source, &mut builder);
 
-		builder.build()
+		builder
 	}
 }
 
@@ -487,7 +492,7 @@
 #[derive(Trace)]
 pub struct InitialUnderscore(pub Thunk<Val>);
 impl ContextInitializer for InitialUnderscore {
-	fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {
+	fn populate(&self, _for_file: Source, builder: &mut InitialContextBuilder) {
 		builder.bind("_", self.0.clone());
 	}
 
@@ -515,10 +520,14 @@
 			path: source.clone(),
 			error: Box::new(e),
 		})?;
-		evaluate(
-			self.create_default_context_with(source, context_initializer),
-			&parsed,
-		)
+		let (ctx, externals) = self
+			.create_default_context_with(source.clone(), context_initializer)
+			.build();
+		let report = analyze::analyze_root(&parsed, externals);
+		if report.errored {
+			return Err(StaticAnalysisError(report.diagnostics_list).into());
+		}
+		evaluate::evaluate(ctx.build(), &report.lir)
 	}
 }
 
modifiedcrates/jrsonnet-evaluator/src/obj/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/obj/mod.rs
+++ b/crates/jrsonnet-evaluator/src/obj/mod.rs
@@ -11,8 +11,8 @@
 };
 
 use educe::Educe;
-use im_rc::{Vector, vector};
-use jrsonnet_gcmodule::{Acyclic, Cc, Trace, Weak, cc_dyn};
+use im_rc::{vector, Vector};
+use jrsonnet_gcmodule::{cc_dyn, Acyclic, Cc, Trace, Weak};
 use jrsonnet_interner::IStr;
 use jrsonnet_ir::Span;
 use rustc_hash::{FxHashMap, FxHashSet};
@@ -23,13 +23,13 @@
 pub use oop::ObjValueBuilder;
 
 use crate::{
-	CcUnbound, MaybeUnbound, Result, Thunk, Unbound, Val,
 	arr::{PickObjectKeyValues, PickObjectValues},
 	bail,
-	error::{ErrorKind::*, suggest_object_fields},
+	error::{suggest_object_fields, ErrorKind::*},
+	evaluate::operator::evaluate_add_op,
 	identity_hash,
-	operator::evaluate_add_op,
 	val::{ArrValue, ThunkValue},
+	CcUnbound, MaybeUnbound, Result, Thunk, Unbound, Val,
 };
 
 #[cfg(not(feature = "exp-preserve-order"))]
@@ -400,6 +400,15 @@
 	this: ObjValue,
 }
 impl SupThis {
+	/// Create a `SupThis` for a freshly constructed object (no super).
+	pub fn new(this: ObjValue) -> Self {
+		Self {
+			sup: CoreIdx {
+				idx: this.0.cores.len(),
+			},
+			this,
+		}
+	}
 	pub fn has_super(&self) -> bool {
 		self.sup.super_exists()
 	}
@@ -501,11 +510,11 @@
 	// }
 	/// Returns amount of visible object fields
 	/// If object only contains hidden fields - may return zero.
-	pub fn len(&self) -> usize {
+	pub fn len(&self) -> u32 {
 		self.fields_visibility()
 			.values()
 			.filter(|d| d.visible())
-			.count()
+			.count() as u32
 	}
 	/// For each field, calls callback.
 	/// If callback returns false - ends iteration prematurely.
modifiedcrates/jrsonnet-evaluator/src/trace/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/trace/mod.rs
+++ b/crates/jrsonnet-evaluator/src/trace/mod.rs
@@ -10,7 +10,7 @@
 #[cfg(feature = "explaining-traces")]
 use jrsonnet_ir::Span;
 
-use crate::{Error, error::ErrorKind};
+use crate::{error::ErrorKind, Error};
 
 /// The way paths should be displayed
 #[derive(Clone, Trace)]
@@ -122,7 +122,7 @@
 				|| path.source_path().to_string(),
 				|r| self.resolver.resolve(r),
 			);
-			let mut offset = error.location.0 as usize;
+			let mut offset = error.location.1 as usize;
 			let is_eof = if offset >= path.code().len() {
 				offset = path.code().len().saturating_sub(1);
 				true
@@ -259,25 +259,64 @@
 		struct ResetData {
 			loc: Span,
 		}
-		use hi_doc::{Formatting, SnippetBuilder, Text, source_to_ansi};
+		use hi_doc::{source_to_ansi, Formatting, SnippetBuilder, Text};
 
 		write!(out, "{}", error.error())?;
 		if let ErrorKind::ImportSyntaxError { path, error } = error.error() {
 			writeln!(out)?;
-			let mut offset = error.location;
-			// To inclusive range
-			if offset.1 > offset.0 {
-				offset.1 -= 1;
-			}
 			let mut builder = SnippetBuilder::new(path.code());
 			builder
 				.error(Text::fragment("syntax error", Formatting::default()))
-				.range(offset.0 as usize..=offset.1 as usize)
+				.range(error.location.range())
 				.build();
 			let source = builder.build();
 			let ansi = source_to_ansi(&source);
 			write!(out, "{ansi}")?;
 		}
+		if let ErrorKind::StaticAnalysisError(diagnostics) = error.error() {
+			use crate::analyze::DiagLevel;
+			let mut builder: Option<SnippetBuilder> = None;
+			let mut current_src: Option<&str> = None;
+			let flush =
+				|builder: Option<SnippetBuilder>, out: &mut dyn std::fmt::Write| -> Result<(), std::fmt::Error> {
+					if let Some(b) = builder {
+						let ansi = source_to_ansi(&b.build());
+						write!(out, "\n{}", ansi.trim_end())?;
+					}
+					Ok(())
+				};
+			for diag in diagnostics {
+				if let Some(span) = &diag.span {
+					let src = span.0.code();
+					if current_src != Some(src) {
+						flush(builder.take(), out)?;
+						builder = Some(SnippetBuilder::new(src));
+						current_src = Some(src);
+					}
+					let b = builder.as_mut().unwrap();
+					let ab = match diag.level {
+						DiagLevel::Error => b.error(Text::fragment(
+							diag.message.clone(),
+							Formatting::default(),
+						)),
+						DiagLevel::Warning => b.warning(Text::fragment(
+							diag.message.clone(),
+							Formatting::default(),
+						)),
+					};
+					ab.range(span.range()).build();
+				} else {
+					flush(builder.take(), out)?;
+					current_src = None;
+					let prefix = match diag.level {
+						DiagLevel::Error => "error",
+						DiagLevel::Warning => "warning",
+					};
+					write!(out, "\n{prefix}: {}", diag.message)?;
+				}
+			}
+			flush(builder, out)?;
+		}
 		let trace = &error.trace();
 		let snippet_builder: RefCell<Option<SnippetBuilder>> = RefCell::new(None);
 		let mut last_location: Option<Span> = None;
modifiedcrates/jrsonnet-evaluator/src/typed/conversions.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/typed/conversions.rs
+++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs
@@ -637,7 +637,7 @@
 		}
 		<Self as Typed>::TYPE.check(&value)?;
 		// Any::downcast_ref::<ByteArray>(&a);
-		let mut out = Vec::with_capacity(a.len());
+		let mut out = Vec::with_capacity(a.len() as usize);
 		for e in a.iter() {
 			let r = e?;
 			out.push(u8::from_untyped(r)?);
modifiedcrates/jrsonnet-interner/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-interner/src/lib.rs
+++ b/crates/jrsonnet-interner/src/lib.rs
@@ -171,6 +171,7 @@
 			let mut pool = pool.borrow_mut();
 
 			if pool.remove(inner).is_none() {
+				// DOC(string-pooling)
 				// On some platforms (i.e i686-windows), try_with will not fail after TLS
 				// destructor is called, but instead re-initialize the TLS with the empty pool.
 				// Allow non-pooled Drop in this case.
modifiedcrates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-macros/src/lib.rs
+++ b/crates/jrsonnet-macros/src/lib.rs
@@ -3,13 +3,14 @@
 use proc_macro2::TokenStream;
 use quote::{quote, quote_spanned};
 use syn::{
-	Attribute, DeriveInput, Error, Expr, ExprClosure, FnArg, GenericArgument, Ident, ItemFn,
-	LitStr, Meta, Pat, Path, PathArguments, Result, ReturnType, Token, Type, parenthesized,
+	parenthesized,
 	parse::{Parse, ParseStream},
 	parse_macro_input,
 	punctuated::Punctuated,
 	spanned::Spanned,
 	token::Comma,
+	Attribute, DeriveInput, Error, Expr, ExprClosure, FnArg, GenericArgument, Ident, ItemFn,
+	LitStr, Meta, Pat, Path, PathArguments, Result, ReturnType, Token, Type,
 };
 
 use self::typed::{derive_from_untyped_inner, derive_into_untyped_inner, derive_typed_inner};
@@ -402,7 +403,7 @@
 				State, Val,
 				function::{builtin::Builtin, FunctionSignature, ParamParse, ParamName, ParamDefault, CallLocation},
 				Result, Context, typed::{Typed, FromUntyped, IntoUntypedResult},
-				parser::Span, params, Thunk,
+				Span, params, Thunk,
 			};
 			params!(
 				#(#params_desc)*
modifiedcrates/jrsonnet-stdlib/src/arrays.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/arrays.rs
+++ b/crates/jrsonnet-stdlib/src/arrays.rs
@@ -1,11 +1,12 @@
 #![allow(non_snake_case)]
 
 use jrsonnet_evaluator::{
-	Either, IStr, ObjValue, ObjValueBuilder, Result, ResultExt, Thunk, Val, bail,
-	function::{FuncVal, NativeFn, builtin},
+	bail, error,
+	function::{builtin, NativeFn},
 	runtime_error,
-	typed::{BoundedI32, BoundedUsize, Either2, FromUntyped},
-	val::{ArrValue, IndexableVal, equals},
+	typed::{BoundedUsize, Either2, FromUntyped},
+	val::{equals, ArrValue, IndexableVal},
+	Either, IStr, ObjValue, ObjValueBuilder, Result, ResultExt, Thunk, Val,
 };
 
 pub fn eval_on_empty(on_empty: Option<Thunk<Val>>) -> Result<Val> {
@@ -17,32 +18,28 @@
 }
 
 #[builtin]
-pub fn builtin_make_array(
-	// Can't use usize because range_exclusive is over i32
-	sz: BoundedI32<0, { i32::MAX }>,
-	func: FuncVal,
-) -> Result<ArrValue> {
-	if *sz == 0 {
+pub fn builtin_make_array(sz: u32, func: NativeFn!((u32,) -> Val)) -> Result<ArrValue> {
+	if sz == 0 {
 		return Ok(ArrValue::empty());
 	}
-	func.evaluate_trivial().map_or_else(
-		// TODO: Different mapped array impl avoiding allocating unnecessary vals
-		|| Ok(ArrValue::range_exclusive(0, *sz).map(FromUntyped::from_untyped(Val::Func(func))?)),
-		|trivial| {
-			#[expect(clippy::cast_sign_loss, reason = "sz is bounded to be larger than 0")]
-			let mut out = Vec::with_capacity(*sz as usize);
-			for _ in 0..*sz {
-				out.push(trivial.clone());
+	// Try eager evaluation: call func(i) immediately for each element.
+	'eager: {
+		let mut out = Vec::with_capacity(sz as usize);
+		for i in 0..sz {
+			match func.call(i) {
+				Ok(v) => out.push(v),
+				Err(_) => break 'eager,
 			}
-			Ok(ArrValue::new(out))
-		},
-	)
+		}
+		return Ok(ArrValue::new(out));
+	}
+	Ok(ArrValue::make(sz, func))
 }
 
 #[builtin]
-pub fn builtin_repeat(what: Either![IStr, ArrValue], count: usize) -> Result<Val> {
+pub fn builtin_repeat(what: Either![IStr, ArrValue], count: u32) -> Result<Val> {
 	Ok(match what {
-		Either2::A(s) => Val::string(s.repeat(count)),
+		Either2::A(s) => Val::string(s.repeat(count as usize)),
 		Either2::B(arr) => Val::Arr(
 			ArrValue::repeated(arr, count)
 				.ok_or_else(|| runtime_error!("repeated length overflow"))?,
@@ -210,14 +207,14 @@
 				let item = item?.clone();
 				if let Val::Arr(items) = item {
 					if !first {
-						out.reserve(joiner_items.len());
+						out.reserve(joiner_items.len() as usize);
 						// TODO: extend
 						for item in joiner_items.iter() {
 							out.push(item?);
 						}
 					}
 					first = false;
-					out.reserve(items.len());
+					out.reserve(items.len() as usize);
 					for item in items.iter() {
 						out.push(item?);
 					}
@@ -256,7 +253,8 @@
 pub fn builtin_lines(arr: ArrValue) -> Result<IndexableVal> {
 	builtin_join(
 		IndexableVal::Str("\n".into()),
-		ArrValue::extended(arr, ArrValue::new(vec![Val::string("")])),
+		ArrValue::extended(arr, ArrValue::new(vec![Val::string("")]))
+			.ok_or_else(|| error!("array is too large"))?,
 	)
 }
 
@@ -380,7 +378,7 @@
 	let newArrLeft = arr.clone().slice(None, Some(at), None);
 	let newArrRight = arr.slice(Some(at + 1), None, None);
 
-	Ok(ArrValue::extended(newArrLeft, newArrRight))
+	Ok(ArrValue::extended(newArrLeft, newArrRight).ok_or_else(|| error!("array is too large"))?)
 }
 
 #[builtin]
@@ -399,20 +397,22 @@
 }
 
 #[builtin]
-pub fn builtin_flatten_arrays(arrs: Vec<ArrValue>) -> ArrValue {
-	pub fn flatten_inner(values: &[ArrValue]) -> ArrValue {
+pub fn builtin_flatten_arrays(arrs: Vec<ArrValue>) -> Result<ArrValue> {
+	pub fn flatten_inner(values: &[ArrValue]) -> Result<ArrValue> {
 		if values.len() == 1 {
-			return values[0].clone();
+			return Ok(values[0].clone());
 		} else if values.len() == 2 {
-			return ArrValue::extended(values[0].clone(), values[1].clone());
+			return ArrValue::extended(values[0].clone(), values[1].clone())
+				.ok_or_else(|| error!("array is too large"));
 		}
 		let (a, b) = values.split_at(values.len() / 2);
-		ArrValue::extended(flatten_inner(a), flatten_inner(b))
+		ArrValue::extended(flatten_inner(a)?, flatten_inner(b)?)
+			.ok_or_else(|| error!("array is too large"))
 	}
 	if arrs.is_empty() {
-		return ArrValue::empty();
+		return Ok(ArrValue::empty());
 	} else if arrs.len() == 1 {
-		return arrs.into_iter().next().expect("single");
+		return Ok(arrs.into_iter().next().expect("single"));
 	}
 	flatten_inner(&arrs)
 }
modifiedcrates/jrsonnet-stdlib/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/lib.rs
+++ b/crates/jrsonnet-stdlib/src/lib.rs
@@ -12,15 +12,9 @@
 pub use encoding::*;
 pub use hash::*;
 use jrsonnet_evaluator::{
-	ContextBuilder, IStr, NumValue, ObjValue, ObjValueBuilder, Thunk, Val,
-	error::Result,
-	function::{CallLocation, FuncVal, builtin_id},
-	tla::TlaArg,
-	trace::PathResolver,
-	typed::SerializeTypedObj as _,
+	IStr, InitialContextBuilder, NumValue, ObjValue, ObjValueBuilder, Source, Thunk, Val, error::Result, function::{CallLocation, FuncVal, builtin_id}, tla::TlaArg, trace::PathResolver, typed::SerializeTypedObj as _
 };
 use jrsonnet_gcmodule::{Acyclic, Cc, Trace};
-use jrsonnet_ir::Source;
 use jrsonnet_macros::{IntoUntyped, Typed};
 pub use manifest::*;
 pub use math::*;
@@ -544,7 +538,7 @@
 	}
 }
 impl jrsonnet_evaluator::ContextInitializer for ContextInitializer {
-	fn populate(&self, source: Source, builder: &mut ContextBuilder) {
+	fn populate(&self, source: Source, builder: &mut InitialContextBuilder) {
 		let mut std = ObjValueBuilder::new();
 		std.with_super(self.stdlib_obj.clone());
 		std.field("thisFile").hide().value({
modifiedcrates/jrsonnet-stdlib/src/misc.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/misc.rs
+++ b/crates/jrsonnet-stdlib/src/misc.rs
@@ -1,22 +1,23 @@
 use std::{cell::RefCell, collections::BTreeSet};
 
 use jrsonnet_evaluator::{
-	Either, IStr, ObjValue, ObjValueBuilder, ResultExt, Thunk, Val, bail,
+	bail,
 	error::{ErrorKind::*, Result},
-	function::{CallLocation, FuncVal, builtin},
+	function::{builtin, CallLocation, FuncVal},
 	manifest::JsonFormat,
 	typed::{Either2, Either4},
-	val::{ArrValue, equals},
+	val::{equals, ArrValue},
+	Either, IStr, ObjValue, ObjValueBuilder, ResultExt, Thunk, Val,
 };
 use jrsonnet_gcmodule::Cc;
 
 use crate::Settings;
 
 #[builtin]
-pub fn builtin_length(x: Either![IStr, ArrValue, ObjValue, FuncVal]) -> usize {
+pub fn builtin_length(x: Either![IStr, ArrValue, ObjValue, FuncVal]) -> u32 {
 	use Either4::*;
 	match x {
-		A(x) => x.chars().count(),
+		A(x) => x.chars().count() as u32,
 		B(x) => x.len(),
 		C(x) => x.len(),
 		D(f) => f.params_len(),
@@ -102,7 +103,7 @@
 			} else if b.len() == a.len() {
 				return equals(&Val::Arr(a), &Val::Arr(b));
 			}
-			for (a, b) in a.iter().take(b.len()).zip(b.iter()) {
+			for (a, b) in a.iter().take(b.len() as usize).zip(b.iter()) {
 				let a = a?;
 				let b = b?;
 				if !equals(&a, &b)? {
@@ -127,7 +128,7 @@
 				return equals(&Val::Arr(a), &Val::Arr(b));
 			}
 			let a_len = a.len();
-			for (a, b) in a.iter().skip(a_len - b.len()).zip(b.iter()) {
+			for (a, b) in a.iter().skip((a_len - b.len()) as usize).zip(b.iter()) {
 				let a = a?;
 				let b = b?;
 				if !equals(&a, &b)? {
modifiedtests/tests/builtin.rsdiffbeforeafterboth
--- a/tests/tests/builtin.rs
+++ b/tests/tests/builtin.rs
@@ -1,11 +1,7 @@
 mod common;
 
 use jrsonnet_evaluator::{
-	ContextBuilder, ContextInitializer, FileImportResolver, Result, State, Thunk, Val,
-	function::{CallLocation, FuncVal, builtin, builtin::Builtin},
-	parser::Source,
-	trace::PathResolver,
-	typed::FromUntyped,
+	ContextInitializer, FileImportResolver, InitialContextBuilder, Result, Source, State, Thunk, Val, function::{CallLocation, FuncVal, builtin, builtin::{Builtin}}, trace::PathResolver, typed::FromUntyped
 };
 use jrsonnet_gcmodule::Trace;
 use jrsonnet_stdlib::ContextInitializer as StdContextInitializer;
@@ -31,7 +27,7 @@
 #[derive(Trace)]
 struct NativeAddContextInitializer;
 impl ContextInitializer for NativeAddContextInitializer {
-	fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {
+	fn populate(&self, _for_file: Source, builder: &mut InitialContextBuilder) {
 		builder.bind("nativeAdd", Thunk::evaluated(Val::function(native_add {})));
 	}
 
@@ -76,7 +72,7 @@
 #[derive(Trace)]
 struct CurryAddContextInitializer;
 impl ContextInitializer for CurryAddContextInitializer {
-	fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {
+	fn populate(&self, _for_file: Source, builder: &mut InitialContextBuilder) {
 		builder.bind("curryAdd", Thunk::evaluated(Val::function(curry_add {})));
 	}
 
modifiedtests/tests/common.rsdiffbeforeafterboth
--- a/tests/tests/common.rs
+++ b/tests/tests/common.rs
@@ -1,8 +1,5 @@
 use jrsonnet_evaluator::{
-	ContextBuilder, ContextInitializer as ContextInitializerT, ObjValueBuilder, Result, Thunk, Val,
-	bail,
-	function::{FuncVal, builtin},
-	parser::Source,
+	ContextBuilder, ContextInitializer as ContextInitializerT, InitialContextBuilder, ObjValueBuilder, Result, Thunk, Val, bail, function::{FuncVal, builtin}, Source
 };
 use jrsonnet_gcmodule::Trace;
 
@@ -68,7 +65,7 @@
 #[allow(dead_code)]
 pub struct ContextInitializer;
 impl ContextInitializerT for ContextInitializer {
-	fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {
+	fn populate(&self, _for_file: Source, builder: &mut InitialContextBuilder) {
 		let mut bobj = ObjValueBuilder::new();
 		bobj.method("assertThrow", assert_throw {});
 		bobj.method("paramNames", param_names {});
modifiedtests/tests/cpp_test_suite.rsdiffbeforeafterboth
--- a/tests/tests/cpp_test_suite.rs
+++ b/tests/tests/cpp_test_suite.rs
@@ -9,9 +9,11 @@
 	gc::WithCapacityExt as _,
 	manifest::JsonFormat,
 	rustc_hash::FxHashMap,
+	stack::limit_stack_depth,
 	tla::TlaArg,
 	trace::{CompactFormat, PathResolver, TraceFormat},
 };
+use jrsonnet_gcmodule::ObjectSpace;
 use jrsonnet_stdlib::ContextInitializer;
 mod common;
 use common::ContextInitializer as TestContextInitializer;
@@ -179,6 +181,12 @@
 			continue;
 		}
 
+		let _stack = if entry.path().file_stem().is_some_and(|e| e == "recursive_function" || e == "tailstrict"|| e == "tailstrict5") {
+			Some(limit_stack_depth(100_000))
+		} else {
+			None
+		};
+
 		if entry
 			.path()
 			.file_name()
@@ -188,7 +196,7 @@
 			continue;
 		}
 
-		println!("test: {}", entry.path().display());
+		eprintln!("test: {}", entry.path().display());
 
 		let result = run(&entry.path(), &root);
 
@@ -212,25 +220,10 @@
 		if let Some(golden_path) = read_file(&golden_override)? {
 			golden = Some(golden_path);
 		}
-
-		// ir-parser has its own override layer
-		#[cfg(feature = "ir-parser")]
-		let ir_parser_override_path = {
-			let p = root_tests
-				.join(format!("{root_dir}_golden_override_ir_parser"))
-				.join(golden_path.file_name().expect("file has basename"));
-			if let Some(golden_path) = read_file(&p)? {
-				golden = Some(golden_path);
-			}
-			p
-		};
 
 		// Otherwise assume test should just not fail and return true.
 		let golden = golden.unwrap_or_else(|| "true".to_owned());
 
-		#[cfg(feature = "ir-parser")]
-		let update_golden_path = &ir_parser_override_path;
-		#[cfg(not(feature = "ir-parser"))]
 		let update_golden_path = &golden_override;
 
 		match (serde_json::from_str::<serde_json::Value>(&result), serde_json::from_str::<serde_json::Value>(&golden)) {
@@ -270,8 +263,11 @@
 				}
 			}
 		}
+		println!("done!");
 	}
 	}
 
+	jrsonnet_gcmodule::with_thread_object_space(ObjectSpace::leak);
+
 	Ok(())
 }
modifiedtests/tests/snapshots/golden__golden@issue172.jsonnet.snapdiffbeforeafterboth
--- a/tests/tests/snapshots/golden__golden@issue172.jsonnet.snap
+++ b/tests/tests/snapshots/golden__golden@issue172.jsonnet.snap
@@ -3,7 +3,4 @@
 expression: result
 input_file: tests/golden/issue172.jsonnet
 ---
-local is not defined: b
-    issue172.jsonnet:1:45-47: local <b> access
-    issue172.jsonnet:1:4-10:  field <value> access
-    elem <0> evaluation
+static analysis errors: undefined local: b
modifiedtests/tests/snapshots/golden__golden@issue23.jsonnet.snapdiffbeforeafterboth
--- a/tests/tests/snapshots/golden__golden@issue23.jsonnet.snap
+++ b/tests/tests/snapshots/golden__golden@issue23.jsonnet.snap
@@ -4,4 +4,4 @@
 input_file: tests/golden/issue23.jsonnet
 ---
 infinite recursion detected
-    issue23.jsonnet:1:1-8: import "issue23.jsonnet"
+    issue23.jsonnet:1:1-8: import
modifiedtests/tests/snapshots/golden__golden@missing_binding.jsonnet.snapdiffbeforeafterboth
--- a/tests/tests/snapshots/golden__golden@missing_binding.jsonnet.snap
+++ b/tests/tests/snapshots/golden__golden@missing_binding.jsonnet.snap
@@ -3,6 +3,5 @@
 expression: result
 input_file: tests/golden/missing_binding.jsonnet
 ---
-local is not defined: sta
+static analysis errors: undefined local: sta
 There is a local with similar name present: std
-    missing_binding.jsonnet:1:1-5: local <sta> access