--- 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"); --- a/crates/jrsonnet-evaluator/src/evaluate/compspec.rs +++ b/crates/jrsonnet-evaluator/src/evaluate/compspec.rs @@ -6,6 +6,9 @@ destructure::{destruct, evaluate_locals_unbound, fill_letrec_binds}, evaluate_field_member_static, evaluate_field_member_unbound, }; +#[cfg(feature = "exp-object-iteration")] +use jrsonnet_interner::IStr; + use crate::{ Context, ObjValue, ObjValueBuilder, Result, Thunk, Val, analyze::{ @@ -18,6 +21,12 @@ evaluate::{evaluate, evaluate_trivial}, }; +enum CachedOver { + Arr(ArrValue), + #[cfg(feature = "exp-object-iteration")] + Obj(ObjValue), +} + trait CompCollector { fn reserve(&mut self, _guaranteed: usize) {} fn collect(&mut self, ctx: Context) -> Result<()>; @@ -215,7 +224,7 @@ Ok(Val::arr(items)) } -fn cache_overs(ctx: &Context, specs: &[LCompSpec]) -> Result>> { +fn cache_overs(ctx: &Context, specs: &[LCompSpec]) -> Result>> { specs .iter() .map(|spec| { @@ -229,7 +238,23 @@ let Val::Arr(arr) = val else { bail!(InComprehensionCanOnlyIterateOverArray) }; - Some(arr) + Some(CachedOver::Arr(arr)) + } + #[cfg(feature = "exp-object-iteration")] + LCompSpec::ForObj { + over, + loop_invariant: true, + .. + } => { + let val = evaluate(ctx.clone(), over)?; + let Val::Obj(obj) = val else { + bail!(TypeMismatch( + "object iteration over", + vec![jrsonnet_types::ValType::Obj], + val.value_type(), + )) + }; + Some(CachedOver::Obj(obj)) } _ => None, }) @@ -240,7 +265,7 @@ fn evaluate_compspecs_eager( ctx: Context, specs: &[LCompSpec], - cached_overs: &[Option], + cached_overs: &[Option], idx: usize, guaranteed_reserve: usize, collector: &mut dyn CompCollector, @@ -269,7 +294,7 @@ over, .. } => { - let arr = if let Some(cached) = &cached_overs[idx] { + let arr = if let Some(CachedOver::Arr(cached)) = &cached_overs[idx] { cached.clone() } else { let arr_val = evaluate(ctx.clone(), over)?; @@ -304,6 +329,10 @@ _ => unreachable!("eager compspecs are not possible with non-full patterns"), } } + #[cfg(feature = "exp-object-iteration")] + LCompSpec::ForObj { .. } => { + unreachable!("eager compspecs filter rejects ForObj"); + } } Ok(()) } @@ -311,7 +340,7 @@ fn evaluate_compspecs( ctx: Context, specs: &[LCompSpec], - cached_overs: &[Option], + cached_overs: &[Option], idx: usize, guaranteed_reserve: usize, collector: &mut dyn CompCollector, @@ -340,7 +369,7 @@ over, .. } => { - let arr = if let Some(cached) = &cached_overs[idx] { + let arr = if let Some(CachedOver::Arr(cached)) = &cached_overs[idx] { cached.clone() } else { let arr_val = evaluate(ctx.clone(), over)?; @@ -365,6 +394,61 @@ )?; } } + #[cfg(feature = "exp-object-iteration")] + LCompSpec::ForObj { + frame_shape, + key, + visibility, + value, + over, + .. + } => { + use jrsonnet_ir::Visibility; + let obj = if let Some(CachedOver::Obj(cached)) = &cached_overs[idx] { + cached.clone() + } else { + let val = evaluate(ctx.clone(), over)?; + let Val::Obj(obj) = val else { + bail!(TypeMismatch( + "object iteration over", + vec![ValType::Obj], + val.value_type(), + )) + }; + obj + }; + let fields = obj.fields_with_visibility( + #[cfg(feature = "exp-preserve-order")] + false, + ); + let pairs: Vec<(IStr, Visibility)> = fields + .into_iter() + .filter(|(_, v)| match visibility { + Visibility::Normal => v.is_visible(), + Visibility::Hidden => !v.is_visible(), + Visibility::Unhide => true, + }) + .collect(); + let inner_reserve = guaranteed_reserve.max(1) * pairs.len(); + for (i, (field_name, _)) in pairs.into_iter().enumerate() { + let key_val = Val::string(field_name.clone()); + let value_thunk = obj + .get_lazy(field_name.clone()) + .expect("field exists, just enumerated"); + let inner_ctx = ctx.pack_captures_sup_this(frame_shape).enter(|fill, ctx| { + fill.set(*key, Thunk::evaluated(key_val)); + destruct(value, fill, value_thunk, &ctx); + }); + evaluate_compspecs( + inner_ctx, + specs, + cached_overs, + idx + 1, + if i == 0 { inner_reserve } else { 0 }, + collector, + )?; + } + } } Ok(()) } --- 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, --- 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");