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

difftreelog

feat forObj evaluation

pvwvukvwYaroslav Bolyukin2026-05-06parent: #30703de.patch.diff
in: master

4 files changed

modifiedcrates/jrsonnet-evaluator/src/analyze.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/analyze.rs
+++ b/crates/jrsonnet-evaluator/src/analyze.rs
@@ -410,6 +410,15 @@
 		/// Is `over` does not depend on any variable introduced by an earlier for-spec in this comprehension chain
 		loop_invariant: bool,
 	},
+	#[cfg(feature = "exp-object-iteration")]
+	ForObj {
+		frame_shape: ClosureShape,
+		key: LocalSlot,
+		visibility: jrsonnet_ir::Visibility,
+		value: LDestruct,
+		over: LExpr,
+		loop_invariant: bool,
+	},
 }
 
 struct FrameAlloc<'s> {
@@ -1863,7 +1872,47 @@
 				(r, rest)
 			}
 			#[cfg(feature = "exp-object-iteration")]
-			CompSpec::ForObjSpec(_) => todo!(),
+			CompSpec::ForObjSpec(data) => {
+				let mut over_taint = AnalysisResult::default();
+				let over_l = analyze(&data.over, stack, &mut over_taint);
+				let loop_invariant = over_taint.local_dependent_depth > outer_depth;
+				taint.taint_by(over_taint);
+
+				let mut alloc = FrameAlloc::new(stack);
+				let closure = alloc.push_locals_closure();
+				let Some((_, key_slot)) = alloc.define_local(data.key.clone(), None) else {
+					stack.pop_closure(closure);
+					return go(idx + 1, specs, outer_depth, stack, taint, inside);
+				};
+				let Some(l_value) = alloc.alloc_destruct(&data.value) else {
+					stack.pop_closure(closure);
+					return go(idx + 1, specs, outer_depth, stack, taint, inside);
+				};
+				let mut pending = alloc.finish();
+
+				let var_analysis = AnalysisResult::default();
+				pending.record_spec_init(&LDestruct::Full(key_slot), var_analysis);
+				pending.record_spec_init(&l_value, var_analysis);
+
+				let body_frame = pending.finish();
+				let (r, mut rest) =
+					go(idx + 1, specs, outer_depth, body_frame.stack, taint, inside);
+				body_frame.finish();
+				let frame_shape = stack.pop_closure(closure);
+
+				rest.insert(
+					0,
+					LCompSpec::ForObj {
+						frame_shape,
+						key: key_slot,
+						visibility: data.visibility,
+						value: l_value,
+						over: over_l,
+						loop_invariant,
+					},
+				);
+				(r, rest)
+			}
 		}
 	}
 	let outer_depth = stack.depth;
@@ -1970,6 +2019,7 @@
 	use super::*;
 
 	#[test]
+	#[cfg(not(feature = "exp-null-coaelse"))]
 	fn snapshots() {
 		glob!("analysis_tests/*.jsonnet", |path| {
 			let code = fs::read_to_string(path).expect("read test file");
modifiedcrates/jrsonnet-evaluator/src/evaluate/compspec.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/evaluate/compspec.rs
1use std::rc::Rc;23use jrsonnet_types::ValType;45use super::{6	destructure::{destruct, evaluate_locals_unbound, fill_letrec_binds},7	evaluate_field_member_static, evaluate_field_member_unbound,8};9use crate::{10	Context, ObjValue, ObjValueBuilder, Result, Thunk, Val,11	analyze::{12		ClosureShape, LArrComp, LBind, LCompSpec, LDestruct, LExpr, LFieldMember, LObjComp,13		LocalSlot,14	},15	arr::ArrValue,16	bail,17	error::ErrorKind::*,18	evaluate::{evaluate, evaluate_trivial},19};2021trait CompCollector {22	fn reserve(&mut self, _guaranteed: usize) {}23	fn collect(&mut self, ctx: Context) -> Result<()>;24}2526struct EagerArrCollector<'a> {27	out: &'a mut Vec<Val>,28	value_shape: &'a ClosureShape,29	value: &'a LExpr,30}31impl CompCollector for EagerArrCollector<'_> {32	fn reserve(&mut self, size_hint: usize) {33		self.out.reserve(size_hint);34	}35	fn collect(&mut self, ctx: Context) -> Result<()> {36		if let Some(v) = evaluate_trivial(self.value) {37			self.out.push(v);38			return Ok(());39		}40		if let LExpr::Slot(slot) = self.value {41			self.out.push(ctx.slot(*slot).evaluate()?);42			return Ok(());43		}44		let env = Context::enter_using(&ctx, self.value_shape);45		self.out.push(evaluate(env, self.value)?);46		Ok(())47	}48}4950struct LazyArrCollector<'a> {51	out: &'a mut Vec<Thunk<Val>>,52	value_shape: &'a ClosureShape,53	value: &'a Rc<LExpr>,54}55impl CompCollector for LazyArrCollector<'_> {56	fn reserve(&mut self, size_hint: usize) {57		self.out.reserve(size_hint);58	}59	fn collect(&mut self, ctx: Context) -> Result<()> {60		if let Some(v) = evaluate_trivial(self.value) {61			self.out.push(Thunk::evaluated(v));62			return Ok(());63		}64		if let LExpr::Slot(slot) = self.value.as_ref() {65			self.out.push(ctx.slot(*slot));66			return Ok(());67		}68		let env = Context::enter_using(&ctx, self.value_shape);69		let value_expr = self.value.clone();70		self.out.push(Thunk!(move || evaluate(env, &value_expr)));71		Ok(())72	}73}7475struct ObjCompCollectorStatic<'a> {76	builder: &'a mut ObjValueBuilder,77	frame_shape: &'a ClosureShape,78	locals: &'a [LBind],79	field: &'a LFieldMember,80}81impl CompCollector for ObjCompCollectorStatic<'_> {82	fn reserve(&mut self, guaranteed: usize) {83		self.builder.reserve_fields(guaranteed);84	}85	fn collect(&mut self, inner_ctx: Context) -> Result<()> {86		// Build the object's A-frame fresh per iteration: captures from87		// the comp's iter ctx, locals = `this` (slot 0, unfilled in the88		// static path) + member-locals via letrec.89		let value_ctx = inner_ctx90			.pack_captures_sup_this(self.frame_shape)91			.enter(|fill, ctx| {92				fill_letrec_binds(fill, &ctx, self.locals);93			});94		evaluate_field_member_static(self.builder, inner_ctx, value_ctx, self.field)95	}96}9798struct ObjCompCollectorUnbound<'a> {99	builder: &'a mut ObjValueBuilder,100	frame_shape: Rc<ClosureShape>,101	locals: Rc<Vec<LBind>>,102	this_slot: Option<LocalSlot>,103	field: &'a LFieldMember,104}105impl CompCollector for ObjCompCollectorUnbound<'_> {106	fn reserve(&mut self, guaranteed: usize) {107		self.builder.reserve_fields(guaranteed);108	}109	fn collect(&mut self, inner_ctx: Context) -> Result<()> {110		let uctx = evaluate_locals_unbound(111			&inner_ctx,112			&self.frame_shape,113			self.this_slot,114			self.locals.clone(),115		);116		evaluate_field_member_unbound(self.builder, inner_ctx, uctx, self.field)117	}118}119120pub fn evaluate_obj_comp(121	super_obj: Option<ObjValue>,122	ctx: Context,123	comp: &LObjComp,124) -> Result<Val> {125	let mut builder = ObjValueBuilder::new();126	if let Some(super_obj) = super_obj {127		builder.with_super(super_obj);128	}129130	let cached_overs = cache_overs(&ctx, &comp.compspecs)?;131	if comp.this.is_some() || comp.uses_super {132		evaluate_compspecs(133			ctx,134			&comp.compspecs,135			&cached_overs,136			0,137			0,138			&mut ObjCompCollectorUnbound {139				builder: &mut builder,140				frame_shape: comp.frame_shape.clone(),141				locals: comp.locals.clone(),142				this_slot: comp.this,143				field: &comp.field,144			},145		)?;146	} else {147		evaluate_compspecs(148			ctx,149			&comp.compspecs,150			&cached_overs,151			0,152			0,153			&mut ObjCompCollectorStatic {154				builder: &mut builder,155				frame_shape: &comp.frame_shape,156				locals: &comp.locals,157				field: &comp.field,158			},159		)?;160	}161162	Ok(Val::Obj(builder.build()))163}164165pub fn evaluate_arr_comp(ctx: Context, comp: &LArrComp) -> Result<Val> {166	let cached_overs = cache_overs(&ctx, &comp.compspecs)?;167168	// Eager fast-path: when the comp has only `if` and `for { destruct: Full(_) }`169	// specs, allocate one Iter A-frame per for-spec and re-set the slot170	// per iteration as long as the frame's refcount stays at 1.171	'eager: {172		let mut out = Vec::new();173174		if comp.compspecs.iter().all(|c| {175			matches!(176				c,177				LCompSpec::If(_)178					| LCompSpec::For {179						destruct: LDestruct::Full(_),180						..181					}182			)183		}) && evaluate_compspecs_eager(184			ctx.clone(),185			&comp.compspecs,186			&cached_overs,187			0,188			0,189			&mut EagerArrCollector {190				out: &mut out,191				value_shape: &comp.value_shape,192				value: &comp.value,193			},194		)195		.is_err()196		{197			break 'eager;198		}199		return Ok(Val::arr(out));200	}201202	let mut items: Vec<Thunk<Val>> = Vec::new();203	evaluate_compspecs(204		ctx,205		&comp.compspecs,206		&cached_overs,207		0,208		0,209		&mut LazyArrCollector {210			out: &mut items,211			value_shape: &comp.value_shape,212			value: &comp.value,213		},214	)?;215	Ok(Val::arr(items))216}217218fn cache_overs(ctx: &Context, specs: &[LCompSpec]) -> Result<Vec<Option<ArrValue>>> {219	specs220		.iter()221		.map(|spec| {222			Ok(match spec {223				LCompSpec::For {224					over,225					loop_invariant: true,226					..227				} => {228					let val = evaluate(ctx.clone(), over)?;229					let Val::Arr(arr) = val else {230						bail!(InComprehensionCanOnlyIterateOverArray)231					};232					Some(arr)233				}234				_ => None,235			})236		})237		.collect::<Result<_>>()238}239240fn evaluate_compspecs_eager(241	ctx: Context,242	specs: &[LCompSpec],243	cached_overs: &[Option<ArrValue>],244	idx: usize,245	guaranteed_reserve: usize,246	collector: &mut dyn CompCollector,247) -> Result<()> {248	if idx >= specs.len() {249		collector.reserve(guaranteed_reserve);250		return collector.collect(ctx);251	}252	match &specs[idx] {253		LCompSpec::If(cond) => {254			let val = evaluate(ctx.clone(), cond)?;255			let Val::Bool(b) = val else {256				bail!(TypeMismatch(257					"if spec condition",258					vec![ValType::Bool],259					val.value_type()260				))261			};262			if b {263				evaluate_compspecs_eager(ctx, specs, cached_overs, idx + 1, 0, collector)?;264			}265		}266		LCompSpec::For {267			frame_shape,268			destruct,269			over,270			..271		} => {272			let arr = if let Some(cached) = &cached_overs[idx] {273				cached.clone()274			} else {275				let arr_val = evaluate(ctx.clone(), over)?;276				let Val::Arr(arr) = arr_val else {277					bail!(InComprehensionCanOnlyIterateOverArray)278				};279				arr280			};281			let inner_reserve = guaranteed_reserve.max(1) * arr.len() as usize;282			match destruct {283				LDestruct::Full(slot) => {284					Context::enter_iter(&ctx, frame_shape, |it| {285						for (i, item) in arr.iter().enumerate() {286							let item = item?;287							let ctx = it.create(|f| {288								f.set(*slot, Thunk::evaluated(item));289							})?;290							evaluate_compspecs_eager(291								ctx,292								specs,293								cached_overs,294								idx + 1,295								if i == 0 { inner_reserve } else { 0 },296								collector,297							)?;298						}299						Ok(())300					})?;301				}302				// TODO: Should not be eager? CoW won't work here303				#[cfg(feature = "exp-destruct")]304				_ => unreachable!("eager compspecs are not possible with non-full patterns"),305			}306		}307	}308	Ok(())309}310311fn evaluate_compspecs(312	ctx: Context,313	specs: &[LCompSpec],314	cached_overs: &[Option<ArrValue>],315	idx: usize,316	guaranteed_reserve: usize,317	collector: &mut dyn CompCollector,318) -> Result<()> {319	if idx >= specs.len() {320		collector.reserve(guaranteed_reserve);321		return collector.collect(ctx);322	}323	match &specs[idx] {324		LCompSpec::If(cond) => {325			let val = evaluate(ctx.clone(), cond)?;326			let Val::Bool(b) = val else {327				bail!(TypeMismatch(328					"if spec condition",329					vec![ValType::Bool],330					val.value_type()331				))332			};333			if b {334				evaluate_compspecs(ctx, specs, cached_overs, idx + 1, 0, collector)?;335			}336		}337		LCompSpec::For {338			frame_shape,339			destruct: dst,340			over,341			..342		} => {343			let arr = if let Some(cached) = &cached_overs[idx] {344				cached.clone()345			} else {346				let arr_val = evaluate(ctx.clone(), over)?;347				let Val::Arr(arr) = arr_val else {348					bail!(InComprehensionCanOnlyIterateOverArray)349				};350				arr351			};352			let inner_reserve = guaranteed_reserve.max(1) * arr.len() as usize;353			for (i, item) in arr.iter().enumerate() {354				let item = item?;355				let inner_ctx = ctx.pack_captures_sup_this(frame_shape).enter(|fill, ctx| {356					destruct(dst, fill, Thunk::evaluated(item), &ctx);357				});358				evaluate_compspecs(359					inner_ctx,360					specs,361					cached_overs,362					idx + 1,363					if i == 0 { inner_reserve } else { 0 },364					collector,365				)?;366			}367		}368	}369	Ok(())370}
after · crates/jrsonnet-evaluator/src/evaluate/compspec.rs
1use std::rc::Rc;23use jrsonnet_types::ValType;45use super::{6	destructure::{destruct, evaluate_locals_unbound, fill_letrec_binds},7	evaluate_field_member_static, evaluate_field_member_unbound,8};9#[cfg(feature = "exp-object-iteration")]10use jrsonnet_interner::IStr;1112use crate::{13	Context, ObjValue, ObjValueBuilder, Result, Thunk, Val,14	analyze::{15		ClosureShape, LArrComp, LBind, LCompSpec, LDestruct, LExpr, LFieldMember, LObjComp,16		LocalSlot,17	},18	arr::ArrValue,19	bail,20	error::ErrorKind::*,21	evaluate::{evaluate, evaluate_trivial},22};2324enum CachedOver {25	Arr(ArrValue),26	#[cfg(feature = "exp-object-iteration")]27	Obj(ObjValue),28}2930trait CompCollector {31	fn reserve(&mut self, _guaranteed: usize) {}32	fn collect(&mut self, ctx: Context) -> Result<()>;33}3435struct EagerArrCollector<'a> {36	out: &'a mut Vec<Val>,37	value_shape: &'a ClosureShape,38	value: &'a LExpr,39}40impl CompCollector for EagerArrCollector<'_> {41	fn reserve(&mut self, size_hint: usize) {42		self.out.reserve(size_hint);43	}44	fn collect(&mut self, ctx: Context) -> Result<()> {45		if let Some(v) = evaluate_trivial(self.value) {46			self.out.push(v);47			return Ok(());48		}49		if let LExpr::Slot(slot) = self.value {50			self.out.push(ctx.slot(*slot).evaluate()?);51			return Ok(());52		}53		let env = Context::enter_using(&ctx, self.value_shape);54		self.out.push(evaluate(env, self.value)?);55		Ok(())56	}57}5859struct LazyArrCollector<'a> {60	out: &'a mut Vec<Thunk<Val>>,61	value_shape: &'a ClosureShape,62	value: &'a Rc<LExpr>,63}64impl CompCollector for LazyArrCollector<'_> {65	fn reserve(&mut self, size_hint: usize) {66		self.out.reserve(size_hint);67	}68	fn collect(&mut self, ctx: Context) -> Result<()> {69		if let Some(v) = evaluate_trivial(self.value) {70			self.out.push(Thunk::evaluated(v));71			return Ok(());72		}73		if let LExpr::Slot(slot) = self.value.as_ref() {74			self.out.push(ctx.slot(*slot));75			return Ok(());76		}77		let env = Context::enter_using(&ctx, self.value_shape);78		let value_expr = self.value.clone();79		self.out.push(Thunk!(move || evaluate(env, &value_expr)));80		Ok(())81	}82}8384struct ObjCompCollectorStatic<'a> {85	builder: &'a mut ObjValueBuilder,86	frame_shape: &'a ClosureShape,87	locals: &'a [LBind],88	field: &'a LFieldMember,89}90impl CompCollector for ObjCompCollectorStatic<'_> {91	fn reserve(&mut self, guaranteed: usize) {92		self.builder.reserve_fields(guaranteed);93	}94	fn collect(&mut self, inner_ctx: Context) -> Result<()> {95		// Build the object's A-frame fresh per iteration: captures from96		// the comp's iter ctx, locals = `this` (slot 0, unfilled in the97		// static path) + member-locals via letrec.98		let value_ctx = inner_ctx99			.pack_captures_sup_this(self.frame_shape)100			.enter(|fill, ctx| {101				fill_letrec_binds(fill, &ctx, self.locals);102			});103		evaluate_field_member_static(self.builder, inner_ctx, value_ctx, self.field)104	}105}106107struct ObjCompCollectorUnbound<'a> {108	builder: &'a mut ObjValueBuilder,109	frame_shape: Rc<ClosureShape>,110	locals: Rc<Vec<LBind>>,111	this_slot: Option<LocalSlot>,112	field: &'a LFieldMember,113}114impl CompCollector for ObjCompCollectorUnbound<'_> {115	fn reserve(&mut self, guaranteed: usize) {116		self.builder.reserve_fields(guaranteed);117	}118	fn collect(&mut self, inner_ctx: Context) -> Result<()> {119		let uctx = evaluate_locals_unbound(120			&inner_ctx,121			&self.frame_shape,122			self.this_slot,123			self.locals.clone(),124		);125		evaluate_field_member_unbound(self.builder, inner_ctx, uctx, self.field)126	}127}128129pub fn evaluate_obj_comp(130	super_obj: Option<ObjValue>,131	ctx: Context,132	comp: &LObjComp,133) -> Result<Val> {134	let mut builder = ObjValueBuilder::new();135	if let Some(super_obj) = super_obj {136		builder.with_super(super_obj);137	}138139	let cached_overs = cache_overs(&ctx, &comp.compspecs)?;140	if comp.this.is_some() || comp.uses_super {141		evaluate_compspecs(142			ctx,143			&comp.compspecs,144			&cached_overs,145			0,146			0,147			&mut ObjCompCollectorUnbound {148				builder: &mut builder,149				frame_shape: comp.frame_shape.clone(),150				locals: comp.locals.clone(),151				this_slot: comp.this,152				field: &comp.field,153			},154		)?;155	} else {156		evaluate_compspecs(157			ctx,158			&comp.compspecs,159			&cached_overs,160			0,161			0,162			&mut ObjCompCollectorStatic {163				builder: &mut builder,164				frame_shape: &comp.frame_shape,165				locals: &comp.locals,166				field: &comp.field,167			},168		)?;169	}170171	Ok(Val::Obj(builder.build()))172}173174pub fn evaluate_arr_comp(ctx: Context, comp: &LArrComp) -> Result<Val> {175	let cached_overs = cache_overs(&ctx, &comp.compspecs)?;176177	// Eager fast-path: when the comp has only `if` and `for { destruct: Full(_) }`178	// specs, allocate one Iter A-frame per for-spec and re-set the slot179	// per iteration as long as the frame's refcount stays at 1.180	'eager: {181		let mut out = Vec::new();182183		if comp.compspecs.iter().all(|c| {184			matches!(185				c,186				LCompSpec::If(_)187					| LCompSpec::For {188						destruct: LDestruct::Full(_),189						..190					}191			)192		}) && evaluate_compspecs_eager(193			ctx.clone(),194			&comp.compspecs,195			&cached_overs,196			0,197			0,198			&mut EagerArrCollector {199				out: &mut out,200				value_shape: &comp.value_shape,201				value: &comp.value,202			},203		)204		.is_err()205		{206			break 'eager;207		}208		return Ok(Val::arr(out));209	}210211	let mut items: Vec<Thunk<Val>> = Vec::new();212	evaluate_compspecs(213		ctx,214		&comp.compspecs,215		&cached_overs,216		0,217		0,218		&mut LazyArrCollector {219			out: &mut items,220			value_shape: &comp.value_shape,221			value: &comp.value,222		},223	)?;224	Ok(Val::arr(items))225}226227fn cache_overs(ctx: &Context, specs: &[LCompSpec]) -> Result<Vec<Option<CachedOver>>> {228	specs229		.iter()230		.map(|spec| {231			Ok(match spec {232				LCompSpec::For {233					over,234					loop_invariant: true,235					..236				} => {237					let val = evaluate(ctx.clone(), over)?;238					let Val::Arr(arr) = val else {239						bail!(InComprehensionCanOnlyIterateOverArray)240					};241					Some(CachedOver::Arr(arr))242				}243				#[cfg(feature = "exp-object-iteration")]244				LCompSpec::ForObj {245					over,246					loop_invariant: true,247					..248				} => {249					let val = evaluate(ctx.clone(), over)?;250					let Val::Obj(obj) = val else {251						bail!(TypeMismatch(252							"object iteration over",253							vec![jrsonnet_types::ValType::Obj],254							val.value_type(),255						))256					};257					Some(CachedOver::Obj(obj))258				}259				_ => None,260			})261		})262		.collect::<Result<_>>()263}264265fn evaluate_compspecs_eager(266	ctx: Context,267	specs: &[LCompSpec],268	cached_overs: &[Option<CachedOver>],269	idx: usize,270	guaranteed_reserve: usize,271	collector: &mut dyn CompCollector,272) -> Result<()> {273	if idx >= specs.len() {274		collector.reserve(guaranteed_reserve);275		return collector.collect(ctx);276	}277	match &specs[idx] {278		LCompSpec::If(cond) => {279			let val = evaluate(ctx.clone(), cond)?;280			let Val::Bool(b) = val else {281				bail!(TypeMismatch(282					"if spec condition",283					vec![ValType::Bool],284					val.value_type()285				))286			};287			if b {288				evaluate_compspecs_eager(ctx, specs, cached_overs, idx + 1, 0, collector)?;289			}290		}291		LCompSpec::For {292			frame_shape,293			destruct,294			over,295			..296		} => {297			let arr = if let Some(CachedOver::Arr(cached)) = &cached_overs[idx] {298				cached.clone()299			} else {300				let arr_val = evaluate(ctx.clone(), over)?;301				let Val::Arr(arr) = arr_val else {302					bail!(InComprehensionCanOnlyIterateOverArray)303				};304				arr305			};306			let inner_reserve = guaranteed_reserve.max(1) * arr.len() as usize;307			match destruct {308				LDestruct::Full(slot) => {309					Context::enter_iter(&ctx, frame_shape, |it| {310						for (i, item) in arr.iter().enumerate() {311							let item = item?;312							let ctx = it.create(|f| {313								f.set(*slot, Thunk::evaluated(item));314							})?;315							evaluate_compspecs_eager(316								ctx,317								specs,318								cached_overs,319								idx + 1,320								if i == 0 { inner_reserve } else { 0 },321								collector,322							)?;323						}324						Ok(())325					})?;326				}327				// TODO: Should not be eager? CoW won't work here328				#[cfg(feature = "exp-destruct")]329				_ => unreachable!("eager compspecs are not possible with non-full patterns"),330			}331		}332		#[cfg(feature = "exp-object-iteration")]333		LCompSpec::ForObj { .. } => {334			unreachable!("eager compspecs filter rejects ForObj");335		}336	}337	Ok(())338}339340fn evaluate_compspecs(341	ctx: Context,342	specs: &[LCompSpec],343	cached_overs: &[Option<CachedOver>],344	idx: usize,345	guaranteed_reserve: usize,346	collector: &mut dyn CompCollector,347) -> Result<()> {348	if idx >= specs.len() {349		collector.reserve(guaranteed_reserve);350		return collector.collect(ctx);351	}352	match &specs[idx] {353		LCompSpec::If(cond) => {354			let val = evaluate(ctx.clone(), cond)?;355			let Val::Bool(b) = val else {356				bail!(TypeMismatch(357					"if spec condition",358					vec![ValType::Bool],359					val.value_type()360				))361			};362			if b {363				evaluate_compspecs(ctx, specs, cached_overs, idx + 1, 0, collector)?;364			}365		}366		LCompSpec::For {367			frame_shape,368			destruct: dst,369			over,370			..371		} => {372			let arr = if let Some(CachedOver::Arr(cached)) = &cached_overs[idx] {373				cached.clone()374			} else {375				let arr_val = evaluate(ctx.clone(), over)?;376				let Val::Arr(arr) = arr_val else {377					bail!(InComprehensionCanOnlyIterateOverArray)378				};379				arr380			};381			let inner_reserve = guaranteed_reserve.max(1) * arr.len() as usize;382			for (i, item) in arr.iter().enumerate() {383				let item = item?;384				let inner_ctx = ctx.pack_captures_sup_this(frame_shape).enter(|fill, ctx| {385					destruct(dst, fill, Thunk::evaluated(item), &ctx);386				});387				evaluate_compspecs(388					inner_ctx,389					specs,390					cached_overs,391					idx + 1,392					if i == 0 { inner_reserve } else { 0 },393					collector,394				)?;395			}396		}397		#[cfg(feature = "exp-object-iteration")]398		LCompSpec::ForObj {399			frame_shape,400			key,401			visibility,402			value,403			over,404			..405		} => {406			use jrsonnet_ir::Visibility;407			let obj = if let Some(CachedOver::Obj(cached)) = &cached_overs[idx] {408				cached.clone()409			} else {410				let val = evaluate(ctx.clone(), over)?;411				let Val::Obj(obj) = val else {412					bail!(TypeMismatch(413						"object iteration over",414						vec![ValType::Obj],415						val.value_type(),416					))417				};418				obj419			};420			let fields = obj.fields_with_visibility(421				#[cfg(feature = "exp-preserve-order")]422				false,423			);424			let pairs: Vec<(IStr, Visibility)> = fields425				.into_iter()426				.filter(|(_, v)| match visibility {427					Visibility::Normal => v.is_visible(),428					Visibility::Hidden => !v.is_visible(),429					Visibility::Unhide => true,430				})431				.collect();432			let inner_reserve = guaranteed_reserve.max(1) * pairs.len();433			for (i, (field_name, _)) in pairs.into_iter().enumerate() {434				let key_val = Val::string(field_name.clone());435				let value_thunk = obj436					.get_lazy(field_name.clone())437					.expect("field exists, just enumerated");438				let inner_ctx = ctx.pack_captures_sup_this(frame_shape).enter(|fill, ctx| {439					fill.set(*key, Thunk::evaluated(key_val));440					destruct(value, fill, value_thunk, &ctx);441				});442				evaluate_compspecs(443					inner_ctx,444					specs,445					cached_overs,446					idx + 1,447					if i == 0 { inner_reserve } else { 0 },448					collector,449				)?;450			}451		}452	}453	Ok(())454}
modifiedcrates/jrsonnet-evaluator/src/obj/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/obj/mod.rs
+++ b/crates/jrsonnet-evaluator/src/obj/mod.rs
@@ -909,6 +909,56 @@
 
 		out
 	}
+	pub fn fields_with_visibility(
+		&self,
+		#[cfg(feature = "exp-preserve-order")] preserve_order: bool,
+	) -> Vec<(IStr, Visibility)> {
+		#[cfg(feature = "exp-preserve-order")]
+		if preserve_order {
+			let (mut fields, mut keys): (Vec<_>, Vec<_>) = self
+				.fields_visibility()
+				.into_iter()
+				.enumerate()
+				.map(|(idx, (k, d))| {
+					(
+						(
+							k,
+							d.exists_visible.expect("non-existing fields filtered out"),
+						),
+						(d.sort_key(), idx),
+					)
+				})
+				.unzip();
+			keys.sort_unstable_by_key(|v| v.0);
+			for i in 0..fields.len() {
+				let x = fields[i].clone();
+				let mut j = i;
+				loop {
+					let k = keys[j].1;
+					keys[j].1 = j;
+					if k == i {
+						break;
+					}
+					fields[j] = fields[k].clone();
+					j = k;
+				}
+				fields[j] = x;
+			}
+			return fields;
+		}
+		let mut fields: Vec<_> = self
+			.fields_visibility()
+			.into_iter()
+			.map(|(k, d)| {
+				(
+					k,
+					d.exists_visible.expect("non-existing fields filtered out"),
+				)
+			})
+			.collect();
+		fields.sort_unstable_by(|a, b| a.0.cmp(&b.0));
+		fields
+	}
 	pub fn fields_ex(
 		&self,
 		include_hidden: bool,
modifiedcrates/jrsonnet-peg-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-peg-parser/src/lib.rs
+++ b/crates/jrsonnet-peg-parser/src/lib.rs
@@ -441,6 +441,7 @@
 	use crate::{ParserSettings, parse};
 
 	#[test]
+	#[cfg(not(feature = "exp-null-coaelse"))]
 	fn snapshots() {
 		glob!("tests/*.jsonnet", |path| {
 			let input = fs::read_to_string(path).expect("read test file");