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

difftreelog

source

crates/jrsonnet-evaluator/src/evaluate/mod.rs18.9 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, ImportKind, LiteralType, ObjBody, ObjMembers, ParamsDesc, Spanned,8};9use jrsonnet_types::ValType;10use rustc_hash::FxHashMap;1112use self::destructure::destruct;13use crate::{14	Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result, ResultExt, SupThis, Unbound, Val, arr::ArrValue, bail, destructure::evaluate_dest, error::{ErrorKind::*, suggest_object_fields}, evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op}, function::{CallLocation, FuncDesc, FuncVal, builtin::{ParamDefault, ParamName, ParamParse}}, gc::WithCapacityExt as _, in_frame, typed::Typed, val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk}, with_state15};16pub mod destructure;17pub mod operator;1819// This is the amount of bytes that need to be left on the stack before increasing the size.20// It must be at least as large as the stack required by any code that does not call21// `ensure_sufficient_stack`.22const RED_ZONE: usize = 100 * 1024; // 100k2324// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then25// on. This flag has performance relevant characteristics. Don't set it too high.26const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB2728/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations29/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit30/// from this.31///32/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.33#[inline]34pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {35	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)36}3738pub fn evaluate_trivial(expr: &Spanned<Expr>) -> Option<Val> {39	fn is_trivial(expr: &Spanned<Expr>) -> bool {40		match &**expr {41			Expr::Str(_)42			| Expr::Num(_)43			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,44			Expr::Arr(a) => a.iter().all(is_trivial),45			_ => false,46		}47	}48	Some(match &**expr {49		Expr::Str(s) => Val::string(s.clone()),50		Expr::Num(n) => {51			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))52		}53		Expr::Literal(LiteralType::False) => Val::Bool(false),54		Expr::Literal(LiteralType::True) => Val::Bool(true),55		Expr::Literal(LiteralType::Null) => Val::Null,56		Expr::Arr(n) => {57			if n.iter().any(|e| !is_trivial(e)) {58				return None;59			}60			Val::Arr(ArrValue::eager(61				n.iter()62					.map(evaluate_trivial)63					.map(|e| e.expect("checked trivial"))64					.collect(),65			))66		}67		_ => return None,68	})69}7071pub fn evaluate_method(72	ctx: Context,73	name: IStr,74	params: ParamsDesc,75	body: Rc<Spanned<Expr>>,76) -> Val {77	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {78		name,79		ctx,80		params_parse: params81			.iter()82			.map(|p| {83				ParamParse::new(84					p.0.name().map_or(ParamName::ANONYMOUS, ParamName::new),85					ParamDefault::exists(p.1.is_some()),86				)87			})88			.collect(),89		params,90		body,91	})))92}9394pub fn evaluate_field_name(ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {95	Ok(match field_name {96		FieldName::Fixed(n) => Some(n.clone()),97		FieldName::Dyn(expr) => in_frame(98			CallLocation::new(&expr.span()),99			|| "evaluating field name".to_string(),100			|| {101				let value = evaluate(ctx, expr)?;102				if matches!(value, Val::Null) {103					Ok(None)104				} else {105					Ok(Some(IStr::from_untyped(value)?))106				}107			},108		)?,109	})110}111112pub fn evaluate_comp(113	ctx: Context,114	specs: &[CompSpec],115	callback: &mut impl FnMut(Context) -> Result<()>,116) -> Result<()> {117	match specs.first() {118		None => callback(ctx)?,119		Some(CompSpec::IfSpec(IfSpecData(cond))) => {120			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {121				evaluate_comp(ctx, &specs[1..], callback)?;122			}123		}124		Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {125			Val::Arr(list) => {126				for item in list.iter_lazy() {127					let fctx = Pending::new();128					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());129					destruct(var, item, fctx.clone(), &mut new_bindings)?;130					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);131132					evaluate_comp(ctx, &specs[1..], callback)?;133				}134			}135			#[cfg(feature = "exp-object-iteration")]136			Val::Obj(obj) => {137				for field in obj.fields(138					// TODO: Should there be ability to preserve iteration order?139					#[cfg(feature = "exp-preserve-order")]140					false,141				) {142					let fctx = Pending::new();143					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());144					let obj = obj.clone();145					let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![146						Thunk::evaluated(Val::string(field.clone())),147						Thunk!(move || obj.get(field).transpose().expect(148							"field exists, as field name was obtained from object.fields()",149						)),150					])));151					destruct(var, value, fctx.clone(), &mut new_bindings)?;152					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);153154					evaluate_comp(ctx, &specs[1..], callback)?;155				}156			}157			_ => bail!(InComprehensionCanOnlyIterateOverArray),158		},159	}160	Ok(())161}162163trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}164impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}165166fn evaluate_object_locals(167	fctx: Context,168	locals: Rc<Vec<BindSpec>>,169) -> impl CloneableUnbound<Context> {170	#[derive(Trace, Clone)]171	struct UnboundLocals {172		fctx: Context,173		locals: Rc<Vec<BindSpec>>,174	}175	impl Unbound for UnboundLocals {176		type Bound = Context;177178		fn bind(&self, sup_this: SupThis) -> Result<Context> {179			let fctx = Context::new_future();180			let mut new_bindings =181				FxHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());182			for b in self.locals.iter() {183				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;184			}185186			let ctx = self.fctx.clone();187188			let ctx = ctx189				.extend_bindings_sup_this(new_bindings, sup_this)190				.into_future(fctx);191192			Ok(ctx)193		}194	}195196	UnboundLocals { fctx, locals }197}198199pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(200	builder: &mut ObjValueBuilder,201	ctx: Context,202	uctx: B,203	field: &FieldMember,204) -> Result<()> {205	let name = evaluate_field_name(ctx, &field.name)?;206	let Some(name) = name else {207		return Ok(());208	};209210	match field {211		FieldMember {212			plus,213			params: None,214			visibility,215			value,216			..217		} => {218			#[derive(Trace)]219			struct UnboundValue<B: Trace> {220				uctx: B,221				value: Rc<Spanned<Expr>>,222				name: IStr,223			}224			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {225				type Bound = Val;226				fn bind(&self, sup_this: SupThis) -> Result<Val> {227					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())228				}229			}230231			builder232				.field(name.clone())233				.with_add(*plus)234				.with_visibility(*visibility)235				.with_location(value.span())236				.bindable(UnboundValue {237					uctx,238					value: value.clone(),239					name,240				})?;241		}242		FieldMember {243			params: Some(params),244			visibility,245			value,246			..247		} => {248			#[derive(Trace)]249			struct UnboundMethod<B: Trace> {250				uctx: B,251				value: Rc<Spanned<Expr>>,252				params: ParamsDesc,253				name: IStr,254			}255			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {256				type Bound = Val;257				fn bind(&self, sup_this: SupThis) -> Result<Val> {258					Ok(evaluate_method(259						self.uctx.bind(sup_this)?,260						self.name.clone(),261						self.params.clone(),262						self.value.clone(),263					))264				}265			}266267			builder268				.field(name.clone())269				.with_visibility(*visibility)270				.with_location(value.span())271				.bindable(UnboundMethod {272					uctx,273					value: value.clone(),274					params: params.clone(),275					name,276				})?;277		}278	}279	Ok(())280}281282#[allow(clippy::too_many_lines)]283pub fn evaluate_member_list_object(ctx: Context, members: &ObjMembers) -> Result<ObjValue> {284	let mut builder = ObjValueBuilder::new();285	let locals = members.locals.clone();286287	// We have single context for all fields, so we can cache binds288	let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));289290	for field in &members.fields {291		evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), &field)?;292	}293294	if !members.asserts.is_empty() {295		#[derive(Trace)]296		struct ObjectAssert<B: Trace> {297			uctx: B,298			asserts: Rc<Vec<AssertStmt>>,299		}300		impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {301			fn run(&self, sup_this: SupThis) -> Result<()> {302				let ctx = self.uctx.bind(sup_this)?;303				for assert in &*self.asserts {304					evaluate_assert(ctx.clone(), &assert)?;305				}306				Ok(())307			}308		}309		builder.assert(ObjectAssert {310			uctx: uctx.clone(),311			asserts: members.asserts.clone(),312		});313	}314315	Ok(builder.build())316}317318pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {319	Ok(match object {320		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,321		ObjBody::ObjComp(obj) => {322			let mut builder = ObjValueBuilder::new();323			let locals = obj.locals.clone();324			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {325				let uctx = evaluate_object_locals(ctx.clone(), locals.clone());326327				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)328			})?;329330			builder.build()331		}332	})333}334335pub fn evaluate_apply(336	ctx: Context,337	value: &Spanned<Expr>,338	args: &ArgsDesc,339	loc: CallLocation<'_>,340	tailstrict: bool,341) -> Result<Val> {342	let value = evaluate(ctx.clone(), value)?;343	Ok(match value {344		Val::Func(f) => {345			let body = || f.evaluate(ctx, loc, args, tailstrict);346			if tailstrict {347				body()?348			} else {349				in_frame(loc, || format!("function <{}> call", f.name()), body)?350			}351		}352		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),353	})354}355356pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {357	let value = &assertion.0;358	let msg = &assertion.1;359	let assertion_result = in_frame(360		CallLocation::new(&value.span()),361		|| "assertion condition".to_owned(),362		|| bool::from_untyped(evaluate(ctx.clone(), value)?),363	)?;364	if !assertion_result {365		in_frame(366			CallLocation::new(&value.span()),367			|| "assertion failure".to_owned(),368			|| {369				if let Some(msg) = msg {370					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));371				}372				bail!(AssertionFailed(Val::Null.to_string()?));373			},374		)?;375	}376	Ok(())377}378379pub fn evaluate_named(ctx: Context, expr: &Spanned<Expr>, name: IStr) -> Result<Val> {380	use Expr::*;381	Ok(match &**expr {382		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),383		_ => evaluate(ctx, expr)?,384	})385}386387#[allow(clippy::too_many_lines)]388pub fn evaluate(ctx: Context, expr: &Spanned<Expr>) -> Result<Val> {389	use Expr::*;390391	if let Some(trivial) = evaluate_trivial(expr) {392		return Ok(trivial);393	}394	let loc = expr.span();395	Ok(match &**expr {396		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),397		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),398		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),399		Literal(LiteralType::True) => Val::Bool(true),400		Literal(LiteralType::False) => Val::Bool(false),401		Literal(LiteralType::Null) => Val::Null,402		Str(v) => Val::string(v.clone()),403		Num(v) => Val::try_num(*v)?,404		// I have tried to remove special behavior from super by implementing standalone-super405		// expresion, but looks like this case still needs special treatment.406		//407		// Note that other jsonnet implementations will fail on `if value in (super)` expression,408		// because the standalone super literal is not supported, that is because in other409		// implementations `in super` treated differently from `in smth_else`.410		BinaryOp(bin)411			if matches!(&*bin.rhs, Expr::Literal(LiteralType::Super))412				&& bin.op == BinaryOpType::In =>413		{414			let sup_this = ctx.try_sup_this()?;415			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.416			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.417			if !sup_this.has_super() {418				return Ok(Val::Bool(false));419			}420			let field = evaluate(ctx, &bin.lhs)?;421			Val::Bool(sup_this.field_in_super(field.to_string()?))422		}423		BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?,424		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,425		Var(name) => in_frame(426			CallLocation::new(&loc),427			|| format!("local <{name}> access"),428			|| ctx.binding(name.clone())?.evaluate(),429		)?,430		Index { indexable, parts } => ensure_sufficient_stack(|| {431			let mut parts = parts.iter();432			let mut indexable = if matches!(&***indexable, Expr::Literal(LiteralType::Super)) {433				let part = parts.next().expect("at least part should exist");434				// sup_this existence check might also be skipped here for null-coalesce...435				// But I believe this might cause errors.436				let sup_this = ctx.try_sup_this()?;437				if !sup_this.has_super() {438					#[cfg(feature = "exp-null-coaelse")]439					if part.null_coaelse {440						return Ok(Val::Null);441					}442					bail!(NoSuperFound)443				}444				let name = evaluate(ctx.clone(), &part.value)?;445446				let Val::Str(name) = name else {447					bail!(ValueIndexMustBeTypeGot(448						ValType::Obj,449						ValType::Str,450						name.value_type(),451					))452				};453454				let name = name.into_flat();455				match sup_this456					.get_super(name.clone())457					.with_description_src(&part.value, || format!("field <{name}> access"))?458				{459					Some(v) => v,460					#[cfg(feature = "exp-null-coaelse")]461					None if part.null_coaelse => return Ok(Val::Null),462					None => {463						let suggestions = suggest_object_fields(464							&sup_this.standalone_super().expect("super exists"),465							name.clone(),466						);467468						bail!(NoSuchField(name, suggestions))469					}470				}471			} else {472				evaluate(ctx.clone(), indexable)?473			};474475			for part in parts {476				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {477					(Val::Obj(v), Val::Str(key)) => match v478						.get(key.clone().into_flat())479						.with_description_src(&part.value, || format!("field <{key}> access"))?480					{481						Some(v) => v,482						#[cfg(feature = "exp-null-coaelse")]483						None if part.null_coaelse => return Ok(Val::Null),484						None => {485							let suggestions = suggest_object_fields(&v, key.clone().into_flat());486487							return Err(Error::from(NoSuchField(488								key.clone().into_flat(),489								suggestions,490							)))491							.with_description_src(&part.value, || format!("field <{key}> access"));492						}493					},494					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(495						ValType::Obj,496						ValType::Str,497						n.value_type(),498					)),499					(Val::Arr(v), Val::Num(n)) => {500						let n = n.get();501						if n.fract() > f64::EPSILON {502							bail!(FractionalIndex)503						}504						if n < 0.0 {505							bail!(ArrayBoundsError(n as isize, v.len()));506						}507						v.get(n as usize)?508							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?509					}510					(Val::Arr(_), Val::Str(n)) => {511						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))512					}513					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(514						ValType::Arr,515						ValType::Num,516						n.value_type(),517					)),518519					(Val::Str(s), Val::Num(n)) => Val::Str({520						let n = n.get();521						if n.fract() > f64::EPSILON {522							bail!(FractionalIndex)523						}524						if n < 0.0 {525							bail!(ArrayBoundsError(n as isize, s.into_flat().chars().count()));526						}527						let v: IStr = s528							.clone()529							.into_flat()530							.chars()531							.skip(n as usize)532							.take(1)533							.collect::<String>()534							.into();535						if v.is_empty() {536							bail!(StringBoundsError(n as usize, s.into_flat().chars().count()))537						}538						StrValue::Flat(v)539					}),540					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(541						ValType::Str,542						ValType::Num,543						n.value_type(),544					)),545					#[cfg(feature = "exp-null-coaelse")]546					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),547					(v, _) => bail!(CantIndexInto(v.value_type())),548				};549			}550			Ok(indexable)551		})?,552		LocalExpr(bindings, returned) => {553			let mut new_bindings: FxHashMap<IStr, Thunk<Val>> =554				FxHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());555			let fctx = Context::new_future();556			for b in bindings {557				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;558			}559			let ctx = ctx.extend_bindings(new_bindings).into_future(fctx);560			evaluate(ctx, &returned.clone())?561		}562		Arr(items) => {563			if items.is_empty() {564				Val::Arr(ArrValue::empty())565			} else {566				Val::Arr(ArrValue::expr(ctx, items.clone()))567			}568		}569		ArrComp(expr, comp_specs) => {570			let mut out = Vec::new();571			evaluate_comp(ctx, comp_specs, &mut |ctx| {572				let expr = expr.clone();573				out.push(Thunk!(move || evaluate(ctx, &expr)));574				Ok(())575			})?;576			Val::Arr(ArrValue::lazy(out))577		}578		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),579		ObjExtend(a, b) => evaluate_add_op(580			&evaluate(ctx.clone(), a)?,581			&Val::Obj(evaluate_object(ctx, b)?),582		)?,583		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {584			evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)585		})?,586		Function(params, body) => {587			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())588		}589		AssertExpr(assert) => {590			evaluate_assert(ctx.clone(), &assert.assert)?;591			evaluate(ctx, &assert.rest)?592		}593		ErrorStmt(e) => in_frame(594			CallLocation::new(&loc),595			|| "error statement".to_owned(),596			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),597		)?,598		IfElse(if_else) => {599			if in_frame(600				CallLocation::new(&loc),601				|| "if condition".to_owned(),602				|| bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.0)?),603			)? {604				evaluate(ctx, &if_else.cond_then)?605			} else {606				match &if_else.cond_else {607					Some(v) => evaluate(ctx, v)?,608					None => Val::Null,609				}610			}611		}612		Slice(slice) => {613			fn parse_idx<T: Typed>(614				loc: CallLocation<'_>,615				ctx: Context,616				expr: Option<&Spanned<Expr>>,617				desc: &'static str,618			) -> Result<Option<T>> {619				if let Some(value) = expr {620					Ok(in_frame(621						loc,622						|| format!("slice {desc}"),623						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),624					)?)625				} else {626					Ok(None)627				}628			}629630			let indexable = evaluate(ctx.clone(), &slice.value)?;631			let loc = CallLocation::new(&loc);632633			let start = parse_idx(loc, ctx.clone(), slice.slice.start.as_ref(), "start")?;634			let end = parse_idx(loc, ctx.clone(), slice.slice.end.as_ref(), "end")?;635			let step = parse_idx(loc, ctx, slice.slice.step.as_ref(), "step")?;636637			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?638		}639		Import(kind, path) => {640			let Expr::Str(path) = &***path else {641				bail!("computed imports are not supported")642			};643			let tmp = loc.clone().0;644			with_state(|s| {645				let resolved_path = s.resolve_from(tmp.source_path(), path)?;646				Ok(match kind {647					ImportKind::Normal => in_frame(648						CallLocation::new(&loc),649						|| format!("import {:?}", path.clone()),650						|| s.import_resolved(resolved_path),651					)?,652					ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?),653					ImportKind::Bin => {654						Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?))655					}656				}) as Result<Val>657			})?658		}659	})660}