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

difftreelog

refactor greately simplify object self/super implementation

xvonlxkpYaroslav Bolyukin2026-02-08parent: #1c69f1a.patch.diff
in: master

18 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -310,6 +310,18 @@
 checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1"
 
 [[package]]
+name = "educe"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417"
+dependencies = [
+ "enum-ordinalize",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
 name = "either"
 version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -340,6 +352,26 @@
 ]
 
 [[package]]
+name = "enum-ordinalize"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0"
+dependencies = [
+ "enum-ordinalize-derive",
+]
+
+[[package]]
+name = "enum-ordinalize-derive"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
 name = "equivalent"
 version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -576,6 +608,7 @@
 dependencies = [
  "annotate-snippets",
  "anyhow",
+ "educe",
  "hi-doc",
  "jrsonnet-gcmodule",
  "jrsonnet-interner",
modifiedcrates/jrsonnet-evaluator/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/Cargo.toml
+++ b/crates/jrsonnet-evaluator/Cargo.toml
@@ -57,6 +57,7 @@
 num-bigint = { workspace = true, features = ["serde"], optional = true }
 
 stacker = "0.1.23"
+educe = { version = "0.6.0", default-features = false, features = ["Clone", "Debug", "Eq", "Hash", "PartialEq"] }
 
 [build-dependencies]
 rustversion = "1.0.22"
modifiedcrates/jrsonnet-evaluator/src/ctx.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/ctx.rs
+++ b/crates/jrsonnet-evaluator/src/ctx.rs
@@ -1,32 +1,27 @@
 use std::fmt::Debug;
 
+use educe::Educe;
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::IStr;
 use rustc_hash::FxHashMap;
 
 use crate::{
 	error::ErrorKind::*, gc::WithCapacityExt as _, map::LayeredHashMap, ObjValue, Pending, Result,
-	Thunk, Val,
+	SupThis, Thunk, Val,
 };
+/// Context keeps information about current lexical code location
+///
+/// This information includes local variables, top-level object (`$`), current object (`this`), and super object (`super`)
+#[derive(Debug, Trace, Clone, Educe)]
+#[educe(PartialEq)]
+pub struct Context(#[educe(PartialEq(method = Cc::ptr_eq))] Cc<ContextInternal>);
 
-#[derive(Trace)]
-struct ContextInternals {
+#[derive(Debug, Trace)]
+struct ContextInternal {
 	dollar: Option<ObjValue>,
-	sup: Option<ObjValue>,
-	this: Option<ObjValue>,
+	sup_this: Option<SupThis>,
 	bindings: LayeredHashMap,
-}
-impl Debug for ContextInternals {
-	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-		f.debug_struct("Context").finish()
-	}
 }
-
-/// Context keeps information about current lexical code location
-///
-/// This information includes local variables, top-level object (`$`), current object (`this`), and super object (`super`)
-#[derive(Debug, Clone, Trace)]
-pub struct Context(Cc<ContextInternals>);
 impl Context {
 	pub fn new_future() -> Pending<Self> {
 		Pending::new()
@@ -36,12 +31,35 @@
 		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.this.as_ref()
+		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()
 	}
 
-	pub fn super_obj(&self) -> Option<&ObjValue> {
-		self.0.sup.as_ref()
+	pub fn try_sup_this(&self) -> Result<SupThis> {
+		self.0
+			.sup_this
+			.clone()
+			.ok_or_else(|| CantUseSelfSupOutsideOfObject.into())
 	}
 
 	pub fn binding(&self, name: IStr) -> Result<Thunk<Val>> {
@@ -83,41 +101,52 @@
 	pub fn with_var(self, name: impl Into<IStr>, value: Val) -> Self {
 		let mut new_bindings = FxHashMap::with_capacity(1);
 		new_bindings.insert(name.into(), Thunk::evaluated(value));
-		self.extend(new_bindings, None, None, None)
+		self.extend_bindings(new_bindings)
 	}
 
 	#[must_use]
-	pub fn extend(
+	pub fn extend_bindings_sup_this(
 		self,
 		new_bindings: FxHashMap<IStr, Thunk<Val>>,
-		new_dollar: Option<ObjValue>,
-		new_sup: Option<ObjValue>,
-		new_this: Option<ObjValue>,
+		sup_this: SupThis,
 	) -> Self {
-		let ctx = &self.0;
-		let dollar = new_dollar.or_else(|| ctx.dollar.clone());
-		let this = new_this.or_else(|| ctx.this.clone());
-		let sup = new_sup.or_else(|| ctx.sup.clone());
+		let ctx = &self;
+		let dollar = ctx
+			.0
+			.dollar
+			.clone()
+			.or_else(|| Some(sup_this.this().clone()));
 		let bindings = if new_bindings.is_empty() {
-			ctx.bindings.clone()
+			ctx.0.bindings.clone()
 		} else {
-			ctx.bindings.clone().extend(new_bindings)
+			ctx.0.bindings.clone().extend(new_bindings)
 		};
-		Self(Cc::new(ContextInternals {
+		Self(Cc::new(ContextInternal {
 			dollar,
-			sup,
-			this,
+			sup_this: Some(sup_this),
 			bindings,
 		}))
 	}
-}
-
-impl PartialEq for Context {
-	fn eq(&self, other: &Self) -> bool {
-		Cc::ptr_eq(&self.0, &other.0)
+	#[must_use]
+	pub fn extend_bindings(self, new_bindings: FxHashMap<IStr, Thunk<Val>>) -> Self {
+		if new_bindings.is_empty() {
+			return self;
+		}
+		let ctx = &self;
+		let bindings = if new_bindings.is_empty() {
+			ctx.0.bindings.clone()
+		} else {
+			ctx.0.bindings.clone().extend(new_bindings)
+		};
+		Self(Cc::new(ContextInternal {
+			dollar: ctx.0.dollar.clone(),
+			sup_this: ctx.0.sup_this.clone(),
+			bindings,
+		}))
 	}
 }
 
+#[derive(Default)]
 pub struct ContextBuilder {
 	bindings: FxHashMap<IStr, Thunk<Val>>,
 	extend: Option<Context>,
@@ -127,20 +156,25 @@
 	pub fn new() -> Self {
 		Self::with_capacity(0)
 	}
+
 	pub fn with_capacity(capacity: usize) -> Self {
 		Self {
 			bindings: FxHashMap::with_capacity(capacity),
 			extend: None,
 		}
 	}
+
 	pub fn extend(parent: Context) -> Self {
 		Self {
 			bindings: FxHashMap::new(),
 			extend: Some(parent),
 		}
 	}
+
 	/// # Panics
-	/// If `name` is already bound
+	///
+	/// If `name` is already bound. Makes no sense to bind same local multiple times,
+	/// unless it is separate context layers.
 	pub fn bind(&mut self, name: impl Into<IStr>, value: Thunk<Val>) -> &mut Self {
 		let old = self.bindings.insert(name.into(), value);
 		assert!(old.is_none(), "variable bound twice in single context call");
@@ -148,14 +182,12 @@
 	}
 	pub fn build(self) -> Context {
 		if let Some(parent) = self.extend {
-			// TODO: replace self.extend with Result<Context, State>, and remove `state` field
-			parent.extend(self.bindings, None, None, None)
+			parent.extend_bindings(self.bindings)
 		} else {
-			Context(Cc::new(ContextInternals {
+			Context(Cc::new(ContextInternal {
 				bindings: LayeredHashMap::new(self.bindings),
 				dollar: None,
-				sup: None,
-				this: None,
+				sup_this: None,
 			}))
 		}
 	}
modifiedcrates/jrsonnet-evaluator/src/dynamic.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/dynamic.rs
+++ b/crates/jrsonnet-evaluator/src/dynamic.rs
@@ -1,11 +1,13 @@
-use std::cell::OnceCell;
+use std::ptr::addr_of;
+use std::{cell::OnceCell, hash::Hasher};
 
+use educe::Educe;
 use jrsonnet_gcmodule::{Cc, Trace};
 
 use crate::{bail, error::ErrorKind::InfiniteRecursionDetected, val::ThunkValue, Result};
 
-// TODO: Replace with OnceCell once in std
-#[derive(Clone, Trace)]
+#[derive(Trace, Educe)]
+#[educe(Clone)]
 pub struct Pending<V: Trace + 'static>(pub Cc<OnceCell<V>>);
 impl<T: Trace + 'static> Pending<T> {
 	pub fn new() -> Self {
@@ -25,7 +27,7 @@
 			.expect("wrapper is filled already");
 	}
 }
-impl<T: Clone + Trace + 'static> Pending<T> {
+impl<T: Trace + 'static + Clone> Pending<T> {
 	/// # Panics
 	/// If wrapper is not yet filled
 	pub fn unwrap(&self) -> T {
@@ -52,3 +54,7 @@
 		Self::new()
 	}
 }
+
+pub fn identity_hash<T, H: Hasher>(v: &Cc<T>, hasher: &mut H) {
+	hasher.write_usize(addr_of!(**v) as usize);
+}
modifiedcrates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -119,10 +119,8 @@
 	#[error("binary operation {1} {0} {2} is not implemented")]
 	BinaryOperatorDoesNotOperateOnValues(BinaryOpType, ValType, ValType),
 
-	#[error("no top level object in this context")]
-	NoTopLevelObjectFound,
-	#[error("self is only usable inside objects")]
-	CantUseSelfOutsideOfObject,
+	#[error("self/super/$ are only usable inside objects")]
+	CantUseSelfSupOutsideOfObject,
 	#[error("no super found")]
 	NoSuperFound,
 
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_parser::{6	ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, FieldMember, FieldName,7	ForSpecData, IfSpecData, LiteralType, LocExpr, Member, ObjBody, ParamsDesc,8};9use jrsonnet_types::ValType;10use rustc_hash::FxHashMap;1112use self::destructure::destruct;13use crate::{14	arr::ArrValue,15	bail,16	destructure::evaluate_dest,17	error::{suggest_object_fields, ErrorKind::*},18	evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},19	function::{CallLocation, FuncDesc, FuncVal},20	gc::WithCapacityExt as _,21	in_frame,22	typed::Typed,23	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},24	Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result, ResultExt,25	Unbound, Val,26};27pub mod destructure;28pub mod operator;2930// This is the amount of bytes that need to be left on the stack before increasing the size.31// It must be at least as large as the stack required by any code that does not call32// `ensure_sufficient_stack`.33const RED_ZONE: usize = 100 * 1024; // 100k3435// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then36// on. This flag has performance relevant characteristics. Don't set it too high.37const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB3839/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations40/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit41/// from this.42///43/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.44#[inline]45pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {46	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)47}4849pub fn evaluate_trivial(expr: &LocExpr) -> Option<Val> {50	fn is_trivial(expr: &LocExpr) -> bool {51		match expr.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			Expr::Parened(e) => is_trivial(e),57			_ => false,58		}59	}60	Some(match expr.expr() {61		Expr::Str(s) => Val::string(s.clone()),62		Expr::Num(n) => {63			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))64		}65		Expr::Literal(LiteralType::False) => Val::Bool(false),66		Expr::Literal(LiteralType::True) => Val::Bool(true),67		Expr::Literal(LiteralType::Null) => Val::Null,68		Expr::Arr(n) => {69			if n.iter().any(|e| !is_trivial(e)) {70				return None;71			}72			Val::Arr(ArrValue::eager(73				n.iter()74					.map(evaluate_trivial)75					.map(|e| e.expect("checked trivial"))76					.collect(),77			))78		}79		Expr::Parened(e) => evaluate_trivial(e)?,80		_ => return None,81	})82}8384pub fn evaluate_method(ctx: Context, name: IStr, params: ParamsDesc, body: LocExpr) -> 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: &FieldName) -> Result<Option<IStr>> {94	Ok(match field_name {95		FieldName::Fixed(n) => Some(n.clone()),96		FieldName::Dyn(expr) => in_frame(97			CallLocation::new(&expr.span()),98			|| "evaluating field name".to_string(),99			|| {100				let value = evaluate(ctx, expr)?;101				if matches!(value, Val::Null) {102					Ok(None)103				} else {104					Ok(Some(IStr::from_untyped(value)?))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))) => {119			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {120				evaluate_comp(ctx, &specs[1..], callback)?;121			}122		}123		Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {124			Val::Arr(list) => {125				for item in list.iter_lazy() {126					let fctx = Pending::new();127					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());128					destruct(var, item, fctx.clone(), &mut new_bindings)?;129					let ctx = ctx130						.clone()131						.extend(new_bindings, None, None, None)132						.into_future(fctx);133134					evaluate_comp(ctx, &specs[1..], callback)?;135				}136			}137			#[cfg(feature = "exp-object-iteration")]138			Val::Obj(obj) => {139				for field in obj.fields(140					// TODO: Should there be ability to preserve iteration order?141					#[cfg(feature = "exp-preserve-order")]142					false,143				) {144					let fctx = Pending::new();145					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());146					let obj = obj.clone();147					let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![148						Thunk::evaluated(Val::string(field.clone())),149						Thunk!(move || obj.get(field).transpose().expect(150							"field exists, as field name was obtained from object.fields()",151						)),152					])));153					destruct(var, value, fctx.clone(), &mut new_bindings)?;154					let ctx = ctx155						.clone()156						.extend(new_bindings, None, None, None)157						.into_future(fctx);158159					evaluate_comp(ctx, &specs[1..], callback)?;160				}161			}162			_ => bail!(InComprehensionCanOnlyIterateOverArray),163		},164	}165	Ok(())166}167168trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}169impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}170171fn evaluate_object_locals(172	fctx: Pending<Context>,173	locals: Rc<Vec<BindSpec>>,174) -> impl CloneableUnbound<Context> {175	#[derive(Trace, Clone)]176	struct UnboundLocals {177		fctx: Pending<Context>,178		locals: Rc<Vec<BindSpec>>,179	}180	impl Unbound for UnboundLocals {181		type Bound = Context;182183		fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Context> {184			let fctx = Context::new_future();185			let mut new_bindings =186				FxHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());187			for b in self.locals.iter() {188				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;189			}190191			let ctx = self.fctx.unwrap();192			let new_dollar = ctx.dollar().cloned().or_else(|| this.clone());193194			let ctx = ctx195				.extend(new_bindings, new_dollar, sup, this)196				.into_future(fctx);197198			Ok(ctx)199		}200	}201202	UnboundLocals { fctx, locals }203}204205pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(206	builder: &mut ObjValueBuilder,207	ctx: Context,208	uctx: B,209	field: &FieldMember,210) -> Result<()> {211	let name = evaluate_field_name(ctx, &field.name)?;212	let Some(name) = name else {213		return Ok(());214	};215216	match field {217		FieldMember {218			plus,219			params: None,220			visibility,221			value,222			..223		} => {224			#[derive(Trace)]225			struct UnboundValue<B: Trace> {226				uctx: B,227				value: LocExpr,228				name: IStr,229			}230			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {231				type Bound = Val;232				fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {233					evaluate_named(self.uctx.bind(sup, this)?, &self.value, self.name.clone())234				}235			}236237			builder238				.field(name.clone())239				.with_add(*plus)240				.with_visibility(*visibility)241				.with_location(value.span())242				.bindable(UnboundValue {243					uctx,244					value: value.clone(),245					name,246				})?;247		}248		FieldMember {249			params: Some(params),250			visibility,251			value,252			..253		} => {254			#[derive(Trace)]255			struct UnboundMethod<B: Trace> {256				uctx: B,257				value: LocExpr,258				params: ParamsDesc,259				name: IStr,260			}261			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {262				type Bound = Val;263				fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {264					Ok(evaluate_method(265						self.uctx.bind(sup, this)?,266						self.name.clone(),267						self.params.clone(),268						self.value.clone(),269					))270				}271			}272273			builder274				.field(name.clone())275				.with_visibility(*visibility)276				.with_location(value.span())277				.bindable(UnboundMethod {278					uctx,279					value: value.clone(),280					params: params.clone(),281					name,282				})?;283		}284	}285	Ok(())286}287288#[allow(clippy::too_many_lines)]289pub fn evaluate_member_list_object(ctx: Context, members: &[Member]) -> Result<ObjValue> {290	let mut builder = ObjValueBuilder::new();291	let locals = Rc::new(292		members293			.iter()294			.filter_map(|m| match m {295				Member::BindStmt(bind) => Some(bind.clone()),296				_ => None,297			})298			.collect::<Vec<_>>(),299	);300301	let fctx = Context::new_future();302303	// We have single context for all fields, so we can cache binds304	let uctx = CachedUnbound::new(evaluate_object_locals(fctx.clone(), locals));305306	for member in members {307		match member {308			Member::Field(field) => {309				evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;310			}311			Member::AssertStmt(stmt) => {312				#[derive(Trace)]313				struct ObjectAssert<B: Trace> {314					uctx: B,315					assert: AssertStmt,316				}317				impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {318					fn run(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<()> {319						let ctx = self.uctx.bind(sup, this)?;320						evaluate_assert(ctx, &self.assert)321					}322				}323				builder.assert(ObjectAssert {324					uctx: uctx.clone(),325					assert: stmt.clone(),326				});327			}328			Member::BindStmt(_) => {329				// Already handled330			}331		}332	}333	let this = builder.build();334	fctx.fill(ctx.extend(FxHashMap::new(), None, None, Some(this.clone())));335	Ok(this)336}337338pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {339	Ok(match object {340		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,341		ObjBody::ObjComp(obj) => {342			let mut builder = ObjValueBuilder::new();343			let locals = Rc::new(344				obj.pre_locals345					.iter()346					.chain(obj.post_locals.iter())347					.cloned()348					.collect::<Vec<_>>(),349			);350			let mut ctxs = vec![];351			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {352				let fctx = Context::new_future();353				ctxs.push((ctx.clone(), fctx.clone()));354				let uctx = evaluate_object_locals(fctx, locals.clone());355356				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)357			})?;358359			let this = builder.build();360			for (ctx, fctx) in ctxs {361				let _ctx = ctx362					.extend(FxHashMap::new(), None, None, Some(this.clone()))363					.into_future(fctx);364			}365			this366		}367	})368}369370pub fn evaluate_apply(371	ctx: Context,372	value: &LocExpr,373	args: &ArgsDesc,374	loc: CallLocation<'_>,375	tailstrict: bool,376) -> Result<Val> {377	let value = evaluate(ctx.clone(), value)?;378	Ok(match value {379		Val::Func(f) => {380			let body = || f.evaluate(ctx, loc, args, tailstrict);381			if tailstrict {382				body()?383			} else {384				in_frame(loc, || format!("function <{}> call", f.name()), body)?385			}386		}387		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),388	})389}390391pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {392	let value = &assertion.0;393	let msg = &assertion.1;394	let assertion_result = in_frame(395		CallLocation::new(&value.span()),396		|| "assertion condition".to_owned(),397		|| bool::from_untyped(evaluate(ctx.clone(), value)?),398	)?;399	if !assertion_result {400		in_frame(401			CallLocation::new(&value.span()),402			|| "assertion failure".to_owned(),403			|| {404				if let Some(msg) = msg {405					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));406				}407				bail!(AssertionFailed(Val::Null.to_string()?));408			},409		)?;410	}411	Ok(())412}413414pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result<Val> {415	use Expr::*;416	Ok(match expr.expr() {417		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),418		_ => evaluate(ctx, expr)?,419	})420}421422#[allow(clippy::too_many_lines)]423pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result<Val> {424	use Expr::*;425426	if let Some(trivial) = evaluate_trivial(expr) {427		return Ok(trivial);428	}429	let loc = expr.span();430	Ok(match expr.expr() {431		Literal(LiteralType::This) => {432			Val::Obj(ctx.this().ok_or(CantUseSelfOutsideOfObject)?.clone())433		}434		Literal(LiteralType::Super) => Val::Obj(435			ctx.super_obj().ok_or(NoSuperFound)?.with_this(436				ctx.this()437					.expect("if super exists - then this should too")438					.clone(),439			),440		),441		Literal(LiteralType::Dollar) => {442			Val::Obj(ctx.dollar().ok_or(NoTopLevelObjectFound)?.clone())443		}444		Literal(LiteralType::True) => Val::Bool(true),445		Literal(LiteralType::False) => Val::Bool(false),446		Literal(LiteralType::Null) => Val::Null,447		Parened(e) => evaluate(ctx, e)?,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(field, BinaryOpType::In, e)457			if matches!(e.expr(), Expr::Literal(LiteralType::Super)) =>458		{459			let Some(super_obj) = ctx.super_obj() else {460				return Ok(Val::Bool(false));461			};462			let field = evaluate(ctx.clone(), field)?;463			Val::Bool(super_obj.has_field_ex(field.to_string()?, true))464		}465		BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,466		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,467		Var(name) => in_frame(468			CallLocation::new(&loc),469			|| format!("local <{name}> access"),470			|| ctx.binding(name.clone())?.evaluate(),471		)?,472		Index { indexable, parts } => ensure_sufficient_stack(|| {473			let mut parts = parts.iter();474			let mut indexable = if matches!(indexable.expr(), Expr::Literal(LiteralType::Super)) {475				let part = parts.next().expect("at least part should exist");476				let Some(super_obj) = ctx.super_obj() else {477					#[cfg(feature = "exp-null-coaelse")]478					if part.null_coaelse {479						return Ok(Val::Null);480					}481					bail!(NoSuperFound)482				};483				let name = evaluate(ctx.clone(), &part.value)?;484485				let Val::Str(name) = name else {486					bail!(ValueIndexMustBeTypeGot(487						ValType::Obj,488						ValType::Str,489						name.value_type(),490					))491				};492493				let this = ctx494					.this()495					.expect("no this found, while super present, should not happen");496				let name = name.into_flat();497				match super_obj498					.get_for(name.clone(), this.clone())499					.with_description_src(&part.value, || format!("field <{name}> access"))?500				{501					Some(v) => v,502					#[cfg(feature = "exp-null-coaelse")]503					None if part.null_coaelse => return Ok(Val::Null),504					None => {505						let suggestions = suggest_object_fields(super_obj, name.clone());506507						bail!(NoSuchField(name, suggestions))508					}509				}510			} else {511				evaluate(ctx.clone(), indexable)?512			};513514			for part in parts {515				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {516					(Val::Obj(v), Val::Str(key)) => match v517						.get(key.clone().into_flat())518						.with_description_src(&part.value, || format!("field <{key}> access"))?519					{520						Some(v) => v,521						#[cfg(feature = "exp-null-coaelse")]522						None if part.null_coaelse => return Ok(Val::Null),523						None => {524							let suggestions = suggest_object_fields(&v, key.clone().into_flat());525526							return Err(Error::from(NoSuchField(527								key.clone().into_flat(),528								suggestions,529							)))530							.with_description_src(&part.value, || format!("field <{key}> access"));531						}532					},533					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(534						ValType::Obj,535						ValType::Str,536						n.value_type(),537					)),538					(Val::Arr(v), Val::Num(n)) => {539						let n = n.get();540						if n.fract() > f64::EPSILON {541							bail!(FractionalIndex)542						}543						if n < 0.0 {544							bail!(ArrayBoundsError(n as isize, v.len()));545						}546						v.get(n as usize)?547							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?548					}549					(Val::Arr(_), Val::Str(n)) => {550						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))551					}552					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(553						ValType::Arr,554						ValType::Num,555						n.value_type(),556					)),557558					(Val::Str(s), Val::Num(n)) => Val::Str({559						let v: IStr = s560							.clone()561							.into_flat()562							.chars()563							.skip(n.get() as usize)564							.take(1)565							.collect::<String>()566							.into();567						if v.is_empty() {568							let size = s.into_flat().chars().count();569							bail!(StringBoundsError(n.get() as usize, size))570						}571						StrValue::Flat(v)572					}),573					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(574						ValType::Str,575						ValType::Num,576						n.value_type(),577					)),578					#[cfg(feature = "exp-null-coaelse")]579					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),580					(v, _) => bail!(CantIndexInto(v.value_type())),581				};582			}583			Ok(indexable)584		})?,585		LocalExpr(bindings, returned) => {586			let mut new_bindings: FxHashMap<IStr, Thunk<Val>> =587				FxHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());588			let fctx = Context::new_future();589			for b in bindings {590				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;591			}592			let ctx = ctx.extend(new_bindings, None, None, None).into_future(fctx);593			evaluate(ctx, &returned.clone())?594		}595		Arr(items) => {596			if items.is_empty() {597				Val::Arr(ArrValue::empty())598			} else if items.len() == 1 {599				let item = items[0].clone();600				Val::Arr(ArrValue::lazy(vec![Thunk!(move || evaluate(ctx, &item))]))601			} else {602				Val::Arr(ArrValue::expr(ctx, items.iter().cloned()))603			}604		}605		ArrComp(expr, comp_specs) => {606			let mut out = Vec::new();607			evaluate_comp(ctx, comp_specs, &mut |ctx| {608				let expr = expr.clone();609				out.push(Thunk!(move || evaluate(ctx, &expr)));610				Ok(())611			})?;612			Val::Arr(ArrValue::lazy(out))613		}614		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),615		ObjExtend(a, b) => evaluate_add_op(616			&evaluate(ctx.clone(), a)?,617			&Val::Obj(evaluate_object(ctx, b)?),618		)?,619		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {620			evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)621		})?,622		Function(params, body) => {623			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())624		}625		AssertExpr(assert, returned) => {626			evaluate_assert(ctx.clone(), assert)?;627			evaluate(ctx, returned)?628		}629		ErrorStmt(e) => in_frame(630			CallLocation::new(&loc),631			|| "error statement".to_owned(),632			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),633		)?,634		IfElse {635			cond,636			cond_then,637			cond_else,638		} => {639			if in_frame(640				CallLocation::new(&loc),641				|| "if condition".to_owned(),642				|| bool::from_untyped(evaluate(ctx.clone(), &cond.0)?),643			)? {644				evaluate(ctx, cond_then)?645			} else {646				match cond_else {647					Some(v) => evaluate(ctx, v)?,648					None => Val::Null,649				}650			}651		}652		Slice(value, desc) => {653			fn parse_idx<T: Typed>(654				loc: CallLocation<'_>,655				ctx: &Context,656				expr: Option<&LocExpr>,657				desc: &'static str,658			) -> Result<Option<T>> {659				if let Some(value) = expr {660					Ok(in_frame(661						loc,662						|| format!("slice {desc}"),663						|| <Option<T>>::from_untyped(evaluate(ctx.clone(), value)?),664					)?)665				} else {666					Ok(None)667				}668			}669670			let indexable = evaluate(ctx.clone(), value)?;671			let loc = CallLocation::new(&loc);672673			let start = parse_idx(loc, &ctx, desc.start.as_ref(), "start")?;674			let end = parse_idx(loc, &ctx, desc.end.as_ref(), "end")?;675			let step = parse_idx(loc, &ctx, desc.step.as_ref(), "step")?;676677			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?678		}679		i @ (Import(path) | ImportStr(path) | ImportBin(path)) => {680			let Expr::Str(path) = &path.expr() else {681				bail!("computed imports are not supported")682			};683			let tmp = loc.clone().0;684			let s = ctx.state();685			let resolved_path = s.resolve_from(tmp.source_path(), path)?;686			match i {687				Import(_) => in_frame(688					CallLocation::new(&loc),689					|| format!("import {:?}", path.clone()),690					|| s.import_resolved(resolved_path),691				)?,692				ImportStr(_) => Val::string(s.import_resolved_str(resolved_path)?),693				ImportBin(_) => Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?)),694				_ => unreachable!(),695			}696		}697	})698}
after · crates/jrsonnet-evaluator/src/evaluate/mod.rs
1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_parser::{6	ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, FieldMember, FieldName,7	ForSpecData, IfSpecData, LiteralType, LocExpr, Member, ObjBody, ParamsDesc,8};9use jrsonnet_types::ValType;10use rustc_hash::FxHashMap;1112use self::destructure::destruct;13use crate::{14	Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result, ResultExt, SupThis, Unbound, Val, arr::ArrValue, bail, destructure::evaluate_dest, error::{ErrorKind::*, suggest_object_fields}, evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op}, function::{CallLocation, FuncDesc, FuncVal}, gc::WithCapacityExt as _, in_frame, typed::Typed, val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk}, with_state15};16pub mod destructure;17pub mod operator;1819// This is the amount of bytes that need to be left on the stack before increasing the size.20// It must be at least as large as the stack required by any code that does not call21// `ensure_sufficient_stack`.22const RED_ZONE: usize = 100 * 1024; // 100k2324// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then25// on. This flag has performance relevant characteristics. Don't set it too high.26const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB2728/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations29/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit30/// from this.31///32/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.33#[inline]34pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {35	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)36}3738pub fn evaluate_trivial(expr: &LocExpr) -> Option<Val> {39	fn is_trivial(expr: &LocExpr) -> bool {40		match expr.expr() {41			Expr::Str(_)42			| Expr::Num(_)43			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,44			Expr::Arr(a) => a.iter().all(is_trivial),45			Expr::Parened(e) => is_trivial(e),46			_ => false,47		}48	}49	Some(match expr.expr() {50		Expr::Str(s) => Val::string(s.clone()),51		Expr::Num(n) => {52			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))53		}54		Expr::Literal(LiteralType::False) => Val::Bool(false),55		Expr::Literal(LiteralType::True) => Val::Bool(true),56		Expr::Literal(LiteralType::Null) => Val::Null,57		Expr::Arr(n) => {58			if n.iter().any(|e| !is_trivial(e)) {59				return None;60			}61			Val::Arr(ArrValue::eager(62				n.iter()63					.map(evaluate_trivial)64					.map(|e| e.expect("checked trivial"))65					.collect(),66			))67		}68		Expr::Parened(e) => evaluate_trivial(e)?,69		_ => return None,70	})71}7273pub fn evaluate_method(ctx: Context, name: IStr, params: ParamsDesc, body: LocExpr) -> Val {74	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {75		name,76		ctx,77		params,78		body,79	})))80}8182pub fn evaluate_field_name(ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {83	Ok(match field_name {84		FieldName::Fixed(n) => Some(n.clone()),85		FieldName::Dyn(expr) => in_frame(86			CallLocation::new(&expr.span()),87			|| "evaluating field name".to_string(),88			|| {89				let value = evaluate(ctx, expr)?;90				if matches!(value, Val::Null) {91					Ok(None)92				} else {93					Ok(Some(IStr::from_untyped(value)?))94				}95			},96		)?,97	})98}99100pub fn evaluate_comp(101	ctx: Context,102	specs: &[CompSpec],103	callback: &mut impl FnMut(Context) -> Result<()>,104) -> Result<()> {105	match specs.first() {106		None => callback(ctx)?,107		Some(CompSpec::IfSpec(IfSpecData(cond))) => {108			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {109				evaluate_comp(ctx, &specs[1..], callback)?;110			}111		}112		Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {113			Val::Arr(list) => {114				for item in list.iter_lazy() {115					let fctx = Pending::new();116					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());117					destruct(var, item, fctx.clone(), &mut new_bindings)?;118					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);119120					evaluate_comp(ctx, &specs[1..], callback)?;121				}122			}123			#[cfg(feature = "exp-object-iteration")]124			Val::Obj(obj) => {125				for field in obj.fields(126					// TODO: Should there be ability to preserve iteration order?127					#[cfg(feature = "exp-preserve-order")]128					false,129				) {130					let fctx = Pending::new();131					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());132					let obj = obj.clone();133					let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![134						Thunk::evaluated(Val::string(field.clone())),135						Thunk!(move || obj.get(field).transpose().expect(136							"field exists, as field name was obtained from object.fields()",137						)),138					])));139					destruct(var, value, fctx.clone(), &mut new_bindings)?;140					let ctx = ctx141						.clone()142						.extend(new_bindings, None, None, None)143						.into_future(fctx);144145					evaluate_comp(ctx, &specs[1..], callback)?;146				}147			}148			_ => bail!(InComprehensionCanOnlyIterateOverArray),149		},150	}151	Ok(())152}153154trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}155impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}156157fn evaluate_object_locals(158	fctx: Context,159	locals: Rc<Vec<BindSpec>>,160) -> impl CloneableUnbound<Context> {161	#[derive(Trace, Clone)]162	struct UnboundLocals {163		fctx: Context,164		locals: Rc<Vec<BindSpec>>,165	}166	impl Unbound for UnboundLocals {167		type Bound = Context;168169		fn bind(&self, sup_this: SupThis) -> Result<Context> {170			let fctx = Context::new_future();171			let mut new_bindings =172				FxHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());173			for b in self.locals.iter() {174				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;175			}176177			let ctx = self.fctx.clone();178179			let ctx = ctx180				.extend_bindings_sup_this(new_bindings, sup_this)181				.into_future(fctx);182183			Ok(ctx)184		}185	}186187	UnboundLocals { fctx, locals }188}189190pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(191	builder: &mut ObjValueBuilder,192	ctx: Context,193	uctx: B,194	field: &FieldMember,195) -> Result<()> {196	let name = evaluate_field_name(ctx, &field.name)?;197	let Some(name) = name else {198		return Ok(());199	};200201	match field {202		FieldMember {203			plus,204			params: None,205			visibility,206			value,207			..208		} => {209			#[derive(Trace)]210			struct UnboundValue<B: Trace> {211				uctx: B,212				value: LocExpr,213				name: IStr,214			}215			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {216				type Bound = Val;217				fn bind(&self, sup_this: SupThis) -> Result<Val> {218					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())219				}220			}221222			builder223				.field(name.clone())224				.with_add(*plus)225				.with_visibility(*visibility)226				.with_location(value.span())227				.bindable(UnboundValue {228					uctx,229					value: value.clone(),230					name,231				})?;232		}233		FieldMember {234			params: Some(params),235			visibility,236			value,237			..238		} => {239			#[derive(Trace)]240			struct UnboundMethod<B: Trace> {241				uctx: B,242				value: LocExpr,243				params: ParamsDesc,244				name: IStr,245			}246			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {247				type Bound = Val;248				fn bind(&self, sup_this: SupThis) -> Result<Val> {249					Ok(evaluate_method(250						self.uctx.bind(sup_this)?,251						self.name.clone(),252						self.params.clone(),253						self.value.clone(),254					))255				}256			}257258			builder259				.field(name.clone())260				.with_visibility(*visibility)261				.with_location(value.span())262				.bindable(UnboundMethod {263					uctx,264					value: value.clone(),265					params: params.clone(),266					name,267				})?;268		}269	}270	Ok(())271}272273#[allow(clippy::too_many_lines)]274pub fn evaluate_member_list_object(ctx: Context, members: &[Member]) -> Result<ObjValue> {275	let mut builder = ObjValueBuilder::new();276	let locals = Rc::new(277		members278			.iter()279			.filter_map(|m| match m {280				Member::BindStmt(bind) => Some(bind.clone()),281				_ => None,282			})283			.collect::<Vec<_>>(),284	);285286	// We have single context for all fields, so we can cache binds287	let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));288289	for member in members {290		match member {291			Member::Field(field) => {292				evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;293			}294			Member::AssertStmt(stmt) => {295				#[derive(Trace)]296				struct ObjectAssert<B: Trace> {297					uctx: B,298					assert: AssertStmt,299				}300				impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {301					fn run(&self, sup_this: SupThis) -> Result<()> {302						let ctx = self.uctx.bind(sup_this)?;303						evaluate_assert(ctx, &self.assert)304					}305				}306				builder.assert(ObjectAssert {307					uctx: uctx.clone(),308					assert: stmt.clone(),309				});310			}311			Member::BindStmt(_) => {312				// Already handled313			}314		}315	}316	Ok(builder.build())317}318319pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {320	Ok(match object {321		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,322		ObjBody::ObjComp(obj) => {323			let mut builder = ObjValueBuilder::new();324			let locals = Rc::new(325				obj.pre_locals326					.iter()327					.chain(obj.post_locals.iter())328					.cloned()329					.collect::<Vec<_>>(),330			);331			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {332				let uctx = evaluate_object_locals(ctx.clone(), locals.clone());333334				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)335			})?;336337			builder.build()338		}339	})340}341342pub fn evaluate_apply(343	ctx: Context,344	value: &LocExpr,345	args: &ArgsDesc,346	loc: CallLocation<'_>,347	tailstrict: bool,348) -> Result<Val> {349	let value = evaluate(ctx.clone(), value)?;350	Ok(match value {351		Val::Func(f) => {352			let body = || f.evaluate(ctx, loc, args, tailstrict);353			if tailstrict {354				body()?355			} else {356				in_frame(loc, || format!("function <{}> call", f.name()), body)?357			}358		}359		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),360	})361}362363pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {364	let value = &assertion.0;365	let msg = &assertion.1;366	let assertion_result = in_frame(367		CallLocation::new(&value.span()),368		|| "assertion condition".to_owned(),369		|| bool::from_untyped(evaluate(ctx.clone(), value)?),370	)?;371	if !assertion_result {372		in_frame(373			CallLocation::new(&value.span()),374			|| "assertion failure".to_owned(),375			|| {376				if let Some(msg) = msg {377					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));378				}379				bail!(AssertionFailed(Val::Null.to_string()?));380			},381		)?;382	}383	Ok(())384}385386pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result<Val> {387	use Expr::*;388	Ok(match expr.expr() {389		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),390		_ => evaluate(ctx, expr)?,391	})392}393394#[allow(clippy::too_many_lines)]395pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result<Val> {396	use Expr::*;397398	if let Some(trivial) = evaluate_trivial(expr) {399		return Ok(trivial);400	}401	let loc = expr.span();402	Ok(match expr.expr() {403		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),404		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),405		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),406		Literal(LiteralType::True) => Val::Bool(true),407		Literal(LiteralType::False) => Val::Bool(false),408		Literal(LiteralType::Null) => Val::Null,409		Parened(e) => evaluate(ctx, e)?,410		Str(v) => Val::string(v.clone()),411		Num(v) => Val::try_num(*v)?,412		// I have tried to remove special behavior from super by implementing standalone-super413		// expresion, but looks like this case still needs special treatment.414		//415		// Note that other jsonnet implementations will fail on `if value in (super)` expression,416		// because the standalone super literal is not supported, that is because in other417		// implementations `in super` treated differently from `in smth_else`.418		BinaryOp(field, BinaryOpType::In, e)419			if matches!(e.expr(), Expr::Literal(LiteralType::Super)) =>420		{421			let sup_this = ctx.try_sup_this()?;422			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.423			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.424			if !sup_this.has_super() {425				return Ok(Val::Bool(false));426			}427			let field = evaluate(ctx, field)?;428			Val::Bool(sup_this.field_in_super(field.to_string()?))429		}430		BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,431		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,432		Var(name) => in_frame(433			CallLocation::new(&loc),434			|| format!("local <{name}> access"),435			|| ctx.binding(name.clone())?.evaluate(),436		)?,437		Index { indexable, parts } => ensure_sufficient_stack(|| {438			let mut parts = parts.iter();439			let mut indexable = if matches!(indexable.expr(), Expr::Literal(LiteralType::Super)) {440				let part = parts.next().expect("at least part should exist");441				// sup_this existence check might also be skipped here for null-coalesce...442				// But I believe this might cause errors.443				let sup_this = ctx.try_sup_this()?;444				if !sup_this.has_super() {445					#[cfg(feature = "exp-null-coaelse")]446					if part.null_coaelse {447						return Ok(Val::Null);448					}449					bail!(NoSuperFound)450				}451				let name = evaluate(ctx.clone(), &part.value)?;452453				let Val::Str(name) = name else {454					bail!(ValueIndexMustBeTypeGot(455						ValType::Obj,456						ValType::Str,457						name.value_type(),458					))459				};460461				let name = name.into_flat();462				match sup_this463					.get_super(name.clone())464					.with_description_src(&part.value, || format!("field <{name}> access"))?465				{466					Some(v) => v,467					#[cfg(feature = "exp-null-coaelse")]468					None if part.null_coaelse => return Ok(Val::Null),469					None => {470						let suggestions = suggest_object_fields(471							&sup_this.standalone_super().expect("super exists"),472							name.clone(),473						);474475						bail!(NoSuchField(name, suggestions))476					}477				}478			} else {479				evaluate(ctx.clone(), indexable)?480			};481482			for part in parts {483				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {484					(Val::Obj(v), Val::Str(key)) => match v485						.get(key.clone().into_flat())486						.with_description_src(&part.value, || format!("field <{key}> access"))?487					{488						Some(v) => v,489						#[cfg(feature = "exp-null-coaelse")]490						None if part.null_coaelse => return Ok(Val::Null),491						None => {492							let suggestions = suggest_object_fields(&v, key.clone().into_flat());493494							return Err(Error::from(NoSuchField(495								key.clone().into_flat(),496								suggestions,497							)))498							.with_description_src(&part.value, || format!("field <{key}> access"));499						}500					},501					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(502						ValType::Obj,503						ValType::Str,504						n.value_type(),505					)),506					(Val::Arr(v), Val::Num(n)) => {507						let n = n.get();508						if n.fract() > f64::EPSILON {509							bail!(FractionalIndex)510						}511						if n < 0.0 {512							bail!(ArrayBoundsError(n as isize, v.len()));513						}514						v.get(n as usize)?515							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?516					}517					(Val::Arr(_), Val::Str(n)) => {518						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))519					}520					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(521						ValType::Arr,522						ValType::Num,523						n.value_type(),524					)),525526					(Val::Str(s), Val::Num(n)) => Val::Str({527						let v: IStr = s528							.clone()529							.into_flat()530							.chars()531							.skip(n.get() as usize)532							.take(1)533							.collect::<String>()534							.into();535						if v.is_empty() {536							let size = s.into_flat().chars().count();537							bail!(StringBoundsError(n.get() as usize, size))538						}539						StrValue::Flat(v)540					}),541					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(542						ValType::Str,543						ValType::Num,544						n.value_type(),545					)),546					#[cfg(feature = "exp-null-coaelse")]547					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),548					(v, _) => bail!(CantIndexInto(v.value_type())),549				};550			}551			Ok(indexable)552		})?,553		LocalExpr(bindings, returned) => {554			let mut new_bindings: FxHashMap<IStr, Thunk<Val>> =555				FxHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());556			let fctx = Context::new_future();557			for b in bindings {558				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;559			}560			let ctx = ctx.extend_bindings(new_bindings).into_future(fctx);561			evaluate(ctx, &returned.clone())?562		}563		Arr(items) => {564			if items.is_empty() {565				Val::Arr(ArrValue::empty())566			} else if items.len() == 1 {567				let item = items[0].clone();568				Val::Arr(ArrValue::lazy(vec![Thunk!(move || evaluate(ctx, &item))]))569			} else {570				Val::Arr(ArrValue::expr(ctx, items.iter().cloned()))571			}572		}573		ArrComp(expr, comp_specs) => {574			let mut out = Vec::new();575			evaluate_comp(ctx, comp_specs, &mut |ctx| {576				let expr = expr.clone();577				out.push(Thunk!(move || evaluate(ctx, &expr)));578				Ok(())579			})?;580			Val::Arr(ArrValue::lazy(out))581		}582		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),583		ObjExtend(a, b) => evaluate_add_op(584			&evaluate(ctx.clone(), a)?,585			&Val::Obj(evaluate_object(ctx, b)?),586		)?,587		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {588			evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)589		})?,590		Function(params, body) => {591			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())592		}593		AssertExpr(assert, returned) => {594			evaluate_assert(ctx.clone(), assert)?;595			evaluate(ctx, returned)?596		}597		ErrorStmt(e) => in_frame(598			CallLocation::new(&loc),599			|| "error statement".to_owned(),600			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),601		)?,602		IfElse {603			cond,604			cond_then,605			cond_else,606		} => {607			if in_frame(608				CallLocation::new(&loc),609				|| "if condition".to_owned(),610				|| bool::from_untyped(evaluate(ctx.clone(), &cond.0)?),611			)? {612				evaluate(ctx, cond_then)?613			} else {614				match cond_else {615					Some(v) => evaluate(ctx, v)?,616					None => Val::Null,617				}618			}619		}620		Slice(value, desc) => {621			fn parse_idx<T: Typed>(622				loc: CallLocation<'_>,623				ctx: Context,624				expr: Option<&LocExpr>,625				desc: &'static str,626			) -> Result<Option<T>> {627				if let Some(value) = expr {628					Ok(in_frame(629						loc,630						|| format!("slice {desc}"),631						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),632					)?)633				} else {634					Ok(None)635				}636			}637638			let indexable = evaluate(ctx.clone(), value)?;639			let loc = CallLocation::new(&loc);640641			let start = parse_idx(loc, ctx.clone(), desc.start.as_ref(), "start")?;642			let end = parse_idx(loc, ctx.clone(), desc.end.as_ref(), "end")?;643			let step = parse_idx(loc, ctx, desc.step.as_ref(), "step")?;644645			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?646		}647		i @ (Import(path) | ImportStr(path) | ImportBin(path)) => {648			let Expr::Str(path) = &path.expr() else {649				bail!("computed imports are not supported")650			};651			let tmp = loc.clone().0;652			with_state(|s| {653				let resolved_path = s.resolve_from(tmp.source_path(), path)?;654				Ok(match i {655					Import(_) => in_frame(656						CallLocation::new(&loc),657						|| format!("import {:?}", path.clone()),658						|| s.import_resolved(resolved_path),659					)?,660					ImportStr(_) => Val::string(s.import_resolved_str(resolved_path)?),661					ImportBin(_) => {662						Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?))663					}664					_ => unreachable!(),665				}) as Result<Val>666			})?667		}668	})669}
modifiedcrates/jrsonnet-evaluator/src/function/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/mod.rs
+++ b/crates/jrsonnet-evaluator/src/function/mod.rs
@@ -40,7 +40,7 @@
 }
 
 /// Represents Jsonnet function defined in code.
-#[derive(Debug, PartialEq, Trace)]
+#[derive(Debug, Trace, PartialEq)]
 pub struct FuncDesc {
 	/// # Example
 	///
modifiedcrates/jrsonnet-evaluator/src/function/parse.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/parse.rs
+++ b/crates/jrsonnet-evaluator/src/function/parse.rs
@@ -129,11 +129,11 @@
 		}
 
 		Ok(body_ctx
-			.extend(passed_args, None, None, None)
-			.extend(defaults, None, None, None)
+			.extend_bindings(passed_args)
+			.extend_bindings(defaults)
 			.into_future(fctx))
 	} else {
-		let body_ctx = body_ctx.extend(passed_args, None, None, None);
+		let body_ctx = body_ctx.extend_bindings(passed_args);
 		Ok(body_ctx)
 	}
 }
@@ -257,7 +257,5 @@
 		}
 	}
 
-	Ok(body_ctx
-		.extend(bindings, None, None, None)
-		.into_future(fctx))
+	Ok(body_ctx.extend_bindings(bindings).into_future(fctx))
 }
modifiedcrates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/lib.rs
+++ b/crates/jrsonnet-evaluator/src/lib.rs
@@ -28,8 +28,10 @@
 	any::Any,
 	cell::{RefCell, RefMut},
 	collections::hash_map::Entry,
+	clone::Clone,
 	fmt::{self, Debug},
 	rc::Rc,
+	marker::PhantomData,
 };
 
 pub use ctx::*;
@@ -65,7 +67,7 @@
 	/// Type of value after object context is bound
 	type Bound;
 	/// Create value bound to specified object context
-	fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Self::Bound>;
+	fn bind(&self, sup_this: SupThis) -> Result<Self::Bound>;
 }
 
 /// Object fields may, or may not depend on `this`/`super`, this enum allows cheaper reuse of object-independent fields for native code
@@ -85,9 +87,9 @@
 }
 impl MaybeUnbound {
 	/// Attach object context to value, if required
-	pub fn evaluate(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {
+	pub fn evaluate(&self, sup_this: SupThis) -> Result<Val> {
 		match self {
-			Self::Unbound(v) => v.0.bind(sup, this),
+			Self::Unbound(v) => v.0.bind(sup_this),
 			Self::Bound(v) => Ok(v.evaluate()?),
 		}
 	}
@@ -105,8 +107,8 @@
 	/// Initialize default file context.
 	/// Has default implementation, which calls `populate`.
 	/// Prefer to always implement `populate` instead.
-	fn initialize(&self, state: State, for_file: Source) -> Context {
-		let mut builder = ContextBuilder::with_capacity(state, self.reserve_vars());
+	fn initialize(&self, for_file: Source) -> Context {
+		let mut builder = ContextBuilder::with_capacity(self.reserve_vars());
 		self.populate(for_file, &mut builder);
 		builder.build()
 	}
@@ -130,11 +132,11 @@
 where
 	T: ContextInitializer,
 {
-	fn initialize(&self, state: State, for_file: Source) -> Context {
+	fn initialize(&self, for_file: Source) -> Context {
 		if let Some(ctx) = self {
-			ctx.initialize(state, for_file)
+			ctx.initialize(for_file)
 		} else {
-			().initialize(state, for_file)
+			().initialize(for_file)
 		}
 	}
 
@@ -237,7 +239,40 @@
 #[derive(Clone, Trace)]
 pub struct State(Cc<EvaluationStateInternals>);
 
+thread_local! {
+	pub static DEFAULT_STATE: State = State::builder().build();
+	pub static STATE: RefCell<Option<State>> = const {RefCell::new(None)};
+}
+pub struct StateEnterGuard(PhantomData<()>);
+impl Drop for StateEnterGuard {
+	fn drop(&mut self) {
+		STATE.with_borrow_mut(|v| *v = None);
+	}
+}
+
+pub fn with_state<V>(v: impl FnOnce(State) -> V) -> V {
+	if let Some(state) = STATE.with_borrow(Clone::clone) {
+		v(state)
+	} else {
+		let s = DEFAULT_STATE.with(Clone::clone);
+		v(s)
+	}
+}
+
 impl State {
+	pub fn enter(&self) -> StateEnterGuard {
+		self.try_enter().expect("entered state already exists")
+	}
+	pub fn try_enter(&self) -> Option<StateEnterGuard> {
+		STATE.with_borrow_mut(|v| {
+			if v.is_none() {
+				*v = Some(self.clone());
+				Some(StateEnterGuard(PhantomData))
+			} else {
+				None
+			}
+		})
+	}
 	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise
 	pub fn import_resolved_str(&self, path: SourcePath) -> Result<IStr> {
 		let mut file_cache = self.file_cache();
@@ -359,7 +394,7 @@
 
 	/// Creates context with all passed global variables
 	pub fn create_default_context(&self, source: Source) -> Context {
-		self.context_initializer().initialize(self.clone(), source)
+		self.context_initializer().initialize(source)
 	}
 
 	/// Creates context with all passed global variables, calling custom modifier
@@ -370,7 +405,6 @@
 	) -> Context {
 		let default_initializer = self.context_initializer();
 		let mut builder = ContextBuilder::with_capacity(
-			self.clone(),
 			default_initializer.reserve_vars() + context_initializer.reserve_vars(),
 		);
 		default_initializer.populate(source.clone(), &mut builder);
modifiedcrates/jrsonnet-evaluator/src/map.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/map.rs
+++ b/crates/jrsonnet-evaluator/src/map.rs
@@ -4,14 +4,14 @@
 
 use crate::{gc::WithCapacityExt as _, Thunk, Val};
 
-#[derive(Trace)]
+#[derive(Trace, Debug)]
 #[trace(tracking(force))]
 pub struct LayeredHashMapInternals {
 	parent: Option<LayeredHashMap>,
 	current: FxHashMap<IStr, Thunk<Val>>,
 }
 
-#[derive(Trace)]
+#[derive(Trace, Debug)]
 pub struct LayeredHashMap(Cc<LayeredHashMapInternals>);
 
 impl LayeredHashMap {
modifiedcrates/jrsonnet-evaluator/src/obj.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/obj.rs
+++ b/crates/jrsonnet-evaluator/src/obj.rs
@@ -1,12 +1,9 @@
 use std::{
-	any::Any,
-	cell::RefCell,
-	fmt::Debug,
-	hash::{Hash, Hasher},
-	ptr::addr_of,
+	any::Any, cell::{Cell, RefCell}, collections::hash_map::Entry, fmt::{self, Debug}, hash::{Hash, Hasher}
 };
 
 use jrsonnet_gcmodule::{cc_dyn, Cc, Trace, Weak};
+use educe::Educe;
 use jrsonnet_interner::IStr;
 use jrsonnet_parser::{Span, Visibility};
 use rustc_hash::{FxHashMap, FxHashSet};
@@ -14,10 +11,11 @@
 use crate::{
 	arr::{PickObjectKeyValues, PickObjectValues},
 	bail,
-	error::{suggest_object_fields, Error, ErrorKind::*},
+	error::{suggest_object_fields, ErrorKind::*},
 	function::{CallLocation, FuncVal},
 	gc::WithCapacityExt as _,
 	in_frame,
+	identity_hash, 
 	operator::evaluate_add_op,
 	val::ArrValue,
 	CcUnbound, MaybeUnbound, Result, Thunk, Unbound, Val,
@@ -43,12 +41,10 @@
 	#[derive(Clone, Copy, Default, Debug, Trace)]
 	pub struct SuperDepth(());
 	impl SuperDepth {
-		pub const fn deeper(self) -> Self {
-			Self(())
-		}
+		pub(super) fn deepen(self) {}
 	}
 
-	#[derive(Clone, Copy)]
+	#[derive(Clone, Copy, Debug)]
 	pub struct FieldSortKey(());
 	impl FieldSortKey {
 		pub const fn new(_: SuperDepth, _: FieldIndex) -> Self {
@@ -74,8 +70,8 @@
 	#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)]
 	pub struct SuperDepth(u32);
 	impl SuperDepth {
-		pub fn deeper(self) -> Self {
-			Self(self.0 + 1)
+		pub(super) fn deepen(&mut self) {
+			*self.0 += 1
 		}
 	}
 
@@ -120,7 +116,7 @@
 	}
 }
 impl Debug for ObjFieldFlags {
-	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 		f.debug_struct("ObjFieldFlags")
 			.field("add", &self.add())
 			.field("visibility", &self.visibility())
@@ -140,34 +136,29 @@
 
 cc_dyn!(CcObjectAssertion, ObjectAssertion);
 pub trait ObjectAssertion: Trace {
-	fn run(&self, super_obj: Option<ObjValue>, this: Option<ObjValue>) -> Result<()>;
+	fn run(&self, sup_this: SupThis) -> Result<()>;
 }
 
 // Field => This
 
-#[derive(Trace)]
+#[derive(Trace, Debug)]
 enum CacheValue {
-	Cached(Val),
-	NotFound,
+	Cached(Result<Option<Val>>),
 	Pending,
-	Errored(Error),
 }
 
 #[allow(clippy::module_name_repetitions)]
 #[derive(Trace)]
 #[trace(tracking(force))]
 pub struct OopObject {
-	sup: Option<ObjValue>,
 	// this: Option<ObjValue>,
 	assertions: Cc<Vec<CcObjectAssertion>>,
-	assertions_ran: RefCell<FxHashSet<ObjValue>>,
 	this_entries: Cc<FxHashMap<IStr, ObjMember>>,
 	value_cache: RefCell<FxHashMap<(IStr, Option<WeakObjValue>), CacheValue>>,
 }
 impl Debug for OopObject {
 	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 		f.debug_struct("OopObject")
-			.field("sup", &self.sup)
 			// .field("assertions", &self.assertions)
 			// .field("assertions_ran", &self.assertions_ran)
 			.field("this_entries", &self.this_entries)
@@ -178,32 +169,36 @@
 
 type EnumFieldsHandler<'a> = dyn FnMut(SuperDepth, FieldIndex, IStr, Visibility) -> bool + 'a;
 
-pub trait ObjectLike: Trace + Any + Debug {
-	fn extend_from(&self, sup: ObjValue) -> ObjValue;
-	/// When using standalone super in object, `this.super_obj.with_this(this)` is executed
-	fn with_this(&self, me: ObjValue, this: ObjValue) -> ObjValue {
-		ObjValue::new(ThisOverride { inner: me, this })
-	}
-	fn this(&self) -> Option<ObjValue> {
-		None
-	}
-	fn len(&self) -> usize;
-	fn is_empty(&self) -> bool;
-	// If callback returns false, iteration stops
-	fn enum_fields(&self, depth: SuperDepth, handler: &mut EnumFieldsHandler<'_>) -> bool;
+#[derive(Trace, Clone)]
+pub enum ValueProcess {
+	None,
+	SuperPlus,
+}
+
+pub trait ObjectCore: Trace + Any + Debug {
+	// If callback returns false, iteration stops, and this call returns false.
+	fn enum_fields_core(
+		&self,
+		super_depth: &mut SuperDepth,
+		handler: &mut EnumFieldsHandler<'_>,
+	) -> bool;
 
 	fn has_field_include_hidden(&self, name: IStr) -> bool;
-	fn has_field(&self, name: IStr) -> bool;
 
-	fn get_for(&self, key: IStr, this: ObjValue) -> Result<Option<Val>>;
-	fn get_for_uncached(&self, key: IStr, this: ObjValue) -> Result<Option<Val>>;
+	fn get_for(&self, key: IStr, sup_this: SupThis) -> Result<Option<(Val, ValueProcess)>>;
+	// fn get_for_uncached(&self, key: IStr, this: ObjValue) -> Result<Option<(Val, ValueProcess)>>;
 	fn field_visibility(&self, field: IStr) -> Option<Visibility>;
 
-	fn run_assertions_raw(&self, this: ObjValue) -> Result<()>;
+	fn run_assertions_raw(&self, sup_this: SupThis) -> Result<()>;
 }
 
 #[derive(Clone, Trace)]
-pub struct WeakObjValue(#[trace(skip)] pub(crate) Weak<dyn ObjectLike>);
+pub struct WeakObjValue(#[trace(skip)] Weak<ObjValueInner>);
+impl Debug for WeakObjValue {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		f.debug_tuple("WeakObjValue").finish()
+	}
+}
 
 impl PartialEq for WeakObjValue {
 	fn eq(&self, other: &Self) -> bool {
@@ -222,50 +217,97 @@
 
 cc_dyn!(
 	#[derive(Clone, Debug)]
-	ObjValue, ObjectLike,
+	ObjCore, ObjectCore,
 	pub fn new() {...}
 );
+#[derive(Trace, Educe)]
+#[educe(Debug)]
+struct ObjValueInner {
+	cores: Vec<ObjCore>,
+	assertions_ran: Cell<bool>,
+	value_cache: RefCell<FxHashMap<(IStr, CoreIdx), CacheValue>>,
+}
 
-#[derive(Debug, Trace)]
-struct EmptyObject;
-impl ObjectLike for EmptyObject {
-	fn extend_from(&self, sup: ObjValue) -> ObjValue {
-		// obj + {} == obj
-		sup
+thread_local! {
+	static RUNNING_ASSERTIONS: RefCell<FxHashSet<ObjValue>> = RefCell::default();
+}
+fn is_asserting(obj: &ObjValue) -> bool {
+	RUNNING_ASSERTIONS.with_borrow(|v| v.contains(obj))
+}
+/// Returns false if already asserting
+fn start_asserting(obj: &ObjValue) -> bool {
+	RUNNING_ASSERTIONS.with_borrow_mut(|v| v.insert(obj.clone()))
+}
+fn finish_asserting(obj: &ObjValue) {
+	RUNNING_ASSERTIONS.with_borrow_mut(|v| {
+		let r = v.remove(obj);
+		debug_assert!(
+			r,
+			"finish_asserting was called before start_asserting or twice"
+		);
+	});
+}
+
+#[allow(clippy::module_name_repetitions)]
+#[derive(Clone, Trace, Debug, Educe)]
+#[educe(PartialEq, Hash, Eq)]
+pub struct ObjValue(
+	#[educe(PartialEq(method(Cc::ptr_eq)), Hash(method(identity_hash)))] Cc<ObjValueInner>,
+);
+
+#[derive(Trace, Debug)]
+struct StandaloneSuperCore {
+	sup: CoreIdx,
+	this: ObjValue,
+}
+impl ObjectCore for StandaloneSuperCore {
+	fn enum_fields_core(
+		&self,
+		super_depth: &mut SuperDepth,
+		handler: &mut EnumFieldsHandler<'_>,
+	) -> bool {
+		self.this
+			.enum_fields_internal(super_depth, handler, self.sup)
 	}
 
-	fn this(&self) -> Option<ObjValue> {
-		None
+	fn has_field_include_hidden(&self, name: IStr) -> bool {
+		self.this.has_field_include_hidden_idx(name, self.sup)
 	}
 
-	fn len(&self) -> usize {
-		0
+	fn get_for(&self, key: IStr, _sup_this: SupThis) -> Result<Option<(Val, ValueProcess)>> {
+		let v = self.this.get_idx(key, self.sup)?;
+		Ok(v.map(|v| (v, ValueProcess::None)))
 	}
 
-	fn is_empty(&self) -> bool {
-		true
+	fn field_visibility(&self, field: IStr) -> Option<Visibility> {
+		self.this.field_visibility_idx(field, self.sup)
 	}
 
-	fn enum_fields(&self, _depth: SuperDepth, _handler: &mut EnumFieldsHandler<'_>) -> bool {
-		false
+	fn run_assertions_raw(&self, _sup_this: SupThis) -> Result<()> {
+		self.this.run_assertions()
 	}
+}
 
-	fn has_field_include_hidden(&self, _name: IStr) -> bool {
-		false
+#[derive(Debug, Trace)]
+struct EmptyObject;
+impl ObjectCore for EmptyObject {
+	fn enum_fields_core(
+		&self,
+		_super_depth: &mut SuperDepth,
+		_handler: &mut EnumFieldsHandler<'_>,
+	) -> bool {
+		true
 	}
 
-	fn has_field(&self, _name: IStr) -> bool {
+	fn has_field_include_hidden(&self, _name: IStr) -> bool {
 		false
 	}
 
-	fn get_for(&self, _key: IStr, _this: ObjValue) -> Result<Option<Val>> {
-		Ok(None)
-	}
-	fn get_for_uncached(&self, _key: IStr, _this: ObjValue) -> Result<Option<Val>> {
+	fn get_for(&self, _key: IStr, _sup_this: SupThis) -> Result<Option<(Val, ValueProcess)>> {
 		Ok(None)
 	}
 
-	fn run_assertions_raw(&self, _this: ObjValue) -> Result<()> {
+	fn run_assertions_raw(&self, _sup_this: SupThis) -> Result<()> {
 		Ok(())
 	}
 
@@ -274,65 +316,79 @@
 	}
 }
 
-#[derive(Trace, Debug)]
-struct ThisOverride {
-	inner: ObjValue,
-	this: ObjValue,
+#[derive(Hash, PartialEq, Eq, Trace, Clone, Copy, Debug)]
+struct CoreIdx {
+	idx: usize,
 }
-impl ObjectLike for ThisOverride {
-	fn with_this(&self, _me: ObjValue, this: ObjValue) -> ObjValue {
-		ObjValue::new(Self {
-			inner: self.inner.clone(),
-			this,
-		})
+impl CoreIdx {
+	fn super_exists(self) -> bool {
+		self.idx != 0
 	}
-
-	fn extend_from(&self, sup: ObjValue) -> ObjValue {
-		self.inner.extend_from(sup).with_this(self.this.clone())
+}
+#[derive(Trace, Clone, PartialEq, Eq, Hash, Debug)]
+pub struct SupThis {
+	sup: CoreIdx,
+	this: ObjValue,
+}
+impl SupThis {
+	pub fn has_super(&self) -> bool {
+		self.sup.super_exists()
 	}
-
-	fn this(&self) -> Option<ObjValue> {
-		Some(self.this.clone())
+	/// Implementation of `"field" in super` operation,
+	/// works faster than standalone super path.
+	///
+	/// In case of no `super` existence, returns false.
+	pub fn field_in_super(&self, field: IStr) -> bool {
+		self.this.has_field_include_hidden_idx(field, self.sup)
 	}
-
-	fn len(&self) -> usize {
-		self.inner.len()
+	/// Implementation of `super.field` operation,
+	/// works faster than standalone super path.
+	///
+	/// In case of no `super` existence, returns `NoSuperFound`
+	pub fn get_super(&self, field: IStr) -> Result<Option<Val>> {
+		if !self.sup.super_exists() {
+			bail!(NoSuperFound);
+		}
+		self.this.get_idx(field, self.sup)
 	}
-
-	fn is_empty(&self) -> bool {
-		self.inner.is_empty()
+	/// `super` with `self` overriden for top-level lookups.
+	/// Exists when super appears outside of `super.field`/`"field" in super` expressions
+	/// Exclusive to jrsonnet.
+	///
+	/// Might return `NoSuperFound` error.
+	pub fn standalone_super(&self) -> Result<ObjValue> {
+		if !self.sup.super_exists() {
+			bail!(NoSuperFound)
+		}
+		Ok(ObjValue::new(StandaloneSuperCore {
+			sup: self.sup,
+			this: self.this.clone(),
+		}))
 	}
-
-	fn enum_fields(&self, depth: SuperDepth, handler: &mut EnumFieldsHandler<'_>) -> bool {
-		self.inner.enum_fields(depth, handler)
+	pub fn this(&self) -> &ObjValue {
+		&self.this
 	}
-
-	fn has_field_include_hidden(&self, name: IStr) -> bool {
-		self.inner.has_field_include_hidden(name)
-	}
-
-	fn has_field(&self, name: IStr) -> bool {
-		self.inner.has_field(name)
+	pub fn downgrade(self) -> WeakSupThis {
+		WeakSupThis {
+			sup: self.sup,
+			this: self.this.downgrade(),
+		}
 	}
-
-	fn get_for(&self, key: IStr, this: ObjValue) -> Result<Option<Val>> {
-		self.inner.get_for(key, this)
-	}
-
-	fn get_for_uncached(&self, key: IStr, this: ObjValue) -> Result<Option<Val>> {
-		self.inner.get_raw(key, this)
-	}
-
-	fn field_visibility(&self, field: IStr) -> Option<Visibility> {
-		self.inner.field_visibility(field)
-	}
-
-	fn run_assertions_raw(&self, this: ObjValue) -> Result<()> {
-		self.inner.run_assertions_raw(this)
-	}
 }
+#[derive(Trace, PartialEq, Eq, Hash, Debug)]
+pub struct WeakSupThis {
+	sup: CoreIdx,
+	this: WeakObjValue,
+}
 
 impl ObjValue {
+	pub fn new(v: impl ObjectCore) -> Self {
+		Self(Cc::new(ObjValueInner {
+			cores: vec![ObjCore::new(v)],
+			assertions_ran: Cell::new(false),
+			value_cache: RefCell::new(FxHashMap::new()),
+		}))
+	}
 	pub fn new_empty() -> Self {
 		Self::new(EmptyObject)
 	}
@@ -363,27 +419,77 @@
 
 	#[must_use]
 	pub fn extend_from(&self, sup: Self) -> Self {
-		self.0.extend_from(sup)
+		let mut cores = sup.0.cores.clone();
+		cores.extend(self.0.cores.iter().cloned());
+		ObjValue(Cc::new(ObjValueInner {
+			cores,
+			value_cache: RefCell::default(),
+			assertions_ran: Cell::new(false),
+		}))
 	}
-	#[must_use]
-	pub fn with_this(&self, this: Self) -> Self {
-		self.0.with_this(self.clone(), this)
-	}
+	// #[must_use]
+	// pub fn with_this(&self, this: Self) -> Self {
+	// 	self.0.with_this(self.clone(), this)
+	// }
+	/// Returns amount of visible object fields
+	/// If object only contains hidden fields - may return zero.
 	pub fn len(&self) -> usize {
-		self.0.len()
+		self.fields_visibility()
+			.iter()
+			.filter(|(_, (visible, _))| *visible)
+			.count()
 	}
 	pub fn is_empty(&self) -> bool {
-		self.0.is_empty()
+		self.len() == 0
+	}
+	/// For each field, calls callback.
+	/// If callback returns false - ends iteration prematurely.
+	///
+	/// Returns false if ended prematurely
+	pub fn enum_fields(&self, handler: &mut EnumFieldsHandler<'_>) -> bool {
+		let mut super_depth = SuperDepth::default();
+		self.enum_fields_internal(
+			&mut super_depth,
+			handler,
+			CoreIdx {
+				idx: self.0.cores.len(),
+			},
+		)
 	}
-	pub fn enum_fields(&self, depth: SuperDepth, handler: &mut EnumFieldsHandler<'_>) -> bool {
-		self.0.enum_fields(depth, handler)
+	fn enum_fields_internal(
+		&self,
+		super_depth: &mut SuperDepth,
+		handler: &mut EnumFieldsHandler<'_>,
+		idx: CoreIdx,
+	) -> bool {
+		for core in self.0.cores[..idx.idx].iter() {
+			if !core.0.enum_fields_core(super_depth, handler) {
+				return false;
+			}
+			super_depth.deepen();
+		}
+		true
 	}
 
 	pub fn has_field_include_hidden(&self, name: IStr) -> bool {
-		self.0.has_field_include_hidden(name)
+		self.has_field_include_hidden_idx(
+			name,
+			CoreIdx {
+				idx: self.0.cores.len(),
+			},
+		)
+	}
+	fn has_field_include_hidden_idx(&self, name: IStr, core: CoreIdx) -> bool {
+		self.0.cores[..core.idx]
+			.iter()
+			.rev()
+			.any(|v| v.0.has_field_include_hidden(name.clone()))
 	}
 	pub fn has_field(&self, name: IStr) -> bool {
-		self.0.has_field(name)
+		match self.field_visibility(name) {
+			Some(Visibility::Unhide | Visibility::Normal) => true,
+			Some(Visibility::Hidden) | None => false,
+		}
 	}
 	pub fn has_field_ex(&self, name: IStr, include_hidden: bool) -> bool {
 		if include_hidden {
@@ -392,14 +498,78 @@
 			self.has_field(name)
 		}
 	}
+	pub fn get(&self, key: IStr) -> Result<Option<Val>> {
+		self.get_idx(
+			key,
+			CoreIdx {
+				idx: self.0.cores.len(),
+			},
+		)
+	}
 
-	pub fn get(&self, key: IStr) -> Result<Option<Val>> {
-		self.run_assertions()?;
-		self.get_for(key, self.0.this().unwrap_or_else(|| self.clone()))
+	fn get_idx(&self, key: IStr, core: CoreIdx) -> Result<Option<Val>> {
+		let cache_key = (key.clone(), core);
+		{
+			let mut cache = self.0.value_cache.borrow_mut();
+			// entry_ref candidate?
+			match cache.entry(cache_key.clone()) {
+				Entry::Occupied(v) => match v.get() {
+					CacheValue::Cached(v) => return v.clone(),
+					CacheValue::Pending => {
+						if !is_asserting(self) {
+							bail!(InfiniteRecursionDetected);
+						}
+					}
+				},
+				Entry::Vacant(v) => {
+					v.insert(CacheValue::Pending);
+				}
+			};
+		}
+		let result = self.get_idx_uncached(key, core);
+		{
+			let mut cache = self.0.value_cache.borrow_mut();
+			cache.insert(cache_key, CacheValue::Cached(result.clone()));
+		}
+		result
 	}
+	fn get_idx_uncached(&self, key: IStr, core: CoreIdx) -> Result<Option<Val>> {
+		self.run_assertions()?;
+		let mut add_stack = Vec::with_capacity(2);
+		for (sup, core) in self.0.cores[..core.idx].iter().enumerate().rev() {
+			let sup_this = SupThis {
+				sup: CoreIdx { idx: sup },
+				this: self.clone(),
+			};
+			if let Some((val, proc)) = core.0.get_for(key.clone(), sup_this)? {
+				match proc {
+					ValueProcess::None if add_stack.is_empty() => return Ok(Some(val)),
+					ValueProcess::None => {
+						add_stack.push(val);
+						break;
+					}
+					ValueProcess::SuperPlus => {
+						add_stack.push(val);
+					}
+				}
+			}
+		}
+		if add_stack.is_empty() {
+			// None of layers had this field
+			return Ok(None);
+		} else if add_stack.len() == 1 {
+			// A layer had this field, but it wanted this field to be added with super.
+			// However, no super had this field, fail-safe
+			return Ok(Some(add_stack.pop().expect("single element on stack")));
+		}
+		let mut values = add_stack.into_iter().rev();
+		let init = values.next().expect("at least 2 elements");
 
-	pub fn get_for(&self, key: IStr, this: Self) -> Result<Option<Val>> {
-		self.0.get_for(key, this)
+		values
+			.try_fold(init, |a, b| evaluate_add_op(&a, &b))
+			.map(Some)
+
+		// self.0.get_raw(key, this)
 	}
 
 	pub fn get_or_bail(&self, key: IStr) -> Result<Val> {
@@ -408,22 +578,48 @@
 			bail!(NoSuchField(key, suggestions))
 		};
 		Ok(value)
-	}
-
-	fn get_raw(&self, key: IStr, this: Self) -> Result<Option<Val>> {
-		self.0.get_for_uncached(key, this)
 	}
 
 	fn field_visibility(&self, field: IStr) -> Option<Visibility> {
-		self.0.field_visibility(field)
+		self.field_visibility_idx(
+			field,
+			CoreIdx {
+				idx: self.0.cores.len(),
+			},
+		)
 	}
+	fn field_visibility_idx(&self, field: IStr, core: CoreIdx) -> Option<Visibility> {
+		let mut exists = false;
+		for ele in self.0.cores[..core.idx].iter().rev() {
+			let vis = ele.0.field_visibility(field.clone());
+			match vis {
+				Some(Visibility::Unhide | Visibility::Hidden) => return vis,
+				Some(Visibility::Normal) => exists = true,
+				None => {}
+			}
+		}
+		exists.then_some(Visibility::Normal)
+	}
 
 	pub fn run_assertions(&self) -> Result<()> {
-		// FIXME: Should it use `self.0.this()` in case of standalone super?
-		self.run_assertions_raw(self.clone())
-	}
-	fn run_assertions_raw(&self, this: Self) -> Result<()> {
-		self.0.run_assertions_raw(this)
+		if self.0.assertions_ran.get() {
+			return Ok(());
+		}
+		if !start_asserting(self) {
+			return Ok(());
+		}
+		for (idx, ele) in self.0.cores.iter().enumerate() {
+			let sup_this = SupThis {
+				sup: CoreIdx { idx },
+				this: self.clone(),
+			};
+			ele.0.run_assertions_raw(sup_this).inspect_err(|_e| {
+				finish_asserting(self);
+			})?;
+		}
+		finish_asserting(self);
+		self.0.assertions_ran.set(true);
+		Ok(())
 	}
 
 	pub fn iter(
@@ -462,24 +658,22 @@
 	}
 	fn fields_visibility(&self) -> FxHashMap<IStr, (bool, FieldSortKey)> {
 		let mut out = FxHashMap::default();
-		self.enum_fields(
-			SuperDepth::default(),
-			&mut |depth, index, name, visibility| {
-				let new_sort_key = FieldSortKey::new(depth, index);
-				let entry = out.entry(name);
-				let (visible, _) = entry.or_insert((true, new_sort_key));
-				match visibility {
-					Visibility::Normal => {}
-					Visibility::Hidden => {
-						*visible = false;
-					}
-					Visibility::Unhide => {
-						*visible = true;
-					}
-				};
-				false
-			},
-		);
+		self.enum_fields(&mut |depth, index, name, visibility| {
+			dbg!(&name, visibility);
+			let new_sort_key = FieldSortKey::new(depth, index);
+			let entry = out.entry(name);
+			let (visible, _) = entry.or_insert((true, new_sort_key));
+			match visibility {
+				Visibility::Normal => {}
+				Visibility::Hidden => {
+					*visible = false;
+				}
+				Visibility::Unhide => {
+					*visible = true;
+				}
+			};
+			false
+		});
 		out
 	}
 	pub fn fields_ex(
@@ -515,8 +709,8 @@
 			return fields;
 		}
 
-		let mut fields: Vec<_> = self
-			.fields_visibility()
+		let mut fields: Vec<_> = dbg!(self
+			.fields_visibility())
 			.into_iter()
 			.filter(|(_, (visible, _))| include_hidden || *visible)
 			.map(|(k, _)| k)
@@ -580,210 +774,65 @@
 
 impl OopObject {
 	pub fn new(
-		sup: Option<ObjValue>,
 		this_entries: Cc<FxHashMap<IStr, ObjMember>>,
 		assertions: Cc<Vec<CcObjectAssertion>>,
 	) -> Self {
 		Self {
-			sup,
-			// this: None,
-			assertions,
-			assertions_ran: RefCell::new(FxHashSet::new()),
 			this_entries,
 			value_cache: RefCell::new(FxHashMap::new()),
+			assertions,
 		}
 	}
-
-	fn evaluate_this(&self, v: &ObjMember, real_this: ObjValue) -> Result<Val> {
-		v.invoke.evaluate(self.sup.clone(), Some(real_this))
-	}
-
-	// FIXME: Duplication between ObjValue and OopObject
-	fn fields_visibility(&self) -> FxHashMap<IStr, (bool, FieldSortKey)> {
-		let mut out = FxHashMap::default();
-		self.enum_fields(
-			SuperDepth::default(),
-			&mut |depth, index, name, visibility| {
-				let new_sort_key = FieldSortKey::new(depth, index);
-				let entry = out.entry(name);
-				let (visible, _) = entry.or_insert((true, new_sort_key));
-				match visibility {
-					Visibility::Normal => {}
-					Visibility::Hidden => {
-						*visible = false;
-					}
-					Visibility::Unhide => {
-						*visible = true;
-					}
-				};
-				false
-			},
-		);
-		out
-	}
 }
 
-impl ObjectLike for OopObject {
-	fn extend_from(&self, sup: ObjValue) -> ObjValue {
-		ObjValue::new(match &self.sup {
-			None => Self::new(
-				Some(sup),
-				self.this_entries.clone(),
-				self.assertions.clone(),
-			),
-			Some(v) => Self::new(
-				Some(v.extend_from(sup)),
-				self.this_entries.clone(),
-				self.assertions.clone(),
-			),
-		})
-	}
-
-	fn len(&self) -> usize {
-		// Maybe it will be better to not compute sort key here?
-		self.fields_visibility()
-			.into_iter()
-			.filter(|(_, (visible, _))| *visible)
-			.count()
-	}
-
-	/// Returns false only if there is any visible entry.
-	///
-	/// Note that object with hidden fields `{a:: 1}` will be reported as empty here.
-	fn is_empty(&self) -> bool {
-		self.len() == 0
-	}
-
-	/// Run callback for every field found in object
-	///
-	/// Returns true if ended prematurely
-	fn enum_fields(&self, depth: SuperDepth, handler: &mut EnumFieldsHandler<'_>) -> bool {
-		if let Some(s) = &self.sup {
-			if s.enum_fields(depth.deeper(), handler) {
-				return true;
-			}
-		}
+impl ObjectCore for OopObject {
+	fn enum_fields_core(
+		&self,
+		super_depth: &mut SuperDepth,
+		handler: &mut EnumFieldsHandler<'_>,
+	) -> bool {
 		for (name, member) in self.this_entries.iter() {
 			if handler(
-				depth,
+				*super_depth,
 				member.original_index,
 				name.clone(),
 				member.flags.visibility(),
 			) {
-				return true;
+				return false;
 			}
 		}
-		false
+		true
 	}
 
 	fn has_field_include_hidden(&self, name: IStr) -> bool {
-		if self.this_entries.contains_key(&name) {
-			true
-		} else if let Some(super_obj) = &self.sup {
-			super_obj.has_field_include_hidden(name)
-		} else {
-			false
-		}
+		self.this_entries.contains_key(&name)
 	}
-	fn has_field(&self, name: IStr) -> bool {
-		self.field_visibility(name)
-			.map_or(false, |v| v.is_visible())
-	}
 
-	fn get_for(&self, key: IStr, this: ObjValue) -> Result<Option<Val>> {
-		let cache_key = (key.clone(), Some(this.clone().downgrade()));
-		if let Some(v) = self.value_cache.borrow().get(&cache_key) {
-			return Ok(match v {
-				CacheValue::Cached(v) => Some(v.clone()),
-				CacheValue::NotFound => None,
-				CacheValue::Pending => bail!(InfiniteRecursionDetected),
-				CacheValue::Errored(e) => return Err(e.clone()),
-			});
-		}
-		self.value_cache
-			.borrow_mut()
-			.insert(cache_key.clone(), CacheValue::Pending);
-		let value = self.get_for_uncached(key, this).inspect_err(|e| {
-			self.value_cache
-				.borrow_mut()
-				.insert(cache_key.clone(), CacheValue::Errored(e.clone()));
-		})?;
-		self.value_cache.borrow_mut().insert(
-			cache_key,
-			value
-				.as_ref()
-				.map_or(CacheValue::NotFound, |v| CacheValue::Cached(v.clone())),
-		);
-		Ok(value)
-	}
-	fn get_for_uncached(&self, key: IStr, real_this: ObjValue) -> Result<Option<Val>> {
-		match (self.this_entries.get(&key), &self.sup) {
-			(Some(k), None) => Ok(Some(self.evaluate_this(k, real_this)?)),
-			(Some(k), Some(super_obj)) => {
-				let our = self.evaluate_this(k, real_this.clone())?;
+	fn get_for(&self, key: IStr, sup_this: SupThis) -> Result<Option<(Val, ValueProcess)>> {
+		match self.this_entries.get(&key) {
+			Some(k) => Ok(Some((
+				k.invoke.evaluate(sup_this)?,
 				if k.flags.add() {
-					super_obj
-						.get_raw(key, real_this)?
-						.map_or(Ok(Some(our.clone())), |v| {
-							Ok(Some(evaluate_add_op(&v, &our)?))
-						})
+					ValueProcess::SuperPlus
 				} else {
-					Ok(Some(our))
-				}
-			}
-			(None, Some(super_obj)) => super_obj.get_raw(key, real_this),
-			(None, None) => Ok(None),
+					ValueProcess::None
+				},
+			))),
+			None => Ok(None),
 		}
 	}
 	fn field_visibility(&self, name: IStr) -> Option<Visibility> {
-		if let Some(m) = self.this_entries.get(&name) {
-			Some(match &m.flags.visibility() {
-				Visibility::Normal => self
-					.sup
-					.as_ref()
-					.and_then(|super_obj| super_obj.field_visibility(name))
-					.unwrap_or(Visibility::Normal),
-				v => *v,
-			})
-		} else if let Some(super_obj) = &self.sup {
-			super_obj.field_visibility(name)
-		} else {
-			None
-		}
+		Some(self.this_entries.get(&name)?.flags.visibility())
 	}
 
-	fn run_assertions_raw(&self, real_this: ObjValue) -> Result<()> {
+	fn run_assertions_raw(&self, sup_this: SupThis) -> Result<()> {
 		if self.assertions.is_empty() {
-			if let Some(super_obj) = &self.sup {
-				super_obj.run_assertions_raw(real_this)?;
-			}
 			return Ok(());
 		}
-		if self.assertions_ran.borrow_mut().insert(real_this.clone()) {
-			for assertion in self.assertions.iter() {
-				if let Err(e) = assertion.0.run(self.sup.clone(), Some(real_this.clone())) {
-					self.assertions_ran.borrow_mut().remove(&real_this);
-					return Err(e);
-				}
-			}
-			if let Some(super_obj) = &self.sup {
-				super_obj.run_assertions_raw(real_this)?;
-			}
+		for assertion in self.assertions.iter() {
+			assertion.0.run(sup_this.clone())?;
 		}
 		Ok(())
-	}
-}
-
-impl PartialEq for ObjValue {
-	fn eq(&self, other: &Self) -> bool {
-		Cc::ptr_eq(&self.0, &other.0)
-	}
-}
-
-impl Eq for ObjValue {}
-impl Hash for ObjValue {
-	fn hash<H: Hasher>(&self, hasher: &mut H) {
-		hasher.write_usize(addr_of!(*self.0).expose_provenance() as usize);
 	}
 }
 
@@ -845,11 +894,8 @@
 		if self.sup.is_none() && self.map.is_empty() && self.assertions.is_empty() {
 			return ObjValue::new_empty();
 		}
-		ObjValue::new(OopObject::new(
-			self.sup,
-			Cc::new(self.map),
-			Cc::new(self.assertions),
-		))
+		let res = ObjValue::new(OopObject::new(Cc::new(self.map), Cc::new(self.assertions)));
+		self.sup.map(|sup| res.extend_from(sup)).unwrap_or(res)
 	}
 }
 impl Default for ObjValueBuilder {
modifiedcrates/jrsonnet-evaluator/src/val.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/val.rs
+++ b/crates/jrsonnet-evaluator/src/val.rs
@@ -23,7 +23,7 @@
 	gc::WithCapacityExt as _,
 	manifest::{ManifestFormat, ToStringFormat},
 	typed::{BoundedUsize, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER},
-	ObjValue, Result, Unbound, WeakObjValue,
+	ObjValue, Result, SupThis, Unbound, WeakSupThis,
 };
 
 pub trait ThunkValue: Trace {
@@ -167,8 +167,6 @@
 		Self::evaluated(T::default())
 	}
 }
-
-type CacheKey = (Option<WeakObjValue>, Option<WeakObjValue>);
 
 #[derive(Trace, Clone)]
 pub struct CachedUnbound<I, T>
@@ -176,7 +174,7 @@
 	I: Unbound<Bound = T>,
 	T: Trace,
 {
-	cache: Cc<RefCell<FxHashMap<CacheKey, T>>>,
+	cache: Cc<RefCell<FxHashMap<WeakSupThis, T>>>,
 	value: I,
 }
 impl<I: Unbound<Bound = T>, T: Trace> CachedUnbound<I, T> {
@@ -189,17 +187,14 @@
 }
 impl<I: Unbound<Bound = T>, T: Clone + Trace> Unbound for CachedUnbound<I, T> {
 	type Bound = T;
-	fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<T> {
-		let cache_key = (
-			sup.as_ref().map(|s| s.clone().downgrade()),
-			this.as_ref().map(|t| t.clone().downgrade()),
-		);
+	fn bind(&self, sup_this: SupThis) -> Result<T> {
+		let cache_key = sup_this.clone().downgrade();
 		{
 			if let Some(t) = self.cache.borrow().get(&cache_key) {
 				return Ok(t.clone());
 			}
 		}
-		let bound = self.value.bind(sup, this)?;
+		let bound = self.value.bind(sup_this)?;
 
 		{
 			let mut cache = self.cache.borrow_mut();
modifiedtests/cpp_test_suite_golden_override/error.static_error_self.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.static_error_self.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.static_error_self.jsonnet.golden
@@ -1,2 +1,2 @@
-self is only usable inside objects
+self/super/$ are only usable inside objects
     elem <0> evaluation
\ No newline at end of file
modifiedtests/cpp_test_suite_golden_override/error.static_error_super.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.static_error_super.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.static_error_super.jsonnet.golden
@@ -1,2 +1,2 @@
-no super found
+self/super/$ are only usable inside objects
     elem <0> evaluation
\ No newline at end of file
addedtests/golden/issue195.jsonnetdiffbeforeafterboth
--- /dev/null
+++ b/tests/golden/issue195.jsonnet
@@ -0,0 +1 @@
+{ x: 42 } { y: { "false": "x" in super } }
addedtests/golden/issue195.jsonnet.goldendiffbeforeafterboth
--- /dev/null
+++ b/tests/golden/issue195.jsonnet.golden
@@ -0,0 +1,6 @@
+{
+    "x": 42,
+    "y": {
+        "false": false
+    }
+}
\ No newline at end of file
modifiedtests/tests/cpp_test_suite.rsdiffbeforeafterboth
--- a/tests/tests/cpp_test_suite.rs
+++ b/tests/tests/cpp_test_suite.rs
@@ -30,6 +30,8 @@
 		.import_resolver(FileImportResolver::default());
 	let s = s.build();
 
+	let _entered = s.enter();
+
 	let trace_format = CompactFormat {
 		resolver: PathResolver::FileName,
 		max_trace: 20,
@@ -61,7 +63,7 @@
 				Val::Obj(o.build())
 			}),
 		);
-		v = apply_tla(s, &args, v).expect("failed to apply tla");
+		v = apply_tla(&args, v).expect("failed to apply tla");
 	}
 
 	match v.manifest(JsonFormat::default()) {
modifiedtests/tests/golden.rsdiffbeforeafterboth
--- a/tests/tests/golden.rs
+++ b/tests/tests/golden.rs
@@ -21,6 +21,8 @@
 	.import_resolver(FileImportResolver::default());
 	let s = s.build();
 
+	let _entered = s.enter();
+
 	let trace_format = CompactFormat {
 		resolver: PathResolver::FileName,
 		max_trace: 20,