git.delta.rocks / jrsonnet / refs/commits / 6f6b79fd0e0d

difftreelog

feat array unification

kxktrsumYaroslav Bolyukin2026-04-25parent: #953b3d0.patch.diff
in: master

11 files changed

modifiedbindings/jsonnet/src/val_make.rsdiffbeforeafterboth
--- a/bindings/jsonnet/src/val_make.rs
+++ b/bindings/jsonnet/src/val_make.rs
@@ -50,7 +50,7 @@
 /// Assign elements with [`jsonnet_json_array_append`].
 #[no_mangle]
 pub extern "C" fn jsonnet_json_make_array(_vm: &VM) -> *mut Val {
-	Box::into_raw(Box::new(Val::Arr(ArrValue::eager(Vec::new()))))
+	Box::into_raw(Box::new(Val::arr(())))
 }
 
 /// Make a `JsonnetJsonValue` representing an object.
modifiedbindings/jsonnet/src/val_modify.rsdiffbeforeafterboth
--- a/bindings/jsonnet/src/val_modify.rs
+++ b/bindings/jsonnet/src/val_modify.rs
@@ -24,7 +24,7 @@
 			}
 
 			new.push(Thunk::evaluated(val.clone()));
-			*arr = Val::Arr(ArrValue::lazy(new));
+			*arr = Val::arr(new);
 		}
 		_ => panic!("should receive array"),
 	}
modifiedcrates/jrsonnet-evaluator/src/arr/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/mod.rs
+++ b/crates/jrsonnet-evaluator/src/arr/mod.rs
@@ -6,7 +6,6 @@
 };
 
 use jrsonnet_gcmodule::{Cc, cc_dyn};
-use jrsonnet_interner::IBytes;
 use jrsonnet_ir::Expr;
 
 use crate::{Context, Result, Thunk, Val, function::NativeFn, typed::IntoUntyped};
@@ -35,28 +34,17 @@
 
 impl ArrValue {
 	pub fn empty() -> Self {
-		Self::new(RangeArray::empty())
+		Self::new(())
 	}
 
 	pub fn expr(ctx: Context, exprs: Rc<Vec<Expr>>) -> Self {
 		Self::new(ExprArray::new(ctx, exprs))
-	}
-
-	pub fn lazy(thunks: Vec<Thunk<Val>>) -> Self {
-		Self::new(LazyArray(thunks))
 	}
 
-	pub fn eager(values: Vec<Val>) -> Self {
-		Self::new(EagerArray(values))
-	}
-
 	pub fn repeated(data: Self, repeats: usize) -> Option<Self> {
 		Some(Self::new(RepeatedArray::new(data, repeats)?))
 	}
 
-	pub fn bytes(bytes: IBytes) -> Self {
-		Self::new(BytesArray(bytes))
-	}
 	pub fn chars(chars: impl Iterator<Item = char>) -> Self {
 		Self::new(CharArray(chars.collect()))
 	}
@@ -83,7 +71,7 @@
 					out.push(i);
 				}
 			}
-			return Ok(Self::eager(out));
+			return Ok(Self::new(out));
 		};
 
 		let mut out = Vec::new();
@@ -92,29 +80,16 @@
 				out.push(i);
 			}
 		}
-		Ok(Self::lazy(out))
+		Ok(Self::new(out))
 	}
 
 	pub fn extended(a: Self, b: Self) -> Self {
-		// TODO: benchmark for an optimal value, currently just a arbitrary choice
-		const ARR_EXTEND_THRESHOLD: usize = 1000;
-
 		if a.is_empty() {
 			b
 		} else if b.is_empty() {
 			a
-		} else if a.len() + b.len() > ARR_EXTEND_THRESHOLD {
+		} else {
 			Self::new(ExtendedArray::new(a, b))
-		} else if let (Some(a), Some(b)) = (a.iter_cheap(), b.iter_cheap()) {
-			let mut out = Vec::with_capacity(a.len() + b.len());
-			out.extend(a);
-			out.extend(b);
-			Self::eager(out)
-		} else {
-			let mut out = Vec::with_capacity(a.len() + b.len());
-			out.extend(a.iter_lazy());
-			out.extend(b.iter_lazy());
-			Self::lazy(out)
 		}
 	}
 
@@ -165,19 +140,15 @@
 		self.0.is_empty()
 	}
 
+	pub fn is_cheap(&self) -> bool {
+		self.0.is_cheap()
+	}
+
 	/// 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>> {
 		self.0.get(index)
-	}
-
-	/// Returns None if get is either non cheap, or out of bounds
-	/// Note that non-cheap access includes errorable values
-	///
-	/// Prefer it to `get_lazy`, but use `get` when you can.
-	fn get_cheap(&self, index: usize) -> Option<Val> {
-		self.0.get_cheap(index)
 	}
 
 	/// Get array element by index, without evaluation.
@@ -196,15 +167,6 @@
 		(0..self.len()).map(|i| self.get_lazy(i).expect("length checked"))
 	}
 
-	/// Prefer it over `iter_lazy`, but do not use it where `iter` will do.
-	pub fn iter_cheap(&self) -> Option<impl ArrayLikeIter<Val> + '_> {
-		if self.is_cheap() {
-			Some((0..self.len()).map(|i| self.get_cheap(i).expect("length and is_cheap checked")))
-		} else {
-			None
-		}
-	}
-
 	/// Return a reversed view on current array.
 	#[must_use]
 	pub fn reversed(self) -> Self {
@@ -213,50 +175,25 @@
 
 	pub fn ptr_eq(a: &Self, b: &Self) -> bool {
 		Cc::ptr_eq(&a.0, &b.0)
-	}
-
-	/// Is this vec supports `.get_cheap()?`
-	pub fn is_cheap(&self) -> bool {
-		self.0.is_cheap()
 	}
 
 	pub fn as_any(&self) -> &dyn Any {
 		&self.0
 	}
 }
-impl From<Vec<Val>> for ArrValue {
-	fn from(value: Vec<Val>) -> Self {
-		Self::eager(value)
-	}
-}
-impl From<Vec<Thunk<Val>>> for ArrValue {
-	fn from(value: Vec<Thunk<Val>>) -> Self {
-		Self::lazy(value)
-	}
-}
-impl FromIterator<Val> for ArrValue {
-	fn from_iter<T: IntoIterator<Item = Val>>(iter: T) -> Self {
-		Self::eager(iter.into_iter().collect())
+impl<T> From<T> for ArrValue
+where
+	T: ArrayLike,
+{
+	fn from(value: T) -> Self {
+		Self::new(value)
 	}
 }
-impl ArrayLike for ArrValue {
-	fn len(&self) -> usize {
-		self.0.len()
-	}
-
-	fn get(&self, index: usize) -> Result<Option<Val>> {
-		self.0.get(index)
-	}
-
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
-		self.0.get_lazy(index)
-	}
-
-	fn get_cheap(&self, index: usize) -> Option<Val> {
-		self.0.get_cheap(index)
-	}
-
-	fn is_cheap(&self) -> bool {
-		self.0.is_cheap()
+impl<I> FromIterator<I> for ArrValue
+where
+	Vec<I>: ArrayLike,
+{
+	fn from_iter<T: IntoIterator<Item = I>>(iter: T) -> Self {
+		Self::new(iter.into_iter().collect::<Vec<_>>())
 	}
 }
modifiedcrates/jrsonnet-evaluator/src/arr/spec.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/spec.rs
+++ b/crates/jrsonnet-evaluator/src/arr/spec.rs
@@ -1,4 +1,10 @@
-use std::{any::Any, cell::RefCell, fmt::Debug, mem::replace, rc::Rc};
+use std::{
+	any::Any,
+	cell::RefCell,
+	fmt::{self, Debug},
+	mem::replace,
+	rc::Rc,
+};
 
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::{IBytes, IStr};
@@ -21,11 +27,45 @@
 	}
 	fn get(&self, index: usize) -> Result<Option<Val>>;
 	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>>;
-	fn get_cheap(&self, index: usize) -> Option<Val>;
 
-	fn is_cheap(&self) -> bool;
+	fn is_cheap(&self) -> bool {
+		false
+	}
+}
+trait ArrayCheap {
+	fn get(&self, index: usize) -> Option<Val>;
+	fn len(&self) -> usize;
 }
+impl<T> ArrayLike for T
+where
+	T: Any + Trace + Debug + ArrayCheap,
+{
+	fn len(&self) -> usize {
+		<T as ArrayCheap>::len(self)
+	}
+
+	fn get(&self, index: usize) -> Result<Option<Val>> {
+		Ok(<T as ArrayCheap>::get(self, index))
+	}
 
+	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
+		<T as ArrayCheap>::get(self, index).map(Thunk::evaluated)
+	}
+
+	fn is_cheap(&self) -> bool {
+		true
+	}
+}
+
+impl ArrayCheap for () {
+	fn len(&self) -> usize {
+		0
+	}
+	fn get(&self, _index: usize) -> Option<Val> {
+		None
+	}
+}
+
 #[derive(Debug, Trace)]
 pub struct SliceArray {
 	pub(crate) inner: ArrValue,
@@ -52,9 +92,6 @@
 		self.inner.get_lazy(self.map_idx(index))
 	}
 
-	fn get_cheap(&self, index: usize) -> Option<Val> {
-		self.inner.get_cheap(self.map_idx(index))
-	}
 	fn is_cheap(&self) -> bool {
 		self.inner.is_cheap()
 	}
@@ -62,47 +99,21 @@
 
 #[derive(Trace, Debug)]
 pub struct CharArray(pub Vec<char>);
-impl ArrayLike for CharArray {
+impl ArrayCheap for CharArray {
 	fn len(&self) -> usize {
-		self.0.len()
+		self.0.as_slice().len()
 	}
-
-	fn get(&self, index: usize) -> Result<Option<Val>> {
-		Ok(self.get_cheap(index))
-	}
-
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
-		self.get_cheap(index).map(Thunk::evaluated)
-	}
-
-	fn get_cheap(&self, index: usize) -> Option<Val> {
-		self.0.get(index).map(|v| Val::string(*v))
-	}
-	fn is_cheap(&self) -> bool {
-		true
+	fn get(&self, index: usize) -> Option<Val> {
+		self.0.as_slice().get(index).map(|v| Val::string(*v))
 	}
 }
 
-#[derive(Trace, Debug)]
-pub struct BytesArray(pub IBytes);
-impl ArrayLike for BytesArray {
+impl ArrayCheap for IBytes {
 	fn len(&self) -> usize {
-		self.0.len()
-	}
-
-	fn get(&self, index: usize) -> Result<Option<Val>> {
-		Ok(self.get_cheap(index))
-	}
-
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
-		self.get_cheap(index).map(Thunk::evaluated)
-	}
-
-	fn get_cheap(&self, index: usize) -> Option<Val> {
-		self.0.get(index).map(|v| Val::Num((*v).into()))
+		self.as_slice().len()
 	}
-	fn is_cheap(&self) -> bool {
-		true
+	fn get(&self, index: usize) -> Option<Val> {
+		self.as_slice().get(index).map(|v| Val::Num((*v).into()))
 	}
 }
 
@@ -190,9 +201,6 @@
 			expr: self.clone(),
 			index,
 		}))
-	}
-	fn get_cheap(&self, _index: usize) -> Option<Val> {
-		None
 	}
 	fn is_cheap(&self) -> bool {
 		false
@@ -275,61 +283,34 @@
 		self.len
 	}
 
-	fn get_cheap(&self, index: usize) -> Option<Val> {
-		if self.split > index {
-			self.a.get_cheap(index)
-		} else {
-			self.b.get_cheap(index - self.split)
-		}
-	}
 	fn is_cheap(&self) -> bool {
 		self.a.is_cheap() && self.b.is_cheap()
 	}
 }
 
-#[derive(Trace, Debug)]
-pub struct LazyArray(pub Vec<Thunk<Val>>);
-impl ArrayLike for LazyArray {
+impl<T> ArrayLike for Vec<T>
+where
+	T: IntoUntyped + Trace + fmt::Debug,
+	for<'a> &'a T: IntoUntyped,
+{
 	fn len(&self) -> usize {
-		self.0.len()
+		self.as_slice().len()
 	}
+
 	fn get(&self, index: usize) -> Result<Option<Val>> {
-		let Some(v) = self.0.get(index) else {
+		let Some(elem) = self.as_slice().get(index) else {
 			return Ok(None);
 		};
-		v.evaluate().map(Some)
-	}
-	fn get_cheap(&self, _index: usize) -> Option<Val> {
-		None
-	}
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
-		self.0.get(index).cloned()
-	}
-	fn is_cheap(&self) -> bool {
-		false
-	}
-}
-
-#[derive(Trace, Debug)]
-pub struct EagerArray(pub Vec<Val>);
-impl ArrayLike for EagerArray {
-	fn len(&self) -> usize {
-		self.0.len()
+		IntoUntyped::into_untyped(elem).map(Some)
 	}
 
-	fn get(&self, index: usize) -> Result<Option<Val>> {
-		Ok(self.0.get(index).cloned())
-	}
-
 	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
-		self.0.get(index).cloned().map(Thunk::evaluated)
+		let elem = self.as_slice().get(index)?;
+		Some(IntoUntyped::into_lazy_untyped(elem))
 	}
 
-	fn get_cheap(&self, index: usize) -> Option<Val> {
-		self.0.get(index).cloned()
-	}
 	fn is_cheap(&self) -> bool {
-		true
+		!T::provides_lazy()
 	}
 }
 
@@ -363,28 +344,12 @@
 		WithExactSize(self.start..=self.end, self.size())
 	}
 }
-
-impl ArrayLike for RangeArray {
-	fn len(&self) -> usize {
-		self.size()
-	}
-	fn is_empty(&self) -> bool {
-		self.size() == 0
-	}
-
-	fn get(&self, index: usize) -> Result<Option<Val>> {
-		Ok(self.get_cheap(index))
-	}
-
-	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
-		self.get_cheap(index).map(Thunk::evaluated)
-	}
-
-	fn get_cheap(&self, index: usize) -> Option<Val> {
+impl ArrayCheap for RangeArray {
+	fn get(&self, index: usize) -> Option<Val> {
 		self.range().nth(index).map(|i| Val::Num(i.into()))
 	}
-	fn is_cheap(&self) -> bool {
-		true
+	fn len(&self) -> usize {
+		self.size()
 	}
 }
 
@@ -403,9 +368,6 @@
 		self.0.get_lazy(self.0.len() - index - 1)
 	}
 
-	fn get_cheap(&self, index: usize) -> Option<Val> {
-		self.0.get_cheap(self.0.len() - index - 1)
-	}
 	fn is_cheap(&self) -> bool {
 		self.0.is_cheap()
 	}
@@ -509,13 +471,6 @@
 			arr: self.clone(),
 			index,
 		}))
-	}
-
-	fn get_cheap(&self, _index: usize) -> Option<Val> {
-		None
-	}
-	fn is_cheap(&self) -> bool {
-		false
 	}
 }
 
@@ -534,6 +489,12 @@
 			total_len,
 		})
 	}
+	fn map_idx(&self, index: usize) -> Option<usize> {
+		if index > self.total_len {
+			return None;
+		}
+		Some(index % self.data.len())
+	}
 }
 
 impl ArrayLike for RepeatedArray {
@@ -542,25 +503,17 @@
 	}
 
 	fn get(&self, index: usize) -> Result<Option<Val>> {
-		if index > self.total_len {
+		let Some(idx) = self.map_idx(index) else {
 			return Ok(None);
-		}
-		self.data.get(index % self.data.len())
+		};
+		self.data.get(idx)
 	}
 
 	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
-		if index > self.total_len {
-			return None;
-		}
-		self.data.get_lazy(index % self.data.len())
+		let idx = self.map_idx(index)?;
+		self.data.get_lazy(idx)
 	}
 
-	fn get_cheap(&self, index: usize) -> Option<Val> {
-		if index > self.total_len {
-			return None;
-		}
-		self.data.get_cheap(index % self.data.len())
-	}
 	fn is_cheap(&self) -> bool {
 		self.data.is_cheap()
 	}
@@ -584,21 +537,17 @@
 	}
 
 	fn get(&self, index: usize) -> Result<Option<Val>> {
-		let Some(key) = self.keys.get(index) else {
+		let Some(key) = self.keys.as_slice().get(index) 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.get(index)?;
+		let key = self.keys.as_slice().get(index)?;
 		Some(self.obj.get_lazy_or_bail(key.clone()))
 	}
 
-	fn get_cheap(&self, _index: usize) -> Option<Val> {
-		None
-	}
-
 	fn is_cheap(&self) -> bool {
 		false
 	}
@@ -628,7 +577,7 @@
 	}
 
 	fn get(&self, index: usize) -> Result<Option<Val>> {
-		let Some(key) = self.keys.get(index) else {
+		let Some(key) = self.keys.as_slice().get(index) else {
 			return Ok(None);
 		};
 		Ok(Some(
@@ -641,7 +590,7 @@
 	}
 
 	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
-		let key = self.keys.get(index)?;
+		let key = self.keys.as_slice().get(index)?;
 		// Nothing can fail in the key part, yet value is still
 		// lazy-evaluated
 		Some(Thunk::evaluated(
@@ -651,10 +600,6 @@
 			})
 			.expect("convertible"),
 		))
-	}
-
-	fn get_cheap(&self, _index: usize) -> Option<Val> {
-		None
 	}
 
 	fn is_cheap(&self) -> bool {
modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/evaluate/mod.rs
1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_ir::{6	ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, ExprParams, FieldMember,7	FieldName, ForSpecData, IfSpecData, ImportKind, LiteralType, ObjBody, ObjMembers, Spanned,8	function::ParamName,9};10use jrsonnet_types::ValType;11use rustc_hash::FxHashMap;1213use self::destructure::destruct;14use crate::{15	Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result, ResultExt,16	SupThis, Unbound, Val,17	arr::ArrValue,18	bail,19	destructure::evaluate_dest,20	error::{ErrorKind::*, suggest_object_fields},21	evaluate::operator::{evaluate_binary_op_special, evaluate_unary_op},22	function::{CallLocation, FuncDesc, FuncVal},23	gc::WithCapacityExt as _,24	in_frame,25	typed::{FromUntyped, IntoUntyped as _, Typed},26	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},27	with_state,28};29pub mod destructure;30pub mod operator;3132// This is the amount of bytes that need to be left on the stack before increasing the size.33// It must be at least as large as the stack required by any code that does not call34// `ensure_sufficient_stack`.35const RED_ZONE: usize = 100 * 1024; // 100k3637// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then38// on. This flag has performance relevant characteristics. Don't set it too high.39const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB4041/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations42/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit43/// from this.44///45/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.46#[inline]47pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {48	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)49}5051pub fn evaluate_trivial(expr: &Expr) -> Option<Val> {52	fn is_trivial(expr: &Expr) -> bool {53		match expr {54			Expr::Str(_)55			| Expr::Num(_)56			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,57			Expr::Arr(a) => a.iter().all(is_trivial),58			_ => false,59		}60	}61	Some(match expr {62		Expr::Str(s) => Val::string(s.clone()),63		Expr::Num(n) => {64			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))65		}66		Expr::Literal(LiteralType::False) => Val::Bool(false),67		Expr::Literal(LiteralType::True) => Val::Bool(true),68		Expr::Literal(LiteralType::Null) => Val::Null,69		Expr::Arr(n) => {70			if n.iter().any(|e| !is_trivial(e)) {71				return None;72			}73			Val::Arr(ArrValue::eager(74				n.iter()75					.map(evaluate_trivial)76					.map(|e| e.expect("checked trivial"))77					.collect(),78			))79		}80		_ => return None,81	})82}8384pub fn evaluate_method(ctx: Context, name: IStr, params: ExprParams, body: Rc<Expr>) -> Val {85	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {86		name,87		ctx,88		params,89		body,90	})))91}9293pub fn evaluate_field_name(ctx: Context, field_name: &Spanned<FieldName>) -> Result<Option<IStr>> {94	Ok(match &field_name.value {95		FieldName::Fixed(n) => Some(n.clone()),96		FieldName::Dyn(expr) => in_frame(97			CallLocation::new(&field_name.span),98			|| "evaluating field name".to_string(),99			|| {100				let v = evaluate(ctx, expr)?;101				Ok(if matches!(v, Val::Null) {102					None103				} else {104					Some(IStr::from_untyped(v)?)105				})106			},107		)?,108	})109}110111pub fn evaluate_comp(112	ctx: Context,113	specs: &[CompSpec],114	callback: &mut impl FnMut(Context) -> Result<()>,115) -> Result<()> {116	match specs.first() {117		None => callback(ctx)?,118		Some(CompSpec::IfSpec(IfSpecData { cond, span: _ })) => {119			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {120				evaluate_comp(ctx, &specs[1..], callback)?;121			}122		}123		Some(CompSpec::ForSpec(ForSpecData {124			destruct: into,125			over,126		})) => {127			match evaluate(ctx.clone(), over)? {128				Val::Arr(list) => {129					for item in list.iter_lazy() {130						let fctx = Pending::new();131						let mut new_bindings = FxHashMap::with_capacity(into.binds_len());132						destruct(into, item, fctx.clone(), &mut new_bindings)?;133						let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);134135						evaluate_comp(ctx, &specs[1..], callback)?;136					}137				}138				#[cfg(feature = "exp-object-iteration")]139				Val::Obj(obj) => {140					for field in obj.fields(141						// TODO: Should there be ability to preserve iteration order?142						#[cfg(feature = "exp-preserve-order")]143						false,144					) {145						let fctx = Pending::new();146						let mut new_bindings = FxHashMap::with_capacity(into.binds_len());147						let obj = obj.clone();148						let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![149							Thunk::evaluated(Val::string(field.clone())),150							Thunk!(move || obj.get(field).transpose().expect(151								"field exists, as field name was obtained from object.fields()",152							)),153						])));154						destruct(into, value, fctx.clone(), &mut new_bindings)?;155						let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);156157						evaluate_comp(ctx, &specs[1..], callback)?;158					}159				}160				_ => bail!(InComprehensionCanOnlyIterateOverArray),161			}162		}163	}164	Ok(())165}166167trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}168impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}169170fn evaluate_object_locals(171	fctx: Context,172	locals: Rc<Vec<BindSpec>>,173) -> impl CloneableUnbound<Context> {174	#[derive(Trace, Clone)]175	struct UnboundLocals {176		fctx: Context,177		locals: Rc<Vec<BindSpec>>,178	}179	impl Unbound for UnboundLocals {180		type Bound = Context;181182		fn bind(&self, sup_this: SupThis) -> Result<Context> {183			let fctx = Context::new_future();184			let mut new_bindings =185				FxHashMap::with_capacity(self.locals.iter().map(BindSpec::binds_len).sum());186			for b in self.locals.iter() {187				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;188			}189190			let ctx = self.fctx.clone();191192			let ctx = ctx193				.extend_bindings_sup_this(new_bindings, sup_this)194				.into_future(fctx);195196			Ok(ctx)197		}198	}199200	UnboundLocals { fctx, locals }201}202203pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(204	builder: &mut ObjValueBuilder,205	ctx: Context,206	uctx: B,207	field: &FieldMember,208) -> Result<()> {209	let name = evaluate_field_name(ctx, &field.name)?;210	let Some(name) = name else {211		return Ok(());212	};213214	match field {215		FieldMember {216			plus,217			params: None,218			visibility,219			value,220			..221		} => {222			#[derive(Trace)]223			struct UnboundValue<B: Trace> {224				uctx: B,225				value: Rc<Expr>,226				name: IStr,227			}228			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {229				type Bound = Val;230				fn bind(&self, sup_this: SupThis) -> Result<Val> {231					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())232				}233			}234235			builder236				.field(name.clone())237				.with_add(*plus)238				.with_visibility(*visibility)239				.with_location(field.name.span.clone())240				.bindable(UnboundValue {241					uctx,242					value: value.clone(),243					name,244				})?;245		}246		FieldMember {247			params: Some(params),248			visibility,249			value,250			..251		} => {252			#[derive(Trace)]253			struct UnboundMethod<B: Trace> {254				uctx: B,255				value: Rc<Expr>,256				params: ExprParams,257				name: IStr,258			}259			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {260				type Bound = Val;261				fn bind(&self, sup_this: SupThis) -> Result<Val> {262					Ok(evaluate_method(263						self.uctx.bind(sup_this)?,264						self.name.clone(),265						self.params.clone(),266						self.value.clone(),267					))268				}269			}270271			builder272				.field(name.clone())273				.with_visibility(*visibility)274				// .with_location(value.span())275				.bindable(UnboundMethod {276					uctx,277					value: value.clone(),278					params: params.clone(),279					name,280				})?;281		}282	}283	Ok(())284}285286#[derive(Trace, Clone)]287struct DirectUnbound(Context);288impl Unbound for DirectUnbound {289	type Bound = Context;290	fn bind(&self, sup_this: SupThis) -> Result<Context> {291		Ok(self292			.0293			.clone()294			.extend_bindings_sup_this(FxHashMap::new(), sup_this))295	}296}297298#[allow(clippy::too_many_lines)]299pub fn evaluate_member_list_object(300	super_obj: Option<ObjValue>,301	ctx: Context,302	members: &ObjMembers,303) -> Result<ObjValue> {304	#[derive(Trace)]305	struct ObjectAssert<B: Trace> {306		uctx: B,307		asserts: Rc<Vec<AssertStmt>>,308	}309	impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {310		fn run(&self, sup_this: SupThis) -> Result<()> {311			let ctx = self.uctx.bind(sup_this)?;312			for assert in &*self.asserts {313				evaluate_assert(ctx.clone(), assert)?;314			}315			Ok(())316		}317	}318319	let mut builder = ObjValueBuilder::new();320	if let Some(super_obj) = super_obj {321		builder.with_super(super_obj);322	}323324	if members.locals.is_empty() {325		// We can use the same context for all field evaluation, it doesn't depends on locals, only on this/super326		let uctx = DirectUnbound(ctx.clone());327		for field in &members.fields {328			evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;329		}330		if !members.asserts.is_empty() {331			builder.assert(ObjectAssert {332				uctx,333				asserts: members.asserts.clone(),334			});335		}336	} else {337		let locals = members.locals.clone();338		// We have single context for all fields, so we can cache them together339		let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));340		for field in &members.fields {341			evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;342		}343		if !members.asserts.is_empty() {344			builder.assert(ObjectAssert {345				uctx,346				asserts: members.asserts.clone(),347			});348		}349	}350351	Ok(builder.build())352}353354pub fn evaluate_object(355	super_obj: Option<ObjValue>,356	ctx: Context,357	object: &ObjBody,358) -> Result<ObjValue> {359	Ok(match object {360		ObjBody::MemberList(members) => evaluate_member_list_object(super_obj, ctx, members)?,361		ObjBody::ObjComp(obj) => {362			let mut builder = ObjValueBuilder::new();363			if let Some(super_obj) = super_obj {364				builder.with_super(super_obj);365			}366			let locals = obj.locals.clone();367			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {368				let uctx = evaluate_object_locals(ctx.clone(), locals.clone());369370				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)371			})?;372373			builder.build()374		}375	})376}377378pub fn evaluate_apply(379	ctx: Context,380	value: &Expr,381	args: &ArgsDesc,382	loc: CallLocation<'_>,383	tailstrict: bool,384) -> Result<Val> {385	let value = evaluate(ctx.clone(), value)?;386	Ok(match value {387		Val::Func(f) => {388			let body = || f.evaluate(ctx, loc, args, tailstrict);389			if tailstrict {390				body()?391			} else {392				in_frame(loc, || format!("function <{}> call", f.name()), body)?393			}394		}395		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),396	})397}398399pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {400	let value = &assertion.0;401	let msg = &assertion.1;402	let assertion_result = in_frame(403		CallLocation::new(&value.span),404		|| "assertion condition".to_owned(),405		|| bool::from_untyped(evaluate(ctx.clone(), value)?),406	)?;407	if !assertion_result {408		in_frame(409			CallLocation::new(&value.span),410			|| "assertion failure".to_owned(),411			|| {412				if let Some(msg) = msg {413					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));414				}415				bail!(AssertionFailed(Val::Null.to_string()?));416			},417		)?;418	}419	Ok(())420}421422pub fn evaluate_named_param(ctx: Context, expr: &Expr, name: ParamName) -> Result<Val> {423	match name {424		ParamName::Named(name) => evaluate_named(ctx, expr, name),425		ParamName::Unnamed => evaluate(ctx, expr),426	}427}428429pub fn evaluate_named(ctx: Context, expr: &Expr, name: IStr) -> Result<Val> {430	use Expr::*;431	Ok(match expr {432		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),433		_ => evaluate(ctx, expr)?,434	})435}436437#[allow(clippy::too_many_lines)]438pub fn evaluate(ctx: Context, expr: &Expr) -> Result<Val> {439	use Expr::*;440441	Ok(match expr {442		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),443		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),444		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),445		Literal(LiteralType::True) => Val::Bool(true),446		Literal(LiteralType::False) => Val::Bool(false),447		Literal(LiteralType::Null) => Val::Null,448		Str(v) => Val::string(v.clone()),449		Num(v) => Val::try_num(*v)?,450		// I have tried to remove special behavior from super by implementing standalone-super451		// expresion, but looks like this case still needs special treatment.452		//453		// Note that other jsonnet implementations will fail on `if value in (super)` expression,454		// because the standalone super literal is not supported, that is because in other455		// implementations `in super` treated differently from `in smth_else`.456		BinaryOp(bin)457			if matches!(&bin.rhs, Expr::Literal(LiteralType::Super))458				&& bin.op == BinaryOpType::In =>459		{460			let sup_this = ctx.try_sup_this()?;461			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.462			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.463			if !sup_this.has_super() {464				return Ok(Val::Bool(false));465			}466			let field = evaluate(ctx, &bin.lhs)?;467			Val::Bool(sup_this.field_in_super(field.to_string()?))468		}469		BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?,470		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,471		Var(name) => in_frame(472			CallLocation::new(&name.span),473			|| format!("local <{}> access", &**name),474			|| ctx.binding((**name).clone())?.evaluate(),475		)?,476		Index { indexable, parts } => ensure_sufficient_stack(|| {477			let mut parts = parts.iter();478			let mut indexable = if matches!(&**indexable, Expr::Literal(LiteralType::Super)) {479				let part = parts.next().expect("at least part should exist");480				// sup_this existence check might also be skipped here for null-coalesce...481				// But I believe this might cause errors.482				let sup_this = ctx.try_sup_this()?;483				if !sup_this.has_super() {484					#[cfg(feature = "exp-null-coaelse")]485					if part.null_coaelse {486						return Ok(Val::Null);487					}488					bail!(NoSuperFound)489				}490				let name = evaluate(ctx.clone(), &part.value)?;491492				let Val::Str(name) = name else {493					bail!(ValueIndexMustBeTypeGot(494						ValType::Obj,495						ValType::Str,496						name.value_type(),497					))498				};499500				let name = name.into_flat();501				match sup_this502					.get_super(name.clone())503					.with_description_src(&part.span, || format!("field <{name}> access"))?504				{505					Some(v) => v,506					#[cfg(feature = "exp-null-coaelse")]507					None if part.null_coaelse => return Ok(Val::Null),508					None => {509						let suggestions = suggest_object_fields(510							&sup_this.standalone_super().expect("super exists"),511							name.clone(),512						);513514						bail!(NoSuchField(name, suggestions))515					}516				}517			} else {518				evaluate(ctx.clone(), indexable)?519			};520521			for part in parts {522				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {523					(Val::Obj(v), Val::Str(key)) => match v524						.get(key.clone().into_flat())525						.with_description_src(&part.span, || format!("field <{key}> access"))?526					{527						Some(v) => v,528						#[cfg(feature = "exp-null-coaelse")]529						None if part.null_coaelse => return Ok(Val::Null),530						None => {531							let suggestions = suggest_object_fields(&v, key.clone().into_flat());532533							return Err(Error::from(NoSuchField(534								key.clone().into_flat(),535								suggestions,536							)))537							.with_description_src(&part.span, || format!("field <{key}> access"));538						}539					},540					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(541						ValType::Obj,542						ValType::Str,543						n.value_type(),544					)),545					(Val::Arr(v), Val::Num(n)) => {546						let n = n.get();547						if n.fract() > f64::EPSILON {548							bail!(FractionalIndex)549						}550						if n < 0.0 {551							#[expect(552								clippy::cast_possible_truncation,553								reason = "it would be truncated anyway"554							)]555							let n = n as isize;556							bail!(ArrayBoundsError(n, v.len()));557						}558						#[expect(559							clippy::cast_possible_truncation,560							clippy::cast_sign_loss,561							reason = "n is checked postive"562						)]563						v.get(n as usize)?564							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?565					}566					(Val::Arr(_), Val::Str(n)) => {567						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))568					}569					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(570						ValType::Arr,571						ValType::Num,572						n.value_type(),573					)),574575					(Val::Str(s), Val::Num(n)) => Val::Str({576						let n = n.get();577						if n.fract() > f64::EPSILON {578							bail!(FractionalIndex)579						}580						if n < 0.0 {581							#[expect(582								clippy::cast_possible_truncation,583								reason = "it would be truncated anyway"584							)]585							let n = n as isize;586							bail!(ArrayBoundsError(n, s.into_flat().chars().count()));587						}588						#[expect(589							clippy::cast_sign_loss,590							clippy::cast_possible_truncation,591							reason = "n is positive, overflow will truncate as expected"592						)]593						let n = n as usize;594						let v: IStr = s595							.clone()596							.into_flat()597							.chars()598							.skip(n)599							.take(1)600							.collect::<String>()601							.into();602						if v.is_empty() {603							bail!(StringBoundsError(n, s.into_flat().chars().count()))604						}605						StrValue::Flat(v)606					}),607					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(608						ValType::Str,609						ValType::Num,610						n.value_type(),611					)),612					#[cfg(feature = "exp-null-coaelse")]613					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),614					(v, _) => bail!(CantIndexInto(v.value_type())),615				};616			}617			Ok(indexable)618		})?,619		LocalExpr(bindings, returned) => {620			let mut new_bindings: FxHashMap<IStr, Thunk<Val>> =621				FxHashMap::with_capacity(bindings.iter().map(BindSpec::binds_len).sum());622			let fctx = Context::new_future();623			for b in bindings {624				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;625			}626			let ctx = ctx.extend_bindings(new_bindings).into_future(fctx);627			evaluate(ctx, returned)?628		}629		Arr(items) => {630			if items.is_empty() {631				Val::Arr(ArrValue::empty())632			} else {633				Val::Arr(ArrValue::expr(ctx, items.clone()))634			}635		}636		ArrComp(expr, comp_specs) => {637			let mut out = Vec::new();638			evaluate_comp(ctx, comp_specs, &mut |ctx| {639				let expr = expr.clone();640				out.push(Thunk!(move || evaluate(ctx, &expr)));641				Ok(())642			})?;643			Val::Arr(ArrValue::lazy(out))644		}645		Obj(body) => Val::Obj(evaluate_object(None, ctx, body)?),646		ObjExtend(a, b) => {647			let base = evaluate(ctx.clone(), a)?;648			match base {649				Val::Obj(base_obj) => Val::Obj(evaluate_object(Some(base_obj), ctx, b)?),650				_ => bail!("ObjExtend lhs should be an object value"),651			}652		}653		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {654			evaluate_apply(ctx, value, args, CallLocation::new(&args.span), *tailstrict)655		})?,656		Function(params, body) => {657			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())658		}659		AssertExpr(assert) => {660			evaluate_assert(ctx.clone(), &assert.assert)?;661			evaluate(ctx, &assert.rest)?662		}663		ErrorStmt(s, e) => in_frame(664			CallLocation::new(s),665			|| "error statement".to_owned(),666			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),667		)?,668		IfElse(if_else) => {669			if in_frame(670				CallLocation::new(&if_else.cond.span),671				|| "if condition".to_owned(),672				|| bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.cond)?),673			)? {674				evaluate(ctx, &if_else.cond_then)?675			} else {676				match &if_else.cond_else {677					Some(v) => evaluate(ctx, v)?,678					None => Val::Null,679				}680			}681		}682		Slice(slice) => {683			fn parse_idx<T: Typed + FromUntyped>(684				ctx: Context,685				expr: Option<&Spanned<Expr>>,686				desc: &'static str,687			) -> Result<Option<T>> {688				if let Some(value) = expr {689					Ok(in_frame(690						CallLocation::new(&value.span),691						|| format!("slice {desc}"),692						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),693					)?)694				} else {695					Ok(None)696				}697			}698699			let indexable = evaluate(ctx.clone(), &slice.value)?;700701			let start = parse_idx(ctx.clone(), slice.slice.start.as_ref(), "start")?;702			let end = parse_idx(ctx.clone(), slice.slice.end.as_ref(), "end")?;703			let step = parse_idx(ctx, slice.slice.step.as_ref(), "step")?;704705			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?706		}707		Import(kind, path) => {708			let Expr::Str(path) = &**path else {709				bail!("computed imports are not supported")710			};711			with_state(|s| {712				let span = &kind.span;713				let resolved_path = s.resolve_from(span.0.source_path(), path)?;714				Ok(match &**kind {715					ImportKind::Normal => in_frame(716						CallLocation::new(span),717						|| format!("import {:?}", path.clone()),718						|| s.import_resolved(resolved_path),719					)?,720					ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?),721					ImportKind::Bin => {722						Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?))723					}724				}) as Result<Val>725			})?726		}727	})728}
after · crates/jrsonnet-evaluator/src/evaluate/mod.rs
1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_ir::{6	ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, ExprParams, FieldMember,7	FieldName, ForSpecData, IfSpecData, ImportKind, LiteralType, ObjBody, ObjMembers, Spanned,8	function::ParamName,9};10use jrsonnet_types::ValType;11use rustc_hash::FxHashMap;1213use self::destructure::destruct;14use crate::{15	Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result, ResultExt,16	SupThis, Unbound, Val,17	arr::ArrValue,18	bail,19	destructure::evaluate_dest,20	error::{ErrorKind::*, suggest_object_fields},21	evaluate::operator::{evaluate_binary_op_special, evaluate_unary_op},22	function::{CallLocation, FuncDesc, FuncVal},23	gc::WithCapacityExt as _,24	in_frame,25	typed::{FromUntyped, IntoUntyped as _, Typed},26	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},27	with_state,28};29pub mod destructure;30pub mod operator;3132// This is the amount of bytes that need to be left on the stack before increasing the size.33// It must be at least as large as the stack required by any code that does not call34// `ensure_sufficient_stack`.35const RED_ZONE: usize = 100 * 1024; // 100k3637// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then38// on. This flag has performance relevant characteristics. Don't set it too high.39const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB4041/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations42/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit43/// from this.44///45/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.46#[inline]47pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {48	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)49}5051pub fn evaluate_trivial(expr: &Expr) -> Option<Val> {52	fn is_trivial(expr: &Expr) -> bool {53		match expr {54			Expr::Str(_)55			| Expr::Num(_)56			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,57			Expr::Arr(a) => a.iter().all(is_trivial),58			_ => false,59		}60	}61	Some(match expr {62		Expr::Str(s) => Val::string(s.clone()),63		Expr::Num(n) => {64			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))65		}66		Expr::Literal(LiteralType::False) => Val::Bool(false),67		Expr::Literal(LiteralType::True) => Val::Bool(true),68		Expr::Literal(LiteralType::Null) => Val::Null,69		Expr::Arr(n) => {70			if n.iter().any(|e| !is_trivial(e)) {71				return None;72			}73			Val::Arr(74				n.iter()75					.map(evaluate_trivial)76					.map(|e| e.expect("checked trivial"))77					.collect(),78			)79		}80		_ => return None,81	})82}8384pub fn evaluate_method(ctx: Context, name: IStr, params: ExprParams, body: Rc<Expr>) -> Val {85	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {86		name,87		ctx,88		params,89		body,90	})))91}9293pub fn evaluate_field_name(ctx: Context, field_name: &Spanned<FieldName>) -> Result<Option<IStr>> {94	Ok(match &field_name.value {95		FieldName::Fixed(n) => Some(n.clone()),96		FieldName::Dyn(expr) => in_frame(97			CallLocation::new(&field_name.span),98			|| "evaluating field name".to_string(),99			|| {100				let v = evaluate(ctx, expr)?;101				Ok(if matches!(v, Val::Null) {102					None103				} else {104					Some(IStr::from_untyped(v)?)105				})106			},107		)?,108	})109}110111pub fn evaluate_comp(112	ctx: Context,113	specs: &[CompSpec],114	callback: &mut impl FnMut(Context) -> Result<()>,115) -> Result<()> {116	match specs.first() {117		None => callback(ctx)?,118		Some(CompSpec::IfSpec(IfSpecData { cond, span: _ })) => {119			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {120				evaluate_comp(ctx, &specs[1..], callback)?;121			}122		}123		Some(CompSpec::ForSpec(ForSpecData {124			destruct: into,125			over,126		})) => {127			match evaluate(ctx.clone(), over)? {128				Val::Arr(list) => {129					for item in list.iter_lazy() {130						let fctx = Pending::new();131						let mut new_bindings = FxHashMap::with_capacity(into.binds_len());132						destruct(into, item, fctx.clone(), &mut new_bindings)?;133						let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);134135						evaluate_comp(ctx, &specs[1..], callback)?;136					}137				}138				#[cfg(feature = "exp-object-iteration")]139				Val::Obj(obj) => {140					for field in obj.fields(141						// TODO: Should there be ability to preserve iteration order?142						#[cfg(feature = "exp-preserve-order")]143						false,144					) {145						let fctx = Pending::new();146						let mut new_bindings = FxHashMap::with_capacity(into.binds_len());147						let obj = obj.clone();148						let value = Thunk::evaluated(Val::arr(vec![149							Thunk::evaluated(Val::string(field.clone())),150							obj.get_lazy(field).transpose().expect(151								"field exists, as field name was obtained from object.fields()",152							),153						]));154						destruct(into, value, fctx.clone(), &mut new_bindings)?;155						let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);156157						evaluate_comp(ctx, &specs[1..], callback)?;158					}159				}160				_ => bail!(InComprehensionCanOnlyIterateOverArray),161			}162		}163	}164	Ok(())165}166167trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}168impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}169170fn evaluate_object_locals(171	fctx: Context,172	locals: Rc<Vec<BindSpec>>,173) -> impl CloneableUnbound<Context> {174	#[derive(Trace, Clone)]175	struct UnboundLocals {176		fctx: Context,177		locals: Rc<Vec<BindSpec>>,178	}179	impl Unbound for UnboundLocals {180		type Bound = Context;181182		fn bind(&self, sup_this: SupThis) -> Result<Context> {183			let fctx = Context::new_future();184			let mut new_bindings =185				FxHashMap::with_capacity(self.locals.iter().map(BindSpec::binds_len).sum());186			for b in self.locals.iter() {187				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;188			}189190			let ctx = self.fctx.clone();191192			let ctx = ctx193				.extend_bindings_sup_this(new_bindings, sup_this)194				.into_future(fctx);195196			Ok(ctx)197		}198	}199200	UnboundLocals { fctx, locals }201}202203pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(204	builder: &mut ObjValueBuilder,205	ctx: Context,206	uctx: B,207	field: &FieldMember,208) -> Result<()> {209	let name = evaluate_field_name(ctx, &field.name)?;210	let Some(name) = name else {211		return Ok(());212	};213214	match field {215		FieldMember {216			plus,217			params: None,218			visibility,219			value,220			..221		} => {222			#[derive(Trace)]223			struct UnboundValue<B: Trace> {224				uctx: B,225				value: Rc<Expr>,226				name: IStr,227			}228			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {229				type Bound = Val;230				fn bind(&self, sup_this: SupThis) -> Result<Val> {231					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())232				}233			}234235			builder236				.field(name.clone())237				.with_add(*plus)238				.with_visibility(*visibility)239				.with_location(field.name.span.clone())240				.bindable(UnboundValue {241					uctx,242					value: value.clone(),243					name,244				})?;245		}246		FieldMember {247			params: Some(params),248			visibility,249			value,250			..251		} => {252			#[derive(Trace)]253			struct UnboundMethod<B: Trace> {254				uctx: B,255				value: Rc<Expr>,256				params: ExprParams,257				name: IStr,258			}259			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {260				type Bound = Val;261				fn bind(&self, sup_this: SupThis) -> Result<Val> {262					Ok(evaluate_method(263						self.uctx.bind(sup_this)?,264						self.name.clone(),265						self.params.clone(),266						self.value.clone(),267					))268				}269			}270271			builder272				.field(name.clone())273				.with_visibility(*visibility)274				// .with_location(value.span())275				.bindable(UnboundMethod {276					uctx,277					value: value.clone(),278					params: params.clone(),279					name,280				})?;281		}282	}283	Ok(())284}285286#[derive(Trace, Clone)]287struct DirectUnbound(Context);288impl Unbound for DirectUnbound {289	type Bound = Context;290	fn bind(&self, sup_this: SupThis) -> Result<Context> {291		Ok(self292			.0293			.clone()294			.extend_bindings_sup_this(FxHashMap::new(), sup_this))295	}296}297298#[allow(clippy::too_many_lines)]299pub fn evaluate_member_list_object(300	super_obj: Option<ObjValue>,301	ctx: Context,302	members: &ObjMembers,303) -> Result<ObjValue> {304	#[derive(Trace)]305	struct ObjectAssert<B: Trace> {306		uctx: B,307		asserts: Rc<Vec<AssertStmt>>,308	}309	impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {310		fn run(&self, sup_this: SupThis) -> Result<()> {311			let ctx = self.uctx.bind(sup_this)?;312			for assert in &*self.asserts {313				evaluate_assert(ctx.clone(), assert)?;314			}315			Ok(())316		}317	}318319	let mut builder = ObjValueBuilder::new();320	if let Some(super_obj) = super_obj {321		builder.with_super(super_obj);322	}323324	if members.locals.is_empty() {325		// We can use the same context for all field evaluation, it doesn't depends on locals, only on this/super326		let uctx = DirectUnbound(ctx.clone());327		for field in &members.fields {328			evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;329		}330		if !members.asserts.is_empty() {331			builder.assert(ObjectAssert {332				uctx,333				asserts: members.asserts.clone(),334			});335		}336	} else {337		let locals = members.locals.clone();338		// We have single context for all fields, so we can cache them together339		let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));340		for field in &members.fields {341			evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;342		}343		if !members.asserts.is_empty() {344			builder.assert(ObjectAssert {345				uctx,346				asserts: members.asserts.clone(),347			});348		}349	}350351	Ok(builder.build())352}353354pub fn evaluate_object(355	super_obj: Option<ObjValue>,356	ctx: Context,357	object: &ObjBody,358) -> Result<ObjValue> {359	Ok(match object {360		ObjBody::MemberList(members) => evaluate_member_list_object(super_obj, ctx, members)?,361		ObjBody::ObjComp(obj) => {362			let mut builder = ObjValueBuilder::new();363			if let Some(super_obj) = super_obj {364				builder.with_super(super_obj);365			}366			let locals = obj.locals.clone();367			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {368				let uctx = evaluate_object_locals(ctx.clone(), locals.clone());369370				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)371			})?;372373			builder.build()374		}375	})376}377378pub fn evaluate_apply(379	ctx: Context,380	value: &Expr,381	args: &ArgsDesc,382	loc: CallLocation<'_>,383	tailstrict: bool,384) -> Result<Val> {385	let value = evaluate(ctx.clone(), value)?;386	Ok(match value {387		Val::Func(f) => {388			let body = || f.evaluate(ctx, loc, args, tailstrict);389			if tailstrict {390				body()?391			} else {392				in_frame(loc, || format!("function <{}> call", f.name()), body)?393			}394		}395		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),396	})397}398399pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {400	let value = &assertion.0;401	let msg = &assertion.1;402	let assertion_result = in_frame(403		CallLocation::new(&value.span),404		|| "assertion condition".to_owned(),405		|| bool::from_untyped(evaluate(ctx.clone(), value)?),406	)?;407	if !assertion_result {408		in_frame(409			CallLocation::new(&value.span),410			|| "assertion failure".to_owned(),411			|| {412				if let Some(msg) = msg {413					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));414				}415				bail!(AssertionFailed(Val::Null.to_string()?));416			},417		)?;418	}419	Ok(())420}421422pub fn evaluate_named_param(ctx: Context, expr: &Expr, name: ParamName) -> Result<Val> {423	match name {424		ParamName::Named(name) => evaluate_named(ctx, expr, name),425		ParamName::Unnamed => evaluate(ctx, expr),426	}427}428429pub fn evaluate_named(ctx: Context, expr: &Expr, name: IStr) -> Result<Val> {430	use Expr::*;431	Ok(match expr {432		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),433		_ => evaluate(ctx, expr)?,434	})435}436437#[allow(clippy::too_many_lines)]438pub fn evaluate(ctx: Context, expr: &Expr) -> Result<Val> {439	use Expr::*;440441	Ok(match expr {442		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),443		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),444		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),445		Literal(LiteralType::True) => Val::Bool(true),446		Literal(LiteralType::False) => Val::Bool(false),447		Literal(LiteralType::Null) => Val::Null,448		Str(v) => Val::string(v.clone()),449		Num(v) => Val::try_num(*v)?,450		// I have tried to remove special behavior from super by implementing standalone-super451		// expresion, but looks like this case still needs special treatment.452		//453		// Note that other jsonnet implementations will fail on `if value in (super)` expression,454		// because the standalone super literal is not supported, that is because in other455		// implementations `in super` treated differently from `in smth_else`.456		BinaryOp(bin)457			if matches!(&bin.rhs, Expr::Literal(LiteralType::Super))458				&& bin.op == BinaryOpType::In =>459		{460			let sup_this = ctx.try_sup_this()?;461			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.462			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.463			if !sup_this.has_super() {464				return Ok(Val::Bool(false));465			}466			let field = evaluate(ctx, &bin.lhs)?;467			Val::Bool(sup_this.field_in_super(field.to_string()?))468		}469		BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?,470		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,471		Var(name) => in_frame(472			CallLocation::new(&name.span),473			|| format!("local <{}> access", &**name),474			|| ctx.binding((**name).clone())?.evaluate(),475		)?,476		Index { indexable, parts } => ensure_sufficient_stack(|| {477			let mut parts = parts.iter();478			let mut indexable = if matches!(&**indexable, Expr::Literal(LiteralType::Super)) {479				let part = parts.next().expect("at least part should exist");480				// sup_this existence check might also be skipped here for null-coalesce...481				// But I believe this might cause errors.482				let sup_this = ctx.try_sup_this()?;483				if !sup_this.has_super() {484					#[cfg(feature = "exp-null-coaelse")]485					if part.null_coaelse {486						return Ok(Val::Null);487					}488					bail!(NoSuperFound)489				}490				let name = evaluate(ctx.clone(), &part.value)?;491492				let Val::Str(name) = name else {493					bail!(ValueIndexMustBeTypeGot(494						ValType::Obj,495						ValType::Str,496						name.value_type(),497					))498				};499500				let name = name.into_flat();501				match sup_this502					.get_super(name.clone())503					.with_description_src(&part.span, || format!("field <{name}> access"))?504				{505					Some(v) => v,506					#[cfg(feature = "exp-null-coaelse")]507					None if part.null_coaelse => return Ok(Val::Null),508					None => {509						let suggestions = suggest_object_fields(510							&sup_this.standalone_super().expect("super exists"),511							name.clone(),512						);513514						bail!(NoSuchField(name, suggestions))515					}516				}517			} else {518				evaluate(ctx.clone(), indexable)?519			};520521			for part in parts {522				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {523					(Val::Obj(v), Val::Str(key)) => match v524						.get(key.clone().into_flat())525						.with_description_src(&part.span, || format!("field <{key}> access"))?526					{527						Some(v) => v,528						#[cfg(feature = "exp-null-coaelse")]529						None if part.null_coaelse => return Ok(Val::Null),530						None => {531							let suggestions = suggest_object_fields(&v, key.into_flat());532533							return Err(Error::from(NoSuchField(534								key.clone().into_flat(),535								suggestions,536							)))537							.with_description_src(&part.span, || format!("field <{key}> access"));538						}539					},540					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(541						ValType::Obj,542						ValType::Str,543						n.value_type(),544					)),545					(Val::Arr(v), Val::Num(n)) => {546						let n = n.get();547						if n.fract() > f64::EPSILON {548							bail!(FractionalIndex)549						}550						if n < 0.0 {551							#[expect(552								clippy::cast_possible_truncation,553								reason = "it would be truncated anyway"554							)]555							let n = n as isize;556							bail!(ArrayBoundsError(n, v.len()));557						}558						#[expect(559							clippy::cast_possible_truncation,560							clippy::cast_sign_loss,561							reason = "n is checked postive"562						)]563						v.get(n as usize)?564							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?565					}566					(Val::Arr(_), Val::Str(n)) => {567						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))568					}569					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(570						ValType::Arr,571						ValType::Num,572						n.value_type(),573					)),574575					(Val::Str(s), Val::Num(n)) => Val::Str({576						let n = n.get();577						if n.fract() > f64::EPSILON {578							bail!(FractionalIndex)579						}580						if n < 0.0 {581							#[expect(582								clippy::cast_possible_truncation,583								reason = "it would be truncated anyway"584							)]585							let n = n as isize;586							bail!(ArrayBoundsError(n, s.into_flat().chars().count()));587						}588						#[expect(589							clippy::cast_sign_loss,590							clippy::cast_possible_truncation,591							reason = "n is positive, overflow will truncate as expected"592						)]593						let n = n as usize;594						let v: IStr = s595							.clone()596							.into_flat()597							.chars()598							.skip(n)599							.take(1)600							.collect::<String>()601							.into();602						if v.is_empty() {603							bail!(StringBoundsError(n, s.into_flat().chars().count()))604						}605						StrValue::Flat(v)606					}),607					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(608						ValType::Str,609						ValType::Num,610						n.value_type(),611					)),612					#[cfg(feature = "exp-null-coaelse")]613					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),614					(v, _) => bail!(CantIndexInto(v.value_type())),615				};616			}617			Ok(indexable)618		})?,619		LocalExpr(bindings, returned) => {620			let mut new_bindings: FxHashMap<IStr, Thunk<Val>> =621				FxHashMap::with_capacity(bindings.iter().map(BindSpec::binds_len).sum());622			let fctx = Context::new_future();623			for b in bindings {624				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;625			}626			let ctx = ctx.extend_bindings(new_bindings).into_future(fctx);627			evaluate(ctx, returned)?628		}629		Arr(items) => {630			if items.is_empty() {631				Val::arr(())632			} else {633				Val::Arr(ArrValue::expr(ctx, items.clone()))634			}635		}636		ArrComp(expr, comp_specs) => {637			let mut out = Vec::new();638			evaluate_comp(ctx, comp_specs, &mut |ctx| {639				let expr = expr.clone();640				out.push(Thunk!(move || evaluate(ctx, &expr)));641				Ok(())642			})?;643			Val::arr(out)644		}645		Obj(body) => Val::Obj(evaluate_object(None, ctx, body)?),646		ObjExtend(a, b) => {647			let base = evaluate(ctx.clone(), a)?;648			match base {649				Val::Obj(base_obj) => Val::Obj(evaluate_object(Some(base_obj), ctx, b)?),650				_ => bail!("ObjExtend lhs should be an object value"),651			}652		}653		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {654			evaluate_apply(ctx, value, args, CallLocation::new(&args.span), *tailstrict)655		})?,656		Function(params, body) => {657			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())658		}659		AssertExpr(assert) => {660			evaluate_assert(ctx.clone(), &assert.assert)?;661			evaluate(ctx, &assert.rest)?662		}663		ErrorStmt(s, e) => in_frame(664			CallLocation::new(s),665			|| "error statement".to_owned(),666			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),667		)?,668		IfElse(if_else) => {669			if in_frame(670				CallLocation::new(&if_else.cond.span),671				|| "if condition".to_owned(),672				|| bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.cond)?),673			)? {674				evaluate(ctx, &if_else.cond_then)?675			} else {676				match &if_else.cond_else {677					Some(v) => evaluate(ctx, v)?,678					None => Val::Null,679				}680			}681		}682		Slice(slice) => {683			fn parse_idx<T: Typed + FromUntyped>(684				ctx: Context,685				expr: Option<&Spanned<Expr>>,686				desc: &'static str,687			) -> Result<Option<T>> {688				if let Some(value) = expr {689					Ok(in_frame(690						CallLocation::new(&value.span),691						|| format!("slice {desc}"),692						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),693					)?)694				} else {695					Ok(None)696				}697			}698699			let indexable = evaluate(ctx.clone(), &slice.value)?;700701			let start = parse_idx(ctx.clone(), slice.slice.start.as_ref(), "start")?;702			let end = parse_idx(ctx.clone(), slice.slice.end.as_ref(), "end")?;703			let step = parse_idx(ctx, slice.slice.step.as_ref(), "step")?;704705			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?706		}707		Import(kind, path) => {708			let Expr::Str(path) = &**path else {709				bail!("computed imports are not supported")710			};711			with_state(|s| {712				let span = &kind.span;713				let resolved_path = s.resolve_from(span.0.source_path(), path)?;714				Ok(match &**kind {715					ImportKind::Normal => in_frame(716						CallLocation::new(span),717						|| format!("import {:?}", path.clone()),718						|| s.import_resolved(resolved_path),719					)?,720					ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?),721					ImportKind::Bin => Val::arr(s.import_resolved_bin(resolved_path)?),722				}) as Result<Val>723			})?724		}725	})726}
modifiedcrates/jrsonnet-evaluator/src/integrations/serde.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/integrations/serde.rs
+++ b/crates/jrsonnet-evaluator/src/integrations/serde.rs
@@ -1,6 +1,6 @@
 use std::borrow::Cow;
 
-use jrsonnet_interner::IStr;
+use jrsonnet_interner::{IBytes, IStr};
 use serde::{
 	Deserialize, Serialize, Serializer,
 	de::{self, Visitor},
@@ -11,8 +11,8 @@
 };
 
 use crate::{
-	Error as JrError, ObjValue, ObjValueBuilder, Result, Val, arr::ArrValue, in_description_frame,
-	runtime_error, val::NumValue,
+	Error as JrError, ObjValue, ObjValueBuilder, Result, Val, in_description_frame, runtime_error,
+	val::NumValue,
 };
 
 impl<'de> Deserialize<'de> for Val {
@@ -90,7 +90,7 @@
 			where
 				E: de::Error,
 			{
-				Ok(Val::Arr(ArrValue::bytes(v.into())))
+				Ok(Val::arr(IBytes::from(v)))
 			}
 
 			fn visit_none<E>(self) -> Result<Self::Value, E>
@@ -130,7 +130,7 @@
 					out.push(val);
 				}
 
-				Ok(Val::Arr(ArrValue::eager(out)))
+				Ok(Val::arr(out))
 			}
 
 			fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
@@ -270,7 +270,7 @@
 	}
 
 	fn end(self) -> Result<Val> {
-		let inner = Val::Arr(ArrValue::eager(self.data));
+		let inner = Val::arr(self.data);
 		if let Some(variant) = self.variant {
 			let mut out = ObjValue::builder_with_capacity(1);
 			out.field(variant).value(inner);
@@ -509,7 +509,7 @@
 	}
 
 	fn serialize_bytes(self, v: &[u8]) -> Result<Val> {
-		Ok(Val::Arr(ArrValue::bytes(v.into())))
+		Ok(Val::arr(IBytes::from(v)))
 	}
 
 	fn serialize_none(self) -> Result<Val> {
modifiedcrates/jrsonnet-evaluator/src/typed/conversions.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/typed/conversions.rs
+++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs
@@ -6,7 +6,7 @@
 
 use crate::{
 	ObjValue, ObjValueBuilder, Result, ResultExt, Thunk, Val,
-	arr::{ArrValue, BytesArray},
+	arr::ArrValue,
 	bail,
 	function::FuncVal,
 	typed::CheckType,
@@ -83,6 +83,12 @@
 pub trait Typed: Sized {
 	const TYPE: &'static ComplexValType;
 }
+impl<T> Typed for &T
+where
+	T: Typed,
+{
+	const TYPE: &'static ComplexValType = <&T as Typed>::TYPE;
+}
 pub trait IntoUntyped: Typed {
 	// Whatever caller should use `into_lazy_untyped` instead of `into_untyped`
 	fn provides_lazy() -> bool {
@@ -93,6 +99,7 @@
 		Thunk::from(Self::into_untyped(typed))
 	}
 }
+
 pub trait IntoUntypedResult: Typed {
 	/// Hack to make builtins be able to return non-result values, and make macros able to convert those values to result
 	/// This method returns identity in impl Typed for Result, and should not be overriden
@@ -157,6 +164,26 @@
 		inner.map(<ThunkIntoUntyped<T>>::default())
 	}
 }
+impl<T> IntoUntyped for &Thunk<T>
+where
+	T: IntoUntyped + Trace + Clone,
+{
+	fn into_untyped(typed: Self) -> Result<Val> {
+		T::into_untyped(typed.evaluate()?)
+	}
+	fn provides_lazy() -> bool {
+		true
+	}
+
+	fn into_lazy_untyped(inner: Self) -> Thunk<Val> {
+		// Avoid lazy mapping
+		let inner = match try_cast_thunk_val(inner.clone()) {
+			Ok(v) => return v,
+			Err(e) => e,
+		};
+		inner.map(<ThunkIntoUntyped<T>>::default())
+	}
+}
 
 fn try_cast_thunk_t<T: 'static>(typed: Thunk<Val>) -> Result<Thunk<T>, Thunk<Val>> {
 	if TypeId::of::<T>() == TypeId::of::<Val>() {
@@ -221,6 +248,11 @@
 				}
 			}
 		}
+		impl IntoUntyped for &$ty {
+			fn into_untyped(value: Self) -> Result<Val> {
+				Ok(Val::Num((*value).into()))
+			}
+		}
 		impl IntoUntyped for $ty {
 			fn into_untyped(value: Self) -> Result<Val> {
 				Ok(Val::Num(value.into()))
@@ -305,6 +337,11 @@
 impl Typed for f64 {
 	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Num);
 }
+impl IntoUntyped for &f64 {
+	fn into_untyped(value: Self) -> Result<Val> {
+		Ok(Val::try_num(*value)?)
+	}
+}
 impl IntoUntyped for f64 {
 	fn into_untyped(value: Self) -> Result<Val> {
 		Ok(Val::try_num(value)?)
@@ -324,7 +361,7 @@
 impl Typed for PositiveF64 {
 	const TYPE: &'static ComplexValType = &ComplexValType::BoundedNumber(Some(0.0), None);
 }
-impl IntoUntyped for PositiveF64 {
+impl IntoUntyped for &PositiveF64 {
 	fn into_untyped(value: Self) -> Result<Val> {
 		Ok(Val::try_num(value.0)?)
 	}
@@ -538,6 +575,11 @@
 impl Typed for Val {
 	const TYPE: &'static ComplexValType = &ComplexValType::Any;
 }
+impl IntoUntyped for &Val {
+	fn into_untyped(typed: Self) -> Result<Val> {
+		Ok(typed.clone())
+	}
+}
 impl IntoUntyped for Val {
 	fn into_untyped(typed: Self) -> Result<Val> {
 		Ok(typed)
@@ -567,9 +609,14 @@
 	const TYPE: &'static ComplexValType =
 		&ComplexValType::ArrayRef(&ComplexValType::BoundedNumber(Some(0.0), Some(255.0)));
 }
+impl IntoUntyped for &IBytes {
+	fn into_untyped(value: Self) -> Result<Val> {
+		Ok(Val::arr(value.clone()))
+	}
+}
 impl IntoUntyped for IBytes {
 	fn into_untyped(value: Self) -> Result<Val> {
-		Ok(Val::Arr(ArrValue::bytes(value)))
+		Ok(Val::arr(value))
 	}
 }
 impl FromUntyped for IBytes {
@@ -578,8 +625,8 @@
 			<Self as Typed>::TYPE.check(&value)?;
 			unreachable!()
 		};
-		if let Some(bytes) = a.as_any().downcast_ref::<BytesArray>() {
-			return Ok(bytes.0.as_slice().into());
+		if let Some(bytes) = a.as_any().downcast_ref::<IBytes>() {
+			return Ok(bytes.clone());
 		}
 		<Self as Typed>::TYPE.check(&value)?;
 		// Any::downcast_ref::<ByteArray>(&a);
@@ -596,7 +643,7 @@
 impl Typed for M1 {
 	const TYPE: &'static ComplexValType = &ComplexValType::BoundedNumber(Some(-1.0), Some(-1.0));
 }
-impl IntoUntyped for M1 {
+impl IntoUntyped for &M1 {
 	fn into_untyped(_: Self) -> Result<Val> {
 		Ok(Val::Num(NumValue::new(-1.0).expect("finite")))
 	}
@@ -728,6 +775,11 @@
 impl Typed for bool {
 	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Bool);
 }
+impl IntoUntyped for &bool {
+	fn into_untyped(value: Self) -> Result<Val> {
+		Ok(Val::Bool(*value))
+	}
+}
 impl IntoUntyped for bool {
 	fn into_untyped(value: Self) -> Result<Val> {
 		Ok(Val::Bool(value))
@@ -764,19 +816,23 @@
 	}
 }
 
-pub struct Null;
-impl Typed for Null {
+impl Typed for () {
 	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Null);
 }
-impl IntoUntyped for Null {
-	fn into_untyped(_: Self) -> Result<Val> {
+impl IntoUntyped for &() {
+	fn into_untyped((): Self) -> Result<Val> {
 		Ok(Val::Null)
 	}
 }
-impl FromUntyped for Null {
+impl IntoUntyped for () {
+	fn into_untyped((): Self) -> Result<Val> {
+		Ok(Val::Null)
+	}
+}
+impl FromUntyped for () {
 	fn from_untyped(value: Val) -> Result<Self> {
 		<Self as Typed>::TYPE.check(&value)?;
-		Ok(Self)
+		Ok(())
 	}
 }
 
@@ -811,9 +867,9 @@
 impl Typed for NumValue {
 	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Num);
 }
-impl IntoUntyped for NumValue {
+impl IntoUntyped for &NumValue {
 	fn into_untyped(typed: Self) -> Result<Val> {
-		Ok(Val::Num(typed))
+		Ok(Val::Num(*typed))
 	}
 }
 impl FromUntyped for NumValue {
modifiedcrates/jrsonnet-evaluator/src/val.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/val.rs
+++ b/crates/jrsonnet-evaluator/src/val.rs
@@ -137,7 +137,7 @@
 
 impl<T> Thunk<T>
 where
-	T: Clone + Trace,
+	T: Trace,
 {
 	pub fn force(&self) -> Result<()> {
 		self.evaluate()?;
@@ -161,7 +161,7 @@
 }
 impl<Input> Thunk<Input>
 where
-	Input: Trace + Clone,
+	Input: Trace,
 {
 	pub fn map<M>(self, mapper: M) -> Thunk<M::Output>
 	where
@@ -355,7 +355,7 @@
 			Self::Tree(Rc::new((a, b, len)))
 		}
 	}
-	pub fn into_flat(self) -> IStr {
+	pub fn into_flat(&self) -> IStr {
 		#[cold]
 		fn write_buf(s: &StrValue, out: &mut String) {
 			match s {
@@ -367,10 +367,10 @@
 			}
 		}
 		match self {
-			Self::Flat(f) => f,
+			Self::Flat(f) => f.clone(),
 			Self::Tree(_) => {
 				let mut buf = String::with_capacity(self.len());
-				write_buf(&self, &mut buf);
+				write_buf(self, &mut buf);
 				buf.into()
 			}
 		}
@@ -701,6 +701,9 @@
 	{
 		Ok(Self::Num(num.try_into()?))
 	}
+	pub fn arr(a: impl ArrayLike) -> Self {
+		Self::Arr(ArrValue::new(a))
+	}
 }
 
 impl From<IStr> for Val {
modifiedcrates/jrsonnet-stdlib/src/arrays.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/arrays.rs
+++ b/crates/jrsonnet-stdlib/src/arrays.rs
@@ -34,7 +34,7 @@
 			for _ in 0..*sz {
 				out.push(trivial.clone());
 			}
-			Ok(ArrValue::eager(out))
+			Ok(ArrValue::new(out))
 		},
 	)
 }
@@ -256,7 +256,7 @@
 pub fn builtin_lines(arr: ArrValue) -> Result<IndexableVal> {
 	builtin_join(
 		IndexableVal::Str("\n".into()),
-		ArrValue::extended(arr, ArrValue::eager(vec![Val::string("")])),
+		ArrValue::extended(arr, ArrValue::new(vec![Val::string("")])),
 	)
 }
 
@@ -468,7 +468,7 @@
 					out.push(ele);
 				}
 			}
-			Val::Arr(ArrValue::eager(out))
+			Val::arr(out)
 		}
 		Val::Obj(o) => {
 			let mut out = ObjValueBuilder::new();
modifiedcrates/jrsonnet-stdlib/src/sets.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/sets.rs
+++ b/crates/jrsonnet-stdlib/src/sets.rs
@@ -29,7 +29,11 @@
 
 #[builtin]
 #[allow(non_snake_case, clippy::redundant_closure)]
-pub fn builtin_set_inter(a: ArrValue, b: ArrValue, #[default] keyF: KeyF) -> Result<ArrValue> {
+pub fn builtin_set_inter(
+	a: ArrValue,
+	b: ArrValue,
+	#[default] keyF: KeyF,
+) -> Result<Vec<Thunk<Val>>> {
 	let mut a = a.iter_lazy();
 	let mut b = b.iter_lazy();
 
@@ -60,12 +64,16 @@
 			}
 		}
 	}
-	Ok(ArrValue::lazy(out))
+	Ok(out)
 }
 
 #[builtin]
 #[allow(non_snake_case, clippy::redundant_closure)]
-pub fn builtin_set_diff(a: ArrValue, b: ArrValue, #[default] keyF: KeyF) -> Result<ArrValue> {
+pub fn builtin_set_diff(
+	a: ArrValue,
+	b: ArrValue,
+	#[default] keyF: KeyF,
+) -> Result<Vec<Thunk<Val>>> {
 	let mut a = a.iter_lazy();
 	let mut b = b.iter_lazy();
 
@@ -103,12 +111,16 @@
 		av = a.next();
 		ak = av.clone().map(keyF).transpose()?;
 	}
-	Ok(ArrValue::lazy(out))
+	Ok(out)
 }
 
 #[builtin]
 #[allow(non_snake_case, clippy::redundant_closure)]
-pub fn builtin_set_union(a: ArrValue, b: ArrValue, #[default] keyF: KeyF) -> Result<ArrValue> {
+pub fn builtin_set_union(
+	a: ArrValue,
+	b: ArrValue,
+	#[default] keyF: KeyF,
+) -> Result<Vec<Thunk<Val>>> {
 	let mut a = a.iter_lazy();
 	let mut b = b.iter_lazy();
 
@@ -154,5 +166,5 @@
 		bv = b.next();
 		bk = bv.clone().map(keyF).transpose()?;
 	}
-	Ok(ArrValue::lazy(out))
+	Ok(out)
 }
modifiedcrates/jrsonnet-stdlib/src/sort.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/sort.rs
+++ b/crates/jrsonnet-stdlib/src/sort.rs
@@ -113,11 +113,11 @@
 		return Ok(values);
 	}
 	if key_getter.is_identity() {
-		Ok(ArrValue::eager(sort_identity(
+		Ok(ArrValue::new(sort_identity(
 			values.iter().collect::<Result<Vec<Val>>>()?,
 		)?))
 	} else {
-		Ok(ArrValue::lazy(sort_keyf(values, key_getter)?))
+		Ok(ArrValue::new(sort_keyf(values, key_getter)?))
 	}
 }
 
@@ -162,11 +162,11 @@
 		return Ok(arr);
 	}
 	if keyF.is_identity() {
-		Ok(ArrValue::eager(uniq_identity(
+		Ok(ArrValue::new(uniq_identity(
 			arr.iter().collect::<Result<Vec<Val>>>()?,
 		)?))
 	} else {
-		Ok(ArrValue::lazy(uniq_keyf(arr, keyF)?))
+		Ok(ArrValue::new(uniq_keyf(arr, keyF)?))
 	}
 }
 
@@ -180,11 +180,11 @@
 		let arr = arr.iter().collect::<Result<Vec<Val>>>()?;
 		let arr = sort_identity(arr)?;
 		let arr = uniq_identity(arr)?;
-		Ok(ArrValue::eager(arr))
+		Ok(ArrValue::new(arr))
 	} else {
 		let arr = sort_keyf(arr, keyF.clone())?;
-		let arr = uniq_keyf(ArrValue::lazy(arr), keyF)?;
-		Ok(ArrValue::lazy(arr))
+		let arr = uniq_keyf(ArrValue::new(arr), keyF)?;
+		Ok(ArrValue::new(arr))
 	}
 }