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
--- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs
@@ -4,7 +4,7 @@
 use jrsonnet_interner::IStr;
 use jrsonnet_parser::{
 	ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, FieldMember, FieldName,
-	ForSpecData, IfSpecData, LiteralType, LocExpr, Member, ObjBody, ParamsDesc,
+	ForSpecData, IfSpecData, ImportKind, LiteralType, Member, ObjBody, ParamsDesc, Spanned,
 };
 use jrsonnet_types::ValType;
 use rustc_hash::FxHashMap;
@@ -46,9 +46,9 @@
 	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)
 }
 
-pub fn evaluate_trivial(expr: &LocExpr) -> Option<Val> {
-	fn is_trivial(expr: &LocExpr) -> bool {
-		match expr.expr() {
+pub fn evaluate_trivial(expr: &Spanned<Expr>) -> Option<Val> {
+	fn is_trivial(expr: &Spanned<Expr>) -> bool {
+		match &**expr {
 			Expr::Str(_)
 			| Expr::Num(_)
 			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,
@@ -56,7 +56,7 @@
 			_ => false,
 		}
 	}
-	Some(match expr.expr() {
+	Some(match &**expr {
 		Expr::Str(s) => Val::string(s.clone()),
 		Expr::Num(n) => {
 			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))
@@ -79,7 +79,12 @@
 	})
 }
 
-pub fn evaluate_method(ctx: Context, name: IStr, params: ParamsDesc, body: LocExpr) -> Val {
+pub fn evaluate_method(
+	ctx: Context,
+	name: IStr,
+	params: ParamsDesc,
+	body: Rc<Spanned<Expr>>,
+) -> Val {
 	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {
 		name,
 		ctx,
@@ -215,7 +220,7 @@
 			#[derive(Trace)]
 			struct UnboundValue<B: Trace> {
 				uctx: B,
-				value: LocExpr,
+				value: Rc<Spanned<Expr>>,
 				name: IStr,
 			}
 			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {
@@ -245,7 +250,7 @@
 			#[derive(Trace)]
 			struct UnboundMethod<B: Trace> {
 				uctx: B,
-				value: LocExpr,
+				value: Rc<Spanned<Expr>>,
 				params: ParamsDesc,
 				name: IStr,
 			}
@@ -301,7 +306,7 @@
 				#[derive(Trace)]
 				struct ObjectAssert<B: Trace> {
 					uctx: B,
-					assert: AssertStmt,
+					assert: Rc<AssertStmt>,
 				}
 				impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {
 					fn run(&self, sup_this: SupThis) -> Result<()> {
@@ -347,7 +352,7 @@
 
 pub fn evaluate_apply(
 	ctx: Context,
-	value: &LocExpr,
+	value: &Spanned<Expr>,
 	args: &ArgsDesc,
 	loc: CallLocation<'_>,
 	tailstrict: bool,
@@ -389,23 +394,23 @@
 	Ok(())
 }
 
-pub fn evaluate_named(ctx: Context, expr: &LocExpr, name: IStr) -> Result<Val> {
+pub fn evaluate_named(ctx: Context, expr: &Spanned<Expr>, name: IStr) -> Result<Val> {
 	use Expr::*;
-	Ok(match expr.expr() {
+	Ok(match &**expr {
 		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),
 		_ => evaluate(ctx, expr)?,
 	})
 }
 
 #[allow(clippy::too_many_lines)]
-pub fn evaluate(ctx: Context, expr: &LocExpr) -> Result<Val> {
+pub fn evaluate(ctx: Context, expr: &Spanned<Expr>) -> Result<Val> {
 	use Expr::*;
 
 	if let Some(trivial) = evaluate_trivial(expr) {
 		return Ok(trivial);
 	}
 	let loc = expr.span();
-	Ok(match expr.expr() {
+	Ok(match &**expr {
 		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),
 		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),
 		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),
@@ -420,8 +425,9 @@
 		// Note that other jsonnet implementations will fail on `if value in (super)` expression,
 		// because the standalone super literal is not supported, that is because in other
 		// implementations `in super` treated differently from `in smth_else`.
-		BinaryOp(field, BinaryOpType::In, e)
-			if matches!(e.expr(), Expr::Literal(LiteralType::Super)) =>
+		BinaryOp(bin)
+			if matches!(&*bin.rhs, Expr::Literal(LiteralType::Super))
+				&& bin.op == BinaryOpType::In =>
 		{
 			let sup_this = ctx.try_sup_this()?;
 			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.
@@ -429,10 +435,10 @@
 			if !sup_this.has_super() {
 				return Ok(Val::Bool(false));
 			}
-			let field = evaluate(ctx, field)?;
+			let field = evaluate(ctx, &bin.lhs)?;
 			Val::Bool(sup_this.field_in_super(field.to_string()?))
 		}
-		BinaryOp(v1, o, v2) => evaluate_binary_op_special(ctx, v1, *o, v2)?,
+		BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?,
 		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,
 		Var(name) => in_frame(
 			CallLocation::new(&loc),
@@ -441,7 +447,7 @@
 		)?,
 		Index { indexable, parts } => ensure_sufficient_stack(|| {
 			let mut parts = parts.iter();
-			let mut indexable = if matches!(indexable.expr(), Expr::Literal(LiteralType::Super)) {
+			let mut indexable = if matches!(&***indexable, Expr::Literal(LiteralType::Super)) {
 				let part = parts.next().expect("at least part should exist");
 				// sup_this existence check might also be skipped here for null-coalesce...
 				// But I believe this might cause errors.
@@ -574,11 +580,8 @@
 		Arr(items) => {
 			if items.is_empty() {
 				Val::Arr(ArrValue::empty())
-			} else if items.len() == 1 {
-				let item = items[0].clone();
-				Val::Arr(ArrValue::lazy(vec![Thunk!(move || evaluate(ctx, &item))]))
 			} else {
-				Val::Arr(ArrValue::expr(ctx, items.iter().cloned()))
+				Val::Arr(ArrValue::expr(ctx, items.clone()))
 			}
 		}
 		ArrComp(expr, comp_specs) => {
@@ -601,38 +604,40 @@
 		Function(params, body) => {
 			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())
 		}
-		AssertExpr(assert, returned) => {
-			evaluate_assert(ctx.clone(), assert)?;
-			evaluate(ctx, returned)?
+		AssertExpr(assert) => {
+			evaluate_assert(ctx.clone(), &assert.assert)?;
+			evaluate(ctx, &assert.rest)?
 		}
 		ErrorStmt(e) => in_frame(
 			CallLocation::new(&loc),
 			|| "error statement".to_owned(),
 			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),
 		)?,
-		IfElse {
-			cond,
-			cond_then,
-			cond_else,
-		} => {
+		IfElse (if_else)
+		// {
+		// 	cond,
+		// 	cond_then,
+		// 	cond_else,
+		// }
+		=> {
 			if in_frame(
 				CallLocation::new(&loc),
 				|| "if condition".to_owned(),
-				|| bool::from_untyped(evaluate(ctx.clone(), &cond.0)?),
+				|| bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.0)?),
 			)? {
-				evaluate(ctx, cond_then)?
+				evaluate(ctx, &if_else.cond_then)?
 			} else {
-				match cond_else {
+				match &if_else.cond_else {
 					Some(v) => evaluate(ctx, v)?,
 					None => Val::Null,
 				}
 			}
 		}
-		Slice(value, desc) => {
+		Slice(slice) => {
 			fn parse_idx<T: Typed>(
 				loc: CallLocation<'_>,
 				ctx: Context,
-				expr: Option<&LocExpr>,
+				expr: Option<&Spanned<Expr>>,
 				desc: &'static str,
 			) -> Result<Option<T>> {
 				if let Some(value) = expr {
@@ -646,33 +651,32 @@
 				}
 			}
 
-			let indexable = evaluate(ctx.clone(), value)?;
+			let indexable = evaluate(ctx.clone(), &slice.value)?;
 			let loc = CallLocation::new(&loc);
 
-			let start = parse_idx(loc, ctx.clone(), desc.start.as_ref(), "start")?;
-			let end = parse_idx(loc, ctx.clone(), desc.end.as_ref(), "end")?;
-			let step = parse_idx(loc, ctx, desc.step.as_ref(), "step")?;
+			let start = parse_idx(loc, ctx.clone(), slice.slice.start.as_ref(), "start")?;
+			let end = parse_idx(loc, ctx.clone(), slice.slice.end.as_ref(), "end")?;
+			let step = parse_idx(loc, ctx, slice.slice.step.as_ref(), "step")?;
 
 			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?
 		}
-		i @ (Import(path) | ImportStr(path) | ImportBin(path)) => {
-			let Expr::Str(path) = &path.expr() else {
+		Import(kind, path) => {
+			let Expr::Str(path) = &***path else {
 				bail!("computed imports are not supported")
 			};
 			let tmp = loc.clone().0;
 			with_state(|s| {
 				let resolved_path = s.resolve_from(tmp.source_path(), path)?;
-				Ok(match i {
-					Import(_) => in_frame(
+				Ok(match kind {
+					ImportKind::Normal => in_frame(
 						CallLocation::new(&loc),
 						|| format!("import {:?}", path.clone()),
 						|| s.import_resolved(resolved_path),
 					)?,
-					ImportStr(_) => Val::string(s.import_resolved_str(resolved_path)?),
-					ImportBin(_) => {
+					ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?),
+					ImportKind::Bin => {
 						Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?))
 					}
-					_ => unreachable!(),
 				}) as Result<Val>
 			})?
 		}
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
before · crates/jrsonnet-parser/src/lib.rs
1#![allow(clippy::redundant_closure_call, clippy::derive_partial_eq_without_eq)]23use std::rc::Rc;45use peg::parser;6mod expr;7pub use expr::*;8pub use jrsonnet_interner::IStr;9pub use peg;10mod location;11mod source;12mod unescape;13pub use location::CodeLocation;14pub use source::{15	Source, SourceDefaultIgnoreJpath, SourceDirectory, SourceFifo, SourceFile, SourcePath,16	SourcePathT, SourceVirtual,17};1819pub struct ParserSettings {20	pub source: Source,21}2223macro_rules! expr_bin {24	($a:ident $op:ident $b:ident) => {25		Expr::BinaryOp($a, $op, $b)26	};27}28macro_rules! expr_un {29	($op:ident $a:ident) => {30		Expr::UnaryOp($op, $a)31	};32}3334parser! {35	grammar jsonnet_parser() for str {36		use peg::ParseLiteral;3738		rule eof() = quiet!{![_]} / expected!("<eof>")39		rule eol() = "\n" / eof()4041		/// Standard C-like comments42		rule comment()43			= "//" (!eol()[_])* eol()44			/ "/*" (!("*/")[_])* "*/"45			/ "#" (!eol()[_])* eol()4647		rule single_whitespace() = quiet!{([' ' | '\r' | '\n' | '\t'] / comment())} / expected!("<whitespace>")48		rule _() = quiet!{([' ' | '\r' | '\n' | '\t']+) / comment()}* / expected!("<whitespace>")4950		/// For comma-delimited elements51		rule comma() = quiet!{_ "," _} / expected!("<comma>")52		rule alpha() -> char = c:$(['_' | 'a'..='z' | 'A'..='Z']) {c.chars().next().unwrap()}53		rule digit() -> char = d:$(['0'..='9']) {d.chars().next().unwrap()}54		rule end_of_ident() = !['0'..='9' | '_' | 'a'..='z' | 'A'..='Z']55		/// Sequence of digits56		rule uint_str() -> &'input str = a:$(digit()+ ("_" digit()+)*) { a }57		/// Number in scientific notation format58		rule number() -> f64 = quiet!{a:$(uint_str() ("." uint_str())? (['e'|'E'] (s:['+'|'-'])? uint_str())?) {? a.replace("_","").parse().map_err(|_| "<number>") }} / expected!("<number>")5960		/// Reserved word followed by any non-alphanumberic61		rule reserved() = ("assert" / "else" / "error" / "false" / "for" / "function" / "if" / "import" / "importstr" / "importbin" / "in" / "local" / "null" / "tailstrict" / "then" / "self" / "super" / "true") end_of_ident()62		rule id() -> IStr = v:$(quiet!{ !reserved() alpha() (alpha() / digit())*} / expected!("<identifier>")) { v.into() }6364		rule keyword(id: &'static str) -> ()65			= ##parse_string_literal(id) end_of_ident()6667		pub rule param(s: &ParserSettings) -> expr::Param = name:destruct(s) expr:(_ "=" _ expr:expr(s){expr})? { expr::Param(name, expr) }68		pub rule params(s: &ParserSettings) -> expr::ParamsDesc69			= params:param(s) ** comma() comma()? { expr::ParamsDesc(Rc::new(params)) }70			/ { expr::ParamsDesc(Rc::new(Vec::new())) }7172		pub rule arg(s: &ParserSettings) -> (Option<IStr>, LocExpr)73			= name:(quiet! { (s:id() _ "=" !['='] _ {s})? } / expected!("<argument name>")) expr:expr(s) {(name, expr)}7475		pub rule args(s: &ParserSettings) -> expr::ArgsDesc76			= args:arg(s)**comma() comma()? {?77				let unnamed_count = args.iter().take_while(|(n, _)| n.is_none()).count();78				let mut unnamed = Vec::with_capacity(unnamed_count);79				let mut named = Vec::with_capacity(args.len() - unnamed_count);80				let mut named_started = false;81				for (name, value) in args {82					if let Some(name) = name {83						named_started = true;84						named.push((name, value));85					} else {86						if named_started {87							return Err("<named argument>")88						}89						unnamed.push(value);90					}91				}92				Ok(expr::ArgsDesc::new(unnamed, named))93			}9495		pub rule destruct_rest() -> expr::DestructRest96			= "..." into:(_ into:id() {into})? {if let Some(into) = into {97				expr::DestructRest::Keep(into)98			} else {expr::DestructRest::Drop}}99		pub rule destruct_array(s: &ParserSettings) -> expr::Destruct100			= "[" _ start:destruct(s)**comma() rest:(101				comma() _ rest:destruct_rest()? end:(102					comma() end:destruct(s)**comma() (_ comma())? {end}103					/ comma()? {Vec::new()}104				) {(rest, end)}105				/ comma()? {(None, Vec::new())}106			) _ "]" {?107				#[cfg(feature = "exp-destruct")] return Ok(expr::Destruct::Array {108					start,109					rest: rest.0,110					end: rest.1,111				});112				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")113			}114		pub rule destruct_object(s: &ParserSettings) -> expr::Destruct115			= "{" _116				fields:(name:id() into:(_ ":" _ into:destruct(s) {into})? default:(_ "=" _ v:expr(s) {v})? {(name, into, default)})**comma()117				rest:(118					comma() rest:destruct_rest()? {rest}119					/ comma()? {None}120				)121			_ "}" {?122				#[cfg(feature = "exp-destruct")] return Ok(expr::Destruct::Object {123					fields,124					rest,125				});126				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")127			}128		pub rule destruct(s: &ParserSettings) -> expr::Destruct129			= v:id() {expr::Destruct::Full(v)}130			/ "?" {?131				#[cfg(feature = "exp-destruct")] return Ok(expr::Destruct::Skip);132				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")133			}134			/ arr:destruct_array(s) {arr}135			/ obj:destruct_object(s) {obj}136137		pub rule bind(s: &ParserSettings) -> expr::BindSpec138			= into:destruct(s) _ "=" _ expr:expr(s) {expr::BindSpec::Field{into, value: expr}}139			/ name:id() _ "(" _ params:params(s) _ ")" _ "=" _ expr:expr(s) {expr::BindSpec::Function{name, params, value: expr}}140141		pub rule assertion(s: &ParserSettings) -> expr::AssertStmt142			= keyword("assert") _ cond:expr(s) msg:(_ ":" _ e:expr(s) {e})? { expr::AssertStmt(cond, msg) }143144		pub rule whole_line() -> &'input str145			= str:$((!['\n'][_])* "\n") {str}146		pub rule string_block() -> String147			= "|||" chomped:"-"? (!['\n']single_whitespace())* "\n"148			empty_lines:$(['\n']*)149			prefix:[' ' | '\t']+ first_line:whole_line()150			lines:("\n" {"\n"} / [' ' | '\t']*<{prefix.len()}> s:whole_line() {s})*151			[' ' | '\t']*<, {prefix.len() - 1}> "|||"152			{153				let mut l = empty_lines.to_owned();154				l.push_str(first_line);155				l.extend(lines);156				if chomped.is_some() {157					debug_assert!(l.ends_with('\n'));158					l.truncate(l.len() - 1);159				}160				l161			}162163		rule hex_char()164			= quiet! { ['0'..='9' | 'a'..='f' | 'A'..='F'] } / expected!("<hex char>")165166		rule string_char(c: rule<()>)167			= (!['\\']!c()[_])+168			/ "\\\\"169			/ "\\u" hex_char() hex_char() hex_char() hex_char()170			/ "\\x" hex_char() hex_char()171			/ ['\\'] (quiet! { ['b' | 'f' | 'n' | 'r' | 't' | '"' | '\''] } / expected!("<escape character>"))172		pub rule string() -> String173			= ['"'] str:$(string_char(<"\"">)*) ['"'] {? unescape::unescape(str).ok_or("<escaped string>")}174			/ ['\''] str:$(string_char(<"\'">)*) ['\''] {? unescape::unescape(str).ok_or("<escaped string>")}175			/ quiet!{ "@'" str:$(("''" / (!['\''][_]))*) "'" {str.replace("''", "'")}176			/ "@\"" str:$(("\"\"" / (!['"'][_]))*) "\"" {str.replace("\"\"", "\"")}177			/ string_block() } / expected!("<string>")178179		pub rule field_name(s: &ParserSettings) -> expr::FieldName180			= name:id() {expr::FieldName::Fixed(name)}181			/ name:string() {expr::FieldName::Fixed(name.into())}182			/ "[" _ expr:expr(s) _ "]" {expr::FieldName::Dyn(expr)}183		pub rule visibility() -> expr::Visibility184			= ":::" {expr::Visibility::Unhide}185			/ "::" {expr::Visibility::Hidden}186			/ ":" {expr::Visibility::Normal}187		pub rule field(s: &ParserSettings) -> expr::FieldMember188			= name:field_name(s) _ plus:"+"? _ visibility:visibility() _ value:expr(s) {expr::FieldMember{189				name,190				plus: plus.is_some(),191				params: None,192				visibility,193				value,194			}}195			/ name:field_name(s) _ "(" _ params:params(s) _ ")" _ visibility:visibility() _ value:expr(s) {expr::FieldMember{196				name,197				plus: false,198				params: Some(params),199				visibility,200				value,201			}}202		pub rule obj_local(s: &ParserSettings) -> BindSpec203			= keyword("local") _ bind:bind(s) {bind}204		pub rule member(s: &ParserSettings) -> expr::Member205			= bind:obj_local(s) {expr::Member::BindStmt(bind)}206			/ assertion:assertion(s) {expr::Member::AssertStmt(assertion)}207			/ field:field(s) {expr::Member::Field(field)}208		pub rule objinside(s: &ParserSettings) -> expr::ObjBody209			= 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})? {210				let mut compspecs = vec![CompSpec::ForSpec(forspec)];211				compspecs.extend(others.unwrap_or_default());212				expr::ObjBody::ObjComp(expr::ObjComp{213					pre_locals,214					field,215					post_locals,216					compspecs,217				})218			}219			/ members:(member(s) ** comma()) comma()? {expr::ObjBody::MemberList(members)}220		pub rule ifspec(s: &ParserSettings) -> IfSpecData221			= keyword("if") _ expr:expr(s) {IfSpecData(expr)}222		pub rule forspec(s: &ParserSettings) -> ForSpecData223			= keyword("for") _ id:destruct(s) _ keyword("in") _ cond:expr(s) {ForSpecData(id, cond)}224		pub rule compspec(s: &ParserSettings) -> Vec<expr::CompSpec>225			= s:(i:ifspec(s) { expr::CompSpec::IfSpec(i) } / f:forspec(s) {expr::CompSpec::ForSpec(f)} ) ** _ {s}226		pub rule local_expr(s: &ParserSettings) -> Expr227			= keyword("local") _ binds:bind(s) ** comma() (_ ",")? _ ";" _ expr:expr(s) { Expr::LocalExpr(binds, expr) }228		pub rule string_expr(s: &ParserSettings) -> Expr229			= s:string() {Expr::Str(s.into())}230		pub rule obj_expr(s: &ParserSettings) -> Expr231			= "{" _ body:objinside(s) _ "}" {Expr::Obj(body)}232		pub rule array_expr(s: &ParserSettings) -> Expr233			= "[" _ elems:(expr(s) ** comma()) _ comma()? "]" {Expr::Arr(elems)}234		pub rule array_comp_expr(s: &ParserSettings) -> Expr235			= "[" _ expr:expr(s) _ comma()? _ forspec:forspec(s) _ others:(others: compspec(s) _ {others})? "]" {236				let mut specs = vec![CompSpec::ForSpec(forspec)];237				specs.extend(others.unwrap_or_default());238				Expr::ArrComp(expr, specs)239			}240		pub rule number_expr(s: &ParserSettings) -> Expr241			= n:number() {? if n.is_finite() {242				Ok(expr::Expr::Num(n))243			} else {244				Err("!!!numbers are finite")245			}}246		pub rule var_expr(s: &ParserSettings) -> Expr247			= n:id() { expr::Expr::Var(n) }248		pub rule id_loc(s: &ParserSettings) -> LocExpr249			= a:position!() n:id() b:position!() { LocExpr::new(expr::Expr::Str(n), Span(s.source.clone(), a as u32,b as u32)) }250		pub rule if_then_else_expr(s: &ParserSettings) -> Expr251			= cond:ifspec(s) _ keyword("then") _ cond_then:expr(s) cond_else:(_ keyword("else") _ e:expr(s) {e})? {Expr::IfElse{252				cond,253				cond_then,254				cond_else,255			}}256257		pub rule literal(s: &ParserSettings) -> Expr258			= v:(259				keyword("null") {LiteralType::Null}260				/ keyword("true") {LiteralType::True}261				/ keyword("false") {LiteralType::False}262				/ keyword("self") {LiteralType::This}263				/ keyword("$") {LiteralType::Dollar}264				/ keyword("super") {LiteralType::Super}265			) {Expr::Literal(v)}266267		pub rule expr_basic(s: &ParserSettings) -> Expr268			= literal(s)269270			/ string_expr(s) / number_expr(s)271			/ array_expr(s)272			/ obj_expr(s)273			/ array_expr(s)274			/ array_comp_expr(s)275276			/ keyword("importstr") _ path:expr(s) {Expr::ImportStr(path)}277			/ keyword("importbin") _ path:expr(s) {Expr::ImportBin(path)}278			/ keyword("import") _ path:expr(s) {Expr::Import(path)}279280			/ var_expr(s)281			/ local_expr(s)282			/ if_then_else_expr(s)283284			/ keyword("function") _ "(" _ params:params(s) _ ")" _ expr:expr(s) {Expr::Function(params, expr)}285			/ assertion:assertion(s) _ ";" _ expr:expr(s) { Expr::AssertExpr(assertion, expr) }286287			/ keyword("error") _ expr:expr(s) { Expr::ErrorStmt(expr) }288289		rule slice_part(s: &ParserSettings) -> Option<LocExpr>290			= _ e:(e:expr(s) _{e})? {e}291		pub rule slice_desc(s: &ParserSettings) -> SliceDesc292			= start:slice_part(s) ":" pair:(end:slice_part(s) step:(":" e:slice_part(s){e})? {(end, step.flatten())})? {293				let (end, step) = if let Some((end, step)) = pair {294					(end, step)295				}else{296					(None, None)297				};298299				SliceDesc { start, end, step }300			}301302		rule binop(x: rule<()>) -> ()303			= quiet!{ x() } / expected!("<binary op>")304		rule unaryop(x: rule<()>) -> ()305			= quiet!{ x() } / expected!("<unary op>")306307		rule ensure_null_coaelse()308			= "" {?309				#[cfg(not(feature = "exp-null-coaelse"))] return Err("!!!experimental null coaelscing was not enabled");310				#[cfg(feature = "exp-null-coaelse")] Ok(())311			}312		use BinaryOpType::*;313		use UnaryOpType::*;314		rule expr(s: &ParserSettings) -> LocExpr315			= precedence! {316				"(" _ e:expr(s) _ ")" {e}317				start:position!() v:@ end:position!() { LocExpr::new(v, Span(s.source.clone(), start as u32, end as u32)) }318				--319				a:(@) _ binop(<"||">) _ b:@ {expr_bin!(a Or b)}320				a:(@) _ binop(<"??">) _ ensure_null_coaelse() b:@ {321					#[cfg(feature = "exp-null-coaelse")] return expr_bin!(a NullCoaelse b);322					unreachable!("ensure_null_coaelse will fail if feature is not enabled")323				}324				--325				a:(@) _ binop(<"&&">) _ b:@ {expr_bin!(a And b)}326				--327				a:(@) _ binop(<"|">) _ b:@ {expr_bin!(a BitOr b)}328				--329				a:@ _ binop(<"^">) _ b:(@) {expr_bin!(a BitXor b)}330				--331				a:(@) _ binop(<"&">) _ b:@ {expr_bin!(a BitAnd b)}332				--333				a:(@) _ binop(<"==">) _ b:@ {expr_bin!(a Eq b)}334				a:(@) _ binop(<"!=">) _ b:@ {expr_bin!(a Neq b)}335				--336				a:(@) _ binop(<"<">) _ b:@ {expr_bin!(a Lt b)}337				a:(@) _ binop(<">">) _ b:@ {expr_bin!(a Gt b)}338				a:(@) _ binop(<"<=">) _ b:@ {expr_bin!(a Lte b)}339				a:(@) _ binop(<">=">) _ b:@ {expr_bin!(a Gte b)}340				a:(@) _ binop(<keyword("in")>) _ b:@ {expr_bin!(a In b)}341				--342				a:(@) _ binop(<"<<">) _ b:@ {expr_bin!(a Lhs b)}343				a:(@) _ binop(<">>">) _ b:@ {expr_bin!(a Rhs b)}344				--345				a:(@) _ binop(<"+">) _ b:@ {expr_bin!(a Add b)}346				a:(@) _ binop(<"-">) _ b:@ {expr_bin!(a Sub b)}347				--348				a:(@) _ binop(<"*">) _ b:@ {expr_bin!(a Mul b)}349				a:(@) _ binop(<"/">) _ b:@ {expr_bin!(a Div b)}350				a:(@) _ binop(<"%">) _ b:@ {expr_bin!(a Mod b)}351				--352						unaryop(<"+">) _ b:@ {expr_un!(Plus b)}353						unaryop(<"-">) _ b:@ {expr_un!(Minus b)}354						unaryop(<"!">) _ b:@ {expr_un!(Not b)}355						unaryop(<"~">) _ b:@ {expr_un!(BitNot b)}356				--357				a:(@) _ "[" _ e:slice_desc(s) _ "]" {Expr::Slice(a, e)}358				indexable:(@) _ parts:index_part(s)+ {Expr::Index{indexable, parts}}359				a:(@) _ "(" _ args:args(s) _ ")" ts:(_ keyword("tailstrict"))? {Expr::Apply(a, args, ts.is_some())}360				a:(@) _ "{" _ body:objinside(s) _ "}" {Expr::ObjExtend(a, body)}361				--362				e:expr_basic(s) {e}363			}364		pub rule index_part(s: &ParserSettings) -> IndexPart365		= n:("?" _ ensure_null_coaelse())? "." _ value:id_loc(s) {IndexPart {366			value,367			#[cfg(feature = "exp-null-coaelse")]368			null_coaelse: n.is_some(),369		}}370		/ n:("?" _ "." _ ensure_null_coaelse())? "[" _ value:expr(s) _ "]" {IndexPart {371			value,372			#[cfg(feature = "exp-null-coaelse")]373			null_coaelse: n.is_some(),374		}}375376		pub rule jsonnet(s: &ParserSettings) -> LocExpr = _ e:expr(s) _ {e}377	}378}379380pub type ParseError = peg::error::ParseError<peg::str::LineCol>;381pub fn parse(str: &str, settings: &ParserSettings) -> Result<LocExpr, ParseError> {382	jsonnet_parser::jsonnet(str, settings)383}384/// Used for importstr values385pub fn string_to_expr(str: IStr, settings: &ParserSettings) -> LocExpr {386	let len = str.len();387	LocExpr::new(Expr::Str(str), Span(settings.source.clone(), 0, len as u32))388}389390#[cfg(test)]391pub mod tests {392	use jrsonnet_interner::IStr;393	use BinaryOpType::*;394395	use super::{expr::*, parse};396	use crate::{source::Source, ParserSettings};397398	macro_rules! parse {399		($s:expr) => {400			parse(401				$s,402				&ParserSettings {403					source: Source::new_virtual("<test>".into(), IStr::empty()),404				},405			)406			.unwrap()407		};408	}409410	macro_rules! el {411		($expr:expr, $from:expr, $to:expr$(,)?) => {412			LocExpr::new(413				$expr,414				Span(415					Source::new_virtual("<test>".into(), IStr::empty()),416					$from,417					$to,418				),419			)420		};421	}422423	#[test]424	fn multiline_string() {425		assert_eq!(426			parse!("|||\n    Hello world!\n     a\n|||"),427			el!(Expr::Str("Hello world!\n a\n".into()), 0, 31),428		);429		assert_eq!(430			parse!("|||\n  Hello world!\n   a\n|||"),431			el!(Expr::Str("Hello world!\n a\n".into()), 0, 27),432		);433		assert_eq!(434			parse!("|||\n\t\tHello world!\n\t\t\ta\n|||"),435			el!(Expr::Str("Hello world!\n\ta\n".into()), 0, 27),436		);437		assert_eq!(438			parse!("|||\n   Hello world!\n    a\n |||"),439			el!(Expr::Str("Hello world!\n a\n".into()), 0, 30),440		);441	}442443	#[test]444	fn slice() {445		parse!("a[1:]");446		parse!("a[1::]");447		parse!("a[:1:]");448		parse!("a[::1]");449		parse!("str[:len - 1]");450	}451452	#[test]453	fn string_escaping() {454		assert_eq!(455			parse!(r#""Hello, \"world\"!""#),456			el!(Expr::Str(r#"Hello, "world"!"#.into()), 0, 19),457		);458		assert_eq!(459			parse!(r#"'Hello \'world\'!'"#),460			el!(Expr::Str("Hello 'world'!".into()), 0, 18),461		);462		assert_eq!(parse!(r#"'\\\\'"#), el!(Expr::Str("\\\\".into()), 0, 6));463	}464465	#[test]466	fn string_unescaping() {467		assert_eq!(468			parse!(r#""Hello\nWorld""#),469			el!(Expr::Str("Hello\nWorld".into()), 0, 14),470		);471	}472473	#[test]474	fn string_verbantim() {475		assert_eq!(476			parse!(r#"@"Hello\n""World""""#),477			el!(Expr::Str("Hello\\n\"World\"".into()), 0, 19),478		);479	}480481	#[test]482	fn imports() {483		assert_eq!(484			parse!("import \"hello\""),485			el!(Expr::Import(el!(Expr::Str("hello".into()), 7, 14)), 0, 14),486		);487		assert_eq!(488			parse!("importstr \"garnish.txt\""),489			el!(490				Expr::ImportStr(el!(Expr::Str("garnish.txt".into()), 10, 23)),491				0,492				23493			)494		);495		assert_eq!(496			parse!("importbin \"garnish.bin\""),497			el!(498				Expr::ImportBin(el!(Expr::Str("garnish.bin".into()), 10, 23)),499				0,500				23501			)502		);503	}504505	#[test]506	fn empty_object() {507		assert_eq!(508			parse!("{}"),509			el!(Expr::Obj(ObjBody::MemberList(vec![])), 0, 2)510		);511	}512513	#[test]514	fn basic_math() {515		assert_eq!(516			parse!("2+2*2"),517			el!(518				Expr::BinaryOp(519					el!(Expr::Num(2.0), 0, 1),520					Add,521					el!(522						Expr::BinaryOp(el!(Expr::Num(2.0), 2, 3), Mul, el!(Expr::Num(2.0), 4, 5)),523						2,524						5525					)526				),527				0,528				5529			)530		);531	}532533	#[test]534	fn basic_math_with_indents() {535		assert_eq!(536			parse!("2	+ 	  2	  *	2   	"),537			el!(538				Expr::BinaryOp(539					el!(Expr::Num(2.0), 0, 1),540					Add,541					el!(542						Expr::BinaryOp(el!(Expr::Num(2.0), 7, 8), Mul, el!(Expr::Num(2.0), 13, 14),),543						7,544						14545					),546				),547				0,548				14549			)550		);551	}552553	#[test]554	fn basic_math_parened() {555		assert_eq!(556			parse!("2+(2+2*2)"),557			el!(558				Expr::BinaryOp(559					el!(Expr::Num(2.0), 0, 1),560					Add,561					el!(562						Expr::BinaryOp(563							el!(Expr::Num(2.0), 3, 4),564							Add,565							el!(566								Expr::BinaryOp(567									el!(Expr::Num(2.0), 5, 6),568									Mul,569									el!(Expr::Num(2.0), 7, 8),570								),571								5,572								8573							),574						),575						3,576						8577					),578				),579				0,580				9581			)582		);583	}584585	/// Comments should not affect parsing586	#[test]587	fn comments() {588		assert_eq!(589			parse!("2//comment\n+//comment\n3/*test*/*/*test*/4"),590			el!(591				Expr::BinaryOp(592					el!(Expr::Num(2.0), 0, 1),593					Add,594					el!(595						Expr::BinaryOp(596							el!(Expr::Num(3.0), 22, 23),597							Mul,598							el!(Expr::Num(4.0), 40, 41)599						),600						22,601						41602					)603				),604				0,605				41606			)607		);608	}609610	#[test]611	fn suffix() {612		// assert_eq!(parse!("std.test"), el!(Expr::Num(2.2)));613		// assert_eq!(parse!("std(2)"), el!(Expr::Num(2.2)));614		// assert_eq!(parse!("std.test(2)"), el!(Expr::Num(2.2)));615		// assert_eq!(parse!("a[b]"), el!(Expr::Num(2.2)))616	}617618	#[test]619	fn array_comp() {620		use Expr::*;621		/*622		`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`,623		`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`624				*/625		assert_eq!(626			parse!("[std.deepJoin(x) for x in arr]"),627			el!(628				ArrComp(629					el!(630						Apply(631							el!(632								Index {633									indexable: el!(Var("std".into()), 1, 4),634									parts: vec![IndexPart {635										value: el!(Str("deepJoin".into()), 5, 13),636										#[cfg(feature = "exp-null-coaelse")]637										null_coaelse: false,638									}],639								},640								1,641								13642							),643							ArgsDesc::new(vec![el!(Var("x".into()), 14, 15)], vec![]),644							false,645						),646						1,647						16648					),649					vec![CompSpec::ForSpec(ForSpecData(650						Destruct::Full("x".into()),651						el!(Var("arr".into()), 26, 29)652					))]653				),654				0,655				30656			),657		)658	}659660	#[test]661	fn reserved() {662		use Expr::*;663		assert_eq!(parse!("null"), el!(Literal(LiteralType::Null), 0, 4));664		assert_eq!(parse!("nulla"), el!(Var("nulla".into()), 0, 5));665	}666667	#[test]668	fn multiple_args_buf() {669		parse!("a(b, null_fields)");670	}671672	#[test]673	fn infix_precedence() {674		use Expr::*;675		assert_eq!(676			parse!("!a && !b"),677			el!(678				BinaryOp(679					el!(UnaryOp(UnaryOpType::Not, el!(Var("a".into()), 1, 2)), 0, 2),680					And,681					el!(UnaryOp(UnaryOpType::Not, el!(Var("b".into()), 7, 8)), 6, 8)682				),683				0,684				8685			)686		);687	}688689	#[test]690	fn infix_precedence_division() {691		use Expr::*;692		assert_eq!(693			parse!("!a / !b"),694			el!(695				BinaryOp(696					el!(UnaryOp(UnaryOpType::Not, el!(Var("a".into()), 1, 2)), 0, 2),697					Div,698					el!(UnaryOp(UnaryOpType::Not, el!(Var("b".into()), 6, 7)), 5, 7)699				),700				0,701				7702			)703		);704	}705706	#[test]707	fn double_negation() {708		use Expr::*;709		assert_eq!(710			parse!("!!a"),711			el!(712				UnaryOp(713					UnaryOpType::Not,714					el!(UnaryOp(UnaryOpType::Not, el!(Var("a".into()), 2, 3)), 1, 3)715				),716				0,717				3718			)719		)720	}721722	#[test]723	fn array_test_error() {724		parse!("[a for a in b if c for e in f]");725		//                    ^^^^ failed code726	}727728	#[test]729	fn missing_newline_between_comment_and_eof() {730		parse!(731			"{a:1}732733			//+213"734		);735	}736737	#[test]738	fn default_param_before_nondefault() {739		parse!("local x(foo = 'foo', bar) = null; null");740	}741742	#[test]743	fn add_location_info_to_all_sub_expressions() {744		use Expr::*;745746		let file_name = Source::new_virtual("<test>".into(), IStr::empty());747		let expr = parse(748			"{} { local x = 1, x: x } + {}",749			&ParserSettings { source: file_name },750		)751		.unwrap();752		assert_eq!(753			expr,754			el!(755				BinaryOp(756					el!(757						ObjExtend(758							el!(Obj(ObjBody::MemberList(vec![])), 0, 2),759							ObjBody::MemberList(vec![760								Member::BindStmt(BindSpec::Field {761									into: Destruct::Full("x".into()),762									value: el!(Num(1.0), 15, 16)763								}),764								Member::Field(FieldMember {765									name: FieldName::Fixed("x".into()),766									plus: false,767									params: None,768									visibility: Visibility::Normal,769									value: el!(Var("x".into()), 21, 22),770								})771							])772						),773						0,774						24775					),776					BinaryOpType::Add,777					el!(Obj(ObjBody::MemberList(vec![])), 27, 29),778				),779				0,780				29781			),782		);783	}784}
after · crates/jrsonnet-parser/src/lib.rs
1#![allow(clippy::redundant_closure_call, clippy::derive_partial_eq_without_eq)]23use std::rc::Rc;45use peg::parser;6mod expr;7pub use expr::*;8pub use jrsonnet_interner::IStr;9pub use peg;10mod location;11mod source;12mod unescape;13pub use location::CodeLocation;14pub use source::{15	Source, SourceDefaultIgnoreJpath, SourceDirectory, SourceFifo, SourceFile, SourcePath,16	SourcePathT, SourceVirtual,17};1819pub struct ParserSettings {20	pub source: Source,21}2223macro_rules! expr_bin {24	($a:ident $op:ident $b:ident) => {25		Expr::BinaryOp(Box::new(BinaryOp {26			lhs: $a,27			op: $op,28			rhs: $b,29		}))30	};31}32macro_rules! expr_un {33	($op:ident $a:ident) => {34		Expr::UnaryOp($op, Box::new($a))35	};36}3738parser! {39	grammar jsonnet_parser() for str {40		use peg::ParseLiteral;4142		rule eof() = quiet!{![_]} / expected!("<eof>")43		rule eol() = "\n" / eof()4445		/// Standard C-like comments46		rule comment()47			= "//" (!eol()[_])* eol()48			/ "/*" (!("*/")[_])* "*/"49			/ "#" (!eol()[_])* eol()5051		rule single_whitespace() = quiet!{([' ' | '\r' | '\n' | '\t'] / comment())} / expected!("<whitespace>")52		rule _() = quiet!{([' ' | '\r' | '\n' | '\t']+) / comment()}* / expected!("<whitespace>")5354		/// For comma-delimited elements55		rule comma() = quiet!{_ "," _} / expected!("<comma>")56		rule alpha() -> char = c:$(['_' | 'a'..='z' | 'A'..='Z']) {c.chars().next().unwrap()}57		rule digit() -> char = d:$(['0'..='9']) {d.chars().next().unwrap()}58		rule end_of_ident() = !['0'..='9' | '_' | 'a'..='z' | 'A'..='Z']59		/// Sequence of digits60		rule uint_str() -> &'input str = a:$(digit()+ ("_" digit()+)*) { a }61		/// Number in scientific notation format62		rule number() -> f64 = quiet!{a:$(uint_str() ("." uint_str())? (['e'|'E'] (s:['+'|'-'])? uint_str())?) {? a.replace("_","").parse().map_err(|_| "<number>") }} / expected!("<number>")6364		/// Reserved word followed by any non-alphanumberic65		rule reserved() = ("assert" / "else" / "error" / "false" / "for" / "function" / "if" / "import" / "importstr" / "importbin" / "in" / "local" / "null" / "tailstrict" / "then" / "self" / "super" / "true") end_of_ident()66		rule id() -> IStr = v:$(quiet!{ !reserved() alpha() (alpha() / digit())*} / expected!("<identifier>")) { v.into() }6768		rule keyword(id: &'static str) -> ()69			= ##parse_string_literal(id) end_of_ident()7071		pub rule param(s: &ParserSettings) -> expr::Param = name:destruct(s) expr:(_ "=" _ expr:expr(s){expr})? { expr::Param(name, expr.map(Rc::new)) }72		pub rule params(s: &ParserSettings) -> expr::ParamsDesc73			= params:param(s) ** comma() comma()? { expr::ParamsDesc(Rc::new(params)) }74			/ { expr::ParamsDesc(Rc::new(Vec::new())) }7576		pub rule arg(s: &ParserSettings) -> (Option<IStr>, Rc<Spanned<Expr>>)77			= name:(quiet! { (s:id() _ "=" !['='] _ {s})? } / expected!("<argument name>")) expr:expr(s) {(name, Rc::new(expr))}7879		pub rule args(s: &ParserSettings) -> expr::ArgsDesc80			= args:arg(s)**comma() comma()? {?81				let unnamed_count = args.iter().take_while(|(n, _)| n.is_none()).count();82				let mut unnamed = Vec::with_capacity(unnamed_count);83				let mut named = Vec::with_capacity(args.len() - unnamed_count);84				let mut named_started = false;85				for (name, value) in args {86					if let Some(name) = name {87						named_started = true;88						named.push((name, value));89					} else {90						if named_started {91							return Err("<named argument>")92						}93						unnamed.push(value);94					}95				}96				Ok(expr::ArgsDesc::new(unnamed, named))97			}9899		pub rule destruct_rest() -> expr::DestructRest100			= "..." into:(_ into:id() {into})? {if let Some(into) = into {101				expr::DestructRest::Keep(into)102			} else {expr::DestructRest::Drop}}103		pub rule destruct_array(s: &ParserSettings) -> expr::Destruct104			= "[" _ start:destruct(s)**comma() rest:(105				comma() _ rest:destruct_rest()? end:(106					comma() end:destruct(s)**comma() (_ comma())? {end}107					/ comma()? {Vec::new()}108				) {(rest, end)}109				/ comma()? {(None, Vec::new())}110			) _ "]" {?111				#[cfg(feature = "exp-destruct")] return Ok(expr::Destruct::Array {112					start,113					rest: rest.0,114					end: rest.1,115				});116				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")117			}118		pub rule destruct_object(s: &ParserSettings) -> expr::Destruct119			= "{" _120				fields:(name:id() into:(_ ":" _ into:destruct(s) {into})? default:(_ "=" _ v:expr(s) {v})? {(name, into, default)})**comma()121				rest:(122					comma() rest:destruct_rest()? {rest}123					/ comma()? {None}124				)125			_ "}" {?126				#[cfg(feature = "exp-destruct")] return Ok(expr::Destruct::Object {127					fields,128					rest,129				});130				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")131			}132		pub rule destruct(s: &ParserSettings) -> expr::Destruct133			= v:id() {expr::Destruct::Full(v)}134			/ "?" {?135				#[cfg(feature = "exp-destruct")] return Ok(expr::Destruct::Skip);136				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")137			}138			/ arr:destruct_array(s) {arr}139			/ obj:destruct_object(s) {obj}140141		pub rule bind(s: &ParserSettings) -> expr::BindSpec142			= into:destruct(s) _ "=" _ expr:expr(s) {expr::BindSpec::Field{into, value: Rc::new(expr)}}143			/ name:id() _ "(" _ params:params(s) _ ")" _ "=" _ expr:expr(s) {expr::BindSpec::Function{name, params, value: Rc::new(expr)}}144145		pub rule assertion(s: &ParserSettings) -> expr::AssertStmt146			= keyword("assert") _ cond:expr(s) msg:(_ ":" _ e:expr(s) {e})? { expr::AssertStmt(cond, msg) }147148		pub rule whole_line() -> &'input str149			= str:$((!['\n'][_])* "\n") {str}150		pub rule string_block() -> String151			= "|||" chomped:"-"? (!['\n']single_whitespace())* "\n"152			empty_lines:$(['\n']*)153			prefix:[' ' | '\t']+ first_line:whole_line()154			lines:("\n" {"\n"} / [' ' | '\t']*<{prefix.len()}> s:whole_line() {s})*155			[' ' | '\t']*<, {prefix.len() - 1}> "|||"156			{157				let mut l = empty_lines.to_owned();158				l.push_str(first_line);159				l.extend(lines);160				if chomped.is_some() {161					debug_assert!(l.ends_with('\n'));162					l.truncate(l.len() - 1);163				}164				l165			}166167		rule hex_char()168			= quiet! { ['0'..='9' | 'a'..='f' | 'A'..='F'] } / expected!("<hex char>")169170		rule string_char(c: rule<()>)171			= (!['\\']!c()[_])+172			/ "\\\\"173			/ "\\u" hex_char() hex_char() hex_char() hex_char()174			/ "\\x" hex_char() hex_char()175			/ ['\\'] (quiet! { ['b' | 'f' | 'n' | 'r' | 't' | '"' | '\''] } / expected!("<escape character>"))176		pub rule string() -> String177			= ['"'] str:$(string_char(<"\"">)*) ['"'] {? unescape::unescape(str).ok_or("<escaped string>")}178			/ ['\''] str:$(string_char(<"\'">)*) ['\''] {? unescape::unescape(str).ok_or("<escaped string>")}179			/ quiet!{ "@'" str:$(("''" / (!['\''][_]))*) "'" {str.replace("''", "'")}180			/ "@\"" str:$(("\"\"" / (!['"'][_]))*) "\"" {str.replace("\"\"", "\"")}181			/ string_block() } / expected!("<string>")182183		pub rule field_name(s: &ParserSettings) -> expr::FieldName184			= name:id() {expr::FieldName::Fixed(name)}185			/ name:string() {expr::FieldName::Fixed(name.into())}186			/ "[" _ expr:expr(s) _ "]" {expr::FieldName::Dyn(expr)}187		pub rule visibility() -> expr::Visibility188			= ":::" {expr::Visibility::Unhide}189			/ "::" {expr::Visibility::Hidden}190			/ ":" {expr::Visibility::Normal}191		pub rule field(s: &ParserSettings) -> expr::FieldMember192			= name:field_name(s) _ plus:"+"? _ visibility:visibility() _ value:expr(s) {expr::FieldMember{193				name,194				plus: plus.is_some(),195				params: None,196				visibility,197				value: Rc::new(value),198			}}199			/ name:field_name(s) _ "(" _ params:params(s) _ ")" _ visibility:visibility() _ value:expr(s) {expr::FieldMember{200				name,201				plus: false,202				params: Some(params),203				visibility,204				value: Rc::new(value),205			}}206		pub rule obj_local(s: &ParserSettings) -> BindSpec207			= keyword("local") _ bind:bind(s) {bind}208		pub rule member(s: &ParserSettings) -> expr::Member209			= bind:obj_local(s) {expr::Member::BindStmt(bind)}210			/ assertion:assertion(s) {expr::Member::AssertStmt(Rc::new(assertion))}211			/ field:field(s) {expr::Member::Field(field)}212		pub rule objinside(s: &ParserSettings) -> expr::ObjBody213			= 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})? {214				let mut compspecs = vec![CompSpec::ForSpec(forspec)];215				compspecs.extend(others.unwrap_or_default());216				expr::ObjBody::ObjComp(expr::ObjComp{217					pre_locals,218					field: Rc::new(field),219					post_locals,220					compspecs,221				})222			}223			/ members:(member(s) ** comma()) comma()? {expr::ObjBody::MemberList(members)}224		pub rule ifspec(s: &ParserSettings) -> IfSpecData225			= keyword("if") _ expr:expr(s) {IfSpecData(expr)}226		pub rule forspec(s: &ParserSettings) -> ForSpecData227			= keyword("for") _ id:destruct(s) _ keyword("in") _ cond:expr(s) {ForSpecData(id, cond)}228		pub rule compspec(s: &ParserSettings) -> Vec<expr::CompSpec>229			= s:(i:ifspec(s) { expr::CompSpec::IfSpec(i) } / f:forspec(s) {expr::CompSpec::ForSpec(f)} ) ** _ {s}230		pub rule local_expr(s: &ParserSettings) -> Expr231			= keyword("local") _ binds:bind(s) ** comma() (_ ",")? _ ";" _ expr:expr(s) { Expr::LocalExpr(binds, Box::new(expr)) }232		pub rule string_expr(s: &ParserSettings) -> Expr233			= s:string() {Expr::Str(s.into())}234		pub rule obj_expr(s: &ParserSettings) -> Expr235			= "{" _ body:objinside(s) _ "}" {Expr::Obj(body)}236		pub rule array_expr(s: &ParserSettings) -> Expr237			= "[" _ elems:(expr(s) ** comma()) _ comma()? "]" {Expr::Arr(Rc::new(elems))}238		pub rule array_comp_expr(s: &ParserSettings) -> Expr239			= "[" _ expr:expr(s) _ comma()? _ forspec:forspec(s) _ others:(others: compspec(s) _ {others})? "]" {240				let mut specs = vec![CompSpec::ForSpec(forspec)];241				specs.extend(others.unwrap_or_default());242				Expr::ArrComp(Rc::new(expr), specs)243			}244		pub rule number_expr(s: &ParserSettings) -> Expr245			= n:number() {? if n.is_finite() {246				Ok(expr::Expr::Num(n))247			} else {248				Err("!!!numbers are finite")249			}}250		pub rule var_expr(s: &ParserSettings) -> Expr251			= n:id() { expr::Expr::Var(n) }252		pub rule id_loc(s: &ParserSettings) -> Spanned<Expr>253			= a:position!() n:id() b:position!() { Spanned::new(expr::Expr::Str(n), Span(s.source.clone(), a as u32,b as u32)) }254		pub rule if_then_else_expr(s: &ParserSettings) -> Expr255			= cond:ifspec(s) _ keyword("then") _ cond_then:expr(s) cond_else:(_ keyword("else") _ e:expr(s) {e})? {Expr::IfElse(Box::new(IfElse{256				cond,257				cond_then,258				cond_else,259			}))}260261		pub rule literal(s: &ParserSettings) -> Expr262			= v:(263				keyword("null") {LiteralType::Null}264				/ keyword("true") {LiteralType::True}265				/ keyword("false") {LiteralType::False}266				/ keyword("self") {LiteralType::This}267				/ keyword("$") {LiteralType::Dollar}268				/ keyword("super") {LiteralType::Super}269			) {Expr::Literal(v)}270271		rule import_kind() -> ImportKind272			= keyword("importstr") { ImportKind::Str }273			/ keyword("importbin") { ImportKind::Bin }274			/ keyword("import") { ImportKind::Normal }275276		pub rule expr_basic(s: &ParserSettings) -> Expr277			= literal(s)278279			/ string_expr(s) / number_expr(s)280			/ array_expr(s)281			/ obj_expr(s)282			/ array_expr(s)283			/ array_comp_expr(s)284285			/ kind:import_kind() _ path:expr(s) {Expr::Import(kind, Box::new(path))}286287			/ var_expr(s)288			/ local_expr(s)289			/ if_then_else_expr(s)290291			/ keyword("function") _ "(" _ params:params(s) _ ")" _ expr:expr(s) {Expr::Function(params, Rc::new(expr))}292			/ assert:assertion(s) _ ";" _ rest:expr(s) { Expr::AssertExpr(Rc::new(AssertExpr{293				assert, rest294			})) }295296			/ keyword("error") _ expr:expr(s) { Expr::ErrorStmt(Box::new(expr)) }297298		rule slice_part(s: &ParserSettings) -> Option<Spanned<Expr>>299			= _ e:(e:expr(s) _{e})? {e}300		pub rule slice_desc(s: &ParserSettings) -> SliceDesc301			= start:slice_part(s) ":" pair:(end:slice_part(s) step:(":" e:slice_part(s){e})? {(end, step.flatten())})? {302				let (end, step) = if let Some((end, step)) = pair {303					(end, step)304				}else{305					(None, None)306				};307308				SliceDesc { start, end, step }309			}310311		rule binop(x: rule<()>) -> ()312			= quiet!{ x() } / expected!("<binary op>")313		rule unaryop(x: rule<()>) -> ()314			= quiet!{ x() } / expected!("<unary op>")315316		rule ensure_null_coaelse()317			= "" {?318				#[cfg(not(feature = "exp-null-coaelse"))] return Err("!!!experimental null coaelscing was not enabled");319				#[cfg(feature = "exp-null-coaelse")] Ok(())320			}321		use BinaryOpType::*;322		use UnaryOpType::*;323		rule expr(s: &ParserSettings) -> Spanned<Expr>324			= precedence! {325				"(" _ e:expr(s) _ ")" {e}326				start:position!() v:@ end:position!() { Spanned::new(v, Span(s.source.clone(), start as u32, end as u32)) }327				--328				a:(@) _ binop(<"||">) _ b:@ {expr_bin!(a Or b)}329				a:(@) _ binop(<"??">) _ ensure_null_coaelse() b:@ {330					#[cfg(feature = "exp-null-coaelse")] return expr_bin!(a NullCoaelse b);331					unreachable!("ensure_null_coaelse will fail if feature is not enabled")332				}333				--334				a:(@) _ binop(<"&&">) _ b:@ {expr_bin!(a And b)}335				--336				a:(@) _ binop(<"|">) _ b:@ {expr_bin!(a BitOr b)}337				--338				a:@ _ binop(<"^">) _ b:(@) {expr_bin!(a BitXor b)}339				--340				a:(@) _ binop(<"&">) _ b:@ {expr_bin!(a BitAnd b)}341				--342				a:(@) _ binop(<"==">) _ b:@ {expr_bin!(a Eq b)}343				a:(@) _ binop(<"!=">) _ b:@ {expr_bin!(a Neq b)}344				--345				a:(@) _ binop(<"<">) _ b:@ {expr_bin!(a Lt b)}346				a:(@) _ binop(<">">) _ b:@ {expr_bin!(a Gt b)}347				a:(@) _ binop(<"<=">) _ b:@ {expr_bin!(a Lte b)}348				a:(@) _ binop(<">=">) _ b:@ {expr_bin!(a Gte b)}349				a:(@) _ binop(<keyword("in")>) _ b:@ {expr_bin!(a In b)}350				--351				a:(@) _ binop(<"<<">) _ b:@ {expr_bin!(a Lhs b)}352				a:(@) _ binop(<">>">) _ b:@ {expr_bin!(a Rhs b)}353				--354				a:(@) _ binop(<"+">) _ b:@ {expr_bin!(a Add b)}355				a:(@) _ binop(<"-">) _ b:@ {expr_bin!(a Sub b)}356				--357				a:(@) _ binop(<"*">) _ b:@ {expr_bin!(a Mul b)}358				a:(@) _ binop(<"/">) _ b:@ {expr_bin!(a Div b)}359				a:(@) _ binop(<"%">) _ b:@ {expr_bin!(a Mod b)}360				--361						unaryop(<"+">) _ b:@ {expr_un!(Plus b)}362						unaryop(<"-">) _ b:@ {expr_un!(Minus b)}363						unaryop(<"!">) _ b:@ {expr_un!(Not b)}364						unaryop(<"~">) _ b:@ {expr_un!(BitNot b)}365				--366				value:(@) _ "[" _ slice:slice_desc(s) _ "]" {Expr::Slice(Box::new(Slice{value, slice}))}367				indexable:(@) _ parts:index_part(s)+ {Expr::Index{indexable: Box::new(indexable), parts}}368				a:(@) _ "(" _ args:args(s) _ ")" ts:(_ keyword("tailstrict"))? {Expr::Apply(Box::new(a), args, ts.is_some())}369				a:(@) _ "{" _ body:objinside(s) _ "}" {Expr::ObjExtend(Rc::new(a), body)}370				--371				e:expr_basic(s) {e}372			}373		pub rule index_part(s: &ParserSettings) -> IndexPart374		= n:("?" _ ensure_null_coaelse())? "." _ value:id_loc(s) {IndexPart {375			value,376			#[cfg(feature = "exp-null-coaelse")]377			null_coaelse: n.is_some(),378		}}379		/ n:("?" _ "." _ ensure_null_coaelse())? "[" _ value:expr(s) _ "]" {IndexPart {380			value,381			#[cfg(feature = "exp-null-coaelse")]382			null_coaelse: n.is_some(),383		}}384385		pub rule jsonnet(s: &ParserSettings) -> Spanned<Expr> = _ e:expr(s) _ {e}386	}387}388389pub type ParseError = peg::error::ParseError<peg::str::LineCol>;390pub fn parse(str: &str, settings: &ParserSettings) -> Result<Spanned<Expr>, ParseError> {391	jsonnet_parser::jsonnet(str, settings)392}393/// Used for importstr values394pub fn string_to_expr(str: IStr, settings: &ParserSettings) -> Spanned<Expr> {395	let len = str.len();396	Spanned::new(Expr::Str(str), Span(settings.source.clone(), 0, len as u32))397}398399#[cfg(test)]400pub mod tests {401	use insta::assert_snapshot;402	use jrsonnet_interner::IStr;403404	use super::parse;405	use crate::{source::Source, ParserSettings};406407	fn parsep(s: &str) -> String {408		let v = parse(409			s,410			&ParserSettings {411				source: Source::new_virtual("<test>".into(), IStr::empty()),412			},413		)414		.unwrap();415		format!("{v:#?}")416	}417418	macro_rules! parse {419		($s:expr) => {420			assert_snapshot!(parsep($s));421		};422	}423424	#[test]425	fn multiline_string() {426		parse!("|||\n    Hello world!\n     a\n|||");427		parse!("|||\n  Hello world!\n   a\n|||");428		parse!("|||\n\t\tHello world!\n\t\t\ta\n|||");429		parse!("|||\n   Hello world!\n    a\n |||");430	}431432	#[test]433	fn slice() {434		parse!("a[1:]");435		parse!("a[1::]");436		parse!("a[:1:]");437		parse!("a[::1]");438		parse!("str[:len - 1]");439	}440441	#[test]442	fn string_escaping() {443		parse!(r#""Hello, \"world\"!""#);444		parse!(r#"'Hello \'world\'!'"#);445		parse!(r#"'\\\\'"#);446	}447448	#[test]449	fn string_unescaping() {450		parse!(r#""Hello\nWorld""#);451	}452453	#[test]454	fn string_verbantim() {455		parse!(r#"@"Hello\n""World""""#);456	}457458	#[test]459	fn imports() {460		parse!("import \"hello\"");461		parse!("importstr \"garnish.txt\"");462		parse!("importbin \"garnish.bin\"");463	}464465	#[test]466	fn empty_object() {467		parse!("{}");468	}469470	#[test]471	fn basic_math() {472		parse!("2+2*2");473		parse!("2	+ 	  2	  *	2   	");474		parse!("2+(2+2*2)");475		parse!("2//comment\n+//comment\n3/*test*/*/*test*/4");476	}477478	#[test]479	fn suffix() {480		parse!("std.test");481		parse!("std(2)");482		parse!("std.test(2)");483		parse!("a[b]");484	}485486	#[test]487	fn array_comp() {488		parse!("[std.deepJoin(x) for x in arr]");489	}490491	#[test]492	fn reserved() {493		parse!("null");494		parse!("nulla");495	}496497	#[test]498	fn multiple_args_buf() {499		parse!("a(b, null_fields)");500	}501502	#[test]503	fn infix_precedence() {504		parse!("!a && !b");505		parse!("!a / !b");506	}507508	#[test]509	fn double_negation() {510		parse!("!!a");511	}512513	#[test]514	fn array_test_error() {515		parse!("[a for a in b if c for e in f]");516	}517518	#[test]519	fn missing_newline_between_comment_and_eof() {520		parse!(521			"{a:1}522523			//+213"524		);525	}526527	#[test]528	fn default_param_before_nondefault() {529		parse!("local x(foo = 'foo', bar) = null; null");530	}531532	#[test]533	fn add_location_info_to_all_sub_expressions() {534		parse!("{} { local x = 1, x: x } + {}");535	}536}
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)
+	}
+}