git.delta.rocks / jrsonnet / refs/commits / 9411dba19aab

difftreelog

refactor(ir) explicit Rc wrapping

yvwlymvmYaroslav Bolyukin2026-03-19parent: #15dca76.patch.diff
in: master

51 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -717,6 +717,7 @@
 name = "jrsonnet-parser"
 version = "0.5.0-pre97"
 dependencies = [
+ "insta",
  "jrsonnet-gcmodule",
  "jrsonnet-interner",
  "peg",
modifiedcrates/jrsonnet-evaluator/src/arr/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/mod.rs
+++ b/crates/jrsonnet-evaluator/src/arr/mod.rs
@@ -1,12 +1,12 @@
 use std::{
 	any::Any,
 	fmt::{self},
-	num::NonZeroU32,
+	num::NonZeroU32, rc::Rc,
 };
 
 use jrsonnet_gcmodule::{cc_dyn, Cc};
 use jrsonnet_interner::IBytes;
-use jrsonnet_parser::LocExpr;
+use jrsonnet_parser::{Expr, Spanned};
 
 use crate::{function::FuncVal, Context, Result, Thunk, Val};
 
@@ -37,7 +37,7 @@
 		Self::new(RangeArray::empty())
 	}
 
-	pub fn expr(ctx: Context, exprs: impl IntoIterator<Item = LocExpr>) -> Self {
+	pub fn expr(ctx: Context, exprs: Rc<Vec<Spanned<Expr>>>) -> Self {
 		Self::new(ExprArray::new(ctx, exprs))
 	}
 
modifiedcrates/jrsonnet-evaluator/src/arr/spec.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/spec.rs
+++ b/crates/jrsonnet-evaluator/src/arr/spec.rs
@@ -1,8 +1,9 @@
-use std::{any::Any, cell::RefCell, fmt::Debug, iter, mem::replace};
+use std::rc::Rc;
+use std::{any::Any, cell::RefCell, fmt::Debug, mem::replace};
 
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::{IBytes, IStr};
-use jrsonnet_parser::LocExpr;
+use jrsonnet_parser::{Expr, Spanned};
 
 use super::ArrValue;
 use crate::{
@@ -103,25 +104,25 @@
 }
 
 #[derive(Debug, Trace, Clone)]
-enum ArrayThunk<T: 'static + Trace> {
+enum ArrayThunk {
 	Computed(Val),
 	Errored(Error),
-	Waiting(T),
+	Waiting,
 	Pending,
 }
 
 #[derive(Debug, Trace, Clone)]
 pub struct ExprArray {
 	ctx: Context,
-	cached: Cc<RefCell<Vec<ArrayThunk<LocExpr>>>>,
+	src: Rc<Vec<Spanned<Expr>>>,
+	cached: Cc<RefCell<Vec<ArrayThunk>>>,
 }
 impl ExprArray {
-	pub fn new(ctx: Context, items: impl IntoIterator<Item = LocExpr>) -> Self {
+	pub fn new(ctx: Context, src: Rc<Vec<Spanned<Expr>>>) -> Self {
 		Self {
 			ctx,
-			cached: Cc::new(RefCell::new(
-				items.into_iter().map(ArrayThunk::Waiting).collect(),
-			)),
+			cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; src.len()])),
+			src,
 		}
 	}
 }
@@ -137,16 +138,16 @@
 			ArrayThunk::Computed(c) => return Ok(Some(c.clone())),
 			ArrayThunk::Errored(e) => return Err(e.clone()),
 			ArrayThunk::Pending => return Err(InfiniteRecursionDetected.into()),
-			ArrayThunk::Waiting(..) => {}
+			ArrayThunk::Waiting => {}
 		};
 
-		let ArrayThunk::Waiting(expr) =
+		let ArrayThunk::Waiting =
 			replace(&mut self.cached.borrow_mut()[index], ArrayThunk::Pending)
 		else {
 			unreachable!()
 		};
 
-		let new_value = match evaluate(self.ctx.clone(), &expr) {
+		let new_value = match evaluate(self.ctx.clone(), &self.src[index]) {
 			Ok(v) => v,
 			Err(e) => {
 				self.cached.borrow_mut()[index] = ArrayThunk::Errored(e.clone());
@@ -163,7 +164,7 @@
 		match &self.cached.borrow()[index] {
 			ArrayThunk::Computed(c) => return Some(Thunk::evaluated(c.clone())),
 			ArrayThunk::Errored(e) => return Some(Thunk::errored(e.clone())),
-			ArrayThunk::Waiting(_) | ArrayThunk::Pending => {}
+			ArrayThunk::Waiting | ArrayThunk::Pending => {}
 		};
 
 		#[derive(Trace)]
@@ -406,7 +407,7 @@
 #[derive(Trace, Debug, Clone)]
 pub struct MappedArray<const WITH_INDEX: bool> {
 	inner: ArrValue,
-	cached: Cc<RefCell<Vec<ArrayThunk<()>>>>,
+	cached: Cc<RefCell<Vec<ArrayThunk>>>,
 	mapper: FuncVal,
 }
 impl<const WITH_INDEX: bool> MappedArray<WITH_INDEX> {
@@ -414,7 +415,7 @@
 		let len = inner.len();
 		Self {
 			inner,
-			cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting(()); len])),
+			cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; len])),
 			mapper,
 		}
 	}
@@ -439,10 +440,10 @@
 			ArrayThunk::Computed(c) => return Ok(Some(c.clone())),
 			ArrayThunk::Errored(e) => return Err(e.clone()),
 			ArrayThunk::Pending => return Err(InfiniteRecursionDetected.into()),
-			ArrayThunk::Waiting(..) => {}
+			ArrayThunk::Waiting => {}
 		};
 
-		let ArrayThunk::Waiting(()) =
+		let ArrayThunk::Waiting =
 			replace(&mut self.cached.borrow_mut()[index], ArrayThunk::Pending)
 		else {
 			unreachable!()
@@ -472,7 +473,7 @@
 		match &self.cached.borrow()[index] {
 			ArrayThunk::Computed(c) => return Some(Thunk::evaluated(c.clone())),
 			ArrayThunk::Errored(e) => return Some(Thunk::errored(e.clone())),
-			ArrayThunk::Waiting(()) | ArrayThunk::Pending => {}
+			ArrayThunk::Waiting | ArrayThunk::Pending => {}
 		};
 
 		#[derive(Trace)]
modifiedcrates/jrsonnet-evaluator/src/async_import.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/async_import.rs
+++ b/crates/jrsonnet-evaluator/src/async_import.rs
@@ -1,10 +1,11 @@
+use std::rc::Rc;
 use std::{any::Any, cell::RefCell, future::Future};
 
 use jrsonnet_gcmodule::Acyclic;
 use jrsonnet_parser::{
-	ArgsDesc, AssertStmt, BindSpec, CompSpec, Destruct, Expr, FieldMember, FieldName, ForSpecData,
-	IfSpecData, LocExpr, Member, ObjBody, Param, ParamsDesc, ParserSettings, SliceDesc, Source,
-	SourcePath,
+	ArgsDesc, AssertExpr, AssertStmt, BindSpec, CompSpec, Destruct, Expr, FieldMember, FieldName,
+	ForSpecData, IfElse, IfSpecData, ImportKind, Member, ObjBody, Param, ParamsDesc,
+	ParserSettings, Slice, SliceDesc, Source, SourcePath, Spanned,
 };
 use rustc_hash::FxHashMap;
 
@@ -19,7 +20,7 @@
 
 // Visits all nodes, trying to find import statements
 #[allow(clippy::too_many_lines)]
-pub fn find_imports(expr: &LocExpr, out: &mut FoundImports) {
+pub fn find_imports(expr: &Spanned<Expr>, out: &mut FoundImports) {
 	fn in_destruct(dest: &Destruct, #[allow(unused_variables)] out: &mut FoundImports) {
 		match dest {
 			#[cfg(feature = "exp-destruct")]
@@ -120,9 +121,9 @@
 							find_imports(value, out);
 						}
 						Member::BindStmt(_) => todo!(),
-						Member::AssertStmt(AssertStmt(expr, expr2)) => {
-							find_imports(expr, out);
-							if let Some(expr) = expr2 {
+						Member::AssertStmt(assert) => {
+							find_imports(&assert.0, out);
+							if let Some(expr) = &assert.1 {
 								find_imports(expr, out);
 							}
 						}
@@ -132,12 +133,12 @@
 			ObjBody::ObjComp(_) => todo!(),
 		}
 	}
-	match &*expr.expr() {
-		Expr::Import(v) | Expr::ImportStr(v) | Expr::ImportBin(v) => {
-			if let Expr::Str(s) = &*v.expr() {
+	match &**expr {
+		Expr::Import(_, v) => {
+			if let Expr::Str(s) = &***v {
 				out.0.push(Import {
 					path: ResolvePathOwned::Str(s.to_string()),
-					expression: matches!(&*expr.expr(), Expr::Import(_)),
+					expression: matches!(&**expr, Expr::Import(ImportKind::Normal, _)),
 				});
 			}
 			// Non-string import will fail in runtime
@@ -146,7 +147,7 @@
 		Expr::Literal(_) | Expr::Str(_) | Expr::Num(_) | Expr::Var(_) => {}
 
 		Expr::Arr(arr) => {
-			for expr in arr {
+			for expr in &**arr {
 				find_imports(expr, out);
 			}
 		}
@@ -159,16 +160,20 @@
 			find_imports(expr, out);
 			in_obj(obj, out);
 		}
-		Expr::BinaryOp(a, _, b) => {
-			find_imports(a, out);
-			find_imports(b, out);
+		Expr::BinaryOp(binop) => {
+			find_imports(&binop.lhs, out);
+			find_imports(&binop.rhs, out);
 		}
-		Expr::AssertExpr(AssertStmt(expr, expr2), then) => {
+		Expr::AssertExpr(assert) => {
+			let AssertExpr {
+				assert: AssertStmt(expr, expr2),
+				rest,
+			} = &**assert;
 			find_imports(expr, out);
 			if let Some(expr) = expr2 {
 				find_imports(expr, out);
 			}
-			find_imports(then, out);
+			find_imports(rest, out);
 		}
 		Expr::LocalExpr(specs, expr) => {
 			in_bind(specs, out);
@@ -188,19 +193,24 @@
 			in_params(params, out);
 			find_imports(expr, out);
 		}
-		Expr::IfElse {
-			cond: IfSpecData(expr),
-			cond_then,
-			cond_else,
-		} => {
+		Expr::IfElse(if_else) => {
+			let IfElse {
+				cond: IfSpecData(expr),
+				cond_then,
+				cond_else,
+			} = &**if_else;
 			find_imports(expr, out);
 			find_imports(cond_then, out);
 			if let Some(expr) = cond_else {
 				find_imports(expr, out);
 			}
 		}
-		Expr::Slice(expr, SliceDesc { start, end, step }) => {
-			find_imports(expr, out);
+		Expr::Slice(slice) => {
+			let Slice {
+				value,
+				slice: SliceDesc { start, end, step },
+			} = &**slice;
+			find_imports(value, out);
 			if let Some(expr) = start {
 				find_imports(expr, out);
 			}
@@ -314,8 +324,9 @@
 						};
 						let source = Source::new(path.clone(), code.clone());
 						// If failed - then skip import
-						file.parsed =
-							jrsonnet_parser::parse(&code, &ParserSettings { source }).ok();
+						file.parsed = jrsonnet_parser::parse(&code, &ParserSettings { source })
+							.map(Rc::new)
+							.ok();
 						if let Some(parsed) = &file.parsed {
 							let mut imports = FoundImports(vec![]);
 							find_imports(parsed, &mut imports);
modifiedcrates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -4,9 +4,9 @@
 	fmt::{Debug, Display},
 };
 
-use jrsonnet_gcmodule::Trace;
+use jrsonnet_gcmodule::{Acyclic, Trace};
 use jrsonnet_interner::IStr;
-use jrsonnet_parser::{BinaryOpType, LocExpr, Source, SourcePath, Span, UnaryOpType};
+use jrsonnet_parser::{BinaryOpType, Source, SourcePath, Span, Spanned, UnaryOpType};
 use jrsonnet_types::ValType;
 use thiserror::Error;
 
@@ -324,7 +324,7 @@
 pub trait ErrorSource {
 	fn to_location(self) -> Option<Span>;
 }
-impl ErrorSource for &LocExpr {
+impl<T: Acyclic> ErrorSource for &Spanned<T> {
 	fn to_location(self) -> Option<Span> {
 		Some(self.span())
 	}
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;10use rustc_hash::FxHashMap;1112use self::destructure::destruct;13use crate::{14	arr::ArrValue,15	bail,16	destructure::evaluate_dest,17	error::{suggest_object_fields, ErrorKind::*},18	evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},19	function::{CallLocation, FuncDesc, FuncVal},20	gc::WithCapacityExt as _,21	in_frame,22	typed::Typed,23	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},24	with_state, Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,25	ResultExt, SupThis, Unbound, Val,26};27pub mod destructure;28pub mod operator;2930// This is the amount of bytes that need to be left on the stack before increasing the size.31// It must be at least as large as the stack required by any code that does not call32// `ensure_sufficient_stack`.33const RED_ZONE: usize = 100 * 1024; // 100k3435// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then36// on. This flag has performance relevant characteristics. Don't set it too high.37const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB3839/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations40/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit41/// from this.42///43/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.44#[inline]45pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {46	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)47}4849pub fn evaluate_trivial(expr: &LocExpr) -> Option<Val> {50	fn is_trivial(expr: &LocExpr) -> bool {51		match expr.expr() {52			Expr::Str(_)53			| Expr::Num(_)54			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,55			Expr::Arr(a) => a.iter().all(is_trivial),56			_ => false,57		}58	}59	Some(match expr.expr() {60		Expr::Str(s) => Val::string(s.clone()),61		Expr::Num(n) => {62			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))63		}64		Expr::Literal(LiteralType::False) => Val::Bool(false),65		Expr::Literal(LiteralType::True) => Val::Bool(true),66		Expr::Literal(LiteralType::Null) => Val::Null,67		Expr::Arr(n) => {68			if n.iter().any(|e| !is_trivial(e)) {69				return None;70			}71			Val::Arr(ArrValue::eager(72				n.iter()73					.map(evaluate_trivial)74					.map(|e| e.expect("checked trivial"))75					.collect(),76			))77		}78		_ => return None,79	})80}8182pub fn evaluate_method(ctx: Context, name: IStr, params: ParamsDesc, body: LocExpr) -> Val {83	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {84		name,85		ctx,86		params,87		body,88	})))89}9091pub fn evaluate_field_name(ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {92	Ok(match field_name {93		FieldName::Fixed(n) => Some(n.clone()),94		FieldName::Dyn(expr) => in_frame(95			CallLocation::new(&expr.span()),96			|| "evaluating field name".to_string(),97			|| {98				let value = evaluate(ctx, expr)?;99				if matches!(value, Val::Null) {100					Ok(None)101				} else {102					Ok(Some(IStr::from_untyped(value)?))103				}104			},105		)?,106	})107}108109pub fn evaluate_comp(110	ctx: Context,111	specs: &[CompSpec],112	callback: &mut impl FnMut(Context) -> Result<()>,113) -> Result<()> {114	match specs.first() {115		None => callback(ctx)?,116		Some(CompSpec::IfSpec(IfSpecData(cond))) => {117			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {118				evaluate_comp(ctx, &specs[1..], callback)?;119			}120		}121		Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {122			Val::Arr(list) => {123				for item in list.iter_lazy() {124					let fctx = Pending::new();125					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());126					destruct(var, item, fctx.clone(), &mut new_bindings)?;127					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);128129					evaluate_comp(ctx, &specs[1..], callback)?;130				}131			}132			#[cfg(feature = "exp-object-iteration")]133			Val::Obj(obj) => {134				for field in obj.fields(135					// TODO: Should there be ability to preserve iteration order?136					#[cfg(feature = "exp-preserve-order")]137					false,138				) {139					let fctx = Pending::new();140					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());141					let obj = obj.clone();142					let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![143						Thunk::evaluated(Val::string(field.clone())),144						Thunk!(move || obj.get(field).transpose().expect(145							"field exists, as field name was obtained from object.fields()",146						)),147					])));148					destruct(var, value, fctx.clone(), &mut new_bindings)?;149					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);150151					evaluate_comp(ctx, &specs[1..], callback)?;152				}153			}154			_ => bail!(InComprehensionCanOnlyIterateOverArray),155		},156	}157	Ok(())158}159160trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}161impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}162163fn evaluate_object_locals(164	fctx: Context,165	locals: Rc<Vec<BindSpec>>,166) -> impl CloneableUnbound<Context> {167	#[derive(Trace, Clone)]168	struct UnboundLocals {169		fctx: Context,170		locals: Rc<Vec<BindSpec>>,171	}172	impl Unbound for UnboundLocals {173		type Bound = Context;174175		fn bind(&self, sup_this: SupThis) -> Result<Context> {176			let fctx = Context::new_future();177			let mut new_bindings =178				FxHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());179			for b in self.locals.iter() {180				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;181			}182183			let ctx = self.fctx.clone();184185			let ctx = ctx186				.extend_bindings_sup_this(new_bindings, sup_this)187				.into_future(fctx);188189			Ok(ctx)190		}191	}192193	UnboundLocals { fctx, locals }194}195196pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(197	builder: &mut ObjValueBuilder,198	ctx: Context,199	uctx: B,200	field: &FieldMember,201) -> Result<()> {202	let name = evaluate_field_name(ctx, &field.name)?;203	let Some(name) = name else {204		return Ok(());205	};206207	match field {208		FieldMember {209			plus,210			params: None,211			visibility,212			value,213			..214		} => {215			#[derive(Trace)]216			struct UnboundValue<B: Trace> {217				uctx: B,218				value: LocExpr,219				name: IStr,220			}221			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {222				type Bound = Val;223				fn bind(&self, sup_this: SupThis) -> Result<Val> {224					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())225				}226			}227228			builder229				.field(name.clone())230				.with_add(*plus)231				.with_visibility(*visibility)232				.with_location(value.span())233				.bindable(UnboundValue {234					uctx,235					value: value.clone(),236					name,237				})?;238		}239		FieldMember {240			params: Some(params),241			visibility,242			value,243			..244		} => {245			#[derive(Trace)]246			struct UnboundMethod<B: Trace> {247				uctx: B,248				value: LocExpr,249				params: ParamsDesc,250				name: IStr,251			}252			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {253				type Bound = Val;254				fn bind(&self, sup_this: SupThis) -> Result<Val> {255					Ok(evaluate_method(256						self.uctx.bind(sup_this)?,257						self.name.clone(),258						self.params.clone(),259						self.value.clone(),260					))261				}262			}263264			builder265				.field(name.clone())266				.with_visibility(*visibility)267				.with_location(value.span())268				.bindable(UnboundMethod {269					uctx,270					value: value.clone(),271					params: params.clone(),272					name,273				})?;274		}275	}276	Ok(())277}278279#[allow(clippy::too_many_lines)]280pub fn evaluate_member_list_object(ctx: Context, members: &[Member]) -> Result<ObjValue> {281	let mut builder = ObjValueBuilder::new();282	let locals = Rc::new(283		members284			.iter()285			.filter_map(|m| match m {286				Member::BindStmt(bind) => Some(bind.clone()),287				_ => None,288			})289			.collect::<Vec<_>>(),290	);291292	// We have single context for all fields, so we can cache binds293	let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));294295	for member in members {296		match member {297			Member::Field(field) => {298				evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;299			}300			Member::AssertStmt(stmt) => {301				#[derive(Trace)]302				struct ObjectAssert<B: Trace> {303					uctx: B,304					assert: AssertStmt,305				}306				impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {307					fn run(&self, sup_this: SupThis) -> Result<()> {308						let ctx = self.uctx.bind(sup_this)?;309						evaluate_assert(ctx, &self.assert)310					}311				}312				builder.assert(ObjectAssert {313					uctx: uctx.clone(),314					assert: stmt.clone(),315				});316			}317			Member::BindStmt(_) => {318				// Already handled319			}320		}321	}322	Ok(builder.build())323}324325pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {326	Ok(match object {327		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,328		ObjBody::ObjComp(obj) => {329			let mut builder = ObjValueBuilder::new();330			let locals = Rc::new(331				obj.pre_locals332					.iter()333					.chain(obj.post_locals.iter())334					.cloned()335					.collect::<Vec<_>>(),336			);337			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {338				let uctx = evaluate_object_locals(ctx.clone(), locals.clone());339340				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)341			})?;342343			builder.build()344		}345	})346}347348pub fn evaluate_apply(349	ctx: Context,350	value: &LocExpr,351	args: &ArgsDesc,352	loc: CallLocation<'_>,353	tailstrict: bool,354) -> Result<Val> {355	let value = evaluate(ctx.clone(), value)?;356	Ok(match value {357		Val::Func(f) => {358			let body = || f.evaluate(ctx, loc, args, tailstrict);359			if tailstrict {360				body()?361			} else {362				in_frame(loc, || format!("function <{}> call", f.name()), body)?363			}364		}365		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),366	})367}368369pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {370	let value = &assertion.0;371	let msg = &assertion.1;372	let assertion_result = in_frame(373		CallLocation::new(&value.span()),374		|| "assertion condition".to_owned(),375		|| bool::from_untyped(evaluate(ctx.clone(), value)?),376	)?;377	if !assertion_result {378		in_frame(379			CallLocation::new(&value.span()),380			|| "assertion failure".to_owned(),381			|| {382				if let Some(msg) = msg {383					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));384				}385				bail!(AssertionFailed(Val::Null.to_string()?));386			},387		)?;388	}389	Ok(())390}391392pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result<Val> {393	use Expr::*;394	Ok(match expr.expr() {395		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),396		_ => evaluate(ctx, expr)?,397	})398}399400#[allow(clippy::too_many_lines)]401pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result<Val> {402	use Expr::*;403404	if let Some(trivial) = evaluate_trivial(expr) {405		return Ok(trivial);406	}407	let loc = expr.span();408	Ok(match expr.expr() {409		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),410		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),411		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),412		Literal(LiteralType::True) => Val::Bool(true),413		Literal(LiteralType::False) => Val::Bool(false),414		Literal(LiteralType::Null) => Val::Null,415		Str(v) => Val::string(v.clone()),416		Num(v) => Val::try_num(*v)?,417		// I have tried to remove special behavior from super by implementing standalone-super418		// expresion, but looks like this case still needs special treatment.419		//420		// Note that other jsonnet implementations will fail on `if value in (super)` expression,421		// because the standalone super literal is not supported, that is because in other422		// implementations `in super` treated differently from `in smth_else`.423		BinaryOp(field, BinaryOpType::In, e)424			if matches!(e.expr(), Expr::Literal(LiteralType::Super)) =>425		{426			let sup_this = ctx.try_sup_this()?;427			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.428			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.429			if !sup_this.has_super() {430				return Ok(Val::Bool(false));431			}432			let field = evaluate(ctx, field)?;433			Val::Bool(sup_this.field_in_super(field.to_string()?))434		}435		BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,436		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,437		Var(name) => in_frame(438			CallLocation::new(&loc),439			|| format!("local <{name}> access"),440			|| ctx.binding(name.clone())?.evaluate(),441		)?,442		Index { indexable, parts } => ensure_sufficient_stack(|| {443			let mut parts = parts.iter();444			let mut indexable = if matches!(indexable.expr(), Expr::Literal(LiteralType::Super)) {445				let part = parts.next().expect("at least part should exist");446				// sup_this existence check might also be skipped here for null-coalesce...447				// But I believe this might cause errors.448				let sup_this = ctx.try_sup_this()?;449				if !sup_this.has_super() {450					#[cfg(feature = "exp-null-coaelse")]451					if part.null_coaelse {452						return Ok(Val::Null);453					}454					bail!(NoSuperFound)455				}456				let name = evaluate(ctx.clone(), &part.value)?;457458				let Val::Str(name) = name else {459					bail!(ValueIndexMustBeTypeGot(460						ValType::Obj,461						ValType::Str,462						name.value_type(),463					))464				};465466				let name = name.into_flat();467				match sup_this468					.get_super(name.clone())469					.with_description_src(&part.value, || format!("field <{name}> access"))?470				{471					Some(v) => v,472					#[cfg(feature = "exp-null-coaelse")]473					None if part.null_coaelse => return Ok(Val::Null),474					None => {475						let suggestions = suggest_object_fields(476							&sup_this.standalone_super().expect("super exists"),477							name.clone(),478						);479480						bail!(NoSuchField(name, suggestions))481					}482				}483			} else {484				evaluate(ctx.clone(), indexable)?485			};486487			for part in parts {488				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {489					(Val::Obj(v), Val::Str(key)) => match v490						.get(key.clone().into_flat())491						.with_description_src(&part.value, || format!("field <{key}> access"))?492					{493						Some(v) => v,494						#[cfg(feature = "exp-null-coaelse")]495						None if part.null_coaelse => return Ok(Val::Null),496						None => {497							let suggestions = suggest_object_fields(&v, key.clone().into_flat());498499							return Err(Error::from(NoSuchField(500								key.clone().into_flat(),501								suggestions,502							)))503							.with_description_src(&part.value, || format!("field <{key}> access"));504						}505					},506					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(507						ValType::Obj,508						ValType::Str,509						n.value_type(),510					)),511					(Val::Arr(v), Val::Num(n)) => {512						let n = n.get();513						if n.fract() > f64::EPSILON {514							bail!(FractionalIndex)515						}516						if n < 0.0 {517							bail!(ArrayBoundsError(n as isize, v.len()));518						}519						v.get(n as usize)?520							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?521					}522					(Val::Arr(_), Val::Str(n)) => {523						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))524					}525					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(526						ValType::Arr,527						ValType::Num,528						n.value_type(),529					)),530531					(Val::Str(s), Val::Num(n)) => Val::Str({532						let n = n.get();533						if n.fract() > f64::EPSILON {534							bail!(FractionalIndex)535						}536						if n < 0.0 {537							bail!(ArrayBoundsError(n as isize, s.into_flat().chars().count()));538						}539						let v: IStr = s540							.clone()541							.into_flat()542							.chars()543							.skip(n as usize)544							.take(1)545							.collect::<String>()546							.into();547						if v.is_empty() {548							bail!(StringBoundsError(n as usize, s.into_flat().chars().count()))549						}550						StrValue::Flat(v)551					}),552					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(553						ValType::Str,554						ValType::Num,555						n.value_type(),556					)),557					#[cfg(feature = "exp-null-coaelse")]558					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),559					(v, _) => bail!(CantIndexInto(v.value_type())),560				};561			}562			Ok(indexable)563		})?,564		LocalExpr(bindings, returned) => {565			let mut new_bindings: FxHashMap<IStr, Thunk<Val>> =566				FxHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());567			let fctx = Context::new_future();568			for b in bindings {569				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;570			}571			let ctx = ctx.extend_bindings(new_bindings).into_future(fctx);572			evaluate(ctx, &returned.clone())?573		}574		Arr(items) => {575			if items.is_empty() {576				Val::Arr(ArrValue::empty())577			} else if items.len() == 1 {578				let item = items[0].clone();579				Val::Arr(ArrValue::lazy(vec![Thunk!(move || evaluate(ctx, &item))]))580			} else {581				Val::Arr(ArrValue::expr(ctx, items.iter().cloned()))582			}583		}584		ArrComp(expr, comp_specs) => {585			let mut out = Vec::new();586			evaluate_comp(ctx, comp_specs, &mut |ctx| {587				let expr = expr.clone();588				out.push(Thunk!(move || evaluate(ctx, &expr)));589				Ok(())590			})?;591			Val::Arr(ArrValue::lazy(out))592		}593		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),594		ObjExtend(a, b) => evaluate_add_op(595			&evaluate(ctx.clone(), a)?,596			&Val::Obj(evaluate_object(ctx, b)?),597		)?,598		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {599			evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)600		})?,601		Function(params, body) => {602			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())603		}604		AssertExpr(assert, returned) => {605			evaluate_assert(ctx.clone(), assert)?;606			evaluate(ctx, returned)?607		}608		ErrorStmt(e) => in_frame(609			CallLocation::new(&loc),610			|| "error statement".to_owned(),611			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),612		)?,613		IfElse {614			cond,615			cond_then,616			cond_else,617		} => {618			if in_frame(619				CallLocation::new(&loc),620				|| "if condition".to_owned(),621				|| bool::from_untyped(evaluate(ctx.clone(), &cond.0)?),622			)? {623				evaluate(ctx, cond_then)?624			} else {625				match cond_else {626					Some(v) => evaluate(ctx, v)?,627					None => Val::Null,628				}629			}630		}631		Slice(value, desc) => {632			fn parse_idx<T: Typed>(633				loc: CallLocation<'_>,634				ctx: Context,635				expr: Option<&LocExpr>,636				desc: &'static str,637			) -> Result<Option<T>> {638				if let Some(value) = expr {639					Ok(in_frame(640						loc,641						|| format!("slice {desc}"),642						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),643					)?)644				} else {645					Ok(None)646				}647			}648649			let indexable = evaluate(ctx.clone(), value)?;650			let loc = CallLocation::new(&loc);651652			let start = parse_idx(loc, ctx.clone(), desc.start.as_ref(), "start")?;653			let end = parse_idx(loc, ctx.clone(), desc.end.as_ref(), "end")?;654			let step = parse_idx(loc, ctx, desc.step.as_ref(), "step")?;655656			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?657		}658		i @ (Import(path) | ImportStr(path) | ImportBin(path)) => {659			let Expr::Str(path) = &path.expr() else {660				bail!("computed imports are not supported")661			};662			let tmp = loc.clone().0;663			with_state(|s| {664				let resolved_path = s.resolve_from(tmp.source_path(), path)?;665				Ok(match i {666					Import(_) => in_frame(667						CallLocation::new(&loc),668						|| format!("import {:?}", path.clone()),669						|| s.import_resolved(resolved_path),670					)?,671					ImportStr(_) => Val::string(s.import_resolved_str(resolved_path)?),672					ImportBin(_) => {673						Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?))674					}675					_ => unreachable!(),676				}) as Result<Val>677			})?678		}679	})680}
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, ImportKind, LiteralType, Member, ObjBody, ParamsDesc, Spanned,8};9use jrsonnet_types::ValType;10use rustc_hash::FxHashMap;1112use self::destructure::destruct;13use crate::{14	arr::ArrValue,15	bail,16	destructure::evaluate_dest,17	error::{suggest_object_fields, ErrorKind::*},18	evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},19	function::{CallLocation, FuncDesc, FuncVal},20	gc::WithCapacityExt as _,21	in_frame,22	typed::Typed,23	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},24	with_state, Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,25	ResultExt, SupThis, Unbound, Val,26};27pub mod destructure;28pub mod operator;2930// This is the amount of bytes that need to be left on the stack before increasing the size.31// It must be at least as large as the stack required by any code that does not call32// `ensure_sufficient_stack`.33const RED_ZONE: usize = 100 * 1024; // 100k3435// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then36// on. This flag has performance relevant characteristics. Don't set it too high.37const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB3839/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations40/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit41/// from this.42///43/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.44#[inline]45pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {46	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)47}4849pub fn evaluate_trivial(expr: &Spanned<Expr>) -> Option<Val> {50	fn is_trivial(expr: &Spanned<Expr>) -> bool {51		match &**expr {52			Expr::Str(_)53			| Expr::Num(_)54			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,55			Expr::Arr(a) => a.iter().all(is_trivial),56			_ => false,57		}58	}59	Some(match &**expr {60		Expr::Str(s) => Val::string(s.clone()),61		Expr::Num(n) => {62			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))63		}64		Expr::Literal(LiteralType::False) => Val::Bool(false),65		Expr::Literal(LiteralType::True) => Val::Bool(true),66		Expr::Literal(LiteralType::Null) => Val::Null,67		Expr::Arr(n) => {68			if n.iter().any(|e| !is_trivial(e)) {69				return None;70			}71			Val::Arr(ArrValue::eager(72				n.iter()73					.map(evaluate_trivial)74					.map(|e| e.expect("checked trivial"))75					.collect(),76			))77		}78		_ => return None,79	})80}8182pub fn evaluate_method(83	ctx: Context,84	name: IStr,85	params: ParamsDesc,86	body: Rc<Spanned<Expr>>,87) -> Val {88	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {89		name,90		ctx,91		params,92		body,93	})))94}9596pub fn evaluate_field_name(ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {97	Ok(match field_name {98		FieldName::Fixed(n) => Some(n.clone()),99		FieldName::Dyn(expr) => in_frame(100			CallLocation::new(&expr.span()),101			|| "evaluating field name".to_string(),102			|| {103				let value = evaluate(ctx, expr)?;104				if matches!(value, Val::Null) {105					Ok(None)106				} else {107					Ok(Some(IStr::from_untyped(value)?))108				}109			},110		)?,111	})112}113114pub fn evaluate_comp(115	ctx: Context,116	specs: &[CompSpec],117	callback: &mut impl FnMut(Context) -> Result<()>,118) -> Result<()> {119	match specs.first() {120		None => callback(ctx)?,121		Some(CompSpec::IfSpec(IfSpecData(cond))) => {122			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {123				evaluate_comp(ctx, &specs[1..], callback)?;124			}125		}126		Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {127			Val::Arr(list) => {128				for item in list.iter_lazy() {129					let fctx = Pending::new();130					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());131					destruct(var, item, fctx.clone(), &mut new_bindings)?;132					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);133134					evaluate_comp(ctx, &specs[1..], callback)?;135				}136			}137			#[cfg(feature = "exp-object-iteration")]138			Val::Obj(obj) => {139				for field in obj.fields(140					// TODO: Should there be ability to preserve iteration order?141					#[cfg(feature = "exp-preserve-order")]142					false,143				) {144					let fctx = Pending::new();145					let mut new_bindings = FxHashMap::with_capacity(var.capacity_hint());146					let obj = obj.clone();147					let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![148						Thunk::evaluated(Val::string(field.clone())),149						Thunk!(move || obj.get(field).transpose().expect(150							"field exists, as field name was obtained from object.fields()",151						)),152					])));153					destruct(var, value, fctx.clone(), &mut new_bindings)?;154					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);155156					evaluate_comp(ctx, &specs[1..], callback)?;157				}158			}159			_ => bail!(InComprehensionCanOnlyIterateOverArray),160		},161	}162	Ok(())163}164165trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}166impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}167168fn evaluate_object_locals(169	fctx: Context,170	locals: Rc<Vec<BindSpec>>,171) -> impl CloneableUnbound<Context> {172	#[derive(Trace, Clone)]173	struct UnboundLocals {174		fctx: Context,175		locals: Rc<Vec<BindSpec>>,176	}177	impl Unbound for UnboundLocals {178		type Bound = Context;179180		fn bind(&self, sup_this: SupThis) -> Result<Context> {181			let fctx = Context::new_future();182			let mut new_bindings =183				FxHashMap::with_capacity(self.locals.iter().map(BindSpec::capacity_hint).sum());184			for b in self.locals.iter() {185				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;186			}187188			let ctx = self.fctx.clone();189190			let ctx = ctx191				.extend_bindings_sup_this(new_bindings, sup_this)192				.into_future(fctx);193194			Ok(ctx)195		}196	}197198	UnboundLocals { fctx, locals }199}200201pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(202	builder: &mut ObjValueBuilder,203	ctx: Context,204	uctx: B,205	field: &FieldMember,206) -> Result<()> {207	let name = evaluate_field_name(ctx, &field.name)?;208	let Some(name) = name else {209		return Ok(());210	};211212	match field {213		FieldMember {214			plus,215			params: None,216			visibility,217			value,218			..219		} => {220			#[derive(Trace)]221			struct UnboundValue<B: Trace> {222				uctx: B,223				value: Rc<Spanned<Expr>>,224				name: IStr,225			}226			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {227				type Bound = Val;228				fn bind(&self, sup_this: SupThis) -> Result<Val> {229					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())230				}231			}232233			builder234				.field(name.clone())235				.with_add(*plus)236				.with_visibility(*visibility)237				.with_location(value.span())238				.bindable(UnboundValue {239					uctx,240					value: value.clone(),241					name,242				})?;243		}244		FieldMember {245			params: Some(params),246			visibility,247			value,248			..249		} => {250			#[derive(Trace)]251			struct UnboundMethod<B: Trace> {252				uctx: B,253				value: Rc<Spanned<Expr>>,254				params: ParamsDesc,255				name: IStr,256			}257			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {258				type Bound = Val;259				fn bind(&self, sup_this: SupThis) -> Result<Val> {260					Ok(evaluate_method(261						self.uctx.bind(sup_this)?,262						self.name.clone(),263						self.params.clone(),264						self.value.clone(),265					))266				}267			}268269			builder270				.field(name.clone())271				.with_visibility(*visibility)272				.with_location(value.span())273				.bindable(UnboundMethod {274					uctx,275					value: value.clone(),276					params: params.clone(),277					name,278				})?;279		}280	}281	Ok(())282}283284#[allow(clippy::too_many_lines)]285pub fn evaluate_member_list_object(ctx: Context, members: &[Member]) -> Result<ObjValue> {286	let mut builder = ObjValueBuilder::new();287	let locals = Rc::new(288		members289			.iter()290			.filter_map(|m| match m {291				Member::BindStmt(bind) => Some(bind.clone()),292				_ => None,293			})294			.collect::<Vec<_>>(),295	);296297	// We have single context for all fields, so we can cache binds298	let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));299300	for member in members {301		match member {302			Member::Field(field) => {303				evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;304			}305			Member::AssertStmt(stmt) => {306				#[derive(Trace)]307				struct ObjectAssert<B: Trace> {308					uctx: B,309					assert: Rc<AssertStmt>,310				}311				impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {312					fn run(&self, sup_this: SupThis) -> Result<()> {313						let ctx = self.uctx.bind(sup_this)?;314						evaluate_assert(ctx, &self.assert)315					}316				}317				builder.assert(ObjectAssert {318					uctx: uctx.clone(),319					assert: stmt.clone(),320				});321			}322			Member::BindStmt(_) => {323				// Already handled324			}325		}326	}327	Ok(builder.build())328}329330pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {331	Ok(match object {332		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,333		ObjBody::ObjComp(obj) => {334			let mut builder = ObjValueBuilder::new();335			let locals = Rc::new(336				obj.pre_locals337					.iter()338					.chain(obj.post_locals.iter())339					.cloned()340					.collect::<Vec<_>>(),341			);342			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {343				let uctx = evaluate_object_locals(ctx.clone(), locals.clone());344345				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)346			})?;347348			builder.build()349		}350	})351}352353pub fn evaluate_apply(354	ctx: Context,355	value: &Spanned<Expr>,356	args: &ArgsDesc,357	loc: CallLocation<'_>,358	tailstrict: bool,359) -> Result<Val> {360	let value = evaluate(ctx.clone(), value)?;361	Ok(match value {362		Val::Func(f) => {363			let body = || f.evaluate(ctx, loc, args, tailstrict);364			if tailstrict {365				body()?366			} else {367				in_frame(loc, || format!("function <{}> call", f.name()), body)?368			}369		}370		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),371	})372}373374pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {375	let value = &assertion.0;376	let msg = &assertion.1;377	let assertion_result = in_frame(378		CallLocation::new(&value.span()),379		|| "assertion condition".to_owned(),380		|| bool::from_untyped(evaluate(ctx.clone(), value)?),381	)?;382	if !assertion_result {383		in_frame(384			CallLocation::new(&value.span()),385			|| "assertion failure".to_owned(),386			|| {387				if let Some(msg) = msg {388					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));389				}390				bail!(AssertionFailed(Val::Null.to_string()?));391			},392		)?;393	}394	Ok(())395}396397pub fn evaluate_named(ctx: Context, expr: &Spanned<Expr>, name: IStr) -> Result<Val> {398	use Expr::*;399	Ok(match &**expr {400		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),401		_ => evaluate(ctx, expr)?,402	})403}404405#[allow(clippy::too_many_lines)]406pub fn evaluate(ctx: Context, expr: &Spanned<Expr>) -> Result<Val> {407	use Expr::*;408409	if let Some(trivial) = evaluate_trivial(expr) {410		return Ok(trivial);411	}412	let loc = expr.span();413	Ok(match &**expr {414		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),415		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),416		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),417		Literal(LiteralType::True) => Val::Bool(true),418		Literal(LiteralType::False) => Val::Bool(false),419		Literal(LiteralType::Null) => Val::Null,420		Str(v) => Val::string(v.clone()),421		Num(v) => Val::try_num(*v)?,422		// I have tried to remove special behavior from super by implementing standalone-super423		// expresion, but looks like this case still needs special treatment.424		//425		// Note that other jsonnet implementations will fail on `if value in (super)` expression,426		// because the standalone super literal is not supported, that is because in other427		// implementations `in super` treated differently from `in smth_else`.428		BinaryOp(bin)429			if matches!(&*bin.rhs, Expr::Literal(LiteralType::Super))430				&& bin.op == BinaryOpType::In =>431		{432			let sup_this = ctx.try_sup_this()?;433			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.434			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.435			if !sup_this.has_super() {436				return Ok(Val::Bool(false));437			}438			let field = evaluate(ctx, &bin.lhs)?;439			Val::Bool(sup_this.field_in_super(field.to_string()?))440		}441		BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?,442		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,443		Var(name) => in_frame(444			CallLocation::new(&loc),445			|| format!("local <{name}> access"),446			|| ctx.binding(name.clone())?.evaluate(),447		)?,448		Index { indexable, parts } => ensure_sufficient_stack(|| {449			let mut parts = parts.iter();450			let mut indexable = if matches!(&***indexable, Expr::Literal(LiteralType::Super)) {451				let part = parts.next().expect("at least part should exist");452				// sup_this existence check might also be skipped here for null-coalesce...453				// But I believe this might cause errors.454				let sup_this = ctx.try_sup_this()?;455				if !sup_this.has_super() {456					#[cfg(feature = "exp-null-coaelse")]457					if part.null_coaelse {458						return Ok(Val::Null);459					}460					bail!(NoSuperFound)461				}462				let name = evaluate(ctx.clone(), &part.value)?;463464				let Val::Str(name) = name else {465					bail!(ValueIndexMustBeTypeGot(466						ValType::Obj,467						ValType::Str,468						name.value_type(),469					))470				};471472				let name = name.into_flat();473				match sup_this474					.get_super(name.clone())475					.with_description_src(&part.value, || format!("field <{name}> access"))?476				{477					Some(v) => v,478					#[cfg(feature = "exp-null-coaelse")]479					None if part.null_coaelse => return Ok(Val::Null),480					None => {481						let suggestions = suggest_object_fields(482							&sup_this.standalone_super().expect("super exists"),483							name.clone(),484						);485486						bail!(NoSuchField(name, suggestions))487					}488				}489			} else {490				evaluate(ctx.clone(), indexable)?491			};492493			for part in parts {494				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {495					(Val::Obj(v), Val::Str(key)) => match v496						.get(key.clone().into_flat())497						.with_description_src(&part.value, || format!("field <{key}> 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(&v, key.clone().into_flat());504505							return Err(Error::from(NoSuchField(506								key.clone().into_flat(),507								suggestions,508							)))509							.with_description_src(&part.value, || format!("field <{key}> access"));510						}511					},512					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(513						ValType::Obj,514						ValType::Str,515						n.value_type(),516					)),517					(Val::Arr(v), Val::Num(n)) => {518						let n = n.get();519						if n.fract() > f64::EPSILON {520							bail!(FractionalIndex)521						}522						if n < 0.0 {523							bail!(ArrayBoundsError(n as isize, v.len()));524						}525						v.get(n as usize)?526							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?527					}528					(Val::Arr(_), Val::Str(n)) => {529						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))530					}531					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(532						ValType::Arr,533						ValType::Num,534						n.value_type(),535					)),536537					(Val::Str(s), Val::Num(n)) => Val::Str({538						let n = n.get();539						if n.fract() > f64::EPSILON {540							bail!(FractionalIndex)541						}542						if n < 0.0 {543							bail!(ArrayBoundsError(n as isize, s.into_flat().chars().count()));544						}545						let v: IStr = s546							.clone()547							.into_flat()548							.chars()549							.skip(n as usize)550							.take(1)551							.collect::<String>()552							.into();553						if v.is_empty() {554							bail!(StringBoundsError(n as usize, s.into_flat().chars().count()))555						}556						StrValue::Flat(v)557					}),558					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(559						ValType::Str,560						ValType::Num,561						n.value_type(),562					)),563					#[cfg(feature = "exp-null-coaelse")]564					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),565					(v, _) => bail!(CantIndexInto(v.value_type())),566				};567			}568			Ok(indexable)569		})?,570		LocalExpr(bindings, returned) => {571			let mut new_bindings: FxHashMap<IStr, Thunk<Val>> =572				FxHashMap::with_capacity(bindings.iter().map(BindSpec::capacity_hint).sum());573			let fctx = Context::new_future();574			for b in bindings {575				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;576			}577			let ctx = ctx.extend_bindings(new_bindings).into_future(fctx);578			evaluate(ctx, &returned.clone())?579		}580		Arr(items) => {581			if items.is_empty() {582				Val::Arr(ArrValue::empty())583			} else {584				Val::Arr(ArrValue::expr(ctx, items.clone()))585			}586		}587		ArrComp(expr, comp_specs) => {588			let mut out = Vec::new();589			evaluate_comp(ctx, comp_specs, &mut |ctx| {590				let expr = expr.clone();591				out.push(Thunk!(move || evaluate(ctx, &expr)));592				Ok(())593			})?;594			Val::Arr(ArrValue::lazy(out))595		}596		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),597		ObjExtend(a, b) => evaluate_add_op(598			&evaluate(ctx.clone(), a)?,599			&Val::Obj(evaluate_object(ctx, b)?),600		)?,601		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {602			evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)603		})?,604		Function(params, body) => {605			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())606		}607		AssertExpr(assert) => {608			evaluate_assert(ctx.clone(), &assert.assert)?;609			evaluate(ctx, &assert.rest)?610		}611		ErrorStmt(e) => in_frame(612			CallLocation::new(&loc),613			|| "error statement".to_owned(),614			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),615		)?,616		IfElse (if_else)617		// {618		// 	cond,619		// 	cond_then,620		// 	cond_else,621		// }622		=> {623			if in_frame(624				CallLocation::new(&loc),625				|| "if condition".to_owned(),626				|| bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.0)?),627			)? {628				evaluate(ctx, &if_else.cond_then)?629			} else {630				match &if_else.cond_else {631					Some(v) => evaluate(ctx, v)?,632					None => Val::Null,633				}634			}635		}636		Slice(slice) => {637			fn parse_idx<T: Typed>(638				loc: CallLocation<'_>,639				ctx: Context,640				expr: Option<&Spanned<Expr>>,641				desc: &'static str,642			) -> Result<Option<T>> {643				if let Some(value) = expr {644					Ok(in_frame(645						loc,646						|| format!("slice {desc}"),647						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),648					)?)649				} else {650					Ok(None)651				}652			}653654			let indexable = evaluate(ctx.clone(), &slice.value)?;655			let loc = CallLocation::new(&loc);656657			let start = parse_idx(loc, ctx.clone(), slice.slice.start.as_ref(), "start")?;658			let end = parse_idx(loc, ctx.clone(), slice.slice.end.as_ref(), "end")?;659			let step = parse_idx(loc, ctx, slice.slice.step.as_ref(), "step")?;660661			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?662		}663		Import(kind, path) => {664			let Expr::Str(path) = &***path else {665				bail!("computed imports are not supported")666			};667			let tmp = loc.clone().0;668			with_state(|s| {669				let resolved_path = s.resolve_from(tmp.source_path(), path)?;670				Ok(match kind {671					ImportKind::Normal => in_frame(672						CallLocation::new(&loc),673						|| format!("import {:?}", path.clone()),674						|| s.import_resolved(resolved_path),675					)?,676					ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?),677					ImportKind::Bin => {678						Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?))679					}680				}) as Result<Val>681			})?682		}683	})684}
modifiedcrates/jrsonnet-evaluator/src/evaluate/operator.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/operator.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/operator.rs
@@ -1,6 +1,6 @@
 use std::cmp::Ordering;
 
-use jrsonnet_parser::{BinaryOpType, LocExpr, UnaryOpType};
+use jrsonnet_parser::{BinaryOpType, Expr, Spanned, UnaryOpType};
 
 use crate::{
 	arr::ArrValue,
@@ -147,9 +147,9 @@
 
 pub fn evaluate_binary_op_special(
 	ctx: Context,
-	a: &LocExpr,
+	a: &Spanned<Expr>,
 	op: BinaryOpType,
-	b: &LocExpr,
+	b: &Spanned<Expr>,
 ) -> Result<Val> {
 	use BinaryOpType::*;
 	use Val::*;
modifiedcrates/jrsonnet-evaluator/src/function/arglike.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/arglike.rs
+++ b/crates/jrsonnet-evaluator/src/function/arglike.rs
@@ -1,8 +1,9 @@
 use std::collections::HashMap;
+use std::rc::Rc;
 
 use jrsonnet_gcmodule::Trace;
 use jrsonnet_interner::IStr;
-use jrsonnet_parser::{ArgsDesc, LocExpr, SourceFifo, SourcePath};
+use jrsonnet_parser::{ArgsDesc, Expr, SourceFifo, SourcePath, Spanned};
 
 use crate::{evaluate, typed::Typed, with_state, Context, Result, Thunk, Val};
 
@@ -13,7 +14,7 @@
 	fn evaluate_arg(&self, ctx: Context, tailstrict: bool) -> Result<Thunk<Val>>;
 }
 
-impl ArgLike for &LocExpr {
+impl ArgLike for &Rc<Spanned<Expr>> {
 	fn evaluate_arg(&self, ctx: Context, tailstrict: bool) -> Result<Thunk<Val>> {
 		Ok(if tailstrict {
 			Thunk::evaluated(evaluate(ctx, self)?)
modifiedcrates/jrsonnet-evaluator/src/function/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/mod.rs
+++ b/crates/jrsonnet-evaluator/src/function/mod.rs
@@ -1,10 +1,10 @@
-use std::fmt::Debug;
+use std::{fmt::Debug, rc::Rc};
 
 pub use arglike::{ArgLike, ArgsLike, TlaArg};
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::IStr;
 pub use jrsonnet_macros::builtin;
-use jrsonnet_parser::{Destruct, Expr, LocExpr, ParamsDesc, Span};
+use jrsonnet_parser::{Destruct, Expr, ParamsDesc, Span, Spanned};
 
 use self::{
 	arglike::OptionalContext,
@@ -66,7 +66,7 @@
 	/// Function parameter definition
 	pub params: ParamsDesc,
 	/// Function body
-	pub body: LocExpr,
+	pub body: Rc<Spanned<Expr>>,
 }
 impl FuncDesc {
 	/// Create body context, but fill arguments without defaults with lazy error
@@ -240,7 +240,7 @@
 					#[cfg(feature = "exp-destruct")]
 					_ => return false,
 				};
-				desc.body.expr() == &Expr::Var(id.clone())
+				**desc.body == Expr::Var(id.clone())
 			}
 			_ => false,
 		}
modifiedcrates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/lib.rs
+++ b/crates/jrsonnet-evaluator/src/lib.rs
@@ -45,7 +45,7 @@
 #[doc(hidden)]
 pub use jrsonnet_macros;
 pub use jrsonnet_parser as parser;
-use jrsonnet_parser::{LocExpr, ParserSettings, Source, SourcePath};
+use jrsonnet_parser::{Expr, ParserSettings, Source, SourcePath, Spanned};
 pub use obj::*;
 pub use rustc_hash;
 use rustc_hash::FxHashMap;
@@ -186,7 +186,7 @@
 struct FileData {
 	string: Option<IStr>,
 	bytes: Option<IBytes>,
-	parsed: Option<LocExpr>,
+	parsed: Option<Rc<Spanned<Expr>>>,
 	evaluated: Option<Val>,
 
 	evaluating: bool,
@@ -350,6 +350,7 @@
 						source: file_name.clone(),
 					},
 				)
+				.map(Rc::new)
 				.map_err(|e| ImportSyntaxError {
 					path: file_name.clone(),
 					error: Box::new(e),
modifiedcrates/jrsonnet-parser/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-parser/Cargo.toml
+++ b/crates/jrsonnet-parser/Cargo.toml
@@ -19,3 +19,6 @@
 static_assertions.workspace = true
 
 peg.workspace = true
+
+[dev-dependencies]
+insta.workspace = true
modifiedcrates/jrsonnet-parser/src/expr.rsdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/expr.rs
+++ b/crates/jrsonnet-parser/src/expr.rs
@@ -14,7 +14,7 @@
 	/// {fixed: 2}
 	Fixed(IStr),
 	/// {["dyn"+"amic"]: 3}
-	Dyn(LocExpr),
+	Dyn(Spanned<Expr>),
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Acyclic)]
@@ -34,8 +34,8 @@
 	}
 }
 
-#[derive(Clone, Debug, PartialEq, Acyclic)]
-pub struct AssertStmt(pub LocExpr, pub Option<LocExpr>);
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct AssertStmt(pub Spanned<Expr>, pub Option<Spanned<Expr>>);
 
 #[derive(Debug, PartialEq, Acyclic)]
 pub struct FieldMember {
@@ -43,14 +43,14 @@
 	pub plus: bool,
 	pub params: Option<ParamsDesc>,
 	pub visibility: Visibility,
-	pub value: LocExpr,
+	pub value: Rc<Spanned<Expr>>,
 }
 
 #[derive(Debug, PartialEq, Acyclic)]
 pub enum Member {
 	Field(FieldMember),
 	BindStmt(BindSpec),
-	AssertStmt(AssertStmt),
+	AssertStmt(Rc<AssertStmt>),
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Acyclic)]
@@ -147,7 +147,7 @@
 
 /// name, default value
 #[derive(Debug, PartialEq, Acyclic)]
-pub struct Param(pub Destruct, pub Option<LocExpr>);
+pub struct Param(pub Destruct, pub Option<Rc<Spanned<Expr>>>);
 
 /// Defined function parameters
 #[derive(Debug, Clone, PartialEq, Acyclic)]
@@ -162,11 +162,11 @@
 
 #[derive(Debug, PartialEq, Acyclic)]
 pub struct ArgsDesc {
-	pub unnamed: Vec<LocExpr>,
-	pub named: Vec<(IStr, LocExpr)>,
+	pub unnamed: Vec<Rc<Spanned<Expr>>>,
+	pub named: Vec<(IStr, Rc<Spanned<Expr>>)>,
 }
 impl ArgsDesc {
-	pub fn new(unnamed: Vec<LocExpr>, named: Vec<(IStr, LocExpr)>) -> Self {
+	pub fn new(unnamed: Vec<Rc<Spanned<Expr>>>, named: Vec<(IStr, Rc<Spanned<Expr>>)>) -> Self {
 		Self { unnamed, named }
 	}
 }
@@ -192,7 +192,7 @@
 	},
 	#[cfg(feature = "exp-destruct")]
 	Object {
-		fields: Vec<(IStr, Option<Destruct>, Option<LocExpr>)>,
+		fields: Vec<(IStr, Option<Destruct>, Option<Spanned<Expr>>)>,
 		rest: Option<DestructRest>,
 	},
 }
@@ -244,12 +244,12 @@
 pub enum BindSpec {
 	Field {
 		into: Destruct,
-		value: LocExpr,
+		value: Rc<Spanned<Expr>>,
 	},
 	Function {
 		name: IStr,
 		params: ParamsDesc,
-		value: LocExpr,
+		value: Rc<Spanned<Expr>>,
 	},
 }
 impl BindSpec {
@@ -262,10 +262,10 @@
 }
 
 #[derive(Debug, PartialEq, Acyclic)]
-pub struct IfSpecData(pub LocExpr);
+pub struct IfSpecData(pub Spanned<Expr>);
 
 #[derive(Debug, PartialEq, Acyclic)]
-pub struct ForSpecData(pub Destruct, pub LocExpr);
+pub struct ForSpecData(pub Destruct, pub Spanned<Expr>);
 
 #[derive(Debug, PartialEq, Acyclic)]
 pub enum CompSpec {
@@ -276,7 +276,7 @@
 #[derive(Debug, PartialEq, Acyclic)]
 pub struct ObjComp {
 	pub pre_locals: Vec<BindSpec>,
-	pub field: FieldMember,
+	pub field: Rc<FieldMember>,
 	pub post_locals: Vec<BindSpec>,
 	pub compspecs: Vec<CompSpec>,
 }
@@ -299,11 +299,44 @@
 
 #[derive(Debug, PartialEq, Acyclic)]
 pub struct SliceDesc {
-	pub start: Option<LocExpr>,
-	pub end: Option<LocExpr>,
-	pub step: Option<LocExpr>,
+	pub start: Option<Spanned<Expr>>,
+	pub end: Option<Spanned<Expr>>,
+	pub step: Option<Spanned<Expr>>,
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct AssertExpr {
+	pub assert: AssertStmt,
+	pub rest: Spanned<Expr>,
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct BinaryOp {
+	pub lhs: Spanned<Expr>,
+	pub op: BinaryOpType,
+	pub rhs: Spanned<Expr>,
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub enum ImportKind {
+	Normal,
+	Str,
+	Bin,
 }
 
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct IfElse {
+	pub cond: IfSpecData,
+	pub cond_then: Spanned<Expr>,
+	pub cond_else: Option<Spanned<Expr>>,
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct Slice {
+	pub value: Spanned<Expr>,
+	pub slice: SliceDesc,
+}
+
 /// Syntax base
 #[derive(Debug, PartialEq, Acyclic)]
 pub enum Expr {
@@ -317,7 +350,7 @@
 	Var(IStr),
 
 	/// Array of expressions: [1, 2, "Hello"]
-	Arr(Vec<LocExpr>),
+	Arr(Rc<Vec<Spanned<Expr>>>),
 	/// Array comprehension:
 	/// ```jsonnet
 	///  ingredients: [
@@ -329,51 +362,43 @@
 	///    ]
 	///  ],
 	/// ```
-	ArrComp(LocExpr, Vec<CompSpec>),
+	ArrComp(Rc<Spanned<Expr>>, Vec<CompSpec>),
 
 	/// Object: {a: 2}
 	Obj(ObjBody),
 	/// Object extension: var1 {b: 2}
-	ObjExtend(LocExpr, ObjBody),
+	ObjExtend(Rc<Spanned<Expr>>, ObjBody),
 
 	/// -2
-	UnaryOp(UnaryOpType, LocExpr),
+	UnaryOp(UnaryOpType, Box<Spanned<Expr>>),
 	/// 2 - 2
-	BinaryOp(LocExpr, BinaryOpType, LocExpr),
+	BinaryOp(Box<BinaryOp>),
 	/// assert 2 == 2 : "Math is broken"
-	AssertExpr(AssertStmt, LocExpr),
+	AssertExpr(Rc<AssertExpr>),
 	/// local a = 2; { b: a }
-	LocalExpr(Vec<BindSpec>, LocExpr),
+	LocalExpr(Vec<BindSpec>, Box<Spanned<Expr>>),
 
-	/// import "hello"
-	Import(LocExpr),
-	/// importStr "file.txt"
-	ImportStr(LocExpr),
-	/// importBin "file.txt"
-	ImportBin(LocExpr),
+	/// import* "hello"
+	Import(ImportKind, Box<Spanned<Expr>>),
 	/// error "I'm broken"
-	ErrorStmt(LocExpr),
+	ErrorStmt(Box<Spanned<Expr>>),
 	/// a(b, c)
-	Apply(LocExpr, ArgsDesc, bool),
+	Apply(Box<Spanned<Expr>>, ArgsDesc, bool),
 	/// a[b], a.b, a?.b
 	Index {
-		indexable: LocExpr,
+		indexable: Box<Spanned<Expr>>,
 		parts: Vec<IndexPart>,
 	},
 	/// function(x) x
-	Function(ParamsDesc, LocExpr),
+	Function(ParamsDesc, Rc<Spanned<Expr>>),
 	/// if true == false then 1 else 2
-	IfElse {
-		cond: IfSpecData,
-		cond_then: LocExpr,
-		cond_else: Option<LocExpr>,
-	},
-	Slice(LocExpr, SliceDesc),
+	IfElse(Box<IfElse>),
+	Slice(Box<Slice>),
 }
 
 #[derive(Debug, PartialEq, Acyclic)]
 pub struct IndexPart {
-	pub value: LocExpr,
+	pub value: Spanned<Expr>,
 	#[cfg(feature = "exp-null-coaelse")]
 	pub null_coaelse: bool,
 }
@@ -396,28 +421,28 @@
 	}
 }
 
-/// Holds AST expression and its location in source file
 #[derive(Clone, PartialEq, Acyclic)]
-pub struct LocExpr(Rc<(Expr, Span)>);
-impl LocExpr {
-	pub fn new(expr: Expr, span: Span) -> Self {
-		Self(Rc::new((expr, span)))
+pub struct Spanned<T: Acyclic>(T, Span);
+impl<T: Acyclic> Deref for Spanned<T> {
+	type Target = T;
+	fn deref(&self) -> &Self::Target {
+		&self.0
 	}
+}
+impl<T: Acyclic> Spanned<T> {
 	#[inline]
-	pub fn span(&self) -> Span {
-		self.0 .1.clone()
+	pub fn new(v: T, s: Span) -> Self {
+		Self(v, s)
 	}
 	#[inline]
-	pub fn expr(&self) -> &Expr {
-		&self.0 .0
+	pub fn span(&self) -> Span {
+		self.1.clone()
 	}
 }
 
-static_assertions::assert_eq_size!(LocExpr, usize);
-
-impl Debug for LocExpr {
+impl<T: Debug + Acyclic> Debug for Spanned<T> {
 	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		let expr = self.expr();
+		let expr = &**self;
 		if f.alternate() {
 			write!(f, "{:#?}", expr)?;
 		} else {
modifiedcrates/jrsonnet-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/lib.rs
+++ b/crates/jrsonnet-parser/src/lib.rs
@@ -22,12 +22,16 @@
 
 macro_rules! expr_bin {
 	($a:ident $op:ident $b:ident) => {
-		Expr::BinaryOp($a, $op, $b)
+		Expr::BinaryOp(Box::new(BinaryOp {
+			lhs: $a,
+			op: $op,
+			rhs: $b,
+		}))
 	};
 }
 macro_rules! expr_un {
 	($op:ident $a:ident) => {
-		Expr::UnaryOp($op, $a)
+		Expr::UnaryOp($op, Box::new($a))
 	};
 }
 
@@ -64,13 +68,13 @@
 		rule keyword(id: &'static str) -> ()
 			= ##parse_string_literal(id) end_of_ident()
 
-		pub rule param(s: &ParserSettings) -> expr::Param = name:destruct(s) expr:(_ "=" _ expr:expr(s){expr})? { expr::Param(name, expr) }
+		pub rule param(s: &ParserSettings) -> expr::Param = name:destruct(s) expr:(_ "=" _ expr:expr(s){expr})? { expr::Param(name, expr.map(Rc::new)) }
 		pub rule params(s: &ParserSettings) -> expr::ParamsDesc
 			= params:param(s) ** comma() comma()? { expr::ParamsDesc(Rc::new(params)) }
 			/ { expr::ParamsDesc(Rc::new(Vec::new())) }
 
-		pub rule arg(s: &ParserSettings) -> (Option<IStr>, LocExpr)
-			= name:(quiet! { (s:id() _ "=" !['='] _ {s})? } / expected!("<argument name>")) expr:expr(s) {(name, expr)}
+		pub rule arg(s: &ParserSettings) -> (Option<IStr>, Rc<Spanned<Expr>>)
+			= name:(quiet! { (s:id() _ "=" !['='] _ {s})? } / expected!("<argument name>")) expr:expr(s) {(name, Rc::new(expr))}
 
 		pub rule args(s: &ParserSettings) -> expr::ArgsDesc
 			= args:arg(s)**comma() comma()? {?
@@ -135,8 +139,8 @@
 			/ obj:destruct_object(s) {obj}
 
 		pub rule bind(s: &ParserSettings) -> expr::BindSpec
-			= into:destruct(s) _ "=" _ expr:expr(s) {expr::BindSpec::Field{into, value: expr}}
-			/ name:id() _ "(" _ params:params(s) _ ")" _ "=" _ expr:expr(s) {expr::BindSpec::Function{name, params, value: expr}}
+			= into:destruct(s) _ "=" _ expr:expr(s) {expr::BindSpec::Field{into, value: Rc::new(expr)}}
+			/ name:id() _ "(" _ params:params(s) _ ")" _ "=" _ expr:expr(s) {expr::BindSpec::Function{name, params, value: Rc::new(expr)}}
 
 		pub rule assertion(s: &ParserSettings) -> expr::AssertStmt
 			= keyword("assert") _ cond:expr(s) msg:(_ ":" _ e:expr(s) {e})? { expr::AssertStmt(cond, msg) }
@@ -190,20 +194,20 @@
 				plus: plus.is_some(),
 				params: None,
 				visibility,
-				value,
+				value: Rc::new(value),
 			}}
 			/ name:field_name(s) _ "(" _ params:params(s) _ ")" _ visibility:visibility() _ value:expr(s) {expr::FieldMember{
 				name,
 				plus: false,
 				params: Some(params),
 				visibility,
-				value,
+				value: Rc::new(value),
 			}}
 		pub rule obj_local(s: &ParserSettings) -> BindSpec
 			= keyword("local") _ bind:bind(s) {bind}
 		pub rule member(s: &ParserSettings) -> expr::Member
 			= bind:obj_local(s) {expr::Member::BindStmt(bind)}
-			/ assertion:assertion(s) {expr::Member::AssertStmt(assertion)}
+			/ assertion:assertion(s) {expr::Member::AssertStmt(Rc::new(assertion))}
 			/ field:field(s) {expr::Member::Field(field)}
 		pub rule objinside(s: &ParserSettings) -> expr::ObjBody
 			= pre_locals:(b: obj_local(s) comma() {b})* &"[" field:field(s) post_locals:(comma() b:obj_local(s) {b})* _ ("," _)? forspec:forspec(s) others:(_ rest:compspec(s) {rest})? {
@@ -211,7 +215,7 @@
 				compspecs.extend(others.unwrap_or_default());
 				expr::ObjBody::ObjComp(expr::ObjComp{
 					pre_locals,
-					field,
+					field: Rc::new(field),
 					post_locals,
 					compspecs,
 				})
@@ -224,18 +228,18 @@
 		pub rule compspec(s: &ParserSettings) -> Vec<expr::CompSpec>
 			= s:(i:ifspec(s) { expr::CompSpec::IfSpec(i) } / f:forspec(s) {expr::CompSpec::ForSpec(f)} ) ** _ {s}
 		pub rule local_expr(s: &ParserSettings) -> Expr
-			= keyword("local") _ binds:bind(s) ** comma() (_ ",")? _ ";" _ expr:expr(s) { Expr::LocalExpr(binds, expr) }
+			= keyword("local") _ binds:bind(s) ** comma() (_ ",")? _ ";" _ expr:expr(s) { Expr::LocalExpr(binds, Box::new(expr)) }
 		pub rule string_expr(s: &ParserSettings) -> Expr
 			= s:string() {Expr::Str(s.into())}
 		pub rule obj_expr(s: &ParserSettings) -> Expr
 			= "{" _ body:objinside(s) _ "}" {Expr::Obj(body)}
 		pub rule array_expr(s: &ParserSettings) -> Expr
-			= "[" _ elems:(expr(s) ** comma()) _ comma()? "]" {Expr::Arr(elems)}
+			= "[" _ elems:(expr(s) ** comma()) _ comma()? "]" {Expr::Arr(Rc::new(elems))}
 		pub rule array_comp_expr(s: &ParserSettings) -> Expr
 			= "[" _ expr:expr(s) _ comma()? _ forspec:forspec(s) _ others:(others: compspec(s) _ {others})? "]" {
 				let mut specs = vec![CompSpec::ForSpec(forspec)];
 				specs.extend(others.unwrap_or_default());
-				Expr::ArrComp(expr, specs)
+				Expr::ArrComp(Rc::new(expr), specs)
 			}
 		pub rule number_expr(s: &ParserSettings) -> Expr
 			= n:number() {? if n.is_finite() {
@@ -245,14 +249,14 @@
 			}}
 		pub rule var_expr(s: &ParserSettings) -> Expr
 			= n:id() { expr::Expr::Var(n) }
-		pub rule id_loc(s: &ParserSettings) -> LocExpr
-			= a:position!() n:id() b:position!() { LocExpr::new(expr::Expr::Str(n), Span(s.source.clone(), a as u32,b as u32)) }
+		pub rule id_loc(s: &ParserSettings) -> Spanned<Expr>
+			= a:position!() n:id() b:position!() { Spanned::new(expr::Expr::Str(n), Span(s.source.clone(), a as u32,b as u32)) }
 		pub rule if_then_else_expr(s: &ParserSettings) -> Expr
-			= cond:ifspec(s) _ keyword("then") _ cond_then:expr(s) cond_else:(_ keyword("else") _ e:expr(s) {e})? {Expr::IfElse{
+			= cond:ifspec(s) _ keyword("then") _ cond_then:expr(s) cond_else:(_ keyword("else") _ e:expr(s) {e})? {Expr::IfElse(Box::new(IfElse{
 				cond,
 				cond_then,
 				cond_else,
-			}}
+			}))}
 
 		pub rule literal(s: &ParserSettings) -> Expr
 			= v:(
@@ -264,6 +268,11 @@
 				/ keyword("super") {LiteralType::Super}
 			) {Expr::Literal(v)}
 
+		rule import_kind() -> ImportKind
+			= keyword("importstr") { ImportKind::Str }
+			/ keyword("importbin") { ImportKind::Bin }
+			/ keyword("import") { ImportKind::Normal }
+
 		pub rule expr_basic(s: &ParserSettings) -> Expr
 			= literal(s)
 
@@ -273,20 +282,20 @@
 			/ array_expr(s)
 			/ array_comp_expr(s)
 
-			/ keyword("importstr") _ path:expr(s) {Expr::ImportStr(path)}
-			/ keyword("importbin") _ path:expr(s) {Expr::ImportBin(path)}
-			/ keyword("import") _ path:expr(s) {Expr::Import(path)}
+			/ kind:import_kind() _ path:expr(s) {Expr::Import(kind, Box::new(path))}
 
 			/ var_expr(s)
 			/ local_expr(s)
 			/ if_then_else_expr(s)
 
-			/ keyword("function") _ "(" _ params:params(s) _ ")" _ expr:expr(s) {Expr::Function(params, expr)}
-			/ assertion:assertion(s) _ ";" _ expr:expr(s) { Expr::AssertExpr(assertion, expr) }
+			/ keyword("function") _ "(" _ params:params(s) _ ")" _ expr:expr(s) {Expr::Function(params, Rc::new(expr))}
+			/ assert:assertion(s) _ ";" _ rest:expr(s) { Expr::AssertExpr(Rc::new(AssertExpr{
+				assert, rest
+			})) }
 
-			/ keyword("error") _ expr:expr(s) { Expr::ErrorStmt(expr) }
+			/ keyword("error") _ expr:expr(s) { Expr::ErrorStmt(Box::new(expr)) }
 
-		rule slice_part(s: &ParserSettings) -> Option<LocExpr>
+		rule slice_part(s: &ParserSettings) -> Option<Spanned<Expr>>
 			= _ e:(e:expr(s) _{e})? {e}
 		pub rule slice_desc(s: &ParserSettings) -> SliceDesc
 			= start:slice_part(s) ":" pair:(end:slice_part(s) step:(":" e:slice_part(s){e})? {(end, step.flatten())})? {
@@ -311,10 +320,10 @@
 			}
 		use BinaryOpType::*;
 		use UnaryOpType::*;
-		rule expr(s: &ParserSettings) -> LocExpr
+		rule expr(s: &ParserSettings) -> Spanned<Expr>
 			= precedence! {
 				"(" _ e:expr(s) _ ")" {e}
-				start:position!() v:@ end:position!() { LocExpr::new(v, Span(s.source.clone(), start as u32, end as u32)) }
+				start:position!() v:@ end:position!() { Spanned::new(v, Span(s.source.clone(), start as u32, end as u32)) }
 				--
 				a:(@) _ binop(<"||">) _ b:@ {expr_bin!(a Or b)}
 				a:(@) _ binop(<"??">) _ ensure_null_coaelse() b:@ {
@@ -354,10 +363,10 @@
 						unaryop(<"!">) _ b:@ {expr_un!(Not b)}
 						unaryop(<"~">) _ b:@ {expr_un!(BitNot b)}
 				--
-				a:(@) _ "[" _ e:slice_desc(s) _ "]" {Expr::Slice(a, e)}
-				indexable:(@) _ parts:index_part(s)+ {Expr::Index{indexable, parts}}
-				a:(@) _ "(" _ args:args(s) _ ")" ts:(_ keyword("tailstrict"))? {Expr::Apply(a, args, ts.is_some())}
-				a:(@) _ "{" _ body:objinside(s) _ "}" {Expr::ObjExtend(a, body)}
+				value:(@) _ "[" _ slice:slice_desc(s) _ "]" {Expr::Slice(Box::new(Slice{value, slice}))}
+				indexable:(@) _ parts:index_part(s)+ {Expr::Index{indexable: Box::new(indexable), parts}}
+				a:(@) _ "(" _ args:args(s) _ ")" ts:(_ keyword("tailstrict"))? {Expr::Apply(Box::new(a), args, ts.is_some())}
+				a:(@) _ "{" _ body:objinside(s) _ "}" {Expr::ObjExtend(Rc::new(a), body)}
 				--
 				e:expr_basic(s) {e}
 			}
@@ -373,71 +382,51 @@
 			null_coaelse: n.is_some(),
 		}}
 
-		pub rule jsonnet(s: &ParserSettings) -> LocExpr = _ e:expr(s) _ {e}
+		pub rule jsonnet(s: &ParserSettings) -> Spanned<Expr> = _ e:expr(s) _ {e}
 	}
 }
 
 pub type ParseError = peg::error::ParseError<peg::str::LineCol>;
-pub fn parse(str: &str, settings: &ParserSettings) -> Result<LocExpr, ParseError> {
+pub fn parse(str: &str, settings: &ParserSettings) -> Result<Spanned<Expr>, ParseError> {
 	jsonnet_parser::jsonnet(str, settings)
 }
 /// Used for importstr values
-pub fn string_to_expr(str: IStr, settings: &ParserSettings) -> LocExpr {
+pub fn string_to_expr(str: IStr, settings: &ParserSettings) -> Spanned<Expr> {
 	let len = str.len();
-	LocExpr::new(Expr::Str(str), Span(settings.source.clone(), 0, len as u32))
+	Spanned::new(Expr::Str(str), Span(settings.source.clone(), 0, len as u32))
 }
 
 #[cfg(test)]
 pub mod tests {
+	use insta::assert_snapshot;
 	use jrsonnet_interner::IStr;
-	use BinaryOpType::*;
 
-	use super::{expr::*, parse};
+	use super::parse;
 	use crate::{source::Source, ParserSettings};
 
+	fn parsep(s: &str) -> String {
+		let v = parse(
+			s,
+			&ParserSettings {
+				source: Source::new_virtual("<test>".into(), IStr::empty()),
+			},
+		)
+		.unwrap();
+		format!("{v:#?}")
+	}
+
 	macro_rules! parse {
 		($s:expr) => {
-			parse(
-				$s,
-				&ParserSettings {
-					source: Source::new_virtual("<test>".into(), IStr::empty()),
-				},
-			)
-			.unwrap()
-		};
-	}
-
-	macro_rules! el {
-		($expr:expr, $from:expr, $to:expr$(,)?) => {
-			LocExpr::new(
-				$expr,
-				Span(
-					Source::new_virtual("<test>".into(), IStr::empty()),
-					$from,
-					$to,
-				),
-			)
+			assert_snapshot!(parsep($s));
 		};
 	}
 
 	#[test]
 	fn multiline_string() {
-		assert_eq!(
-			parse!("|||\n    Hello world!\n     a\n|||"),
-			el!(Expr::Str("Hello world!\n a\n".into()), 0, 31),
-		);
-		assert_eq!(
-			parse!("|||\n  Hello world!\n   a\n|||"),
-			el!(Expr::Str("Hello world!\n a\n".into()), 0, 27),
-		);
-		assert_eq!(
-			parse!("|||\n\t\tHello world!\n\t\t\ta\n|||"),
-			el!(Expr::Str("Hello world!\n\ta\n".into()), 0, 27),
-		);
-		assert_eq!(
-			parse!("|||\n   Hello world!\n    a\n |||"),
-			el!(Expr::Str("Hello world!\n a\n".into()), 0, 30),
-		);
+		parse!("|||\n    Hello world!\n     a\n|||");
+		parse!("|||\n  Hello world!\n   a\n|||");
+		parse!("|||\n\t\tHello world!\n\t\t\ta\n|||");
+		parse!("|||\n   Hello world!\n    a\n |||");
 	}
 
 	#[test]
@@ -451,217 +440,58 @@
 
 	#[test]
 	fn string_escaping() {
-		assert_eq!(
-			parse!(r#""Hello, \"world\"!""#),
-			el!(Expr::Str(r#"Hello, "world"!"#.into()), 0, 19),
-		);
-		assert_eq!(
-			parse!(r#"'Hello \'world\'!'"#),
-			el!(Expr::Str("Hello 'world'!".into()), 0, 18),
-		);
-		assert_eq!(parse!(r#"'\\\\'"#), el!(Expr::Str("\\\\".into()), 0, 6));
+		parse!(r#""Hello, \"world\"!""#);
+		parse!(r#"'Hello \'world\'!'"#);
+		parse!(r#"'\\\\'"#);
 	}
 
 	#[test]
 	fn string_unescaping() {
-		assert_eq!(
-			parse!(r#""Hello\nWorld""#),
-			el!(Expr::Str("Hello\nWorld".into()), 0, 14),
-		);
+		parse!(r#""Hello\nWorld""#);
 	}
 
 	#[test]
 	fn string_verbantim() {
-		assert_eq!(
-			parse!(r#"@"Hello\n""World""""#),
-			el!(Expr::Str("Hello\\n\"World\"".into()), 0, 19),
-		);
+		parse!(r#"@"Hello\n""World""""#);
 	}
 
 	#[test]
 	fn imports() {
-		assert_eq!(
-			parse!("import \"hello\""),
-			el!(Expr::Import(el!(Expr::Str("hello".into()), 7, 14)), 0, 14),
-		);
-		assert_eq!(
-			parse!("importstr \"garnish.txt\""),
-			el!(
-				Expr::ImportStr(el!(Expr::Str("garnish.txt".into()), 10, 23)),
-				0,
-				23
-			)
-		);
-		assert_eq!(
-			parse!("importbin \"garnish.bin\""),
-			el!(
-				Expr::ImportBin(el!(Expr::Str("garnish.bin".into()), 10, 23)),
-				0,
-				23
-			)
-		);
+		parse!("import \"hello\"");
+		parse!("importstr \"garnish.txt\"");
+		parse!("importbin \"garnish.bin\"");
 	}
 
 	#[test]
 	fn empty_object() {
-		assert_eq!(
-			parse!("{}"),
-			el!(Expr::Obj(ObjBody::MemberList(vec![])), 0, 2)
-		);
+		parse!("{}");
 	}
 
 	#[test]
 	fn basic_math() {
-		assert_eq!(
-			parse!("2+2*2"),
-			el!(
-				Expr::BinaryOp(
-					el!(Expr::Num(2.0), 0, 1),
-					Add,
-					el!(
-						Expr::BinaryOp(el!(Expr::Num(2.0), 2, 3), Mul, el!(Expr::Num(2.0), 4, 5)),
-						2,
-						5
-					)
-				),
-				0,
-				5
-			)
-		);
-	}
-
-	#[test]
-	fn basic_math_with_indents() {
-		assert_eq!(
-			parse!("2	+ 	  2	  *	2   	"),
-			el!(
-				Expr::BinaryOp(
-					el!(Expr::Num(2.0), 0, 1),
-					Add,
-					el!(
-						Expr::BinaryOp(el!(Expr::Num(2.0), 7, 8), Mul, el!(Expr::Num(2.0), 13, 14),),
-						7,
-						14
-					),
-				),
-				0,
-				14
-			)
-		);
+		parse!("2+2*2");
+		parse!("2	+ 	  2	  *	2   	");
+		parse!("2+(2+2*2)");
+		parse!("2//comment\n+//comment\n3/*test*/*/*test*/4");
 	}
 
 	#[test]
-	fn basic_math_parened() {
-		assert_eq!(
-			parse!("2+(2+2*2)"),
-			el!(
-				Expr::BinaryOp(
-					el!(Expr::Num(2.0), 0, 1),
-					Add,
-					el!(
-						Expr::BinaryOp(
-							el!(Expr::Num(2.0), 3, 4),
-							Add,
-							el!(
-								Expr::BinaryOp(
-									el!(Expr::Num(2.0), 5, 6),
-									Mul,
-									el!(Expr::Num(2.0), 7, 8),
-								),
-								5,
-								8
-							),
-						),
-						3,
-						8
-					),
-				),
-				0,
-				9
-			)
-		);
-	}
-
-	/// Comments should not affect parsing
-	#[test]
-	fn comments() {
-		assert_eq!(
-			parse!("2//comment\n+//comment\n3/*test*/*/*test*/4"),
-			el!(
-				Expr::BinaryOp(
-					el!(Expr::Num(2.0), 0, 1),
-					Add,
-					el!(
-						Expr::BinaryOp(
-							el!(Expr::Num(3.0), 22, 23),
-							Mul,
-							el!(Expr::Num(4.0), 40, 41)
-						),
-						22,
-						41
-					)
-				),
-				0,
-				41
-			)
-		);
-	}
-
-	#[test]
 	fn suffix() {
-		// assert_eq!(parse!("std.test"), el!(Expr::Num(2.2)));
-		// assert_eq!(parse!("std(2)"), el!(Expr::Num(2.2)));
-		// assert_eq!(parse!("std.test(2)"), el!(Expr::Num(2.2)));
-		// assert_eq!(parse!("a[b]"), el!(Expr::Num(2.2)))
+		parse!("std.test");
+		parse!("std(2)");
+		parse!("std.test(2)");
+		parse!("a[b]");
 	}
 
 	#[test]
 	fn array_comp() {
-		use Expr::*;
-		/*
-		`ArrComp(Apply(Index(Var("std") from "test.jsonnet":1-4, Var("deepJoin") from "test.jsonnet":5-13) from "test.jsonnet":1-13, ArgsDesc { unnamed: [Var("x") from "test.jsonnet":14-15], named: [] }, false) from "test.jsonnet":1-16, [ForSpec(ForSpecData("x", Var("arr") from "test.jsonnet":26-29))]) from "test.jsonnet":0-30`,
-		`ArrComp(Apply(Index(Var("std") from "test.jsonnet":1-4, Str("deepJoin") from "test.jsonnet":5-13) from "test.jsonnet":1-13, ArgsDesc { unnamed: [Var("x") from "test.jsonnet":14-15], named: [] }, false) from "test.jsonnet":1-16, [ForSpec(ForSpecData("x", Var("arr") from "test.jsonnet":26-29))]) from "test.jsonnet":0-30`
-				*/
-		assert_eq!(
-			parse!("[std.deepJoin(x) for x in arr]"),
-			el!(
-				ArrComp(
-					el!(
-						Apply(
-							el!(
-								Index {
-									indexable: el!(Var("std".into()), 1, 4),
-									parts: vec![IndexPart {
-										value: el!(Str("deepJoin".into()), 5, 13),
-										#[cfg(feature = "exp-null-coaelse")]
-										null_coaelse: false,
-									}],
-								},
-								1,
-								13
-							),
-							ArgsDesc::new(vec![el!(Var("x".into()), 14, 15)], vec![]),
-							false,
-						),
-						1,
-						16
-					),
-					vec![CompSpec::ForSpec(ForSpecData(
-						Destruct::Full("x".into()),
-						el!(Var("arr".into()), 26, 29)
-					))]
-				),
-				0,
-				30
-			),
-		)
+		parse!("[std.deepJoin(x) for x in arr]");
 	}
 
 	#[test]
 	fn reserved() {
-		use Expr::*;
-		assert_eq!(parse!("null"), el!(Literal(LiteralType::Null), 0, 4));
-		assert_eq!(parse!("nulla"), el!(Var("nulla".into()), 0, 5));
+		parse!("null");
+		parse!("nulla");
 	}
 
 	#[test]
@@ -671,58 +501,18 @@
 
 	#[test]
 	fn infix_precedence() {
-		use Expr::*;
-		assert_eq!(
-			parse!("!a && !b"),
-			el!(
-				BinaryOp(
-					el!(UnaryOp(UnaryOpType::Not, el!(Var("a".into()), 1, 2)), 0, 2),
-					And,
-					el!(UnaryOp(UnaryOpType::Not, el!(Var("b".into()), 7, 8)), 6, 8)
-				),
-				0,
-				8
-			)
-		);
-	}
-
-	#[test]
-	fn infix_precedence_division() {
-		use Expr::*;
-		assert_eq!(
-			parse!("!a / !b"),
-			el!(
-				BinaryOp(
-					el!(UnaryOp(UnaryOpType::Not, el!(Var("a".into()), 1, 2)), 0, 2),
-					Div,
-					el!(UnaryOp(UnaryOpType::Not, el!(Var("b".into()), 6, 7)), 5, 7)
-				),
-				0,
-				7
-			)
-		);
+		parse!("!a && !b");
+		parse!("!a / !b");
 	}
 
 	#[test]
 	fn double_negation() {
-		use Expr::*;
-		assert_eq!(
-			parse!("!!a"),
-			el!(
-				UnaryOp(
-					UnaryOpType::Not,
-					el!(UnaryOp(UnaryOpType::Not, el!(Var("a".into()), 2, 3)), 1, 3)
-				),
-				0,
-				3
-			)
-		)
+		parse!("!!a");
 	}
 
 	#[test]
 	fn array_test_error() {
 		parse!("[a for a in b if c for e in f]");
-		//                    ^^^^ failed code
 	}
 
 	#[test]
@@ -741,44 +531,6 @@
 
 	#[test]
 	fn add_location_info_to_all_sub_expressions() {
-		use Expr::*;
-
-		let file_name = Source::new_virtual("<test>".into(), IStr::empty());
-		let expr = parse(
-			"{} { local x = 1, x: x } + {}",
-			&ParserSettings { source: file_name },
-		)
-		.unwrap();
-		assert_eq!(
-			expr,
-			el!(
-				BinaryOp(
-					el!(
-						ObjExtend(
-							el!(Obj(ObjBody::MemberList(vec![])), 0, 2),
-							ObjBody::MemberList(vec![
-								Member::BindStmt(BindSpec::Field {
-									into: Destruct::Full("x".into()),
-									value: el!(Num(1.0), 15, 16)
-								}),
-								Member::Field(FieldMember {
-									name: FieldName::Fixed("x".into()),
-									plus: false,
-									params: None,
-									visibility: Visibility::Normal,
-									value: el!(Var("x".into()), 21, 22),
-								})
-							])
-						),
-						0,
-						24
-					),
-					BinaryOpType::Add,
-					el!(Obj(ObjBody::MemberList(vec![])), 27, 29),
-				),
-				0,
-				29
-			),
-		);
+		parse!("{} { local x = 1, x: x } + {}");
 	}
 }
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__add_location_info_to_all_sub_expressions.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__add_location_info_to_all_sub_expressions.snap
@@ -0,0 +1,48 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"{} { local x = 1, x: x } + {}\")"
+---
+BinaryOp(
+    BinaryOp {
+        lhs: ObjExtend(
+            Obj(
+                MemberList(
+                    [],
+                ),
+            ) from virtual:<test>:0-2,
+            MemberList(
+                [
+                    BindStmt(
+                        Field {
+                            into: Full(
+                                "x",
+                            ),
+                            value: Num(
+                                1.0,
+                            ) from virtual:<test>:15-16,
+                        },
+                    ),
+                    Field(
+                        FieldMember {
+                            name: Fixed(
+                                "x",
+                            ),
+                            plus: false,
+                            params: None,
+                            visibility: Normal,
+                            value: Var(
+                                "x",
+                            ) from virtual:<test>:21-22,
+                        },
+                    ),
+                ],
+            ),
+        ) from virtual:<test>:0-24,
+        op: Add,
+        rhs: Obj(
+            MemberList(
+                [],
+            ),
+        ) from virtual:<test>:27-29,
+    },
+) from virtual:<test>:0-29
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__array_comp.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__array_comp.snap
@@ -0,0 +1,41 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"[std.deepJoin(x) for x in arr]\")"
+---
+ArrComp(
+    Apply(
+        Index {
+            indexable: Var(
+                "std",
+            ) from virtual:<test>:1-4,
+            parts: [
+                IndexPart {
+                    value: Str(
+                        "deepJoin",
+                    ) from virtual:<test>:5-13,
+                },
+            ],
+        } from virtual:<test>:1-13,
+        ArgsDesc {
+            unnamed: [
+                Var(
+                    "x",
+                ) from virtual:<test>:14-15,
+            ],
+            named: [],
+        },
+        false,
+    ) from virtual:<test>:1-16,
+    [
+        ForSpec(
+            ForSpecData(
+                Full(
+                    "x",
+                ),
+                Var(
+                    "arr",
+                ) from virtual:<test>:26-29,
+            ),
+        ),
+    ],
+) from virtual:<test>:0-30
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__array_test_error.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__array_test_error.snap
@@ -0,0 +1,38 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"[a for a in b if c for e in f]\")"
+---
+ArrComp(
+    Var(
+        "a",
+    ) from virtual:<test>:1-2,
+    [
+        ForSpec(
+            ForSpecData(
+                Full(
+                    "a",
+                ),
+                Var(
+                    "b",
+                ) from virtual:<test>:12-13,
+            ),
+        ),
+        IfSpec(
+            IfSpecData(
+                Var(
+                    "c",
+                ) from virtual:<test>:17-18,
+            ),
+        ),
+        ForSpec(
+            ForSpecData(
+                Full(
+                    "e",
+                ),
+                Var(
+                    "f",
+                ) from virtual:<test>:28-29,
+            ),
+        ),
+    ],
+) from virtual:<test>:0-30
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math-2.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math-2.snap
@@ -0,0 +1,23 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"2\t+ \t  2\t  *\t2   \t\")"
+---
+BinaryOp(
+    BinaryOp {
+        lhs: Num(
+            2.0,
+        ) from virtual:<test>:0-1,
+        op: Add,
+        rhs: BinaryOp(
+            BinaryOp {
+                lhs: Num(
+                    2.0,
+                ) from virtual:<test>:7-8,
+                op: Mul,
+                rhs: Num(
+                    2.0,
+                ) from virtual:<test>:13-14,
+            },
+        ) from virtual:<test>:7-14,
+    },
+) from virtual:<test>:0-14
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math-3.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math-3.snap
@@ -0,0 +1,31 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"2+(2+2*2)\")"
+---
+BinaryOp(
+    BinaryOp {
+        lhs: Num(
+            2.0,
+        ) from virtual:<test>:0-1,
+        op: Add,
+        rhs: BinaryOp(
+            BinaryOp {
+                lhs: Num(
+                    2.0,
+                ) from virtual:<test>:3-4,
+                op: Add,
+                rhs: BinaryOp(
+                    BinaryOp {
+                        lhs: Num(
+                            2.0,
+                        ) from virtual:<test>:5-6,
+                        op: Mul,
+                        rhs: Num(
+                            2.0,
+                        ) from virtual:<test>:7-8,
+                    },
+                ) from virtual:<test>:5-8,
+            },
+        ) from virtual:<test>:3-8,
+    },
+) from virtual:<test>:0-9
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math-4.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math-4.snap
@@ -0,0 +1,23 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"2//comment\\n+//comment\\n3/*test*/*/*test*/4\")"
+---
+BinaryOp(
+    BinaryOp {
+        lhs: Num(
+            2.0,
+        ) from virtual:<test>:0-1,
+        op: Add,
+        rhs: BinaryOp(
+            BinaryOp {
+                lhs: Num(
+                    3.0,
+                ) from virtual:<test>:22-23,
+                op: Mul,
+                rhs: Num(
+                    4.0,
+                ) from virtual:<test>:40-41,
+            },
+        ) from virtual:<test>:22-41,
+    },
+) from virtual:<test>:0-41
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math.snap
@@ -0,0 +1,23 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"2+2*2\")"
+---
+BinaryOp(
+    BinaryOp {
+        lhs: Num(
+            2.0,
+        ) from virtual:<test>:0-1,
+        op: Add,
+        rhs: BinaryOp(
+            BinaryOp {
+                lhs: Num(
+                    2.0,
+                ) from virtual:<test>:2-3,
+                op: Mul,
+                rhs: Num(
+                    2.0,
+                ) from virtual:<test>:4-5,
+            },
+        ) from virtual:<test>:2-5,
+    },
+) from virtual:<test>:0-5
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__default_param_before_nondefault.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__default_param_before_nondefault.snap
@@ -0,0 +1,37 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"local x(foo = 'foo', bar) = null; null\")"
+---
+LocalExpr(
+    [
+        Function {
+            name: "x",
+            params: ParamsDesc(
+                [
+                    Param(
+                        Full(
+                            "foo",
+                        ),
+                        Some(
+                            Str(
+                                "foo",
+                            ) from virtual:<test>:14-19,
+                        ),
+                    ),
+                    Param(
+                        Full(
+                            "bar",
+                        ),
+                        None,
+                    ),
+                ],
+            ),
+            value: Literal(
+                Null,
+            ) from virtual:<test>:28-32,
+        },
+    ],
+    Literal(
+        Null,
+    ) from virtual:<test>:34-38,
+) from virtual:<test>:0-38
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__double_negation.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__double_negation.snap
@@ -0,0 +1,13 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"!!a\")"
+---
+UnaryOp(
+    Not,
+    UnaryOp(
+        Not,
+        Var(
+            "a",
+        ) from virtual:<test>:2-3,
+    ) from virtual:<test>:1-3,
+) from virtual:<test>:0-3
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__empty_object.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__empty_object.snap
@@ -0,0 +1,9 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"{}\")"
+---
+Obj(
+    MemberList(
+        [],
+    ),
+) from virtual:<test>:0-2
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__imports-2.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__imports-2.snap
@@ -0,0 +1,10 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"importstr \\\"garnish.txt\\\"\")"
+---
+Import(
+    Str,
+    Str(
+        "garnish.txt",
+    ) from virtual:<test>:10-23,
+) from virtual:<test>:0-23
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__imports-3.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__imports-3.snap
@@ -0,0 +1,10 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"importbin \\\"garnish.bin\\\"\")"
+---
+Import(
+    Bin,
+    Str(
+        "garnish.bin",
+    ) from virtual:<test>:10-23,
+) from virtual:<test>:0-23
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__imports.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__imports.snap
@@ -0,0 +1,10 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"import \\\"hello\\\"\")"
+---
+Import(
+    Normal,
+    Str(
+        "hello",
+    ) from virtual:<test>:7-14,
+) from virtual:<test>:0-14
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__infix_precedence-2.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__infix_precedence-2.snap
@@ -0,0 +1,21 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"!a / !b\")"
+---
+BinaryOp(
+    BinaryOp {
+        lhs: UnaryOp(
+            Not,
+            Var(
+                "a",
+            ) from virtual:<test>:1-2,
+        ) from virtual:<test>:0-2,
+        op: Div,
+        rhs: UnaryOp(
+            Not,
+            Var(
+                "b",
+            ) from virtual:<test>:6-7,
+        ) from virtual:<test>:5-7,
+    },
+) from virtual:<test>:0-7
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__infix_precedence.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__infix_precedence.snap
@@ -0,0 +1,21 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"!a && !b\")"
+---
+BinaryOp(
+    BinaryOp {
+        lhs: UnaryOp(
+            Not,
+            Var(
+                "a",
+            ) from virtual:<test>:1-2,
+        ) from virtual:<test>:0-2,
+        op: And,
+        rhs: UnaryOp(
+            Not,
+            Var(
+                "b",
+            ) from virtual:<test>:7-8,
+        ) from virtual:<test>:6-8,
+    },
+) from virtual:<test>:0-8
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__missing_newline_between_comment_and_eof.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__missing_newline_between_comment_and_eof.snap
@@ -0,0 +1,23 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"{a:1}\n\n\t\t\t//+213\")"
+---
+Obj(
+    MemberList(
+        [
+            Field(
+                FieldMember {
+                    name: Fixed(
+                        "a",
+                    ),
+                    plus: false,
+                    params: None,
+                    visibility: Normal,
+                    value: Num(
+                        1.0,
+                    ) from virtual:<test>:3-4,
+                },
+            ),
+        ],
+    ),
+) from virtual:<test>:0-5
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string-2.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string-2.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"|||\\n  Hello world!\\n   a\\n|||\")"
+---
+Str(
+    "Hello world!\n a\n",
+) from virtual:<test>:0-27
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string-3.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string-3.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"|||\\n\\t\\tHello world!\\n\\t\\t\\ta\\n|||\")"
+---
+Str(
+    "Hello world!\n\ta\n",
+) from virtual:<test>:0-27
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string-4.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string-4.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"|||\\n   Hello world!\\n    a\\n |||\")"
+---
+Str(
+    "Hello world!\n a\n",
+) from virtual:<test>:0-30
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"|||\\n    Hello world!\\n     a\\n|||\")"
+---
+Str(
+    "Hello world!\n a\n",
+) from virtual:<test>:0-31
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiple_args_buf.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiple_args_buf.snap
@@ -0,0 +1,21 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"a(b, null_fields)\")"
+---
+Apply(
+    Var(
+        "a",
+    ) from virtual:<test>:0-1,
+    ArgsDesc {
+        unnamed: [
+            Var(
+                "b",
+            ) from virtual:<test>:2-3,
+            Var(
+                "null_fields",
+            ) from virtual:<test>:5-16,
+        ],
+        named: [],
+    },
+    false,
+) from virtual:<test>:0-17
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__reserved-2.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__reserved-2.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"nulla\")"
+---
+Var(
+    "nulla",
+) from virtual:<test>:0-5
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__reserved.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__reserved.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"null\")"
+---
+Literal(
+    Null,
+) from virtual:<test>:0-4
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-2.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-2.snap
@@ -0,0 +1,20 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"a[1::]\")"
+---
+Slice(
+    Slice {
+        value: Var(
+            "a",
+        ) from virtual:<test>:0-1,
+        slice: SliceDesc {
+            start: Some(
+                Num(
+                    1.0,
+                ) from virtual:<test>:2-3,
+            ),
+            end: None,
+            step: None,
+        },
+    },
+) from virtual:<test>:0-6
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-3.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-3.snap
@@ -0,0 +1,20 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"a[:1:]\")"
+---
+Slice(
+    Slice {
+        value: Var(
+            "a",
+        ) from virtual:<test>:0-1,
+        slice: SliceDesc {
+            start: None,
+            end: Some(
+                Num(
+                    1.0,
+                ) from virtual:<test>:3-4,
+            ),
+            step: None,
+        },
+    },
+) from virtual:<test>:0-6
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-4.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-4.snap
@@ -0,0 +1,20 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"a[::1]\")"
+---
+Slice(
+    Slice {
+        value: Var(
+            "a",
+        ) from virtual:<test>:0-1,
+        slice: SliceDesc {
+            start: None,
+            end: None,
+            step: Some(
+                Num(
+                    1.0,
+                ) from virtual:<test>:4-5,
+            ),
+        },
+    },
+) from virtual:<test>:0-6
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-5.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-5.snap
@@ -0,0 +1,28 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"str[:len - 1]\")"
+---
+Slice(
+    Slice {
+        value: Var(
+            "str",
+        ) from virtual:<test>:0-3,
+        slice: SliceDesc {
+            start: None,
+            end: Some(
+                BinaryOp(
+                    BinaryOp {
+                        lhs: Var(
+                            "len",
+                        ) from virtual:<test>:5-8,
+                        op: Sub,
+                        rhs: Num(
+                            1.0,
+                        ) from virtual:<test>:11-12,
+                    },
+                ) from virtual:<test>:5-12,
+            ),
+            step: None,
+        },
+    },
+) from virtual:<test>:0-13
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice.snap
@@ -0,0 +1,20 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"a[1:]\")"
+---
+Slice(
+    Slice {
+        value: Var(
+            "a",
+        ) from virtual:<test>:0-1,
+        slice: SliceDesc {
+            start: Some(
+                Num(
+                    1.0,
+                ) from virtual:<test>:2-3,
+            ),
+            end: None,
+            step: None,
+        },
+    },
+) from virtual:<test>:0-5
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_escaping-2.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_escaping-2.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(r#\"'Hello \\'world\\'!'\"#)"
+---
+Str(
+    "Hello 'world'!",
+) from virtual:<test>:0-18
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_escaping-3.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_escaping-3.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(r#\"'\\\\\\\\'\"#)"
+---
+Str(
+    "\\\\",
+) from virtual:<test>:0-6
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_escaping.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_escaping.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(r#\"\"Hello, \\\"world\\\"!\"\"#)"
+---
+Str(
+    "Hello, \"world\"!",
+) from virtual:<test>:0-19
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_unescaping.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_unescaping.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(r#\"\"Hello\\nWorld\"\"#)"
+---
+Str(
+    "Hello\nWorld",
+) from virtual:<test>:0-14
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_verbantim.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_verbantim.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(r#\"@\"Hello\\n\"\"World\"\"\"\"#)"
+---
+Str(
+    "Hello\\n\"World\"",
+) from virtual:<test>:0-19
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix-2.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix-2.snap
@@ -0,0 +1,18 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"std(2)\")"
+---
+Apply(
+    Var(
+        "std",
+    ) from virtual:<test>:0-3,
+    ArgsDesc {
+        unnamed: [
+            Num(
+                2.0,
+            ) from virtual:<test>:4-5,
+        ],
+        named: [],
+    },
+    false,
+) from virtual:<test>:0-6
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix-3.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix-3.snap
@@ -0,0 +1,27 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"std.test(2)\")"
+---
+Apply(
+    Index {
+        indexable: Var(
+            "std",
+        ) from virtual:<test>:0-3,
+        parts: [
+            IndexPart {
+                value: Str(
+                    "test",
+                ) from virtual:<test>:4-8,
+            },
+        ],
+    } from virtual:<test>:0-8,
+    ArgsDesc {
+        unnamed: [
+            Num(
+                2.0,
+            ) from virtual:<test>:9-10,
+        ],
+        named: [],
+    },
+    false,
+) from virtual:<test>:0-11
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix-4.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix-4.snap
@@ -0,0 +1,16 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"a[b]\")"
+---
+Index {
+    indexable: Var(
+        "a",
+    ) from virtual:<test>:0-1,
+    parts: [
+        IndexPart {
+            value: Var(
+                "b",
+            ) from virtual:<test>:2-3,
+        },
+    ],
+} from virtual:<test>:0-4
addedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix.snap
@@ -0,0 +1,16 @@
+---
+source: crates/jrsonnet-parser/src/lib.rs
+expression: "parsep(\"std.test\")"
+---
+Index {
+    indexable: Var(
+        "std",
+    ) from virtual:<test>:0-3,
+    parts: [
+        IndexPart {
+            value: Str(
+                "test",
+            ) from virtual:<test>:4-8,
+        },
+    ],
+} from virtual:<test>:0-8
modifiedcrates/jrsonnet-parser/src/source.rsdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/source.rs
+++ b/crates/jrsonnet-parser/src/source.rs
@@ -79,7 +79,7 @@
 /// search location is applicable
 ///
 /// Resolver may also return custom implementations of this trait, for example it may return http url in case of remotely loaded files
-#[derive(Eq, Debug, Clone, Acyclic)]
+#[derive(Eq, Clone, Acyclic)]
 pub struct SourcePath(Rc<dyn SourcePathT>);
 impl SourcePath {
 	pub fn new(inner: impl SourcePathT) -> Self {
@@ -111,6 +111,11 @@
 		write!(f, "{}", self.0)
 	}
 }
+impl Debug for SourcePath {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "{:?}", self.0)
+	}
+}
 impl Default for SourcePath {
 	fn default() -> Self {
 		Self(Rc::new(SourceDefault))
@@ -213,13 +218,18 @@
 ///
 /// It is used for --ext-code=.../--tla-code=.../standard library source code by default,
 /// and user can construct arbitrary values by hand, without asking import resolver
-#[derive(Acyclic, Hash, PartialEq, Eq, Debug, Clone)]
+#[derive(Acyclic, Hash, PartialEq, Eq, Clone)]
 pub struct SourceVirtual(pub IStr);
 impl Display for SourceVirtual {
 	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		write!(f, "{}", self.0)
+		write!(f, "virtual:{}", self.0)
 	}
 }
+impl fmt::Debug for SourceVirtual {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "virtual:{}", self.0)
+	}
+}
 impl SourcePathT for SourceVirtual {
 	fn is_default(&self) -> bool {
 		true
@@ -263,7 +273,7 @@
 
 /// Either real file, or virtual
 /// Hash of FileName always have same value as raw Path, to make it possible to use with raw_entry_mut
-#[derive(Clone, PartialEq, Eq, Debug, Acyclic)]
+#[derive(Clone, PartialEq, Eq, Acyclic)]
 pub struct Source(pub Rc<(SourcePath, IStr)>);
 
 impl Source {
@@ -290,3 +300,8 @@
 		location_to_offset(&self.0 .1, line, column)
 	}
 }
+impl fmt::Debug for Source {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "{:?}", self.0 .0)
+	}
+}