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

difftreelog

feat bring back null coaelse

xprpsquvYaroslav Bolyukin2026-05-06parent: #a2182e0.patch.diff
in: master

5 files changed

modifiedcrates/jrsonnet-evaluator/src/evaluate/compspec.rsdiffbeforeafterboth
after · crates/jrsonnet-evaluator/src/evaluate/compspec.rs
1use std::rc::Rc;23#[cfg(feature = "exp-object-iteration")]4use jrsonnet_interner::IStr;5use jrsonnet_types::ValType;67use super::{8	destructure::{destruct, evaluate_locals_unbound, fill_letrec_binds},9	evaluate_field_member_static, evaluate_field_member_unbound,10};11use crate::{12	Context, ObjValue, ObjValueBuilder, Result, Thunk, Val,13	analyze::{14		ClosureShape, LArrComp, LBind, LCompSpec, LDestruct, LExpr, LFieldMember, LObjComp,15		LocalSlot,16	},17	arr::ArrValue,18	bail,19	error::ErrorKind::*,20	evaluate::{evaluate, evaluate_trivial},21};2223enum CachedOver {24	Arr(ArrValue),25	#[cfg(feature = "exp-object-iteration")]26	Obj(ObjValue),27}2829trait CompCollector {30	fn reserve(&mut self, _guaranteed: usize) {}31	fn collect(&mut self, ctx: Context) -> Result<()>;32}3334struct EagerArrCollector<'a> {35	out: &'a mut Vec<Val>,36	value_shape: &'a ClosureShape,37	value: &'a LExpr,38}39impl CompCollector for EagerArrCollector<'_> {40	fn reserve(&mut self, size_hint: usize) {41		self.out.reserve(size_hint);42	}43	fn collect(&mut self, ctx: Context) -> Result<()> {44		if let Some(v) = evaluate_trivial(self.value) {45			self.out.push(v);46			return Ok(());47		}48		if let LExpr::Slot(slot) = self.value {49			self.out.push(ctx.slot(*slot).evaluate()?);50			return Ok(());51		}52		let env = Context::enter_using(&ctx, self.value_shape);53		self.out.push(evaluate(env, self.value)?);54		Ok(())55	}56}5758struct LazyArrCollector<'a> {59	out: &'a mut Vec<Thunk<Val>>,60	value_shape: &'a ClosureShape,61	value: &'a Rc<LExpr>,62}63impl CompCollector for LazyArrCollector<'_> {64	fn reserve(&mut self, size_hint: usize) {65		self.out.reserve(size_hint);66	}67	fn collect(&mut self, ctx: Context) -> Result<()> {68		if let Some(v) = evaluate_trivial(self.value) {69			self.out.push(Thunk::evaluated(v));70			return Ok(());71		}72		if let LExpr::Slot(slot) = self.value.as_ref() {73			self.out.push(ctx.slot(*slot));74			return Ok(());75		}76		let env = Context::enter_using(&ctx, self.value_shape);77		let value_expr = self.value.clone();78		self.out.push(Thunk!(move || evaluate(env, &value_expr)));79		Ok(())80	}81}8283struct ObjCompCollectorStatic<'a> {84	builder: &'a mut ObjValueBuilder,85	frame_shape: &'a ClosureShape,86	locals: &'a [LBind],87	field: &'a LFieldMember,88}89impl CompCollector for ObjCompCollectorStatic<'_> {90	fn reserve(&mut self, guaranteed: usize) {91		self.builder.reserve_fields(guaranteed);92	}93	fn collect(&mut self, inner_ctx: Context) -> Result<()> {94		// Build the object's A-frame fresh per iteration: captures from95		// the comp's iter ctx, locals = `this` (slot 0, unfilled in the96		// static path) + member-locals via letrec.97		let value_ctx = inner_ctx98			.pack_captures_sup_this(self.frame_shape)99			.enter(|fill, ctx| {100				fill_letrec_binds(fill, &ctx, self.locals);101			});102		evaluate_field_member_static(self.builder, inner_ctx, value_ctx, self.field)103	}104}105106struct ObjCompCollectorUnbound<'a> {107	builder: &'a mut ObjValueBuilder,108	frame_shape: Rc<ClosureShape>,109	locals: Rc<Vec<LBind>>,110	this_slot: Option<LocalSlot>,111	field: &'a LFieldMember,112}113impl CompCollector for ObjCompCollectorUnbound<'_> {114	fn reserve(&mut self, guaranteed: usize) {115		self.builder.reserve_fields(guaranteed);116	}117	fn collect(&mut self, inner_ctx: Context) -> Result<()> {118		let uctx = evaluate_locals_unbound(119			&inner_ctx,120			&self.frame_shape,121			self.this_slot,122			self.locals.clone(),123		);124		evaluate_field_member_unbound(self.builder, inner_ctx, uctx, self.field)125	}126}127128pub fn evaluate_obj_comp(129	super_obj: Option<ObjValue>,130	ctx: Context,131	comp: &LObjComp,132) -> Result<Val> {133	let mut builder = ObjValueBuilder::new();134	if let Some(super_obj) = super_obj {135		builder.with_super(super_obj);136	}137138	let cached_overs = cache_overs(&ctx, &comp.compspecs)?;139	if comp.this.is_some() || comp.uses_super {140		evaluate_compspecs(141			ctx,142			&comp.compspecs,143			&cached_overs,144			0,145			0,146			&mut ObjCompCollectorUnbound {147				builder: &mut builder,148				frame_shape: comp.frame_shape.clone(),149				locals: comp.locals.clone(),150				this_slot: comp.this,151				field: &comp.field,152			},153		)?;154	} else {155		evaluate_compspecs(156			ctx,157			&comp.compspecs,158			&cached_overs,159			0,160			0,161			&mut ObjCompCollectorStatic {162				builder: &mut builder,163				frame_shape: &comp.frame_shape,164				locals: &comp.locals,165				field: &comp.field,166			},167		)?;168	}169170	Ok(Val::Obj(builder.build()))171}172173pub fn evaluate_arr_comp(ctx: Context, comp: &LArrComp) -> Result<Val> {174	let cached_overs = cache_overs(&ctx, &comp.compspecs)?;175176	// Eager fast-path: when the comp has only `if` and `for { destruct: Full(_) }`177	// specs, allocate one Iter A-frame per for-spec and re-set the slot178	// per iteration as long as the frame's refcount stays at 1.179	'eager: {180		let mut out = Vec::new();181182		if comp.compspecs.iter().all(|c| {183			matches!(184				c,185				LCompSpec::If(_)186					| LCompSpec::For {187						destruct: LDestruct::Full(_),188						..189					}190			)191		}) && evaluate_compspecs_eager(192			ctx.clone(),193			&comp.compspecs,194			&cached_overs,195			0,196			0,197			&mut EagerArrCollector {198				out: &mut out,199				value_shape: &comp.value_shape,200				value: &comp.value,201			},202		)203		.is_err()204		{205			break 'eager;206		}207		return Ok(Val::arr(out));208	}209210	let mut items: Vec<Thunk<Val>> = Vec::new();211	evaluate_compspecs(212		ctx,213		&comp.compspecs,214		&cached_overs,215		0,216		0,217		&mut LazyArrCollector {218			out: &mut items,219			value_shape: &comp.value_shape,220			value: &comp.value,221		},222	)?;223	Ok(Val::arr(items))224}225226fn cache_overs(ctx: &Context, specs: &[LCompSpec]) -> Result<Vec<Option<CachedOver>>> {227	specs228		.iter()229		.map(|spec| {230			Ok(match spec {231				LCompSpec::For {232					over,233					loop_invariant: true,234					..235				} => {236					let val = evaluate(ctx.clone(), over)?;237					let Val::Arr(arr) = val else {238						bail!(InComprehensionCanOnlyIterateOverArray)239					};240					Some(CachedOver::Arr(arr))241				}242				#[cfg(feature = "exp-object-iteration")]243				LCompSpec::ForObj {244					over,245					loop_invariant: true,246					..247				} => {248					let val = evaluate(ctx.clone(), over)?;249					let Val::Obj(obj) = val else {250						bail!(TypeMismatch(251							"object iteration over",252							vec![jrsonnet_types::ValType::Obj],253							val.value_type(),254						))255					};256					Some(CachedOver::Obj(obj))257				}258				_ => None,259			})260		})261		.collect::<Result<_>>()262}263264fn evaluate_compspecs_eager(265	ctx: Context,266	specs: &[LCompSpec],267	cached_overs: &[Option<CachedOver>],268	idx: usize,269	guaranteed_reserve: usize,270	collector: &mut dyn CompCollector,271) -> Result<()> {272	if idx >= specs.len() {273		collector.reserve(guaranteed_reserve);274		return collector.collect(ctx);275	}276	match &specs[idx] {277		LCompSpec::If(cond) => {278			let val = evaluate(ctx.clone(), cond)?;279			let Val::Bool(b) = val else {280				bail!(TypeMismatch(281					"if spec condition",282					vec![ValType::Bool],283					val.value_type()284				))285			};286			if b {287				evaluate_compspecs_eager(ctx, specs, cached_overs, idx + 1, 0, collector)?;288			}289		}290		LCompSpec::For {291			frame_shape,292			destruct,293			over,294			..295		} => {296			let arr = if let Some(CachedOver::Arr(cached)) = &cached_overs[idx] {297				cached.clone()298			} else {299				let arr_val = evaluate(ctx.clone(), over)?;300				let Val::Arr(arr) = arr_val else {301					bail!(InComprehensionCanOnlyIterateOverArray)302				};303				arr304			};305			let inner_reserve = guaranteed_reserve.max(1) * arr.len() as usize;306			match destruct {307				LDestruct::Full(slot) => {308					Context::enter_iter(&ctx, frame_shape, |it| {309						for (i, item) in arr.iter().enumerate() {310							let item = item?;311							let ctx = it.create(|f| {312								f.set(*slot, Thunk::evaluated(item));313							})?;314							evaluate_compspecs_eager(315								ctx,316								specs,317								cached_overs,318								idx + 1,319								if i == 0 { inner_reserve } else { 0 },320								collector,321							)?;322						}323						Ok(())324					})?;325				}326				// TODO: Should not be eager? CoW won't work here327				#[cfg(feature = "exp-destruct")]328				_ => unreachable!("eager compspecs are not possible with non-full patterns"),329			}330		}331		#[cfg(feature = "exp-object-iteration")]332		LCompSpec::ForObj { .. } => {333			unreachable!("eager compspecs filter rejects ForObj");334		}335	}336	Ok(())337}338339fn evaluate_compspecs(340	ctx: Context,341	specs: &[LCompSpec],342	cached_overs: &[Option<CachedOver>],343	idx: usize,344	guaranteed_reserve: usize,345	collector: &mut dyn CompCollector,346) -> Result<()> {347	if idx >= specs.len() {348		collector.reserve(guaranteed_reserve);349		return collector.collect(ctx);350	}351	match &specs[idx] {352		LCompSpec::If(cond) => {353			let val = evaluate(ctx.clone(), cond)?;354			let Val::Bool(b) = val else {355				bail!(TypeMismatch(356					"if spec condition",357					vec![ValType::Bool],358					val.value_type()359				))360			};361			if b {362				evaluate_compspecs(ctx, specs, cached_overs, idx + 1, 0, collector)?;363			}364		}365		LCompSpec::For {366			frame_shape,367			destruct: dst,368			over,369			..370		} => {371			let arr = if let Some(CachedOver::Arr(cached)) = &cached_overs[idx] {372				cached.clone()373			} else {374				let arr_val = evaluate(ctx.clone(), over)?;375				let Val::Arr(arr) = arr_val else {376					bail!(InComprehensionCanOnlyIterateOverArray)377				};378				arr379			};380			let inner_reserve = guaranteed_reserve.max(1) * arr.len() as usize;381			for (i, item) in arr.iter().enumerate() {382				let item = item?;383				let inner_ctx = ctx.pack_captures_sup_this(frame_shape).enter(|fill, ctx| {384					destruct(dst, fill, Thunk::evaluated(item), &ctx);385				});386				evaluate_compspecs(387					inner_ctx,388					specs,389					cached_overs,390					idx + 1,391					if i == 0 { inner_reserve } else { 0 },392					collector,393				)?;394			}395		}396		#[cfg(feature = "exp-object-iteration")]397		LCompSpec::ForObj {398			frame_shape,399			key,400			visibility,401			value,402			over,403			..404		} => {405			use jrsonnet_ir::Visibility;406			let obj = if let Some(CachedOver::Obj(cached)) = &cached_overs[idx] {407				cached.clone()408			} else {409				let val = evaluate(ctx.clone(), over)?;410				let Val::Obj(obj) = val else {411					bail!(TypeMismatch(412						"object iteration over",413						vec![ValType::Obj],414						val.value_type(),415					))416				};417				obj418			};419			let fields = obj.fields_with_visibility(420				#[cfg(feature = "exp-preserve-order")]421				false,422			);423			let pairs: Vec<(IStr, Visibility)> = fields424				.into_iter()425				.filter(|(_, v)| match visibility {426					Visibility::Normal => v.is_visible(),427					Visibility::Hidden => !v.is_visible(),428					Visibility::Unhide => true,429				})430				.collect();431			let inner_reserve = guaranteed_reserve.max(1) * pairs.len();432			for (i, (field_name, _)) in pairs.into_iter().enumerate() {433				let key_val = Val::string(field_name.clone());434				let value_thunk = obj435					.get_lazy(field_name.clone())436					.expect("field exists, just enumerated");437				let inner_ctx = ctx.pack_captures_sup_this(frame_shape).enter(|fill, ctx| {438					fill.set(*key, Thunk::evaluated(key_val));439					destruct(value, fill, value_thunk, &ctx);440				});441				evaluate_compspecs(442					inner_ctx,443					specs,444					cached_overs,445					idx + 1,446					if i == 0 { inner_reserve } else { 0 },447					collector,448				)?;449			}450		}451	}452	Ok(())453}
modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs
@@ -236,7 +236,7 @@
 				.transpose()?;
 			Val::from(indexable.slice(start, end, step)?)
 		}
-		LExpr::Super => Val::Obj(ctx.try_sup_this()?.standalone_super()?),
+		LExpr::Super => Val::Obj(ctx.try_sup_this()?.standalone_super().ok_or(NoSuperFound)?),
 		LExpr::Import {
 			kind,
 			kind_span,
@@ -337,108 +337,128 @@
 }
 
 fn evaluate_index(ctx: Context, indexable: &LExpr, parts: &[LIndexPart]) -> Result<Val> {
-	let mut value = if matches!(indexable, LExpr::Super) {
+	let mut parts = parts.iter();
+	let mut indexable = if matches!(indexable, LExpr::Super) {
+		let part = parts.next().expect("at least part should exist");
+		// sup_this existence check might also be skipped here for null-coalesce...
+		// But I believe this might cause errors.
 		let sup_this = ctx.try_sup_this()?;
-		// First part must be evaluated to get the super field name
-		if parts.is_empty() {
-			bail!(RuntimeError("super requires an index".into()))
+
+		if !sup_this.has_super() {
+			#[cfg(feature = "exp-null-coaelse")]
+			if part.null_coaelse {
+				return Ok(Val::Null);
+			}
+			bail!(NoSuperFound);
 		}
-		let key_val = evaluate(ctx.clone(), &parts[0].value)?;
-		let Val::Str(key) = &key_val else {
+		let name = evaluate(ctx.clone(), &part.value)?;
+
+		let Val::Str(name) = name else {
 			bail!(ValueIndexMustBeTypeGot(
 				ValType::Obj,
 				ValType::Str,
-				key_val.value_type(),
+				name.value_type(),
 			))
 		};
-		let field = key.clone().into_flat();
-		if let Some(v) = sup_this.get_super(field.clone())? {
-			// Continue with remaining parts
-			let mut value = v;
-			for part in &parts[1..] {
-				value = index_val(ctx.clone(), CallLocation::new(&part.span), value, part)?;
+
+		let name = name.into_flat();
+		match sup_this
+			.get_super(name.clone())
+			.with_description_src(&part.span, || format!("super field <{name}> access"))?
+		{
+			Some(v) => v,
+			#[cfg(feature = "exp-null-coaelse")]
+			None if part.null_coaelse => return Ok(Val::Null),
+			None => {
+				let suggestions = suggest_object_fields(
+					&sup_this.standalone_super().expect("super exists"),
+					name.clone(),
+				);
+				bail!(NoSuchField(name, suggestions))
 			}
-			return Ok(value);
 		}
-		let suggestions = suggest_object_fields(sup_this.this(), field.clone());
-		bail!(NoSuchField(field, suggestions))
 	} else {
 		evaluate(ctx.clone(), indexable)?
 	};
 
 	for part in parts {
-		value = index_val(ctx.clone(), CallLocation::new(&part.span), value, part)?;
-	}
-	Ok(value)
-}
-
-fn index_val(ctx: Context, loc: CallLocation<'_>, value: Val, part: &LIndexPart) -> Result<Val> {
-	let key_val = evaluate(ctx, &part.value)?;
-	Ok(match (&value, &key_val) {
-		(Val::Obj(obj), Val::Str(key)) => {
-			let field = key.clone().into_flat();
-			if let Some(v) = obj
-				.get(field.clone())
-				.with_description_src(loc, || format!("field <{field}> access"))?
-			{
-				v
-			} else {
-				bail!(NoSuchField(
-					field.clone(),
-					suggest_object_fields(obj, field)
-				))
+		let ctx = ctx.clone();
+		let loc = CallLocation::new(&part.span);
+		let value = indexable;
+		let key_val = evaluate(ctx, &part.value)?;
+		indexable = match (&value, &key_val) {
+			(Val::Obj(obj), Val::Str(key)) => {
+				let key = key.clone().into_flat();
+				match obj
+					.get(key.clone())
+					.with_description_src(loc, || format!("field <{key}> access"))?
+				{
+					Some(v) => v,
+					#[cfg(feature = "exp-null-coaelse")]
+					None if part.null_coaelse => return Ok(Val::Null),
+					None => {
+						return Err(Error::from(NoSuchField(
+							key.clone(),
+							suggest_object_fields(obj, key.clone()),
+						)))
+						.with_description_src(loc, || format!("field <{key}> access"));
+					}
+				}
 			}
-		}
-		(Val::Arr(arr), Val::Num(idx)) => {
-			let n = idx.get();
-			if n.fract() > f64::EPSILON {
-				bail!(FractionalIndex)
+			(Val::Arr(arr), Val::Num(idx)) => {
+				let n = idx.get();
+				if n.fract() > f64::EPSILON {
+					bail!(FractionalIndex)
+				}
+				if n < 0.0 {
+					bail!(ArrayBoundsError(
+						n as isize, // truncation is fine for error display
+						arr.len()
+					));
+				}
+				#[expect(
+					clippy::cast_possible_truncation,
+					clippy::cast_sign_loss,
+					reason = "n is checked positive"
+				)]
+				let i = n as u32;
+				arr.get(i)
+					.with_description_src(loc, || format!("element <{i}> access"))?
+					.ok_or_else(|| ArrayBoundsError(i as isize, arr.len()))?
 			}
-			if n < 0.0 {
-				bail!(ArrayBoundsError(
-					n as isize, // truncation is fine for error display
-					arr.len()
-				));
+			(Val::Str(s), Val::Num(idx)) => {
+				let n = idx.get();
+				if n.fract() > f64::EPSILON {
+					bail!(FractionalIndex)
+				}
+				let flat = s.clone().into_flat();
+				if n < 0.0 {
+					bail!(ArrayBoundsError(
+						n as isize, // truncation is fine for error display
+						flat.chars().count() as u32
+					));
+				}
+				#[expect(
+					clippy::cast_possible_truncation,
+					clippy::cast_sign_loss,
+					reason = "n is checked positive, overflow will truncate as expected"
+				)]
+				let i = n as usize;
+				let Some(char) = flat.chars().nth(i) else {
+					bail!(StringBoundsError(i, flat.chars().count()))
+				};
+				Val::string(char)
 			}
-			#[expect(
-				clippy::cast_possible_truncation,
-				clippy::cast_sign_loss,
-				reason = "n is checked positive"
-			)]
-			let i = n as u32;
-			arr.get(i)
-				.with_description_src(loc, || format!("element <{i}> access"))?
-				.ok_or_else(|| ArrayBoundsError(i as isize, arr.len()))?
-		}
-		(Val::Str(s), Val::Num(idx)) => {
-			let n = idx.get();
-			if n.fract() > f64::EPSILON {
-				bail!(FractionalIndex)
-			}
-			let flat = s.clone().into_flat();
-			if n < 0.0 {
-				bail!(ArrayBoundsError(
-					n as isize, // truncation is fine for error display
-					flat.chars().count() as u32
-				));
-			}
-			#[expect(
-				clippy::cast_possible_truncation,
-				clippy::cast_sign_loss,
-				reason = "n is checked positive, overflow will truncate as expected"
-			)]
-			let i = n as usize;
-			let Some(char) = flat.chars().nth(i) else {
-				bail!(StringBoundsError(i, flat.chars().count()))
-			};
-			Val::string(char)
-		}
-		_ => bail!(ValueIndexMustBeTypeGot(
-			value.value_type(),
-			ValType::Str,
-			key_val.value_type()
-		)),
-	})
+			#[cfg(feature = "exp-null-coaelse")]
+			(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),
+			_ => bail!(ValueIndexMustBeTypeGot(
+				value.value_type(),
+				ValType::Str,
+				key_val.value_type()
+			)),
+		};
+	}
+	Ok(indexable)
 }
 
 fn evaluate_obj_body(super_obj: Option<ObjValue>, ctx: Context, body: &LObjBody) -> Result<Val> {
modifiedcrates/jrsonnet-evaluator/src/obj/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/obj/mod.rs
+++ b/crates/jrsonnet-evaluator/src/obj/mod.rs
@@ -430,17 +430,17 @@
 	/// 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> {
+	/// Returns None if no `super` found
+	pub fn standalone_super(&self) -> Option<ObjValue> {
 		if !self.sup.super_exists() {
-			bail!(NoSuperFound)
+			return None;
 		}
 		let mut out = ObjValue::builder();
 		out.extend_with_core(StandaloneSuperCore {
 			sup: self.sup,
 			this: self.this.clone(),
 		});
-		Ok(out.build())
+		Some(out.build())
 	}
 	pub fn this(&self) -> &ObjValue {
 		&self.this
modifiedtests/cpp_test_suite_golden_override/error.field_not_exist.jsonnet.goldendiffbeforeafterboth
--- a/tests/cpp_test_suite_golden_override/error.field_not_exist.jsonnet.golden
+++ b/tests/cpp_test_suite_golden_override/error.field_not_exist.jsonnet.golden
@@ -1 +1,2 @@
-no such field: y
\ No newline at end of file
+no such field: y
+    error.field_not_exist.jsonnet:17:10-10: field <y> access
\ No newline at end of file
modifiedtests/go_testdata_golden_override/builtinObjectRemoveKey_super_assert.jsonnet.goldendiffbeforeafterboth
--- a/tests/go_testdata_golden_override/builtinObjectRemoveKey_super_assert.jsonnet.golden
+++ b/tests/go_testdata_golden_override/builtinObjectRemoveKey_super_assert.jsonnet.golden
@@ -1,2 +1,3 @@
 no such field: x
+    builtinObjectRemoveKey_super_assert.jsonnet:2:15-15: field <x> access
     builtinObjectRemoveKey_super_assert.jsonnet:2:10-15: assertion condition
\ No newline at end of file