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

difftreelog

fix accept null as std.slice argument/in slicing syntax

Yaroslav Bolyukin2024-11-03parent: #7160d47.patch.diff
in: master

3 files changed

modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/evaluate/mod.rs
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;1011use self::destructure::destruct;12use crate::{13	arr::ArrValue,14	bail,15	destructure::evaluate_dest,16	error::{suggest_object_fields, ErrorKind::*},17	evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},18	function::{CallLocation, FuncDesc, FuncVal},19	in_frame,20	typed::Typed,21	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},22	Context, Error, GcHashMap, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,23	ResultExt, Unbound, Val,24};25pub mod destructure;26pub mod operator;2728// This is the amount of bytes that need to be left on the stack before increasing the size.29// It must be at least as large as the stack required by any code that does not call30// `ensure_sufficient_stack`.31const RED_ZONE: usize = 100 * 1024; // 100k3233// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then34// on. This flag has performance relevant characteristics. Don't set it too high.35const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB3637/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations38/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit39/// from this.40///41/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.42#[inline]43pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {44	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)45}4647pub fn evaluate_trivial(expr: &LocExpr) -> Option<Val> {48	fn is_trivial(expr: &LocExpr) -> bool {49		match expr.expr() {50			Expr::Str(_)51			| Expr::Num(_)52			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,53			Expr::Arr(a) => a.iter().all(is_trivial),54			Expr::Parened(e) => is_trivial(e),55			_ => false,56		}57	}58	Some(match expr.expr() {59		Expr::Str(s) => Val::string(s.clone()),60		Expr::Num(n) => {61			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))62		}63		Expr::Literal(LiteralType::False) => Val::Bool(false),64		Expr::Literal(LiteralType::True) => Val::Bool(true),65		Expr::Literal(LiteralType::Null) => Val::Null,66		Expr::Arr(n) => {67			if n.iter().any(|e| !is_trivial(e)) {68				return None;69			}70			Val::Arr(ArrValue::eager(71				n.iter()72					.map(evaluate_trivial)73					.map(|e| e.expect("checked trivial"))74					.collect(),75			))76		}77		Expr::Parened(e) => evaluate_trivial(e)?,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 = GcHashMap::with_capacity(var.capacity_hint());126					destruct(var, item, fctx.clone(), &mut new_bindings)?;127					let ctx = ctx128						.clone()129						.extend(new_bindings, None, None, None)130						.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 = GcHashMap::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 = ctx153						.clone()154						.extend(new_bindings, None, None, None)155						.into_future(fctx);156157					evaluate_comp(ctx, &specs[1..], callback)?;158				}159			}160			_ => bail!(InComprehensionCanOnlyIterateOverArray),161		},162	}163	Ok(())164}165166trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}167impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}168169fn evaluate_object_locals(170	fctx: Pending<Context>,171	locals: Rc<Vec<BindSpec>>,172) -> impl CloneableUnbound<Context> {173	#[derive(Trace, Clone)]174	struct UnboundLocals {175		fctx: Pending<Context>,176		locals: Rc<Vec<BindSpec>>,177	}178	impl Unbound for UnboundLocals {179		type Bound = Context;180181		fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Context> {182			let fctx = Context::new_future();183			let mut new_bindings =184				GcHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());185			for b in self.locals.iter() {186				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;187			}188189			let ctx = self.fctx.unwrap();190			let new_dollar = ctx.dollar().cloned().or_else(|| this.clone());191192			let ctx = ctx193				.extend(new_bindings, new_dollar, sup, this)194				.into_future(fctx);195196			Ok(ctx)197		}198	}199200	UnboundLocals { fctx, locals }201}202203pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(204	builder: &mut ObjValueBuilder,205	ctx: Context,206	uctx: B,207	field: &FieldMember,208) -> Result<()> {209	let name = evaluate_field_name(ctx, &field.name)?;210	let Some(name) = name else {211		return Ok(());212	};213214	match field {215		FieldMember {216			plus,217			params: None,218			visibility,219			value,220			..221		} => {222			#[derive(Trace)]223			struct UnboundValue<B: Trace> {224				uctx: B,225				value: LocExpr,226				name: IStr,227			}228			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {229				type Bound = Val;230				fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {231					evaluate_named(self.uctx.bind(sup, this)?, &self.value, self.name.clone())232				}233			}234235			builder236				.field(name.clone())237				.with_add(*plus)238				.with_visibility(*visibility)239				.with_location(value.span())240				.bindable(UnboundValue {241					uctx,242					value: value.clone(),243					name,244				})?;245		}246		FieldMember {247			params: Some(params),248			visibility,249			value,250			..251		} => {252			#[derive(Trace)]253			struct UnboundMethod<B: Trace> {254				uctx: B,255				value: LocExpr,256				params: ParamsDesc,257				name: IStr,258			}259			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {260				type Bound = Val;261				fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {262					Ok(evaluate_method(263						self.uctx.bind(sup, this)?,264						self.name.clone(),265						self.params.clone(),266						self.value.clone(),267					))268				}269			}270271			builder272				.field(name.clone())273				.with_visibility(*visibility)274				.with_location(value.span())275				.bindable(UnboundMethod {276					uctx,277					value: value.clone(),278					params: params.clone(),279					name,280				})?;281		}282	}283	Ok(())284}285286#[allow(clippy::too_many_lines)]287pub fn evaluate_member_list_object(ctx: Context, members: &[Member]) -> Result<ObjValue> {288	let mut builder = ObjValueBuilder::new();289	let locals = Rc::new(290		members291			.iter()292			.filter_map(|m| match m {293				Member::BindStmt(bind) => Some(bind.clone()),294				_ => None,295			})296			.collect::<Vec<_>>(),297	);298299	let fctx = Context::new_future();300301	// We have single context for all fields, so we can cache binds302	let uctx = CachedUnbound::new(evaluate_object_locals(fctx.clone(), locals));303304	for member in members {305		match member {306			Member::Field(field) => {307				evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;308			}309			Member::AssertStmt(stmt) => {310				#[derive(Trace)]311				struct ObjectAssert<B: Trace> {312					uctx: B,313					assert: AssertStmt,314				}315				impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {316					fn run(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<()> {317						let ctx = self.uctx.bind(sup, this)?;318						evaluate_assert(ctx, &self.assert)319					}320				}321				builder.assert(ObjectAssert {322					uctx: uctx.clone(),323					assert: stmt.clone(),324				});325			}326			Member::BindStmt(_) => {327				// Already handled328			}329		}330	}331	let this = builder.build();332	fctx.fill(ctx.extend(GcHashMap::new(), None, None, Some(this.clone())));333	Ok(this)334}335336pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {337	Ok(match object {338		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,339		ObjBody::ObjComp(obj) => {340			let mut builder = ObjValueBuilder::new();341			let locals = Rc::new(342				obj.pre_locals343					.iter()344					.chain(obj.post_locals.iter())345					.cloned()346					.collect::<Vec<_>>(),347			);348			let mut ctxs = vec![];349			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {350				let fctx = Context::new_future();351				ctxs.push((ctx.clone(), fctx.clone()));352				let uctx = evaluate_object_locals(fctx, locals.clone());353354				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)355			})?;356357			let this = builder.build();358			for (ctx, fctx) in ctxs {359				let _ctx = ctx360					.extend(GcHashMap::new(), None, None, Some(this.clone()))361					.into_future(fctx);362			}363			this364		}365	})366}367368pub fn evaluate_apply(369	ctx: Context,370	value: &LocExpr,371	args: &ArgsDesc,372	loc: CallLocation<'_>,373	tailstrict: bool,374) -> Result<Val> {375	let value = evaluate(ctx.clone(), value)?;376	Ok(match value {377		Val::Func(f) => {378			let body = || f.evaluate(ctx, loc, args, tailstrict);379			if tailstrict {380				body()?381			} else {382				in_frame(loc, || format!("function <{}> call", f.name()), body)?383			}384		}385		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),386	})387}388389pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {390	let value = &assertion.0;391	let msg = &assertion.1;392	let assertion_result = in_frame(393		CallLocation::new(&value.span()),394		|| "assertion condition".to_owned(),395		|| bool::from_untyped(evaluate(ctx.clone(), value)?),396	)?;397	if !assertion_result {398		in_frame(399			CallLocation::new(&value.span()),400			|| "assertion failure".to_owned(),401			|| {402				if let Some(msg) = msg {403					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));404				}405				bail!(AssertionFailed(Val::Null.to_string()?));406			},407		)?;408	}409	Ok(())410}411412pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result<Val> {413	use Expr::*;414	Ok(match expr.expr() {415		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),416		_ => evaluate(ctx, expr)?,417	})418}419420#[allow(clippy::too_many_lines)]421pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result<Val> {422	use Expr::*;423424	if let Some(trivial) = evaluate_trivial(expr) {425		return Ok(trivial);426	}427	let loc = expr.span();428	Ok(match expr.expr() {429		Literal(LiteralType::This) => {430			Val::Obj(ctx.this().ok_or(CantUseSelfOutsideOfObject)?.clone())431		}432		Literal(LiteralType::Super) => Val::Obj(433			ctx.super_obj().ok_or(NoSuperFound)?.with_this(434				ctx.this()435					.expect("if super exists - then this should too")436					.clone(),437			),438		),439		Literal(LiteralType::Dollar) => {440			Val::Obj(ctx.dollar().ok_or(NoTopLevelObjectFound)?.clone())441		}442		Literal(LiteralType::True) => Val::Bool(true),443		Literal(LiteralType::False) => Val::Bool(false),444		Literal(LiteralType::Null) => Val::Null,445		Parened(e) => evaluate(ctx, e)?,446		Str(v) => Val::string(v.clone()),447		Num(v) => Val::try_num(*v)?,448		// I have tried to remove special behavior from super by implementing standalone-super449		// expresion, but looks like this case still needs special treatment.450		//451		// Note that other jsonnet implementations will fail on `if value in (super)` expression,452		// because the standalone super literal is not supported, that is because in other453		// implementations `in super` treated differently from in `smth_else`.454		BinaryOp(field, BinaryOpType::In, e)455			if matches!(e.expr(), Expr::Literal(LiteralType::Super)) =>456		{457			let Some(super_obj) = ctx.super_obj() else {458				return Ok(Val::Bool(false));459			};460			let field = evaluate(ctx.clone(), field)?;461			Val::Bool(super_obj.has_field_ex(field.to_string()?, true))462		}463		BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,464		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,465		Var(name) => in_frame(466			CallLocation::new(&loc),467			|| format!("local <{name}> access"),468			|| ctx.binding(name.clone())?.evaluate(),469		)?,470		Index { indexable, parts } => ensure_sufficient_stack(|| {471			let mut parts = parts.iter();472			let mut indexable = if matches!(indexable.expr(), Expr::Literal(LiteralType::Super)) {473				let part = parts.next().expect("at least part should exist");474				let Some(super_obj) = ctx.super_obj() else {475					#[cfg(feature = "exp-null-coaelse")]476					if part.null_coaelse {477						return Ok(Val::Null);478					}479					bail!(NoSuperFound)480				};481				let name = evaluate(ctx.clone(), &part.value)?;482483				let Val::Str(name) = name else {484					bail!(ValueIndexMustBeTypeGot(485						ValType::Obj,486						ValType::Str,487						name.value_type(),488					))489				};490491				let this = ctx492					.this()493					.expect("no this found, while super present, should not happen");494				let name = name.into_flat();495				match super_obj496					.get_for(name.clone(), this.clone())497					.with_description_src(&part.value, || format!("field <{name}> access"))?498				{499					Some(v) => v,500					#[cfg(feature = "exp-null-coaelse")]501					None if part.null_coaelse => return Ok(Val::Null),502					None => {503						let suggestions = suggest_object_fields(super_obj, name.clone());504505						bail!(NoSuchField(name, suggestions))506					}507				}508			} else {509				evaluate(ctx.clone(), indexable)?510			};511512			for part in parts {513				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {514					(Val::Obj(v), Val::Str(key)) => match v515						.get(key.clone().into_flat())516						.with_description_src(&part.value, || format!("field <{key}> access"))?517					{518						Some(v) => v,519						#[cfg(feature = "exp-null-coaelse")]520						None if part.null_coaelse => return Ok(Val::Null),521						None => {522							let suggestions = suggest_object_fields(&v, key.clone().into_flat());523524							return Err(Error::from(NoSuchField(525								key.clone().into_flat(),526								suggestions,527							)))528							.with_description_src(&part.value, || format!("field <{key}> access"));529						}530					},531					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(532						ValType::Obj,533						ValType::Str,534						n.value_type(),535					)),536					(Val::Arr(v), Val::Num(n)) => {537						let n = n.get();538						if n.fract() > f64::EPSILON {539							bail!(FractionalIndex)540						}541						if n < 0.0 {542							bail!(ArrayBoundsError(n as isize, v.len()));543						}544						v.get(n as usize)?545							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?546					}547					(Val::Arr(_), Val::Str(n)) => {548						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))549					}550					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(551						ValType::Arr,552						ValType::Num,553						n.value_type(),554					)),555556					(Val::Str(s), Val::Num(n)) => Val::Str({557						let v: IStr = s558							.clone()559							.into_flat()560							.chars()561							.skip(n.get() as usize)562							.take(1)563							.collect::<String>()564							.into();565						if v.is_empty() {566							let size = s.into_flat().chars().count();567							bail!(StringBoundsError(n.get() as usize, size))568						}569						StrValue::Flat(v)570					}),571					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(572						ValType::Str,573						ValType::Num,574						n.value_type(),575					)),576					#[cfg(feature = "exp-null-coaelse")]577					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),578					(v, _) => bail!(CantIndexInto(v.value_type())),579				};580			}581			Ok(indexable)582		})?,583		LocalExpr(bindings, returned) => {584			let mut new_bindings: GcHashMap<IStr, Thunk<Val>> =585				GcHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());586			let fctx = Context::new_future();587			for b in bindings {588				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;589			}590			let ctx = ctx.extend(new_bindings, None, None, None).into_future(fctx);591			evaluate(ctx, &returned.clone())?592		}593		Arr(items) => {594			if items.is_empty() {595				Val::Arr(ArrValue::empty())596			} else if items.len() == 1 {597				let item = items[0].clone();598				Val::Arr(ArrValue::lazy(vec![Thunk!(move || evaluate(ctx, &item))]))599			} else {600				Val::Arr(ArrValue::expr(ctx, items.iter().cloned()))601			}602		}603		ArrComp(expr, comp_specs) => {604			let mut out = Vec::new();605			evaluate_comp(ctx, comp_specs, &mut |ctx| {606				let expr = expr.clone();607				out.push(Thunk!(move || evaluate(ctx, &expr)));608				Ok(())609			})?;610			Val::Arr(ArrValue::lazy(out))611		}612		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),613		ObjExtend(a, b) => evaluate_add_op(614			&evaluate(ctx.clone(), a)?,615			&Val::Obj(evaluate_object(ctx, b)?),616		)?,617		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {618			evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)619		})?,620		Function(params, body) => {621			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())622		}623		AssertExpr(assert, returned) => {624			evaluate_assert(ctx.clone(), assert)?;625			evaluate(ctx, returned)?626		}627		ErrorStmt(e) => in_frame(628			CallLocation::new(&loc),629			|| "error statement".to_owned(),630			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),631		)?,632		IfElse {633			cond,634			cond_then,635			cond_else,636		} => {637			if in_frame(638				CallLocation::new(&loc),639				|| "if condition".to_owned(),640				|| bool::from_untyped(evaluate(ctx.clone(), &cond.0)?),641			)? {642				evaluate(ctx, cond_then)?643			} else {644				match cond_else {645					Some(v) => evaluate(ctx, v)?,646					None => Val::Null,647				}648			}649		}650		Slice(value, desc) => {651			fn parse_idx<T: Typed>(652				loc: CallLocation<'_>,653				ctx: &Context,654				expr: Option<&LocExpr>,655				desc: &'static str,656			) -> Result<Option<T>> {657				if let Some(value) = expr {658					Ok(Some(in_frame(659						loc,660						|| format!("slice {desc}"),661						|| T::from_untyped(evaluate(ctx.clone(), value)?),662					)?))663				} else {664					Ok(None)665				}666			}667668			let indexable = evaluate(ctx.clone(), value)?;669			let loc = CallLocation::new(&loc);670671			let start = parse_idx(loc, &ctx, desc.start.as_ref(), "start")?;672			let end = parse_idx(loc, &ctx, desc.end.as_ref(), "end")?;673			let step = parse_idx(loc, &ctx, desc.step.as_ref(), "step")?;674675			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?676		}677		i @ (Import(path) | ImportStr(path) | ImportBin(path)) => {678			let Expr::Str(path) = &path.expr() else {679				bail!("computed imports are not supported")680			};681			let tmp = loc.clone().0;682			let s = ctx.state();683			let resolved_path = s.resolve_from(tmp.source_path(), path as &str)?;684			match i {685				Import(_) => in_frame(686					CallLocation::new(&loc),687					|| format!("import {:?}", path.clone()),688					|| s.import_resolved(resolved_path),689				)?,690				ImportStr(_) => Val::string(s.import_resolved_str(resolved_path)?),691				ImportBin(_) => Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?)),692				_ => unreachable!(),693			}694		}695	})696}
after · crates/jrsonnet-evaluator/src/evaluate/mod.rs
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;1011use self::destructure::destruct;12use crate::{13	arr::ArrValue,14	bail,15	destructure::evaluate_dest,16	error::{suggest_object_fields, ErrorKind::*},17	evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},18	function::{CallLocation, FuncDesc, FuncVal},19	in_frame,20	typed::Typed,21	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},22	Context, Error, GcHashMap, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,23	ResultExt, Unbound, Val,24};25pub mod destructure;26pub mod operator;2728// This is the amount of bytes that need to be left on the stack before increasing the size.29// It must be at least as large as the stack required by any code that does not call30// `ensure_sufficient_stack`.31const RED_ZONE: usize = 100 * 1024; // 100k3233// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then34// on. This flag has performance relevant characteristics. Don't set it too high.35const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB3637/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations38/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit39/// from this.40///41/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.42#[inline]43pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {44	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)45}4647pub fn evaluate_trivial(expr: &LocExpr) -> Option<Val> {48	fn is_trivial(expr: &LocExpr) -> bool {49		match expr.expr() {50			Expr::Str(_)51			| Expr::Num(_)52			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,53			Expr::Arr(a) => a.iter().all(is_trivial),54			Expr::Parened(e) => is_trivial(e),55			_ => false,56		}57	}58	Some(match expr.expr() {59		Expr::Str(s) => Val::string(s.clone()),60		Expr::Num(n) => {61			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))62		}63		Expr::Literal(LiteralType::False) => Val::Bool(false),64		Expr::Literal(LiteralType::True) => Val::Bool(true),65		Expr::Literal(LiteralType::Null) => Val::Null,66		Expr::Arr(n) => {67			if n.iter().any(|e| !is_trivial(e)) {68				return None;69			}70			Val::Arr(ArrValue::eager(71				n.iter()72					.map(evaluate_trivial)73					.map(|e| e.expect("checked trivial"))74					.collect(),75			))76		}77		Expr::Parened(e) => evaluate_trivial(e)?,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 = GcHashMap::with_capacity(var.capacity_hint());126					destruct(var, item, fctx.clone(), &mut new_bindings)?;127					let ctx = ctx128						.clone()129						.extend(new_bindings, None, None, None)130						.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 = GcHashMap::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 = ctx153						.clone()154						.extend(new_bindings, None, None, None)155						.into_future(fctx);156157					evaluate_comp(ctx, &specs[1..], callback)?;158				}159			}160			_ => bail!(InComprehensionCanOnlyIterateOverArray),161		},162	}163	Ok(())164}165166trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}167impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}168169fn evaluate_object_locals(170	fctx: Pending<Context>,171	locals: Rc<Vec<BindSpec>>,172) -> impl CloneableUnbound<Context> {173	#[derive(Trace, Clone)]174	struct UnboundLocals {175		fctx: Pending<Context>,176		locals: Rc<Vec<BindSpec>>,177	}178	impl Unbound for UnboundLocals {179		type Bound = Context;180181		fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Context> {182			let fctx = Context::new_future();183			let mut new_bindings =184				GcHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());185			for b in self.locals.iter() {186				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;187			}188189			let ctx = self.fctx.unwrap();190			let new_dollar = ctx.dollar().cloned().or_else(|| this.clone());191192			let ctx = ctx193				.extend(new_bindings, new_dollar, sup, this)194				.into_future(fctx);195196			Ok(ctx)197		}198	}199200	UnboundLocals { fctx, locals }201}202203pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(204	builder: &mut ObjValueBuilder,205	ctx: Context,206	uctx: B,207	field: &FieldMember,208) -> Result<()> {209	let name = evaluate_field_name(ctx, &field.name)?;210	let Some(name) = name else {211		return Ok(());212	};213214	match field {215		FieldMember {216			plus,217			params: None,218			visibility,219			value,220			..221		} => {222			#[derive(Trace)]223			struct UnboundValue<B: Trace> {224				uctx: B,225				value: LocExpr,226				name: IStr,227			}228			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {229				type Bound = Val;230				fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {231					evaluate_named(self.uctx.bind(sup, this)?, &self.value, self.name.clone())232				}233			}234235			builder236				.field(name.clone())237				.with_add(*plus)238				.with_visibility(*visibility)239				.with_location(value.span())240				.bindable(UnboundValue {241					uctx,242					value: value.clone(),243					name,244				})?;245		}246		FieldMember {247			params: Some(params),248			visibility,249			value,250			..251		} => {252			#[derive(Trace)]253			struct UnboundMethod<B: Trace> {254				uctx: B,255				value: LocExpr,256				params: ParamsDesc,257				name: IStr,258			}259			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {260				type Bound = Val;261				fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {262					Ok(evaluate_method(263						self.uctx.bind(sup, this)?,264						self.name.clone(),265						self.params.clone(),266						self.value.clone(),267					))268				}269			}270271			builder272				.field(name.clone())273				.with_visibility(*visibility)274				.with_location(value.span())275				.bindable(UnboundMethod {276					uctx,277					value: value.clone(),278					params: params.clone(),279					name,280				})?;281		}282	}283	Ok(())284}285286#[allow(clippy::too_many_lines)]287pub fn evaluate_member_list_object(ctx: Context, members: &[Member]) -> Result<ObjValue> {288	let mut builder = ObjValueBuilder::new();289	let locals = Rc::new(290		members291			.iter()292			.filter_map(|m| match m {293				Member::BindStmt(bind) => Some(bind.clone()),294				_ => None,295			})296			.collect::<Vec<_>>(),297	);298299	let fctx = Context::new_future();300301	// We have single context for all fields, so we can cache binds302	let uctx = CachedUnbound::new(evaluate_object_locals(fctx.clone(), locals));303304	for member in members {305		match member {306			Member::Field(field) => {307				evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;308			}309			Member::AssertStmt(stmt) => {310				#[derive(Trace)]311				struct ObjectAssert<B: Trace> {312					uctx: B,313					assert: AssertStmt,314				}315				impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {316					fn run(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<()> {317						let ctx = self.uctx.bind(sup, this)?;318						evaluate_assert(ctx, &self.assert)319					}320				}321				builder.assert(ObjectAssert {322					uctx: uctx.clone(),323					assert: stmt.clone(),324				});325			}326			Member::BindStmt(_) => {327				// Already handled328			}329		}330	}331	let this = builder.build();332	fctx.fill(ctx.extend(GcHashMap::new(), None, None, Some(this.clone())));333	Ok(this)334}335336pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {337	Ok(match object {338		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,339		ObjBody::ObjComp(obj) => {340			let mut builder = ObjValueBuilder::new();341			let locals = Rc::new(342				obj.pre_locals343					.iter()344					.chain(obj.post_locals.iter())345					.cloned()346					.collect::<Vec<_>>(),347			);348			let mut ctxs = vec![];349			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {350				let fctx = Context::new_future();351				ctxs.push((ctx.clone(), fctx.clone()));352				let uctx = evaluate_object_locals(fctx, locals.clone());353354				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)355			})?;356357			let this = builder.build();358			for (ctx, fctx) in ctxs {359				let _ctx = ctx360					.extend(GcHashMap::new(), None, None, Some(this.clone()))361					.into_future(fctx);362			}363			this364		}365	})366}367368pub fn evaluate_apply(369	ctx: Context,370	value: &LocExpr,371	args: &ArgsDesc,372	loc: CallLocation<'_>,373	tailstrict: bool,374) -> Result<Val> {375	let value = evaluate(ctx.clone(), value)?;376	Ok(match value {377		Val::Func(f) => {378			let body = || f.evaluate(ctx, loc, args, tailstrict);379			if tailstrict {380				body()?381			} else {382				in_frame(loc, || format!("function <{}> call", f.name()), body)?383			}384		}385		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),386	})387}388389pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {390	let value = &assertion.0;391	let msg = &assertion.1;392	let assertion_result = in_frame(393		CallLocation::new(&value.span()),394		|| "assertion condition".to_owned(),395		|| bool::from_untyped(evaluate(ctx.clone(), value)?),396	)?;397	if !assertion_result {398		in_frame(399			CallLocation::new(&value.span()),400			|| "assertion failure".to_owned(),401			|| {402				if let Some(msg) = msg {403					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));404				}405				bail!(AssertionFailed(Val::Null.to_string()?));406			},407		)?;408	}409	Ok(())410}411412pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result<Val> {413	use Expr::*;414	Ok(match expr.expr() {415		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),416		_ => evaluate(ctx, expr)?,417	})418}419420#[allow(clippy::too_many_lines)]421pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result<Val> {422	use Expr::*;423424	if let Some(trivial) = evaluate_trivial(expr) {425		return Ok(trivial);426	}427	let loc = expr.span();428	Ok(match expr.expr() {429		Literal(LiteralType::This) => {430			Val::Obj(ctx.this().ok_or(CantUseSelfOutsideOfObject)?.clone())431		}432		Literal(LiteralType::Super) => Val::Obj(433			ctx.super_obj().ok_or(NoSuperFound)?.with_this(434				ctx.this()435					.expect("if super exists - then this should too")436					.clone(),437			),438		),439		Literal(LiteralType::Dollar) => {440			Val::Obj(ctx.dollar().ok_or(NoTopLevelObjectFound)?.clone())441		}442		Literal(LiteralType::True) => Val::Bool(true),443		Literal(LiteralType::False) => Val::Bool(false),444		Literal(LiteralType::Null) => Val::Null,445		Parened(e) => evaluate(ctx, e)?,446		Str(v) => Val::string(v.clone()),447		Num(v) => Val::try_num(*v)?,448		// I have tried to remove special behavior from super by implementing standalone-super449		// expresion, but looks like this case still needs special treatment.450		//451		// Note that other jsonnet implementations will fail on `if value in (super)` expression,452		// because the standalone super literal is not supported, that is because in other453		// implementations `in super` treated differently from in `smth_else`.454		BinaryOp(field, BinaryOpType::In, e)455			if matches!(e.expr(), Expr::Literal(LiteralType::Super)) =>456		{457			let Some(super_obj) = ctx.super_obj() else {458				return Ok(Val::Bool(false));459			};460			let field = evaluate(ctx.clone(), field)?;461			Val::Bool(super_obj.has_field_ex(field.to_string()?, true))462		}463		BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,464		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,465		Var(name) => in_frame(466			CallLocation::new(&loc),467			|| format!("local <{name}> access"),468			|| ctx.binding(name.clone())?.evaluate(),469		)?,470		Index { indexable, parts } => ensure_sufficient_stack(|| {471			let mut parts = parts.iter();472			let mut indexable = if matches!(indexable.expr(), Expr::Literal(LiteralType::Super)) {473				let part = parts.next().expect("at least part should exist");474				let Some(super_obj) = ctx.super_obj() else {475					#[cfg(feature = "exp-null-coaelse")]476					if part.null_coaelse {477						return Ok(Val::Null);478					}479					bail!(NoSuperFound)480				};481				let name = evaluate(ctx.clone(), &part.value)?;482483				let Val::Str(name) = name else {484					bail!(ValueIndexMustBeTypeGot(485						ValType::Obj,486						ValType::Str,487						name.value_type(),488					))489				};490491				let this = ctx492					.this()493					.expect("no this found, while super present, should not happen");494				let name = name.into_flat();495				match super_obj496					.get_for(name.clone(), this.clone())497					.with_description_src(&part.value, || format!("field <{name}> access"))?498				{499					Some(v) => v,500					#[cfg(feature = "exp-null-coaelse")]501					None if part.null_coaelse => return Ok(Val::Null),502					None => {503						let suggestions = suggest_object_fields(super_obj, name.clone());504505						bail!(NoSuchField(name, suggestions))506					}507				}508			} else {509				evaluate(ctx.clone(), indexable)?510			};511512			for part in parts {513				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {514					(Val::Obj(v), Val::Str(key)) => match v515						.get(key.clone().into_flat())516						.with_description_src(&part.value, || format!("field <{key}> access"))?517					{518						Some(v) => v,519						#[cfg(feature = "exp-null-coaelse")]520						None if part.null_coaelse => return Ok(Val::Null),521						None => {522							let suggestions = suggest_object_fields(&v, key.clone().into_flat());523524							return Err(Error::from(NoSuchField(525								key.clone().into_flat(),526								suggestions,527							)))528							.with_description_src(&part.value, || format!("field <{key}> access"));529						}530					},531					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(532						ValType::Obj,533						ValType::Str,534						n.value_type(),535					)),536					(Val::Arr(v), Val::Num(n)) => {537						let n = n.get();538						if n.fract() > f64::EPSILON {539							bail!(FractionalIndex)540						}541						if n < 0.0 {542							bail!(ArrayBoundsError(n as isize, v.len()));543						}544						v.get(n as usize)?545							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?546					}547					(Val::Arr(_), Val::Str(n)) => {548						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))549					}550					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(551						ValType::Arr,552						ValType::Num,553						n.value_type(),554					)),555556					(Val::Str(s), Val::Num(n)) => Val::Str({557						let v: IStr = s558							.clone()559							.into_flat()560							.chars()561							.skip(n.get() as usize)562							.take(1)563							.collect::<String>()564							.into();565						if v.is_empty() {566							let size = s.into_flat().chars().count();567							bail!(StringBoundsError(n.get() as usize, size))568						}569						StrValue::Flat(v)570					}),571					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(572						ValType::Str,573						ValType::Num,574						n.value_type(),575					)),576					#[cfg(feature = "exp-null-coaelse")]577					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),578					(v, _) => bail!(CantIndexInto(v.value_type())),579				};580			}581			Ok(indexable)582		})?,583		LocalExpr(bindings, returned) => {584			let mut new_bindings: GcHashMap<IStr, Thunk<Val>> =585				GcHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());586			let fctx = Context::new_future();587			for b in bindings {588				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;589			}590			let ctx = ctx.extend(new_bindings, None, None, None).into_future(fctx);591			evaluate(ctx, &returned.clone())?592		}593		Arr(items) => {594			if items.is_empty() {595				Val::Arr(ArrValue::empty())596			} else if items.len() == 1 {597				let item = items[0].clone();598				Val::Arr(ArrValue::lazy(vec![Thunk!(move || evaluate(ctx, &item))]))599			} else {600				Val::Arr(ArrValue::expr(ctx, items.iter().cloned()))601			}602		}603		ArrComp(expr, comp_specs) => {604			let mut out = Vec::new();605			evaluate_comp(ctx, comp_specs, &mut |ctx| {606				let expr = expr.clone();607				out.push(Thunk!(move || evaluate(ctx, &expr)));608				Ok(())609			})?;610			Val::Arr(ArrValue::lazy(out))611		}612		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),613		ObjExtend(a, b) => evaluate_add_op(614			&evaluate(ctx.clone(), a)?,615			&Val::Obj(evaluate_object(ctx, b)?),616		)?,617		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {618			evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)619		})?,620		Function(params, body) => {621			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())622		}623		AssertExpr(assert, returned) => {624			evaluate_assert(ctx.clone(), assert)?;625			evaluate(ctx, returned)?626		}627		ErrorStmt(e) => in_frame(628			CallLocation::new(&loc),629			|| "error statement".to_owned(),630			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),631		)?,632		IfElse {633			cond,634			cond_then,635			cond_else,636		} => {637			if in_frame(638				CallLocation::new(&loc),639				|| "if condition".to_owned(),640				|| bool::from_untyped(evaluate(ctx.clone(), &cond.0)?),641			)? {642				evaluate(ctx, cond_then)?643			} else {644				match cond_else {645					Some(v) => evaluate(ctx, v)?,646					None => Val::Null,647				}648			}649		}650		Slice(value, desc) => {651			fn parse_idx<T: Typed>(652				loc: CallLocation<'_>,653				ctx: &Context,654				expr: Option<&LocExpr>,655				desc: &'static str,656			) -> Result<Option<T>> {657				if let Some(value) = expr {658					Ok(in_frame(659						loc,660						|| format!("slice {desc}"),661						|| <Option<T>>::from_untyped(evaluate(ctx.clone(), value)?),662					)?)663				} else {664					Ok(None)665				}666			}667668			let indexable = evaluate(ctx.clone(), value)?;669			let loc = CallLocation::new(&loc);670671			let start = parse_idx(loc, &ctx, desc.start.as_ref(), "start")?;672			let end = parse_idx(loc, &ctx, desc.end.as_ref(), "end")?;673			let step = parse_idx(loc, &ctx, desc.step.as_ref(), "step")?;674675			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?676		}677		i @ (Import(path) | ImportStr(path) | ImportBin(path)) => {678			let Expr::Str(path) = &path.expr() else {679				bail!("computed imports are not supported")680			};681			let tmp = loc.clone().0;682			let s = ctx.state();683			let resolved_path = s.resolve_from(tmp.source_path(), path as &str)?;684			match i {685				Import(_) => in_frame(686					CallLocation::new(&loc),687					|| format!("import {:?}", path.clone()),688					|| s.import_resolved(resolved_path),689				)?,690				ImportStr(_) => Val::string(s.import_resolved_str(resolved_path)?),691				ImportBin(_) => Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?)),692				_ => unreachable!(),693			}694		}695	})696}
modifiedcrates/jrsonnet-evaluator/src/typed/conversions.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/typed/conversions.rs
+++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs
@@ -655,6 +655,26 @@
 	}
 }
 
+impl<T> Typed for Option<T>
+where
+	T: Typed,
+{
+	const TYPE: &'static ComplexValType =
+		&ComplexValType::UnionRef(&[&ComplexValType::Simple(ValType::Null), T::TYPE]);
+
+	fn into_untyped(typed: Self) -> Result<Val> {
+		typed.map_or_else(|| Ok(Val::Null), |v| T::into_untyped(v))
+	}
+
+	fn from_untyped(untyped: Val) -> Result<Self> {
+		if matches!(untyped, Val::Null) {
+			Ok(None)
+		} else {
+			T::from_untyped(untyped).map(Some)
+		}
+	}
+}
+
 pub struct NativeFn<D: NativeDesc>(D::Value);
 impl<D: NativeDesc> Deref for NativeFn<D> {
 	type Target = D::Value;
modifiedcrates/jrsonnet-stdlib/src/arrays.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/arrays.rs
+++ b/crates/jrsonnet-stdlib/src/arrays.rs
@@ -48,11 +48,13 @@
 #[builtin]
 pub fn builtin_slice(
 	indexable: IndexableVal,
-	index: Option<i32>,
-	end: Option<i32>,
-	step: Option<BoundedUsize<1, { i32::MAX as usize }>>,
+	index: Option<Option<i32>>,
+	end: Option<Option<i32>>,
+	step: Option<Option<BoundedUsize<1, { i32::MAX as usize }>>>,
 ) -> Result<Val> {
-	indexable.slice(index, end, step).map(Val::from)
+	indexable
+		.slice(index.flatten(), end.flatten(), step.flatten())
+		.map(Val::from)
 }
 
 #[builtin]