git.delta.rocks / jrsonnet / refs/commits / 15dca76ce252

difftreelog

source

crates/jrsonnet-evaluator/src/evaluate/mod.rs19.0 KiBsourcehistory
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	with_state, Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,25	ResultExt, SupThis, 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			_ => false,57		}58	}59	Some(match expr.expr() {60		Expr::Str(s) => Val::string(s.clone()),61		Expr::Num(n) => {62			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))63		}64		Expr::Literal(LiteralType::False) => Val::Bool(false),65		Expr::Literal(LiteralType::True) => Val::Bool(true),66		Expr::Literal(LiteralType::Null) => Val::Null,67		Expr::Arr(n) => {68			if n.iter().any(|e| !is_trivial(e)) {69				return None;70			}71			Val::Arr(ArrValue::eager(72				n.iter()73					.map(evaluate_trivial)74					.map(|e| e.expect("checked trivial"))75					.collect(),76			))77		}78		_ => return None,79	})80}8182pub fn evaluate_method(ctx: Context, name: IStr, params: ParamsDesc, body: LocExpr) -> Val {83	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {84		name,85		ctx,86		params,87		body,88	})))89}9091pub fn evaluate_field_name(ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {92	Ok(match field_name {93		FieldName::Fixed(n) => Some(n.clone()),94		FieldName::Dyn(expr) => in_frame(95			CallLocation::new(&expr.span()),96			|| "evaluating field name".to_string(),97			|| {98				let value = evaluate(ctx, expr)?;99				if matches!(value, Val::Null) {100					Ok(None)101				} else {102					Ok(Some(IStr::from_untyped(value)?))103				}104			},105		)?,106	})107}108109pub fn evaluate_comp(110	ctx: Context,111	specs: &[CompSpec],112	callback: &mut impl FnMut(Context) -> Result<()>,113) -> Result<()> {114	match specs.first() {115		None => callback(ctx)?,116		Some(CompSpec::IfSpec(IfSpecData(cond))) => {117			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {118				evaluate_comp(ctx, &specs[1..], callback)?;119			}120		}121		Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {122			Val::Arr(list) => {123				for item in list.iter_lazy() {124					let fctx = Pending::new();125					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());126					destruct(var, item, fctx.clone(), &mut new_bindings)?;127					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);128129					evaluate_comp(ctx, &specs[1..], callback)?;130				}131			}132			#[cfg(feature = "exp-object-iteration")]133			Val::Obj(obj) => {134				for field in obj.fields(135					// TODO: Should there be ability to preserve iteration order?136					#[cfg(feature = "exp-preserve-order")]137					false,138				) {139					let fctx = Pending::new();140					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());141					let obj = obj.clone();142					let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![143						Thunk::evaluated(Val::string(field.clone())),144						Thunk!(move || obj.get(field).transpose().expect(145							"field exists, as field name was obtained from object.fields()",146						)),147					])));148					destruct(var, value, fctx.clone(), &mut new_bindings)?;149					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);150151					evaluate_comp(ctx, &specs[1..], callback)?;152				}153			}154			_ => bail!(InComprehensionCanOnlyIterateOverArray),155		},156	}157	Ok(())158}159160trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}161impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}162163fn evaluate_object_locals(164	fctx: Context,165	locals: Rc<Vec<BindSpec>>,166) -> impl CloneableUnbound<Context> {167	#[derive(Trace, Clone)]168	struct UnboundLocals {169		fctx: Context,170		locals: Rc<Vec<BindSpec>>,171	}172	impl Unbound for UnboundLocals {173		type Bound = Context;174175		fn bind(&self, sup_this: SupThis) -> Result<Context> {176			let fctx = Context::new_future();177			let mut new_bindings =178				FxHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());179			for b in self.locals.iter() {180				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;181			}182183			let ctx = self.fctx.clone();184185			let ctx = ctx186				.extend_bindings_sup_this(new_bindings, sup_this)187				.into_future(fctx);188189			Ok(ctx)190		}191	}192193	UnboundLocals { fctx, locals }194}195196pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(197	builder: &mut ObjValueBuilder,198	ctx: Context,199	uctx: B,200	field: &FieldMember,201) -> Result<()> {202	let name = evaluate_field_name(ctx, &field.name)?;203	let Some(name) = name else {204		return Ok(());205	};206207	match field {208		FieldMember {209			plus,210			params: None,211			visibility,212			value,213			..214		} => {215			#[derive(Trace)]216			struct UnboundValue<B: Trace> {217				uctx: B,218				value: LocExpr,219				name: IStr,220			}221			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {222				type Bound = Val;223				fn bind(&self, sup_this: SupThis) -> Result<Val> {224					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())225				}226			}227228			builder229				.field(name.clone())230				.with_add(*plus)231				.with_visibility(*visibility)232				.with_location(value.span())233				.bindable(UnboundValue {234					uctx,235					value: value.clone(),236					name,237				})?;238		}239		FieldMember {240			params: Some(params),241			visibility,242			value,243			..244		} => {245			#[derive(Trace)]246			struct UnboundMethod<B: Trace> {247				uctx: B,248				value: LocExpr,249				params: ParamsDesc,250				name: IStr,251			}252			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {253				type Bound = Val;254				fn bind(&self, sup_this: SupThis) -> Result<Val> {255					Ok(evaluate_method(256						self.uctx.bind(sup_this)?,257						self.name.clone(),258						self.params.clone(),259						self.value.clone(),260					))261				}262			}263264			builder265				.field(name.clone())266				.with_visibility(*visibility)267				.with_location(value.span())268				.bindable(UnboundMethod {269					uctx,270					value: value.clone(),271					params: params.clone(),272					name,273				})?;274		}275	}276	Ok(())277}278279#[allow(clippy::too_many_lines)]280pub fn evaluate_member_list_object(ctx: Context, members: &[Member]) -> Result<ObjValue> {281	let mut builder = ObjValueBuilder::new();282	let locals = Rc::new(283		members284			.iter()285			.filter_map(|m| match m {286				Member::BindStmt(bind) => Some(bind.clone()),287				_ => None,288			})289			.collect::<Vec<_>>(),290	);291292	// We have single context for all fields, so we can cache binds293	let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));294295	for member in members {296		match member {297			Member::Field(field) => {298				evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;299			}300			Member::AssertStmt(stmt) => {301				#[derive(Trace)]302				struct ObjectAssert<B: Trace> {303					uctx: B,304					assert: AssertStmt,305				}306				impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {307					fn run(&self, sup_this: SupThis) -> Result<()> {308						let ctx = self.uctx.bind(sup_this)?;309						evaluate_assert(ctx, &self.assert)310					}311				}312				builder.assert(ObjectAssert {313					uctx: uctx.clone(),314					assert: stmt.clone(),315				});316			}317			Member::BindStmt(_) => {318				// Already handled319			}320		}321	}322	Ok(builder.build())323}324325pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {326	Ok(match object {327		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,328		ObjBody::ObjComp(obj) => {329			let mut builder = ObjValueBuilder::new();330			let locals = Rc::new(331				obj.pre_locals332					.iter()333					.chain(obj.post_locals.iter())334					.cloned()335					.collect::<Vec<_>>(),336			);337			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {338				let uctx = evaluate_object_locals(ctx.clone(), locals.clone());339340				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)341			})?;342343			builder.build()344		}345	})346}347348pub fn evaluate_apply(349	ctx: Context,350	value: &LocExpr,351	args: &ArgsDesc,352	loc: CallLocation<'_>,353	tailstrict: bool,354) -> Result<Val> {355	let value = evaluate(ctx.clone(), value)?;356	Ok(match value {357		Val::Func(f) => {358			let body = || f.evaluate(ctx, loc, args, tailstrict);359			if tailstrict {360				body()?361			} else {362				in_frame(loc, || format!("function <{}> call", f.name()), body)?363			}364		}365		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),366	})367}368369pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {370	let value = &assertion.0;371	let msg = &assertion.1;372	let assertion_result = in_frame(373		CallLocation::new(&value.span()),374		|| "assertion condition".to_owned(),375		|| bool::from_untyped(evaluate(ctx.clone(), value)?),376	)?;377	if !assertion_result {378		in_frame(379			CallLocation::new(&value.span()),380			|| "assertion failure".to_owned(),381			|| {382				if let Some(msg) = msg {383					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));384				}385				bail!(AssertionFailed(Val::Null.to_string()?));386			},387		)?;388	}389	Ok(())390}391392pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result<Val> {393	use Expr::*;394	Ok(match expr.expr() {395		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),396		_ => evaluate(ctx, expr)?,397	})398}399400#[allow(clippy::too_many_lines)]401pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result<Val> {402	use Expr::*;403404	if let Some(trivial) = evaluate_trivial(expr) {405		return Ok(trivial);406	}407	let loc = expr.span();408	Ok(match expr.expr() {409		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),410		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),411		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),412		Literal(LiteralType::True) => Val::Bool(true),413		Literal(LiteralType::False) => Val::Bool(false),414		Literal(LiteralType::Null) => Val::Null,415		Str(v) => Val::string(v.clone()),416		Num(v) => Val::try_num(*v)?,417		// I have tried to remove special behavior from super by implementing standalone-super418		// expresion, but looks like this case still needs special treatment.419		//420		// Note that other jsonnet implementations will fail on `if value in (super)` expression,421		// because the standalone super literal is not supported, that is because in other422		// implementations `in super` treated differently from `in smth_else`.423		BinaryOp(field, BinaryOpType::In, e)424			if matches!(e.expr(), Expr::Literal(LiteralType::Super)) =>425		{426			let sup_this = ctx.try_sup_this()?;427			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.428			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.429			if !sup_this.has_super() {430				return Ok(Val::Bool(false));431			}432			let field = evaluate(ctx, field)?;433			Val::Bool(sup_this.field_in_super(field.to_string()?))434		}435		BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,436		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,437		Var(name) => in_frame(438			CallLocation::new(&loc),439			|| format!("local <{name}> access"),440			|| ctx.binding(name.clone())?.evaluate(),441		)?,442		Index { indexable, parts } => ensure_sufficient_stack(|| {443			let mut parts = parts.iter();444			let mut indexable = if matches!(indexable.expr(), Expr::Literal(LiteralType::Super)) {445				let part = parts.next().expect("at least part should exist");446				// sup_this existence check might also be skipped here for null-coalesce...447				// But I believe this might cause errors.448				let sup_this = ctx.try_sup_this()?;449				if !sup_this.has_super() {450					#[cfg(feature = "exp-null-coaelse")]451					if part.null_coaelse {452						return Ok(Val::Null);453					}454					bail!(NoSuperFound)455				}456				let name = evaluate(ctx.clone(), &part.value)?;457458				let Val::Str(name) = name else {459					bail!(ValueIndexMustBeTypeGot(460						ValType::Obj,461						ValType::Str,462						name.value_type(),463					))464				};465466				let name = name.into_flat();467				match sup_this468					.get_super(name.clone())469					.with_description_src(&part.value, || format!("field <{name}> access"))?470				{471					Some(v) => v,472					#[cfg(feature = "exp-null-coaelse")]473					None if part.null_coaelse => return Ok(Val::Null),474					None => {475						let suggestions = suggest_object_fields(476							&sup_this.standalone_super().expect("super exists"),477							name.clone(),478						);479480						bail!(NoSuchField(name, suggestions))481					}482				}483			} else {484				evaluate(ctx.clone(), indexable)?485			};486487			for part in parts {488				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {489					(Val::Obj(v), Val::Str(key)) => match v490						.get(key.clone().into_flat())491						.with_description_src(&part.value, || format!("field <{key}> access"))?492					{493						Some(v) => v,494						#[cfg(feature = "exp-null-coaelse")]495						None if part.null_coaelse => return Ok(Val::Null),496						None => {497							let suggestions = suggest_object_fields(&v, key.clone().into_flat());498499							return Err(Error::from(NoSuchField(500								key.clone().into_flat(),501								suggestions,502							)))503							.with_description_src(&part.value, || format!("field <{key}> access"));504						}505					},506					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(507						ValType::Obj,508						ValType::Str,509						n.value_type(),510					)),511					(Val::Arr(v), Val::Num(n)) => {512						let n = n.get();513						if n.fract() > f64::EPSILON {514							bail!(FractionalIndex)515						}516						if n < 0.0 {517							bail!(ArrayBoundsError(n as isize, v.len()));518						}519						v.get(n as usize)?520							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?521					}522					(Val::Arr(_), Val::Str(n)) => {523						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))524					}525					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(526						ValType::Arr,527						ValType::Num,528						n.value_type(),529					)),530531					(Val::Str(s), Val::Num(n)) => Val::Str({532						let n = n.get();533						if n.fract() > f64::EPSILON {534							bail!(FractionalIndex)535						}536						if n < 0.0 {537							bail!(ArrayBoundsError(n as isize, s.into_flat().chars().count()));538						}539						let v: IStr = s540							.clone()541							.into_flat()542							.chars()543							.skip(n as usize)544							.take(1)545							.collect::<String>()546							.into();547						if v.is_empty() {548							bail!(StringBoundsError(n as usize, s.into_flat().chars().count()))549						}550						StrValue::Flat(v)551					}),552					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(553						ValType::Str,554						ValType::Num,555						n.value_type(),556					)),557					#[cfg(feature = "exp-null-coaelse")]558					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),559					(v, _) => bail!(CantIndexInto(v.value_type())),560				};561			}562			Ok(indexable)563		})?,564		LocalExpr(bindings, returned) => {565			let mut new_bindings: FxHashMap<IStr, Thunk<Val>> =566				FxHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());567			let fctx = Context::new_future();568			for b in bindings {569				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;570			}571			let ctx = ctx.extend_bindings(new_bindings).into_future(fctx);572			evaluate(ctx, &returned.clone())?573		}574		Arr(items) => {575			if items.is_empty() {576				Val::Arr(ArrValue::empty())577			} else if items.len() == 1 {578				let item = items[0].clone();579				Val::Arr(ArrValue::lazy(vec![Thunk!(move || evaluate(ctx, &item))]))580			} else {581				Val::Arr(ArrValue::expr(ctx, items.iter().cloned()))582			}583		}584		ArrComp(expr, comp_specs) => {585			let mut out = Vec::new();586			evaluate_comp(ctx, comp_specs, &mut |ctx| {587				let expr = expr.clone();588				out.push(Thunk!(move || evaluate(ctx, &expr)));589				Ok(())590			})?;591			Val::Arr(ArrValue::lazy(out))592		}593		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),594		ObjExtend(a, b) => evaluate_add_op(595			&evaluate(ctx.clone(), a)?,596			&Val::Obj(evaluate_object(ctx, b)?),597		)?,598		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {599			evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)600		})?,601		Function(params, body) => {602			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())603		}604		AssertExpr(assert, returned) => {605			evaluate_assert(ctx.clone(), assert)?;606			evaluate(ctx, returned)?607		}608		ErrorStmt(e) => in_frame(609			CallLocation::new(&loc),610			|| "error statement".to_owned(),611			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),612		)?,613		IfElse {614			cond,615			cond_then,616			cond_else,617		} => {618			if in_frame(619				CallLocation::new(&loc),620				|| "if condition".to_owned(),621				|| bool::from_untyped(evaluate(ctx.clone(), &cond.0)?),622			)? {623				evaluate(ctx, cond_then)?624			} else {625				match cond_else {626					Some(v) => evaluate(ctx, v)?,627					None => Val::Null,628				}629			}630		}631		Slice(value, desc) => {632			fn parse_idx<T: Typed>(633				loc: CallLocation<'_>,634				ctx: Context,635				expr: Option<&LocExpr>,636				desc: &'static str,637			) -> Result<Option<T>> {638				if let Some(value) = expr {639					Ok(in_frame(640						loc,641						|| format!("slice {desc}"),642						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),643					)?)644				} else {645					Ok(None)646				}647			}648649			let indexable = evaluate(ctx.clone(), value)?;650			let loc = CallLocation::new(&loc);651652			let start = parse_idx(loc, ctx.clone(), desc.start.as_ref(), "start")?;653			let end = parse_idx(loc, ctx.clone(), desc.end.as_ref(), "end")?;654			let step = parse_idx(loc, ctx, desc.step.as_ref(), "step")?;655656			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?657		}658		i @ (Import(path) | ImportStr(path) | ImportBin(path)) => {659			let Expr::Str(path) = &path.expr() else {660				bail!("computed imports are not supported")661			};662			let tmp = loc.clone().0;663			with_state(|s| {664				let resolved_path = s.resolve_from(tmp.source_path(), path)?;665				Ok(match i {666					Import(_) => in_frame(667						CallLocation::new(&loc),668						|| format!("import {:?}", path.clone()),669						|| s.import_resolved(resolved_path),670					)?,671					ImportStr(_) => Val::string(s.import_resolved_str(resolved_path)?),672					ImportBin(_) => {673						Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?))674					}675					_ => unreachable!(),676				}) as Result<Val>677			})?678		}679	})680}