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

difftreelog

source

crates/jrsonnet-evaluator/src/evaluate/mod.rs16.7 KiBsourcehistory
1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_ir::ImportKind;6use jrsonnet_types::ValType;78use self::{9	compspec::{evaluate_arr_comp, evaluate_obj_comp},10	destructure::{build_b_thunk_uno, evaluate_local_expr, evaluate_locals_unbound},11	operator::evaluate_binary_op_special,12};13use crate::{14	Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Result, ResultExt as _, SupThis,15	Unbound, Val,16	analyze::{17		ClosureShape, LArgsDesc, LAssertStmt, LExpr, LFieldMember, LFieldName, LFunction,18		LIndexPart, LObjAsserts, LObjBody, LObjMembers, LSlot,19	},20	arr::ArrValue,21	bail,22	error::{ErrorKind::*, suggest_object_fields},23	evaluate::{destructure::fill_letrec_binds, operator::evaluate_unary_op},24	function::{CallLocation, FuncDesc, FuncVal, prepared::PreparedFuncVal},25	in_frame,26	typed::{BoundedUsize, FromUntyped as _},27	val::{CachedUnbound, Thunk},28	with_state,29};3031pub mod compspec;32pub mod destructure;33pub mod operator;3435// This is the amount of bytes that need to be left on the stack before increasing the size.36// It must be at least as large as the stack required by any code that does not call37// `ensure_sufficient_stack`.38const RED_ZONE: usize = 100 * 1024;3940// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then41// on. This flag has performance relevant characteristics. Don't set it too high.42const STACK_PER_RECURSION: usize = 1024 * 1024;4344/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations45/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit46/// from this.47///48/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.49#[inline]50pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {51	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)52}5354pub fn evaluate_trivial(expr: &LExpr) -> Option<Val> {55	// TODO: Eager trivial array56	Some(match expr {57		LExpr::Str(s) => Val::string(s.clone()),58		LExpr::Num(n) => Val::Num(*n),59		LExpr::Bool(false) => Val::Bool(false),60		LExpr::Bool(true) => Val::Bool(true),61		LExpr::Null => Val::Null,62		_ => return None,63	})64}6566pub fn evaluate_method(ctx: Context, name: IStr, func: &Rc<LFunction>) -> Val {67	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {68		name,69		body_captures: ctx.pack_captures_sup_this(&func.body_shape),70		func: func.clone(),71	})))72}7374pub fn evaluate_field_name(ctx: Context, field_name: &LFieldName) -> Result<Option<IStr>> {75	Ok(match field_name {76		LFieldName::Fixed(n) => Some(n.clone()),77		LFieldName::Dyn(expr) => in_frame(78			// TODO: Spanned<LFieldName>79			CallLocation::native(),80			|| "evaluating field name".to_string(),81			|| {82				let v = evaluate(ctx.clone(), expr)?;83				Ok(if matches!(v, Val::Null) {84					None85				} else {86					Some(IStr::from_untyped(v)?)87				})88			},89		)?,90	})91}9293pub fn evaluate_thunk(ctx: Context, expr: Rc<LExpr>, tailstrict: bool) -> Result<Thunk<Val>> {94	match &*expr {95		LExpr::Slot(LSlot::Local(i)) => return Ok(ctx.local(*i)),96		LExpr::Slot(LSlot::Capture(i)) => return Ok(ctx.capture(*i)),97		_ => {98			if let Some(v) = evaluate_trivial(&expr) {99				return Ok(Thunk::evaluated(v));100			}101		}102	}103	Ok(if tailstrict {104		Thunk::evaluated(evaluate(ctx, &expr)?)105	} else {106		Thunk!(move || { evaluate(ctx, &expr) })107	})108}109110mod names {111	use crate::names;112113	names! {114		anonymous: "anonymous",115	}116}117118pub fn evaluate(ctx: Context, expr: &LExpr) -> Result<Val> {119	Ok(match expr {120		LExpr::Null => Val::Null,121		LExpr::Bool(b) => Val::Bool(*b),122		LExpr::Str(s) => Val::string(s.clone()),123		LExpr::Num(n) => Val::Num(*n),124		LExpr::Slot(slot) => ctx.slot(*slot).evaluate()?,125		LExpr::BadLocal(name) => panic!("unresolvable reference: {name}"),126		LExpr::Arr { shape, items } => Val::Arr(ArrValue::expr(ctx, shape, items.clone())),127		LExpr::UnaryOp(op, value) => {128			let value = evaluate(ctx, value)?;129			evaluate_unary_op(*op, &value)?130		}131		LExpr::BinaryOp { lhs, op, rhs } => evaluate_binary_op_special(ctx, lhs, *op, rhs)?,132		LExpr::LocalExpr(local_expr) => evaluate_local_expr(ctx, local_expr)?,133		LExpr::IfElse {134			cond,135			cond_then,136			cond_else,137		} => {138			let cond_val = evaluate(ctx.clone(), cond)?;139			let Val::Bool(b) = cond_val else {140				bail!(TypeMismatch(141					"if condition",142					vec![ValType::Bool],143					cond_val.value_type()144				))145			};146			if b {147				evaluate(ctx, cond_then)?148			} else if let Some(e) = cond_else {149				evaluate(ctx, e)?150			} else {151				Val::Null152			}153		}154		LExpr::Error(s, e) => in_frame(155			CallLocation::new(s),156			|| "error statement".to_owned(),157			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),158		)?,159		LExpr::AssertExpr { assert, rest } => {160			evaluate_assert(ctx.clone(), assert)?;161			evaluate(ctx, rest)?162		}163164		LExpr::Function(func) => evaluate_method(165			ctx,166			func.name.clone().unwrap_or_else(names::anonymous),167			func,168		),169		LExpr::IdentityFunction => Val::Func(FuncVal::identity()),170		LExpr::Apply {171			applicable,172			args,173			tailstrict,174		} => evaluate_apply(175			ctx,176			applicable,177			args,178			CallLocation::new(&args.span),179			*tailstrict,180		)?,181		LExpr::Index { indexable, parts } => evaluate_index(ctx, indexable, parts)?,182		LExpr::Obj(body) => evaluate_obj_body(None, ctx, body)?,183		LExpr::ObjExtend(lhs, body) => {184			let lhs_val = evaluate(ctx.clone(), lhs)?;185			let Val::Obj(lhs_obj) = lhs_val else {186				bail!(TypeMismatch(187					"object extend lhs",188					vec![ValType::Obj],189					lhs_val.value_type(),190				))191			};192			evaluate_obj_body(Some(lhs_obj), ctx, body)?193		}194		LExpr::ArrComp(comp) => evaluate_arr_comp(ctx, comp)?,195		LExpr::Slice(slice) => {196			let val = evaluate(ctx.clone(), &slice.value)?;197			let indexable = val.into_indexable()?;198			let start = slice199				.start200				.as_ref()201				.map(|e| evaluate(ctx.clone(), e))202				.transpose()?203				.map(|v| -> Result<i32> { i32::from_untyped(v).description("slice start value") })204				.transpose()?;205			let end = slice206				.end207				.as_ref()208				.map(|e| evaluate(ctx.clone(), e))209				.transpose()?210				.map(|v| -> Result<i32> { i32::from_untyped(v).description("slice end value") })211				.transpose()?;212			let step = slice213				.step214				.as_ref()215				.map(|e| evaluate(ctx, e))216				.transpose()?217				.map(|v| -> Result<BoundedUsize<1, { i32::MAX as usize }>> {218					BoundedUsize::from_untyped(v).description("slice step value")219				})220				.transpose()?;221			Val::from(indexable.slice(start, end, step)?)222		}223		LExpr::Super => Val::Obj(ctx.try_sup_this()?.standalone_super().ok_or(NoSuperFound)?),224		LExpr::Import {225			kind,226			kind_span,227			path,228		} => with_state(|state| {229			let resolved = state.resolve_from(kind_span.0.source_path(), &path.clone())?;230			Ok::<_, Error>(match kind.value {231				ImportKind::Normal => in_frame(232					CallLocation::new(&kind.span),233					|| "import".to_string(),234					|| state.import_resolved(resolved),235				)?,236				ImportKind::Str => Val::string(state.import_resolved_str(resolved)?),237				ImportKind::Bin => Val::arr(state.import_resolved_bin(resolved)?),238			})239		})?,240	})241}242243fn evaluate_apply(244	ctx: Context,245	applicable: &LExpr,246	args: &LArgsDesc,247	loc: CallLocation<'_>,248	tailstrict: bool,249) -> Result<Val> {250	let func_val = evaluate(ctx.clone(), applicable)?;251	let Val::Func(func) = func_val else {252		bail!(OnlyFunctionsCanBeCalledGot(func_val.value_type()))253	};254255	if func.is_identity() && args.names.is_empty() && args.unnamed.len() == 1 {256		return evaluate_thunk(ctx, args.unnamed[0].clone(), tailstrict)?.evaluate();257	}258259	let name = func.name();260261	if args.names.is_empty() && args.unnamed.len() == 1 && func.params().len() == 1 {262		use crate::function::prepared::PreparedCall;263		let prepared_inline = PreparedCall::empty();264		let arg = evaluate_thunk(ctx, args.unnamed[0].clone(), tailstrict)?;265		let arg_slice = std::slice::from_ref(&arg);266		return in_frame(267			loc,268			|| format!("function <{name}> call"),269			|| {270				func.evaluate_prepared(271					&prepared_inline,272					CallLocation::native(),273					arg_slice,274					&[],275					tailstrict,276				)277			},278		);279	}280281	let unnamed = args282		.unnamed283		.iter()284		.cloned()285		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))286		.collect::<Result<Vec<_>>>()?;287288	// Fast path: positional-only multi-arg call fully covering the289	// params, no defaults.290	if args.names.is_empty() && unnamed.len() == func.params().len() {291		use crate::function::prepared::PreparedCall;292		let prepared_inline = PreparedCall::empty();293		return in_frame(294			loc,295			|| format!("function <{name}> call"),296			|| {297				func.evaluate_prepared(298					&prepared_inline,299					CallLocation::native(),300					&unnamed,301					&[],302					tailstrict,303				)304			},305		);306	}307308	let named = args309		.values310		.iter()311		.cloned()312		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))313		.collect::<Result<Vec<_>>>()?;314	let prepare = PreparedFuncVal::new(func, unnamed.len(), &args.names)315		.with_description_src(loc, || format!("function <{name}> preparation"))?;316	in_frame(317		loc,318		|| format!("function <{name}> call"),319		|| prepare.call(CallLocation::native(), &unnamed, &named),320	)321}322323fn evaluate_index(ctx: Context, indexable: &LExpr, parts: &[LIndexPart]) -> Result<Val> {324	let mut parts = parts.iter();325	let mut indexable = if matches!(indexable, LExpr::Super) {326		let part = parts.next().expect("at least part should exist");327		// sup_this existence check might also be skipped here for null-coalesce...328		// But I believe this might cause errors.329		let sup_this = ctx.try_sup_this()?;330331		if !sup_this.has_super() {332			#[cfg(feature = "exp-null-coaelse")]333			if part.null_coaelse {334				return Ok(Val::Null);335			}336			bail!(NoSuperFound);337		}338		let name = evaluate(ctx.clone(), &part.value)?;339340		let Val::Str(name) = name else {341			bail!(ValueIndexMustBeTypeGot(342				ValType::Obj,343				ValType::Str,344				name.value_type(),345			))346		};347348		let name = name.into_flat();349		match sup_this350			.get_super(name.clone())351			.with_description_src(&part.span, || format!("super field <{name}> access"))?352		{353			Some(v) => v,354			#[cfg(feature = "exp-null-coaelse")]355			None if part.null_coaelse => return Ok(Val::Null),356			None => {357				let suggestions = suggest_object_fields(358					&sup_this.standalone_super().expect("super exists"),359					name.clone(),360				);361				bail!(NoSuchField(name, suggestions))362			}363		}364	} else {365		evaluate(ctx.clone(), indexable)?366	};367368	for part in parts {369		let ctx = ctx.clone();370		let loc = CallLocation::new(&part.span);371		let value = indexable;372		let key_val = evaluate(ctx, &part.value)?;373		indexable = match (&value, &key_val) {374			(Val::Obj(obj), Val::Str(key)) => {375				let key = key.clone().into_flat();376				match obj377					.get(key.clone())378					.with_description_src(loc, || format!("field <{key}> access"))?379				{380					Some(v) => v,381					#[cfg(feature = "exp-null-coaelse")]382					None if part.null_coaelse => return Ok(Val::Null),383					None => {384						return Err(Error::from(NoSuchField(385							key.clone(),386							suggest_object_fields(obj, key.clone()),387						)))388						.with_description_src(loc, || format!("field <{key}> access"));389					}390				}391			}392			(Val::Arr(arr), Val::Num(idx)) => {393				let n = idx.get();394				if n.fract() > f64::EPSILON {395					bail!(FractionalIndex)396				}397				let len = arr.len();398				if n < 0.0 || n > f64::from(len) {399					bail!(ArrayBoundsError(n, len));400				}401				#[expect(402					clippy::cast_possible_truncation,403					clippy::cast_sign_loss,404					reason = "n is checked positive"405				)]406				let i = n as u32;407				arr.get(i)408					.with_description_src(loc, || format!("element <{i}> access"))?409					.ok_or_else(|| ArrayBoundsError(n, len))?410			}411			(Val::Str(s), Val::Num(idx)) => {412				let n = idx.get();413				if n.fract() > f64::EPSILON {414					bail!(FractionalIndex)415				}416				#[expect(417					clippy::cast_possible_truncation,418					clippy::cast_sign_loss,419					reason = "n is checked positive, overflow will truncate as expected"420				)]421				let i = n as usize;422				let flat = s.clone().into_flat();423				#[allow(clippy::cast_possible_truncation, reason = "string is max 4g")]424				if n >= 0.0425					&& n <= f64::from(u32::MAX)426					&& let Some(char) = flat.chars().nth(i)427				{428					Val::string(char)429				} else {430					let len = flat.chars().count();431					bail!(StringBoundsError(n, len as u32))432				}433			}434			#[cfg(feature = "exp-null-coaelse")]435			(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),436			_ => bail!(ValueIndexMustBeTypeGot(437				value.value_type(),438				ValType::Str,439				key_val.value_type()440			)),441		};442	}443	Ok(indexable)444}445446fn evaluate_obj_body(super_obj: Option<ObjValue>, ctx: Context, body: &LObjBody) -> Result<Val> {447	match body {448		LObjBody::MemberList(members) => evaluate_obj_members(super_obj, ctx, members),449		LObjBody::ObjComp(comp) => evaluate_obj_comp(super_obj, ctx, comp),450	}451}452453pub fn evaluate_field_member_unbound<B: Unbound<Bound = Context> + Clone>(454	builder: &mut ObjValueBuilder,455	ctx: Context,456	uctx: B,457	field: &LFieldMember,458) -> Result<()> {459	#[derive(Trace)]460	struct UnboundValue<B: Trace> {461		uctx: B,462		value: Rc<(ClosureShape, LExpr)>,463		name: IStr,464	}465	impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {466		type Bound = Val;467		fn bind(&self, sup_this: SupThis) -> Result<Val> {468			let a_ctx = self.uctx.bind(sup_this)?;469			let b_ctx = Context::enter_using(&a_ctx, &self.value.0);470			evaluate(b_ctx, &self.value.1)471		}472	}473474	let LFieldMember {475		name,476		plus,477		visibility,478		value,479	} = field;480	let Some(name) = evaluate_field_name(ctx, name)? else {481		return Ok(());482	};483484	builder485		.field(name.clone())486		.with_add(*plus)487		.with_visibility(*visibility)488		.bindable(UnboundValue {489			uctx,490			value: value.clone(),491			name,492		})493}494pub fn evaluate_field_member_static(495	builder: &mut ObjValueBuilder,496	field_ctx: Context,497	value_ctx: Context,498	field: &LFieldMember,499) -> Result<()> {500	let LFieldMember {501		name,502		plus,503		visibility,504		value,505	} = field;506	let Some(name) = evaluate_field_name(field_ctx, name)? else {507		return Ok(());508	};509510	let thunk = build_b_thunk_uno(&value_ctx, value.clone());511	builder512		.field(name)513		.with_add(*plus)514		.with_visibility(*visibility)515		.try_thunk(thunk)?;516	Ok(())517}518519fn evaluate_obj_members(520	super_obj: Option<ObjValue>,521	ctx: Context,522	members: &LObjMembers,523) -> Result<Val> {524	let mut builder = ObjValueBuilder::with_capacity(members.fields.len());525	if let Some(sup) = super_obj {526		builder.with_super(sup);527	}528529	let needs_unbound = members.this.is_some() || members.uses_super;530531	if needs_unbound {532		let uctx = CachedUnbound::new(evaluate_locals_unbound(533			&ctx,534			&members.frame_shape,535			members.this,536			members.locals.clone(),537		));538		for field in &members.fields {539			evaluate_field_member_unbound(&mut builder, ctx.clone(), uctx.clone(), field)?;540		}541		if let Some(asserts_block) = &members.asserts {542			builder.assert(evaluate_object_assertions_unbound(543				uctx,544				asserts_block.clone(),545			));546		}547	} else {548		let a_ctx = ctx549			.pack_captures_sup_this(&members.frame_shape)550			.enter(|fill, ctx| {551				fill_letrec_binds(fill, ctx, &members.locals);552			});553		for field in &members.fields {554			evaluate_field_member_static(&mut builder, ctx.clone(), a_ctx.clone(), field)?;555		}556		if let Some(asserts_block) = &members.asserts {557			builder.assert(evaluate_object_assertions_static(558				a_ctx,559				asserts_block.clone(),560			));561		}562	}563564	Ok(Val::Obj(builder.build()))565}566567pub fn evaluate_assert(ctx: Context, assertion: &LAssertStmt) -> Result<()> {568	let LAssertStmt { cond, message } = assertion;569	let assertion_result = in_frame(570		CallLocation::new(&cond.span),571		|| "assertion condition".to_owned(),572		|| bool::from_untyped(evaluate(ctx.clone(), cond)?),573	)?;574	if !assertion_result {575		in_frame(576			CallLocation::new(&cond.span),577			|| "assertion failure".to_owned(),578			|| {579				if let Some(msg) = message {580					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));581				}582				bail!(AssertionFailed(Val::Null.to_string()?));583			},584		)?;585	}586	Ok(())587}588589fn evaluate_object_assertions_unbound<B: Unbound<Bound = Context>>(590	uctx: B,591	asserts: Rc<LObjAsserts>,592) -> impl ObjectAssertion {593	#[derive(Trace)]594	struct ObjectAssert<B: Trace> {595		uctx: B,596		asserts: Rc<LObjAsserts>,597	}598	impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {599		fn run(&self, sup_this: SupThis) -> Result<()> {600			let a_ctx = self.uctx.bind(sup_this)?;601			let assert_env = Context::enter_using(&a_ctx, &self.asserts.shape);602			for assert in &self.asserts.asserts {603				evaluate_assert(assert_env.clone(), assert)?;604			}605			Ok(())606		}607	}608	ObjectAssert { uctx, asserts }609}610fn evaluate_object_assertions_static(611	a_ctx: Context,612	asserts: Rc<LObjAsserts>,613) -> impl ObjectAssertion {614	#[derive(Trace)]615	struct ObjectAssert {616		assert_env: Context,617		asserts: Rc<LObjAsserts>,618	}619	impl ObjectAssertion for ObjectAssert {620		fn run(&self, _sup_this: SupThis) -> Result<()> {621			for assert in &self.asserts.asserts {622				evaluate_assert(self.assert_env.clone(), assert)?;623			}624			Ok(())625		}626	}627	let assert_env = Context::enter_using(&a_ctx, &asserts.shape);628	ObjectAssert {629		assert_env,630		asserts,631	}632}