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

difftreelog

source

crates/jrsonnet-evaluator/src/evaluate/mod.rs16.8 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::{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}117118#[allow(clippy::too_many_lines)]119pub fn evaluate(ctx: Context, expr: &LExpr) -> Result<Val> {120	Ok(match expr {121		LExpr::Null => Val::Null,122		LExpr::Bool(b) => Val::Bool(*b),123		LExpr::Str(s) => Val::string(s.clone()),124		LExpr::Num(n) => Val::Num(*n),125		LExpr::Slot(slot) => ctx.slot(*slot).evaluate()?,126		LExpr::BadLocal(name) => panic!("unresolvable reference: {name}"),127		LExpr::Arr { shape, items } => Val::Arr(ArrValue::expr(ctx, shape, items.clone())),128		LExpr::UnaryOp(op, value) => {129			let value = evaluate(ctx, value)?;130			evaluate_unary_op(*op, &value)?131		}132		LExpr::BinaryOp { lhs, op, rhs } => evaluate_binary_op_special(ctx, lhs, *op, rhs)?,133		LExpr::LocalExpr(local_expr) => evaluate_local_expr(ctx, local_expr)?,134		LExpr::IfElse {135			cond,136			cond_then,137			cond_else,138		} => {139			let cond_val = evaluate(ctx.clone(), cond)?;140			let Val::Bool(b) = cond_val else {141				bail!(TypeMismatch(142					"if condition",143					vec![ValType::Bool],144					cond_val.value_type()145				))146			};147			if b {148				evaluate(ctx, cond_then)?149			} else if let Some(e) = cond_else {150				evaluate(ctx, e)?151			} else {152				Val::Null153			}154		}155		LExpr::Error(s, e) => in_frame(156			CallLocation::new(s),157			|| "error statement".to_owned(),158			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),159		)?,160		LExpr::AssertExpr { assert, rest } => {161			evaluate_assert(ctx.clone(), assert)?;162			evaluate(ctx, rest)?163		}164165		LExpr::Function(func) => evaluate_method(166			ctx,167			func.name.clone().unwrap_or_else(names::anonymous),168			func,169		),170		LExpr::IdentityFunction => Val::Func(FuncVal::identity()),171		LExpr::Apply {172			applicable,173			args,174			tailstrict,175		} => evaluate_apply(176			ctx,177			applicable,178			args,179			CallLocation::new(&args.span),180			*tailstrict,181		)?,182		LExpr::Index { indexable, parts } => evaluate_index(ctx, indexable, parts)?,183		LExpr::Obj(body) => evaluate_obj_body(None, ctx, body)?,184		LExpr::ObjExtend(lhs, body) => {185			let lhs_val = evaluate(ctx.clone(), lhs)?;186			let Val::Obj(lhs_obj) = lhs_val else {187				bail!(TypeMismatch(188					"object extend lhs",189					vec![ValType::Obj],190					lhs_val.value_type(),191				))192			};193			evaluate_obj_body(Some(lhs_obj), ctx, body)?194		}195		LExpr::ArrComp(comp) => evaluate_arr_comp(ctx, comp)?,196		LExpr::Slice(slice) => {197			let val = evaluate(ctx.clone(), &slice.value)?;198			let indexable = val.into_indexable()?;199			let start = slice200				.start201				.as_ref()202				.map(|e| evaluate(ctx.clone(), e))203				.transpose()?204				.map(|v| -> Result<i32> { i32::from_untyped(v).description("slice start value") })205				.transpose()?;206			let end = slice207				.end208				.as_ref()209				.map(|e| evaluate(ctx.clone(), e))210				.transpose()?211				.map(|v| -> Result<i32> { i32::from_untyped(v).description("slice end value") })212				.transpose()?;213			let step = slice214				.step215				.as_ref()216				.map(|e| evaluate(ctx, e))217				.transpose()?218				.map(|v| -> Result<BoundedUsize<1, { i32::MAX as usize }>> {219					BoundedUsize::from_untyped(v).description("slice step value")220				})221				.transpose()?;222			Val::from(indexable.slice32(start, end, step)?)223		}224		LExpr::Super => Val::Obj(ctx.try_sup_this()?.standalone_super().ok_or(NoSuperFound)?),225		LExpr::Import {226			kind,227			kind_span,228			path,229		} => with_state(|state| {230			let resolved = state.resolve_from(kind_span.0.source_path(), &path.clone())?;231			Ok::<_, Error>(match kind.value {232				ImportKind::Normal => in_frame(233					CallLocation::new(&kind.span),234					|| "import".to_string(),235					|| state.import_resolved(resolved),236				)?,237				ImportKind::Str => Val::string(state.import_resolved_str(resolved)?),238				ImportKind::Bin => Val::arr(state.import_resolved_bin(resolved)?),239			})240		})?,241	})242}243244fn evaluate_apply(245	ctx: Context,246	applicable: &LExpr,247	args: &LArgsDesc,248	loc: CallLocation<'_>,249	tailstrict: bool,250) -> Result<Val> {251	let func_val = evaluate(ctx.clone(), applicable)?;252	let Val::Func(func) = func_val else {253		bail!(OnlyFunctionsCanBeCalledGot(func_val.value_type()))254	};255256	if func.is_identity() && args.names.is_empty() && args.unnamed.len() == 1 {257		return evaluate_thunk(ctx, args.unnamed[0].clone(), tailstrict)?.evaluate();258	}259260	let name = func.name();261262	if args.names.is_empty() && args.unnamed.len() == 1 && func.params().len() == 1 {263		use crate::function::prepared::PreparedCall;264		let prepared_inline = PreparedCall::empty();265		let arg = evaluate_thunk(ctx, args.unnamed[0].clone(), tailstrict)?;266		let arg_slice = std::slice::from_ref(&arg);267		return in_frame(268			loc,269			|| format!("function <{name}> call"),270			|| {271				func.evaluate_prepared(272					&prepared_inline,273					CallLocation::native(),274					arg_slice,275					&[],276					tailstrict,277				)278			},279		);280	}281282	let unnamed = args283		.unnamed284		.iter()285		.cloned()286		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))287		.collect::<Result<Vec<_>>>()?;288289	// Fast path: positional-only multi-arg call fully covering the290	// params, no defaults.291	if args.names.is_empty() && unnamed.len() == func.params().len() {292		use crate::function::prepared::PreparedCall;293		let prepared_inline = PreparedCall::empty();294		return in_frame(295			loc,296			|| format!("function <{name}> call"),297			|| {298				func.evaluate_prepared(299					&prepared_inline,300					CallLocation::native(),301					&unnamed,302					&[],303					tailstrict,304				)305			},306		);307	}308309	let named = args310		.values311		.iter()312		.cloned()313		.map(|e| evaluate_thunk(ctx.clone(), e, tailstrict))314		.collect::<Result<Vec<_>>>()?;315	let prepare = PreparedFuncVal::new(func, unnamed.len(), &args.names)316		.with_description_src(loc, || format!("function <{name}> preparation"))?;317	in_frame(318		loc,319		|| format!("function <{name}> call"),320		|| prepare.call(CallLocation::native(), &unnamed, &named),321	)322}323324#[allow(clippy::too_many_lines)]325fn evaluate_index(ctx: Context, indexable: &LExpr, parts: &[LIndexPart]) -> Result<Val> {326	let mut parts = parts.iter();327	let mut indexable = if matches!(indexable, LExpr::Super) {328		let part = parts.next().expect("at least part should exist");329		// sup_this existence check might also be skipped here for null-coalesce...330		// But I believe this might cause errors.331		let sup_this = ctx.try_sup_this()?;332333		if !sup_this.has_super() {334			#[cfg(feature = "exp-null-coaelse")]335			if part.null_coaelse {336				return Ok(Val::Null);337			}338			bail!(NoSuperFound);339		}340		let name = evaluate(ctx.clone(), &part.value)?;341342		let Val::Str(name) = name else {343			bail!(ValueIndexMustBeTypeGot(344				ValType::Obj,345				ValType::Str,346				name.value_type(),347			))348		};349350		let name = name.into_flat();351		match sup_this352			.get_super(name.clone())353			.with_description_src(&part.span, || format!("super field <{name}> access"))?354		{355			Some(v) => v,356			#[cfg(feature = "exp-null-coaelse")]357			None if part.null_coaelse => return Ok(Val::Null),358			None => {359				let suggestions = suggest_object_fields(360					&sup_this.standalone_super().expect("super exists"),361					name.clone(),362				);363				bail!(NoSuchField(name, suggestions))364			}365		}366	} else {367		evaluate(ctx.clone(), indexable)?368	};369370	for part in parts {371		let ctx = ctx.clone();372		let loc = CallLocation::new(&part.span);373		let value = indexable;374		let key_val = evaluate(ctx, &part.value)?;375		indexable = match (&value, &key_val) {376			(Val::Obj(obj), Val::Str(key)) => {377				let key = key.clone().into_flat();378				match obj379					.get(key.clone())380					.with_description_src(loc, || format!("field <{key}> access"))?381				{382					Some(v) => v,383					#[cfg(feature = "exp-null-coaelse")]384					None if part.null_coaelse => return Ok(Val::Null),385					None => {386						return Err(Error::from(NoSuchField(387							key.clone(),388							suggest_object_fields(obj, key.clone()),389						)))390						.with_description_src(loc, || format!("field <{key}> access"));391					}392				}393			}394			(Val::Arr(arr), Val::Num(idx)) => {395				let n = idx.get();396				if n.fract() > f64::EPSILON {397					bail!(FractionalIndex)398				}399				let len = arr.len32();400				if n < 0.0 || n > f64::from(len) {401					bail!(ArrayBoundsError(n, len));402				}403				#[expect(404					clippy::cast_possible_truncation,405					clippy::cast_sign_loss,406					reason = "n is checked range"407				)]408				let i = n as u32;409				arr.get32(i)410					.with_description_src(loc, || format!("element <{i}> access"))?411					.ok_or_else(|| ArrayBoundsError(n, len))?412			}413			(Val::Str(s), Val::Num(idx)) => {414				let n = idx.get();415				if n.fract() > f64::EPSILON {416					bail!(FractionalIndex)417				}418				#[expect(419					clippy::cast_possible_truncation,420					clippy::cast_sign_loss,421					reason = "n is checked positive, overflow will truncate as expected"422				)]423				let i = n as usize;424				let flat = s.clone().into_flat();425				#[allow(clippy::cast_possible_truncation, reason = "string is max 4g")]426				if n >= 0.0427					&& n <= f64::from(u32::MAX)428					&& let Some(char) = flat.chars().nth(i)429				{430					Val::string(char)431				} else {432					let len = flat.chars().count();433					bail!(StringBoundsError(n, len as u32))434				}435			}436			#[cfg(feature = "exp-null-coaelse")]437			(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),438			_ => bail!(ValueIndexMustBeTypeGot(439				value.value_type(),440				ValType::Str,441				key_val.value_type()442			)),443		};444	}445	Ok(indexable)446}447448fn evaluate_obj_body(super_obj: Option<ObjValue>, ctx: Context, body: &LObjBody) -> Result<Val> {449	match body {450		LObjBody::MemberList(members) => evaluate_obj_members(super_obj, ctx, members),451		LObjBody::ObjComp(comp) => evaluate_obj_comp(super_obj, ctx, comp),452	}453}454455pub fn evaluate_field_member_unbound<B: Unbound<Bound = Context> + Clone>(456	builder: &mut ObjValueBuilder,457	ctx: Context,458	uctx: B,459	field: &LFieldMember,460) -> Result<()> {461	#[derive(Trace)]462	struct UnboundValue<B: Trace> {463		uctx: B,464		value: Rc<(ClosureShape, LExpr)>,465		name: IStr,466	}467	impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {468		type Bound = Val;469		fn bind(&self, sup_this: SupThis) -> Result<Val> {470			let a_ctx = self.uctx.bind(sup_this)?;471			let b_ctx = Context::enter_using(&a_ctx, &self.value.0);472			evaluate(b_ctx, &self.value.1)473		}474	}475476	let LFieldMember {477		name,478		plus,479		visibility,480		value,481	} = field;482	let Some(name) = evaluate_field_name(ctx, name)? else {483		return Ok(());484	};485486	builder487		.field(name.clone())488		.with_add(*plus)489		.with_visibility(*visibility)490		.bindable(UnboundValue {491			uctx,492			value: value.clone(),493			name,494		})495}496pub fn evaluate_field_member_static(497	builder: &mut ObjValueBuilder,498	field_ctx: Context,499	value_ctx: Context,500	field: &LFieldMember,501) -> Result<()> {502	let LFieldMember {503		name,504		plus,505		visibility,506		value,507	} = field;508	let Some(name) = evaluate_field_name(field_ctx, name)? else {509		return Ok(());510	};511512	let env = Context::enter_using(&value_ctx, &value.0);513	let value = value.clone();514	builder515		.field(name)516		.with_add(*plus)517		.with_visibility(*visibility)518		.try_thunk(Thunk!(move || evaluate(env, &value.1)))?;519	Ok(())520}521522fn evaluate_obj_members(523	super_obj: Option<ObjValue>,524	ctx: Context,525	members: &LObjMembers,526) -> Result<Val> {527	let mut builder = ObjValueBuilder::with_capacity(members.fields.len());528	if let Some(sup) = super_obj {529		builder.with_super(sup);530	}531532	let needs_unbound = members.this.is_some() || members.uses_super;533534	if needs_unbound {535		let uctx = CachedUnbound::new(evaluate_locals_unbound(536			&ctx,537			&members.frame_shape,538			members.this,539			members.locals.clone(),540		));541		for field in &members.fields {542			evaluate_field_member_unbound(&mut builder, ctx.clone(), uctx.clone(), field)?;543		}544		if let Some(asserts_block) = &members.asserts {545			builder.assert(evaluate_object_assertions_unbound(546				uctx,547				asserts_block.clone(),548			));549		}550	} else {551		let a_ctx = ctx552			.pack_captures_sup_this(&members.frame_shape)553			.enter(|fill, ctx| {554				fill_letrec_binds(fill, ctx, &members.locals);555			});556		for field in &members.fields {557			evaluate_field_member_static(&mut builder, ctx.clone(), a_ctx.clone(), field)?;558		}559		if let Some(asserts_block) = &members.asserts {560			builder.assert(evaluate_object_assertions_static(561				a_ctx,562				asserts_block.clone(),563			));564		}565	}566567	Ok(Val::Obj(builder.build()))568}569570pub fn evaluate_assert(ctx: Context, assertion: &LAssertStmt) -> Result<()> {571	let LAssertStmt { cond, message } = assertion;572	let assertion_result = in_frame(573		CallLocation::new(&cond.span),574		|| "assertion condition".to_owned(),575		|| bool::from_untyped(evaluate(ctx.clone(), cond)?),576	)?;577	if !assertion_result {578		in_frame(579			CallLocation::new(&cond.span),580			|| "assertion failure".to_owned(),581			|| {582				if let Some(msg) = message {583					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));584				}585				bail!(AssertionFailed(Val::Null.to_string()?));586			},587		)?;588	}589	Ok(())590}591592fn evaluate_object_assertions_unbound<B: Unbound<Bound = Context>>(593	uctx: B,594	asserts: Rc<LObjAsserts>,595) -> impl ObjectAssertion {596	#[derive(Trace)]597	struct ObjectAssert<B: Trace> {598		uctx: B,599		asserts: Rc<LObjAsserts>,600	}601	impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {602		fn run(&self, sup_this: SupThis) -> Result<()> {603			let a_ctx = self.uctx.bind(sup_this)?;604			let assert_env = Context::enter_using(&a_ctx, &self.asserts.shape);605			for assert in &self.asserts.asserts {606				evaluate_assert(assert_env.clone(), assert)?;607			}608			Ok(())609		}610	}611	ObjectAssert { uctx, asserts }612}613fn evaluate_object_assertions_static(614	a_ctx: Context,615	asserts: Rc<LObjAsserts>,616) -> impl ObjectAssertion {617	#[derive(Trace)]618	struct ObjectAssert {619		assert_env: Context,620		asserts: Rc<LObjAsserts>,621	}622	impl ObjectAssertion for ObjectAssert {623		fn run(&self, _sup_this: SupThis) -> Result<()> {624			for assert in &self.asserts.asserts {625				evaluate_assert(self.assert_env.clone(), assert)?;626			}627			Ok(())628		}629	}630	let assert_env = Context::enter_using(&a_ctx, &asserts.shape);631	ObjectAssert {632		assert_env,633		asserts,634	}635}