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

difftreelog

feat allow iteration over objects in comprehensions

Yaroslav Bolyukin2022-11-09parent: #1b7795b.patch.diff
in: master
exp-object-iteration feature

3 files changed

modifiedcmds/jrsonnet/Cargo.tomldiffbeforeafterboth
--- a/cmds/jrsonnet/Cargo.toml
+++ b/cmds/jrsonnet/Cargo.toml
@@ -17,6 +17,9 @@
 ]
 # Destructuring of locals
 exp-destruct = ["jrsonnet-evaluator/exp-destruct"]
+# Iteration over objects yields [key, value] elements
+exp-object-iteration = ["jrsonnet-evaluator/exp-object-iteration"]
+
 # std.thisFile support
 legacy-this-file = ["jrsonnet-cli/legacy-this-file"]
 
modifiedcrates/jrsonnet-evaluator/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/Cargo.toml
+++ b/crates/jrsonnet-evaluator/Cargo.toml
@@ -20,6 +20,9 @@
 exp-preserve-order = []
 # Implements field destructuring
 exp-destruct = ["jrsonnet-parser/exp-destruct"]
+# Iteration over objects yields [key, value] elements
+exp-object-iteration = []
+
 # Improves performance, and implements some useful things using nightly-only features
 nightly = []
 
modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/evaluate/mod.rs
1use std::{cmp::Ordering, rc::Rc};23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_parser::{6	ArgsDesc, AssertStmt, BindSpec, CompSpec, Expr, FieldMember, FieldName, ForSpecData,7	IfSpecData, LiteralType, LocExpr, Member, ObjBody, ParamsDesc,8};9use jrsonnet_types::ValType;1011use self::destructure::destruct;12use crate::{13	destructure::evaluate_dest,14	error::Error::*,15	evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},16	function::{CallLocation, FuncDesc, FuncVal},17	tb, throw,18	typed::Typed,19	val::{ArrValue, CachedUnbound, IndexableVal, Thunk, ThunkValue},20	Context, GcHashMap, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result, State,21	Unbound, Val,22};23pub mod destructure;24pub mod operator;2526pub fn evaluate_method(ctx: Context, name: IStr, params: ParamsDesc, body: LocExpr) -> Val {27	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {28		name,29		ctx,30		params,31		body,32	})))33}3435pub fn evaluate_field_name(ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {36	Ok(match field_name {37		FieldName::Fixed(n) => Some(n.clone()),38		FieldName::Dyn(expr) => State::push(39			CallLocation::new(&expr.1),40			|| "evaluating field name".to_string(),41			|| {42				let value = evaluate(ctx, expr)?;43				if matches!(value, Val::Null) {44					Ok(None)45				} else {46					Ok(Some(IStr::from_untyped(value)?))47				}48			},49		)?,50	})51}5253pub fn evaluate_comp(54	ctx: Context,55	specs: &[CompSpec],56	callback: &mut impl FnMut(Context) -> Result<()>,57) -> Result<()> {58	match specs.get(0) {59		None => callback(ctx)?,60		Some(CompSpec::IfSpec(IfSpecData(cond))) => {61			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {62				evaluate_comp(ctx, &specs[1..], callback)?;63			}64		}65		Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {66			Val::Arr(list) => {67				for item in list.iter_lazy() {68					let fctx = Pending::new();69					let mut new_bindings = GcHashMap::new();70					destruct(var, item, fctx.clone(), &mut new_bindings)?;71					let ctx = ctx72						.clone()73						.extend(new_bindings, None, None, None)74						.into_future(fctx);7576					evaluate_comp(ctx, &specs[1..], callback)?;77				}78			}79			_ => throw!(InComprehensionCanOnlyIterateOverArray),80		},81	}82	Ok(())83}8485trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}86impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}8788fn evaluate_object_locals(89	fctx: Pending<Context>,90	locals: Rc<Vec<BindSpec>>,91) -> impl CloneableUnbound<Context> {92	#[derive(Trace, Clone)]93	struct UnboundLocals {94		fctx: Pending<Context>,95		locals: Rc<Vec<BindSpec>>,96	}97	impl Unbound for UnboundLocals {98		type Bound = Context;99100		fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Context> {101			let fctx = Context::new_future();102			let mut new_bindings = GcHashMap::new();103			for b in self.locals.iter() {104				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;105			}106107			let ctx = self.fctx.unwrap();108			let new_dollar = ctx.dollar().clone().or_else(|| this.clone());109110			let ctx = ctx111				.extend(new_bindings, new_dollar, sup, this)112				.into_future(fctx);113114			Ok(ctx)115		}116	}117118	UnboundLocals { fctx, locals }119}120121pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(122	builder: &mut ObjValueBuilder,123	ctx: Context,124	uctx: B,125	field: &FieldMember,126) -> Result<()> {127	let name = evaluate_field_name(ctx.clone(), &field.name)?;128	let Some(name) = name else {129		return Ok(());130	};131132	match field {133		FieldMember {134			plus,135			params: None,136			visibility,137			value,138			..139		} => {140			#[derive(Trace)]141			struct UnboundValue<B: Trace> {142				uctx: B,143				value: LocExpr,144				name: IStr,145			}146			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {147				type Bound = Val;148				fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {149					Ok(evaluate_named(150						self.uctx.bind(sup, this)?,151						&self.value,152						self.name.clone(),153					)?)154				}155			}156157			builder158				.member(name.clone())159				.with_add(*plus)160				.with_visibility(*visibility)161				.with_location(value.1.clone())162				.bindable(tb!(UnboundValue {163					uctx: uctx.clone(),164					value: value.clone(),165					name: name.clone()166				}))?;167		}168		FieldMember {169			params: Some(params),170			value,171			..172		} => {173			#[derive(Trace)]174			struct UnboundMethod<B: Trace> {175				uctx: B,176				value: LocExpr,177				params: ParamsDesc,178				name: IStr,179			}180			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {181				type Bound = Val;182				fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {183					Ok(evaluate_method(184						self.uctx.bind(sup, this)?,185						self.name.clone(),186						self.params.clone(),187						self.value.clone(),188					))189				}190			}191192			builder193				.member(name.clone())194				.hide()195				.with_location(value.1.clone())196				.bindable(tb!(UnboundMethod {197					uctx: uctx.clone(),198					value: value.clone(),199					params: params.clone(),200					name: name.clone()201				}))?;202		}203	}204	Ok(())205}206207#[allow(clippy::too_many_lines)]208pub fn evaluate_member_list_object(ctx: Context, members: &[Member]) -> Result<ObjValue> {209	let mut builder = ObjValueBuilder::new();210	let locals = Rc::new(211		members212			.iter()213			.filter_map(|m| match m {214				Member::BindStmt(bind) => Some(bind.clone()),215				_ => None,216			})217			.collect::<Vec<_>>(),218	);219220	let fctx = Context::new_future();221222	// We have single context for all fields, so we can cache binds223	let uctx = CachedUnbound::new(evaluate_object_locals(fctx.clone(), locals));224225	for member in members.iter() {226		match member {227			Member::Field(field) => {228				evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), &field)?229			}230			Member::AssertStmt(stmt) => {231				#[derive(Trace)]232				struct ObjectAssert<B: Trace> {233					uctx: B,234					assert: AssertStmt,235				}236				impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {237					fn run(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<()> {238						let ctx = self.uctx.bind(sup, this)?;239						evaluate_assert(ctx, &self.assert)240					}241				}242				builder.assert(tb!(ObjectAssert {243					uctx: uctx.clone(),244					assert: stmt.clone(),245				}));246			}247			Member::BindStmt(_) => {248				// Already handled249			}250		}251	}252	let this = builder.build();253	fctx.fill(ctx.extend(GcHashMap::new(), None, None, Some(this.clone())));254	Ok(this)255}256257pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {258	Ok(match object {259		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,260		ObjBody::ObjComp(obj) => {261			let mut builder = ObjValueBuilder::new();262			let locals = Rc::new(263				obj.pre_locals264					.iter()265					.chain(obj.post_locals.iter())266					.cloned()267					.collect::<Vec<_>>(),268			);269			let mut ctxs = vec![];270			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {271				let fctx = Context::new_future();272				ctxs.push((ctx.clone(), fctx.clone()));273				let uctx = evaluate_object_locals(fctx, locals.clone());274275				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)276			})?;277278			let this = builder.build();279			for (ctx, fctx) in ctxs {280				let _ctx = ctx281					.extend(GcHashMap::new(), None, None, Some(this.clone()))282					.into_future(fctx);283			}284			this285		}286	})287}288289pub fn evaluate_apply(290	ctx: Context,291	value: &LocExpr,292	args: &ArgsDesc,293	loc: CallLocation<'_>,294	tailstrict: bool,295) -> Result<Val> {296	let value = evaluate(ctx.clone(), value)?;297	Ok(match value {298		Val::Func(f) => {299			let body = || f.evaluate(ctx, loc, args, tailstrict);300			if tailstrict {301				body()?302			} else {303				State::push(loc, || format!("function <{}> call", f.name()), body)?304			}305		}306		v => throw!(OnlyFunctionsCanBeCalledGot(v.value_type())),307	})308}309310pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {311	let value = &assertion.0;312	let msg = &assertion.1;313	let assertion_result = State::push(314		CallLocation::new(&value.1),315		|| "assertion condition".to_owned(),316		|| bool::from_untyped(evaluate(ctx.clone(), value)?),317	)?;318	if !assertion_result {319		State::push(320			CallLocation::new(&value.1),321			|| "assertion failure".to_owned(),322			|| {323				if let Some(msg) = msg {324					throw!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));325				}326				throw!(AssertionFailed(Val::Null.to_string()?));327			},328		)?;329	}330	Ok(())331}332333pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result<Val> {334	use Expr::*;335	let LocExpr(raw_expr, _loc) = expr;336	Ok(match &**raw_expr {337		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),338		_ => evaluate(ctx, expr)?,339	})340}341342#[allow(clippy::too_many_lines)]343pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result<Val> {344	use Expr::*;345	let LocExpr(expr, loc) = expr;346	// let bp = with_state(|s| s.0.stop_at.borrow().clone());347	Ok(match &**expr {348		Literal(LiteralType::This) => {349			Val::Obj(ctx.this().clone().ok_or(CantUseSelfOutsideOfObject)?)350		}351		Literal(LiteralType::Super) => Val::Obj(352			ctx.super_obj().clone().ok_or(NoSuperFound)?.with_this(353				ctx.this()354					.clone()355					.expect("if super exists - then this should to"),356			),357		),358		Literal(LiteralType::Dollar) => {359			Val::Obj(ctx.dollar().clone().ok_or(NoTopLevelObjectFound)?)360		}361		Literal(LiteralType::True) => Val::Bool(true),362		Literal(LiteralType::False) => Val::Bool(false),363		Literal(LiteralType::Null) => Val::Null,364		Parened(e) => evaluate(ctx, e)?,365		Str(v) => Val::Str(v.clone()),366		Num(v) => Val::new_checked_num(*v)?,367		BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,368		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,369		Var(name) => State::push(370			CallLocation::new(loc),371			|| format!("variable <{name}> access"),372			|| ctx.binding(name.clone())?.evaluate(),373		)?,374		Index(value, index) => match (evaluate(ctx.clone(), value)?, evaluate(ctx, index)?) {375			(Val::Obj(v), Val::Str(key)) => State::push(376				CallLocation::new(loc),377				|| format!("field <{key}> access"),378				|| match v.get(key.clone()) {379					Ok(Some(v)) => Ok(v),380					#[cfg(not(feature = "friendly-errors"))]381					Ok(None) => throw!(NoSuchField(key.clone(), vec![])),382					#[cfg(feature = "friendly-errors")]383					Ok(None) => {384						let mut heap = Vec::new();385						for field in v.fields_ex(386							true,387							#[cfg(feature = "exp-preserve-order")]388							false,389						) {390							let conf = strsim::jaro_winkler(&field as &str, &key as &str);391							if conf < 0.8 {392								continue;393							}394							heap.push((conf, field));395						}396						heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));397398						throw!(NoSuchField(399							key.clone(),400							heap.into_iter().map(|(_, v)| v).collect()401						))402					}403					Err(e) => Err(e),404				},405			)?,406			(Val::Obj(_), n) => throw!(ValueIndexMustBeTypeGot(407				ValType::Obj,408				ValType::Str,409				n.value_type(),410			)),411412			(Val::Arr(v), Val::Num(n)) => {413				if n.fract() > f64::EPSILON {414					throw!(FractionalIndex)415				}416				v.get(n as usize)?417					.ok_or_else(|| ArrayBoundsError(n as usize, v.len()))?418			}419			(Val::Arr(_), Val::Str(n)) => throw!(AttemptedIndexAnArrayWithString(n)),420			(Val::Arr(_), n) => throw!(ValueIndexMustBeTypeGot(421				ValType::Arr,422				ValType::Num,423				n.value_type(),424			)),425426			(Val::Str(s), Val::Num(n)) => Val::Str({427				let v: IStr = s428					.chars()429					.skip(n as usize)430					.take(1)431					.collect::<String>()432					.into();433				if v.is_empty() {434					let size = s.chars().count();435					throw!(StringBoundsError(n as usize, size))436				}437				v438			}),439			(Val::Str(_), n) => throw!(ValueIndexMustBeTypeGot(440				ValType::Str,441				ValType::Num,442				n.value_type(),443			)),444445			(v, _) => throw!(CantIndexInto(v.value_type())),446		},447		LocalExpr(bindings, returned) => {448			let mut new_bindings: GcHashMap<IStr, Thunk<Val>> =449				GcHashMap::with_capacity(bindings.len());450			let fctx = Context::new_future();451			for b in bindings {452				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;453			}454			let ctx = ctx.extend(new_bindings, None, None, None).into_future(fctx);455			evaluate(ctx, &returned.clone())?456		}457		Arr(items) => {458			let mut out = Vec::with_capacity(items.len());459			for item in items {460				// TODO: Implement ArrValue::Lazy with same context for every element?461				#[derive(Trace)]462				struct ArrayElement {463					ctx: Context,464					item: LocExpr,465				}466				impl ThunkValue for ArrayElement {467					type Output = Val;468					fn get(self: Box<Self>) -> Result<Val> {469						evaluate(self.ctx, &self.item)470					}471				}472				out.push(Thunk::new(tb!(ArrayElement {473					ctx: ctx.clone(),474					item: item.clone(),475				})));476			}477			Val::Arr(out.into())478		}479		ArrComp(expr, comp_specs) => {480			let mut out = Vec::new();481			evaluate_comp(ctx, comp_specs, &mut |ctx| {482				out.push(evaluate(ctx, expr)?);483				Ok(())484			})?;485			Val::Arr(ArrValue::Eager(Cc::new(out)))486		}487		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),488		ObjExtend(a, b) => evaluate_add_op(489			&evaluate(ctx.clone(), a)?,490			&Val::Obj(evaluate_object(ctx, b)?),491		)?,492		Apply(value, args, tailstrict) => {493			evaluate_apply(ctx, value, args, CallLocation::new(loc), *tailstrict)?494		}495		Function(params, body) => {496			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())497		}498		AssertExpr(assert, returned) => {499			evaluate_assert(ctx.clone(), assert)?;500			evaluate(ctx, returned)?501		}502		ErrorStmt(e) => State::push(503			CallLocation::new(loc),504			|| "error statement".to_owned(),505			|| throw!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),506		)?,507		IfElse {508			cond,509			cond_then,510			cond_else,511		} => {512			if State::push(513				CallLocation::new(loc),514				|| "if condition".to_owned(),515				|| bool::from_untyped(evaluate(ctx.clone(), &cond.0)?),516			)? {517				evaluate(ctx, cond_then)?518			} else {519				match cond_else {520					Some(v) => evaluate(ctx, v)?,521					None => Val::Null,522				}523			}524		}525		Slice(value, desc) => {526			fn parse_idx<T: Typed>(527				loc: CallLocation<'_>,528				ctx: &Context,529				expr: &Option<LocExpr>,530				desc: &'static str,531			) -> Result<Option<T>> {532				if let Some(value) = expr {533					Ok(Some(State::push(534						loc,535						|| format!("slice {desc}"),536						|| T::from_untyped(evaluate(ctx.clone(), value)?),537					)?))538				} else {539					Ok(None)540				}541			}542543			let indexable = evaluate(ctx.clone(), value)?;544			let loc = CallLocation::new(loc);545546			let start = parse_idx(loc, &ctx, &desc.start, "start")?;547			let end = parse_idx(loc, &ctx, &desc.end, "end")?;548			let step = parse_idx(loc, &ctx, &desc.step, "step")?;549550			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?551		}552		i @ (Import(path) | ImportStr(path) | ImportBin(path)) => {553			let tmp = loc.clone().0;554			let s = ctx.state();555			let resolved_path = s.resolve_from(tmp.source_path(), path as &str)?;556			match i {557				Import(_) => State::push(558					CallLocation::new(loc),559					|| format!("import {:?}", path.clone()),560					|| s.import_resolved(resolved_path),561				)?,562				ImportStr(_) => Val::Str(s.import_resolved_str(resolved_path)?),563				ImportBin(_) => Val::Arr(ArrValue::Bytes(s.import_resolved_bin(resolved_path)?)),564				_ => unreachable!(),565			}566		}567	})568}