git.delta.rocks / jrsonnet / refs/commits / 44f6e2c9e550

difftreelog

refactor split peg parser and ir

kzvrllptYaroslav Bolyukin2026-03-22parent: #1ddb92c.patch.diff
in: master

109 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -609,7 +609,7 @@
  "jrsonnet-cli",
  "jrsonnet-evaluator",
  "jrsonnet-gcmodule",
- "jrsonnet-parser",
+ "jrsonnet-ir",
  "mimallocator",
  "serde",
  "serde_json",
@@ -623,7 +623,7 @@
  "clap",
  "jrsonnet-evaluator",
  "jrsonnet-gcmodule",
- "jrsonnet-parser",
+ "jrsonnet-ir",
  "jrsonnet-stdlib",
 ]
 
@@ -637,8 +637,9 @@
  "hi-doc",
  "jrsonnet-gcmodule",
  "jrsonnet-interner",
+ "jrsonnet-ir",
  "jrsonnet-macros",
- "jrsonnet-parser",
+ "jrsonnet-peg-parser",
  "jrsonnet-types",
  "num-bigint",
  "pathdiff",
@@ -704,6 +705,17 @@
 ]
 
 [[package]]
+name = "jrsonnet-ir"
+version = "0.5.0-pre97"
+dependencies = [
+ "insta",
+ "jrsonnet-gcmodule",
+ "jrsonnet-interner",
+ "peg",
+ "static_assertions",
+]
+
+[[package]]
 name = "jrsonnet-macros"
 version = "0.5.0-pre97"
 dependencies = [
@@ -714,14 +726,12 @@
 ]
 
 [[package]]
-name = "jrsonnet-parser"
+name = "jrsonnet-peg-parser"
 version = "0.5.0-pre97"
 dependencies = [
  "insta",
- "jrsonnet-gcmodule",
- "jrsonnet-interner",
+ "jrsonnet-ir",
  "peg",
- "static_assertions",
 ]
 
 [[package]]
@@ -746,8 +756,8 @@
  "base64",
  "jrsonnet-evaluator",
  "jrsonnet-gcmodule",
+ "jrsonnet-ir",
  "jrsonnet-macros",
- "jrsonnet-parser",
  "lru",
  "md5",
  "num-bigint",
@@ -812,7 +822,7 @@
  "jrsonnet-evaluator",
  "jrsonnet-gcmodule",
  "jrsonnet-interner",
- "jrsonnet-parser",
+ "jrsonnet-ir",
  "jrsonnet-stdlib",
 ]
 
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,7 +13,8 @@
 [workspace.dependencies]
 jrsonnet-evaluator = { path = "./crates/jrsonnet-evaluator", version = "0.5.0-pre97" }
 jrsonnet-macros = { path = "./crates/jrsonnet-macros", version = "0.5.0-pre97" }
-jrsonnet-parser = { path = "./crates/jrsonnet-parser", version = "0.5.0-pre97" }
+jrsonnet-ir = { path = "./crates/jrsonnet-ir", version = "0.5.0-pre97" }
+jrsonnet-peg-parser = { path = "./crates/jrsonnet-peg-parser", version = "0.5.0-pre97" }
 jrsonnet-rowan-parser = { path = "./crates/jrsonnet-rowan-parser", version = "0.5.0-pre97" }
 jrsonnet-interner = { path = "./crates/jrsonnet-interner", version = "0.5.0-pre97" }
 jrsonnet-stdlib = { path = "./crates/jrsonnet-stdlib", version = "0.5.0-pre97" }
modifiedbindings/jsonnet/Cargo.tomldiffbeforeafterboth
--- a/bindings/jsonnet/Cargo.toml
+++ b/bindings/jsonnet/Cargo.toml
@@ -20,7 +20,7 @@
 
 [dependencies]
 jrsonnet-evaluator.workspace = true
-jrsonnet-parser.workspace = true
+jrsonnet-ir.workspace = true
 jrsonnet-stdlib.workspace = true
 jrsonnet-gcmodule.workspace = true
 jrsonnet-interner.workspace = true
modifiedbindings/jsonnet/src/import.rsdiffbeforeafterboth
--- a/bindings/jsonnet/src/import.rs
+++ b/bindings/jsonnet/src/import.rs
@@ -17,7 +17,7 @@
 	AsPathLike, ImportResolver, ResolvePath,
 };
 use jrsonnet_gcmodule::Acyclic;
-use jrsonnet_parser::{SourceDirectory, SourceFile, SourcePath};
+use jrsonnet_ir::{SourceDirectory, SourceFile, SourcePath};
 
 use crate::VM;
 
modifiedbindings/jsonnet/src/lib.rsdiffbeforeafterboth
--- a/bindings/jsonnet/src/lib.rs
+++ b/bindings/jsonnet/src/lib.rs
@@ -31,7 +31,7 @@
 	AsPathLike, FileImportResolver, IStr, ImportResolver, Result, State, Val,
 };
 use jrsonnet_gcmodule::Acyclic;
-use jrsonnet_parser::SourcePath;
+use jrsonnet_ir::SourcePath;
 use jrsonnet_stdlib::ContextInitializer;
 
 /// WASM stub
modifiedcmds/jrsonnet/Cargo.tomldiffbeforeafterboth
--- a/cmds/jrsonnet/Cargo.toml
+++ b/cmds/jrsonnet/Cargo.toml
@@ -37,7 +37,7 @@
 # obj?.field, obj?.['field']
 exp-null-coaelse = [
     "jrsonnet-evaluator/exp-null-coaelse",
-    "jrsonnet-parser/exp-null-coaelse",
+    "jrsonnet-ir/exp-null-coaelse",
     "jrsonnet-cli/exp-null-coaelse",
 ]
 # --exp-apply
@@ -45,7 +45,7 @@
 
 [dependencies]
 jrsonnet-evaluator.workspace = true
-jrsonnet-parser.workspace = true
+jrsonnet-ir.workspace = true
 jrsonnet-cli.workspace = true
 jrsonnet-gcmodule.workspace = true
 
modifiedcmds/jrsonnet/src/main.rsdiffbeforeafterboth
--- a/cmds/jrsonnet/src/main.rs
+++ b/cmds/jrsonnet/src/main.rs
@@ -11,7 +11,7 @@
 	error::{Error as JrError, ErrorKind},
 	ResultExt, State, Val,
 };
-use jrsonnet_parser::{SourceDefaultIgnoreJpath, SourcePath};
+use jrsonnet_ir::{SourceDefaultIgnoreJpath, SourcePath};
 
 #[cfg(feature = "mimalloc")]
 #[global_allocator]
modifiedcrates/jrsonnet-cli/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-cli/Cargo.toml
+++ b/crates/jrsonnet-cli/Cargo.toml
@@ -29,7 +29,7 @@
 
 [dependencies]
 jrsonnet-evaluator = { workspace = true, features = ["explaining-traces"] }
-jrsonnet-parser.workspace = true
+jrsonnet-ir.workspace = true
 jrsonnet-stdlib.workspace = true
 jrsonnet-gcmodule.workspace = true
 
modifiedcrates/jrsonnet-evaluator/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/Cargo.toml
+++ b/crates/jrsonnet-evaluator/Cargo.toml
@@ -22,17 +22,18 @@
 # Allows to preserve field order in objects
 exp-preserve-order = []
 # Implements field destructuring
-exp-destruct = ["jrsonnet-parser/exp-destruct"]
+exp-destruct = ["jrsonnet-peg-parser/exp-destruct"]
 # Iteration over objects yields [key, value] elements
 exp-object-iteration = []
 # Bigint type
 exp-bigint = ["num-bigint", "jrsonnet-types/exp-bigint"]
 # obj?.field, obj?.['field']
-exp-null-coaelse = ["jrsonnet-parser/exp-null-coaelse"]
+exp-null-coaelse = ["jrsonnet-peg-parser/exp-null-coaelse"]
 
 [dependencies]
 jrsonnet-interner.workspace = true
-jrsonnet-parser.workspace = true
+jrsonnet-ir.workspace = true
+jrsonnet-peg-parser.workspace = true
 jrsonnet-types.workspace = true
 jrsonnet-macros.workspace = true
 jrsonnet-gcmodule.workspace = true
modifiedcrates/jrsonnet-evaluator/src/arr/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/mod.rs
+++ b/crates/jrsonnet-evaluator/src/arr/mod.rs
@@ -7,7 +7,7 @@
 
 use jrsonnet_gcmodule::{cc_dyn, Cc};
 use jrsonnet_interner::IBytes;
-use jrsonnet_parser::{Expr, Spanned};
+use jrsonnet_ir::{Expr, Spanned};
 
 use crate::{function::NativeFn, Context, Result, Thunk, Val};
 
@@ -85,7 +85,7 @@
 
 	pub fn extended(a: Self, b: Self) -> Self {
 		// TODO: benchmark for an optimal value, currently just a arbitrary choice
-		const ARR_EXTEND_THRESHOLD: usize = 100;
+		const ARR_EXTEND_THRESHOLD: usize = 1000;
 
 		if a.is_empty() {
 			b
modifiedcrates/jrsonnet-evaluator/src/arr/spec.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/spec.rs
+++ b/crates/jrsonnet-evaluator/src/arr/spec.rs
@@ -3,7 +3,7 @@
 
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::{IBytes, IStr};
-use jrsonnet_parser::{Expr, Spanned};
+use jrsonnet_ir::{Expr, Spanned};
 
 use super::ArrValue;
 use crate::function::NativeFn;
modifiedcrates/jrsonnet-evaluator/src/async_import.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/async_import.rs
+++ b/crates/jrsonnet-evaluator/src/async_import.rs
@@ -2,11 +2,12 @@
 use std::{any::Any, cell::RefCell, future::Future};
 
 use jrsonnet_gcmodule::Acyclic;
-use jrsonnet_parser::{
+use jrsonnet_ir::{
 	ArgsDesc, AssertExpr, AssertStmt, BindSpec, CompSpec, Destruct, Expr, ExprParam, ExprParams,
-	FieldMember, FieldName, ForSpecData, IfElse, IfSpecData, ImportKind, ObjBody, ParserSettings,
-	Slice, SliceDesc, Source, SourcePath, Spanned,
+	FieldMember, FieldName, ForSpecData, IfElse, IfSpecData, ImportKind, ObjBody, Slice, SliceDesc,
+	Source, SourcePath, Spanned,
 };
+use jrsonnet_peg_parser::ParserSettings;
 use rustc_hash::FxHashMap;
 
 use crate::{AsPathLike, FileData, ImportResolver, ResolvePathOwned, State};
@@ -322,7 +323,7 @@
 						};
 						let source = Source::new(path.clone(), code.clone());
 						// If failed - then skip import
-						file.parsed = jrsonnet_parser::parse(&code, &ParserSettings { source })
+						file.parsed = jrsonnet_peg_parser::parse(&code, &ParserSettings { source })
 							.map(Rc::new)
 							.ok();
 						if let Some(parsed) = &file.parsed {
modifiedcrates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -2,7 +2,7 @@
 
 use jrsonnet_gcmodule::{Acyclic, Trace};
 use jrsonnet_interner::IStr;
-use jrsonnet_parser::{BinaryOpType, Source, SourcePath, Span, Spanned, UnaryOpType};
+use jrsonnet_ir::{BinaryOpType, Source, SourcePath, Span, Spanned, UnaryOpType};
 use jrsonnet_types::ValType;
 use thiserror::Error;
 
@@ -169,7 +169,7 @@
 	ImportSyntaxError {
 		path: Source,
 		#[trace(skip)]
-		error: Box<jrsonnet_parser::ParseError>,
+		error: Box<jrsonnet_peg_parser::ParseError>,
 	},
 
 	#[error("runtime error: {}", format_empty_str(.0))]
modifiedcrates/jrsonnet-evaluator/src/evaluate/destructure.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/destructure.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/destructure.rs
@@ -1,7 +1,7 @@
 use std::{collections::HashMap, hash::BuildHasher};
 
 use jrsonnet_interner::IStr;
-use jrsonnet_parser::{BindSpec, Destruct};
+use jrsonnet_ir::{BindSpec, Destruct};
 
 use crate::{
 	bail,
@@ -31,7 +31,7 @@
 		Destruct::Skip => {}
 		#[cfg(feature = "exp-destruct")]
 		Destruct::Array { start, rest, end } => {
-			use jrsonnet_parser::DestructRest;
+			use jrsonnet_ir::DestructRest;
 
 			let min_len = start.len() + end.len();
 			let has_rest = rest.is_some();
modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/evaluate/mod.rs
1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_parser::{6	function::ParamName, ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, ExprParams,7	FieldMember, FieldName, ForSpecData, IfSpecData, ImportKind, LiteralType, ObjBody, ObjMembers,8	Spanned,9};10use jrsonnet_types::ValType;11use rustc_hash::FxHashMap;1213use self::destructure::destruct;14use crate::{15	arr::ArrValue,16	bail,17	destructure::evaluate_dest,18	error::{suggest_object_fields, ErrorKind::*},19	evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},20	function::{CallLocation, FuncDesc, FuncVal},21	gc::WithCapacityExt as _,22	in_frame,23	typed::{FromUntyped, IntoUntyped as _, Typed},24	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},25	with_state, Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,26	ResultExt, SupThis, Unbound, Val,27};28pub mod destructure;29pub mod operator;3031// This is the amount of bytes that need to be left on the stack before increasing the size.32// It must be at least as large as the stack required by any code that does not call33// `ensure_sufficient_stack`.34const RED_ZONE: usize = 100 * 1024; // 100k3536// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then37// on. This flag has performance relevant characteristics. Don't set it too high.38const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB3940/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations41/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit42/// from this.43///44/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.45#[inline]46pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {47	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)48}4950pub fn evaluate_trivial(expr: &Spanned<Expr>) -> Option<Val> {51	fn is_trivial(expr: &Spanned<Expr>) -> bool {52		match &**expr {53			Expr::Str(_)54			| Expr::Num(_)55			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,56			Expr::Arr(a) => a.iter().all(is_trivial),57			_ => false,58		}59	}60	Some(match &**expr {61		Expr::Str(s) => Val::string(s.clone()),62		Expr::Num(n) => {63			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))64		}65		Expr::Literal(LiteralType::False) => Val::Bool(false),66		Expr::Literal(LiteralType::True) => Val::Bool(true),67		Expr::Literal(LiteralType::Null) => Val::Null,68		Expr::Arr(n) => {69			if n.iter().any(|e| !is_trivial(e)) {70				return None;71			}72			Val::Arr(ArrValue::eager(73				n.iter()74					.map(evaluate_trivial)75					.map(|e| e.expect("checked trivial"))76					.collect(),77			))78		}79		_ => return None,80	})81}8283pub fn evaluate_method(84	ctx: Context,85	name: IStr,86	params: ExprParams,87	body: Rc<Spanned<Expr>>,88) -> Val {89	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {90		name,91		ctx,92		params,93		body,94	})))95}9697pub fn evaluate_field_name(ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {98	Ok(match field_name {99		FieldName::Fixed(n) => Some(n.clone()),100		FieldName::Dyn(expr) => in_frame(101			CallLocation::new(&expr.span()),102			|| "evaluating field name".to_string(),103			|| {104				let value = evaluate(ctx, expr)?;105				if matches!(value, Val::Null) {106					Ok(None)107				} else {108					Ok(Some(IStr::from_untyped(value)?))109				}110			},111		)?,112	})113}114115pub fn evaluate_comp(116	ctx: Context,117	specs: &[CompSpec],118	callback: &mut impl FnMut(Context) -> Result<()>,119) -> Result<()> {120	match specs.first() {121		None => callback(ctx)?,122		Some(CompSpec::IfSpec(IfSpecData(cond))) => {123			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {124				evaluate_comp(ctx, &specs[1..], callback)?;125			}126		}127		Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {128			Val::Arr(list) => {129				for item in list.iter_lazy() {130					let fctx = Pending::new();131					let mut new_bindings = FxHashMap::with_capacity(var.binds_len());132					destruct(var, item, fctx.clone(), &mut new_bindings)?;133					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);134135					evaluate_comp(ctx, &specs[1..], callback)?;136				}137			}138			#[cfg(feature = "exp-object-iteration")]139			Val::Obj(obj) => {140				for field in obj.fields(141					// TODO: Should there be ability to preserve iteration order?142					#[cfg(feature = "exp-preserve-order")]143					false,144				) {145					let fctx = Pending::new();146					let mut new_bindings = FxHashMap::with_capacity(var.binds_len());147					let obj = obj.clone();148					let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![149						Thunk::evaluated(Val::string(field.clone())),150						Thunk!(move || obj.get(field).transpose().expect(151							"field exists, as field name was obtained from object.fields()",152						)),153					])));154					destruct(var, value, fctx.clone(), &mut new_bindings)?;155					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);156157					evaluate_comp(ctx, &specs[1..], callback)?;158				}159			}160			_ => bail!(InComprehensionCanOnlyIterateOverArray),161		},162	}163	Ok(())164}165166trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}167impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}168169fn evaluate_object_locals(170	fctx: Context,171	locals: Rc<Vec<BindSpec>>,172) -> impl CloneableUnbound<Context> {173	#[derive(Trace, Clone)]174	struct UnboundLocals {175		fctx: Context,176		locals: Rc<Vec<BindSpec>>,177	}178	impl Unbound for UnboundLocals {179		type Bound = Context;180181		fn bind(&self, sup_this: SupThis) -> Result<Context> {182			let fctx = Context::new_future();183			let mut new_bindings =184				FxHashMap::with_capacity(self.locals.iter().map(BindSpec::binds_len).sum());185			for b in self.locals.iter() {186				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;187			}188189			let ctx = self.fctx.clone();190191			let ctx = ctx192				.extend_bindings_sup_this(new_bindings, sup_this)193				.into_future(fctx);194195			Ok(ctx)196		}197	}198199	UnboundLocals { fctx, locals }200}201202pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(203	builder: &mut ObjValueBuilder,204	ctx: Context,205	uctx: B,206	field: &FieldMember,207) -> Result<()> {208	let name = evaluate_field_name(ctx, &field.name)?;209	let Some(name) = name else {210		return Ok(());211	};212213	match field {214		FieldMember {215			plus,216			params: None,217			visibility,218			value,219			..220		} => {221			#[derive(Trace)]222			struct UnboundValue<B: Trace> {223				uctx: B,224				value: Rc<Spanned<Expr>>,225				name: IStr,226			}227			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {228				type Bound = Val;229				fn bind(&self, sup_this: SupThis) -> Result<Val> {230					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())231				}232			}233234			builder235				.field(name.clone())236				.with_add(*plus)237				.with_visibility(*visibility)238				.with_location(value.span())239				.bindable(UnboundValue {240					uctx,241					value: value.clone(),242					name,243				})?;244		}245		FieldMember {246			params: Some(params),247			visibility,248			value,249			..250		} => {251			#[derive(Trace)]252			struct UnboundMethod<B: Trace> {253				uctx: B,254				value: Rc<Spanned<Expr>>,255				params: ExprParams,256				name: IStr,257			}258			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {259				type Bound = Val;260				fn bind(&self, sup_this: SupThis) -> Result<Val> {261					Ok(evaluate_method(262						self.uctx.bind(sup_this)?,263						self.name.clone(),264						self.params.clone(),265						self.value.clone(),266					))267				}268			}269270			builder271				.field(name.clone())272				.with_visibility(*visibility)273				.with_location(value.span())274				.bindable(UnboundMethod {275					uctx,276					value: value.clone(),277					params: params.clone(),278					name,279				})?;280		}281	}282	Ok(())283}284285#[allow(clippy::too_many_lines)]286pub fn evaluate_member_list_object(ctx: Context, members: &ObjMembers) -> Result<ObjValue> {287	let mut builder = ObjValueBuilder::new();288	let locals = members.locals.clone();289290	// We have single context for all fields, so we can cache binds291	let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));292293	for field in &members.fields {294		evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;295	}296297	if !members.asserts.is_empty() {298		#[derive(Trace)]299		struct ObjectAssert<B: Trace> {300			uctx: B,301			asserts: Rc<Vec<AssertStmt>>,302		}303		impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {304			fn run(&self, sup_this: SupThis) -> Result<()> {305				let ctx = self.uctx.bind(sup_this)?;306				for assert in &*self.asserts {307					evaluate_assert(ctx.clone(), assert)?;308				}309				Ok(())310			}311		}312		builder.assert(ObjectAssert {313			uctx,314			asserts: members.asserts.clone(),315		});316	}317318	Ok(builder.build())319}320321pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {322	Ok(match object {323		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,324		ObjBody::ObjComp(obj) => {325			let mut builder = ObjValueBuilder::new();326			let locals = obj.locals.clone();327			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {328				let uctx = evaluate_object_locals(ctx.clone(), locals.clone());329330				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)331			})?;332333			builder.build()334		}335	})336}337338pub fn evaluate_apply(339	ctx: Context,340	value: &Spanned<Expr>,341	args: &ArgsDesc,342	loc: CallLocation<'_>,343	tailstrict: bool,344) -> Result<Val> {345	let value = evaluate(ctx.clone(), value)?;346	Ok(match value {347		Val::Func(f) => {348			let body = || f.evaluate(ctx, loc, args, tailstrict);349			if tailstrict {350				body()?351			} else {352				in_frame(loc, || format!("function <{}> call", f.name()), body)?353			}354		}355		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),356	})357}358359pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {360	let value = &assertion.0;361	let msg = &assertion.1;362	let assertion_result = in_frame(363		CallLocation::new(&value.span()),364		|| "assertion condition".to_owned(),365		|| bool::from_untyped(evaluate(ctx.clone(), value)?),366	)?;367	if !assertion_result {368		in_frame(369			CallLocation::new(&value.span()),370			|| "assertion failure".to_owned(),371			|| {372				if let Some(msg) = msg {373					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));374				}375				bail!(AssertionFailed(Val::Null.to_string()?));376			},377		)?;378	}379	Ok(())380}381382pub fn evaluate_named_param(ctx: Context, expr: &Spanned<Expr>, name: ParamName) -> Result<Val> {383	match name {384		ParamName::Named(name) => evaluate_named(ctx, expr, name),385		ParamName::Unnamed => evaluate(ctx, expr),386	}387}388389pub fn evaluate_named(ctx: Context, expr: &Spanned<Expr>, name: IStr) -> Result<Val> {390	use Expr::*;391	Ok(match &**expr {392		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),393		_ => evaluate(ctx, expr)?,394	})395}396397#[allow(clippy::too_many_lines)]398pub fn evaluate(ctx: Context, expr: &Spanned<Expr>) -> Result<Val> {399	use Expr::*;400401	if let Some(trivial) = evaluate_trivial(expr) {402		return Ok(trivial);403	}404	let loc = expr.span();405	Ok(match &**expr {406		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),407		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),408		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),409		Literal(LiteralType::True) => Val::Bool(true),410		Literal(LiteralType::False) => Val::Bool(false),411		Literal(LiteralType::Null) => Val::Null,412		Str(v) => Val::string(v.clone()),413		Num(v) => Val::try_num(*v)?,414		// I have tried to remove special behavior from super by implementing standalone-super415		// expresion, but looks like this case still needs special treatment.416		//417		// Note that other jsonnet implementations will fail on `if value in (super)` expression,418		// because the standalone super literal is not supported, that is because in other419		// implementations `in super` treated differently from `in smth_else`.420		BinaryOp(bin)421			if matches!(&*bin.rhs, Expr::Literal(LiteralType::Super))422				&& bin.op == BinaryOpType::In =>423		{424			let sup_this = ctx.try_sup_this()?;425			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.426			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.427			if !sup_this.has_super() {428				return Ok(Val::Bool(false));429			}430			let field = evaluate(ctx, &bin.lhs)?;431			Val::Bool(sup_this.field_in_super(field.to_string()?))432		}433		BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?,434		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,435		Var(name) => in_frame(436			CallLocation::new(&loc),437			|| format!("local <{name}> access"),438			|| ctx.binding(name.clone())?.evaluate(),439		)?,440		Index { indexable, parts } => ensure_sufficient_stack(|| {441			let mut parts = parts.iter();442			let mut indexable = if matches!(&***indexable, Expr::Literal(LiteralType::Super)) {443				let part = parts.next().expect("at least part should exist");444				// sup_this existence check might also be skipped here for null-coalesce...445				// But I believe this might cause errors.446				let sup_this = ctx.try_sup_this()?;447				if !sup_this.has_super() {448					#[cfg(feature = "exp-null-coaelse")]449					if part.null_coaelse {450						return Ok(Val::Null);451					}452					bail!(NoSuperFound)453				}454				let name = evaluate(ctx.clone(), &part.value)?;455456				let Val::Str(name) = name else {457					bail!(ValueIndexMustBeTypeGot(458						ValType::Obj,459						ValType::Str,460						name.value_type(),461					))462				};463464				let name = name.into_flat();465				match sup_this466					.get_super(name.clone())467					.with_description_src(&part.value, || format!("field <{name}> access"))?468				{469					Some(v) => v,470					#[cfg(feature = "exp-null-coaelse")]471					None if part.null_coaelse => return Ok(Val::Null),472					None => {473						let suggestions = suggest_object_fields(474							&sup_this.standalone_super().expect("super exists"),475							name.clone(),476						);477478						bail!(NoSuchField(name, suggestions))479					}480				}481			} else {482				evaluate(ctx.clone(), indexable)?483			};484485			for part in parts {486				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {487					(Val::Obj(v), Val::Str(key)) => match v488						.get(key.clone().into_flat())489						.with_description_src(&part.value, || format!("field <{key}> access"))?490					{491						Some(v) => v,492						#[cfg(feature = "exp-null-coaelse")]493						None if part.null_coaelse => return Ok(Val::Null),494						None => {495							let suggestions = suggest_object_fields(&v, key.clone().into_flat());496497							return Err(Error::from(NoSuchField(498								key.clone().into_flat(),499								suggestions,500							)))501							.with_description_src(&part.value, || format!("field <{key}> access"));502						}503					},504					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(505						ValType::Obj,506						ValType::Str,507						n.value_type(),508					)),509					(Val::Arr(v), Val::Num(n)) => {510						let n = n.get();511						if n.fract() > f64::EPSILON {512							bail!(FractionalIndex)513						}514						if n < 0.0 {515							bail!(ArrayBoundsError(n as isize, v.len()));516						}517						v.get(n as usize)?518							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?519					}520					(Val::Arr(_), Val::Str(n)) => {521						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))522					}523					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(524						ValType::Arr,525						ValType::Num,526						n.value_type(),527					)),528529					(Val::Str(s), Val::Num(n)) => Val::Str({530						let n = n.get();531						if n.fract() > f64::EPSILON {532							bail!(FractionalIndex)533						}534						if n < 0.0 {535							bail!(ArrayBoundsError(n as isize, s.into_flat().chars().count()));536						}537						let v: IStr = s538							.clone()539							.into_flat()540							.chars()541							.skip(n as usize)542							.take(1)543							.collect::<String>()544							.into();545						if v.is_empty() {546							bail!(StringBoundsError(n as usize, s.into_flat().chars().count()))547						}548						StrValue::Flat(v)549					}),550					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(551						ValType::Str,552						ValType::Num,553						n.value_type(),554					)),555					#[cfg(feature = "exp-null-coaelse")]556					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),557					(v, _) => bail!(CantIndexInto(v.value_type())),558				};559			}560			Ok(indexable)561		})?,562		LocalExpr(bindings, returned) => {563			let mut new_bindings: FxHashMap<IStr, Thunk<Val>> =564				FxHashMap::with_capacity(bindings.iter().map(BindSpec::binds_len).sum());565			let fctx = Context::new_future();566			for b in bindings {567				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;568			}569			let ctx = ctx.extend_bindings(new_bindings).into_future(fctx);570			evaluate(ctx, returned)?571		}572		Arr(items) => {573			if items.is_empty() {574				Val::Arr(ArrValue::empty())575			} else {576				Val::Arr(ArrValue::expr(ctx, items.clone()))577			}578		}579		ArrComp(expr, comp_specs) => {580			let mut out = Vec::new();581			evaluate_comp(ctx, comp_specs, &mut |ctx| {582				let expr = expr.clone();583				out.push(Thunk!(move || evaluate(ctx, &expr)));584				Ok(())585			})?;586			Val::Arr(ArrValue::lazy(out))587		}588		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),589		ObjExtend(a, b) => evaluate_add_op(590			&evaluate(ctx.clone(), a)?,591			&Val::Obj(evaluate_object(ctx, b)?),592		)?,593		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {594			evaluate_apply(ctx, value, args, CallLocation::new(&loc), *tailstrict)595		})?,596		Function(params, body) => {597			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())598		}599		AssertExpr(assert) => {600			evaluate_assert(ctx.clone(), &assert.assert)?;601			evaluate(ctx, &assert.rest)?602		}603		ErrorStmt(e) => in_frame(604			CallLocation::new(&loc),605			|| "error statement".to_owned(),606			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),607		)?,608		IfElse(if_else) => {609			if in_frame(610				CallLocation::new(&loc),611				|| "if condition".to_owned(),612				|| bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.0)?),613			)? {614				evaluate(ctx, &if_else.cond_then)?615			} else {616				match &if_else.cond_else {617					Some(v) => evaluate(ctx, v)?,618					None => Val::Null,619				}620			}621		}622		Slice(slice) => {623			fn parse_idx<T: Typed + FromUntyped>(624				loc: CallLocation<'_>,625				ctx: Context,626				expr: Option<&Spanned<Expr>>,627				desc: &'static str,628			) -> Result<Option<T>> {629				if let Some(value) = expr {630					Ok(in_frame(631						loc,632						|| format!("slice {desc}"),633						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),634					)?)635				} else {636					Ok(None)637				}638			}639640			let indexable = evaluate(ctx.clone(), &slice.value)?;641			let loc = CallLocation::new(&loc);642643			let start = parse_idx(loc, ctx.clone(), slice.slice.start.as_ref(), "start")?;644			let end = parse_idx(loc, ctx.clone(), slice.slice.end.as_ref(), "end")?;645			let step = parse_idx(loc, ctx, slice.slice.step.as_ref(), "step")?;646647			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?648		}649		Import(kind, path) => {650			let Expr::Str(path) = &***path else {651				bail!("computed imports are not supported")652			};653			let tmp = loc.clone().0;654			with_state(|s| {655				let resolved_path = s.resolve_from(tmp.source_path(), path)?;656				Ok(match kind {657					ImportKind::Normal => in_frame(658						CallLocation::new(&loc),659						|| format!("import {:?}", path.clone()),660						|| s.import_resolved(resolved_path),661					)?,662					ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?),663					ImportKind::Bin => {664						Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?))665					}666				}) as Result<Val>667			})?668		}669	})670}
after · crates/jrsonnet-evaluator/src/evaluate/mod.rs
1use std::rc::Rc;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;5use jrsonnet_ir::{6	function::ParamName, ArgsDesc, AssertStmt, BinaryOpType, BindSpec, CompSpec, Expr, ExprParams,7	FieldMember, FieldName, ForSpecData, IfSpecData, ImportKind, LiteralType, ObjBody, ObjMembers,8	Spanned,9};10use jrsonnet_types::ValType;11use rustc_hash::FxHashMap;1213use self::destructure::destruct;14use crate::{15	arr::ArrValue,16	bail,17	destructure::evaluate_dest,18	error::{suggest_object_fields, ErrorKind::*},19	evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},20	function::{CallLocation, FuncDesc, FuncVal},21	gc::WithCapacityExt as _,22	in_frame,23	typed::{FromUntyped, IntoUntyped as _, Typed},24	val::{CachedUnbound, IndexableVal, NumValue, StrValue, Thunk},25	with_state, Context, Error, ObjValue, ObjValueBuilder, ObjectAssertion, Pending, Result,26	ResultExt, SupThis, Unbound, Val,27};28pub mod destructure;29pub mod operator;3031// This is the amount of bytes that need to be left on the stack before increasing the size.32// It must be at least as large as the stack required by any code that does not call33// `ensure_sufficient_stack`.34const RED_ZONE: usize = 100 * 1024; // 100k3536// Only the first stack that is pushed, grows exponentially (2^n * STACK_PER_RECURSION) from then37// on. This flag has performance relevant characteristics. Don't set it too high.38const STACK_PER_RECURSION: usize = 1024 * 1024; // 1MB3940/// Grows the stack on demand to prevent stack overflow. Call this in strategic locations41/// to "break up" recursive calls. E.g. almost any call to `visit_expr` or equivalent can benefit42/// from this.43///44/// Should not be sprinkled around carelessly, as it causes a little bit of overhead.45#[inline]46pub fn ensure_sufficient_stack<R>(f: impl FnOnce() -> R) -> R {47	stacker::maybe_grow(RED_ZONE, STACK_PER_RECURSION, f)48}4950pub fn evaluate_trivial(expr: &Expr) -> Option<Val> {51	fn is_trivial(expr: &Expr) -> bool {52		match &*expr {53			Expr::Str(_)54			| Expr::Num(_)55			| Expr::Literal(LiteralType::False | LiteralType::True | LiteralType::Null) => true,56			Expr::Arr(a) => a.iter().all(|e| is_trivial(&**e)),57			_ => false,58		}59	}60	Some(match &*expr {61		Expr::Str(s) => Val::string(s.clone()),62		Expr::Num(n) => {63			Val::Num(NumValue::new(*n).expect("parser will not allow non-finite values"))64		}65		Expr::Literal(LiteralType::False) => Val::Bool(false),66		Expr::Literal(LiteralType::True) => Val::Bool(true),67		Expr::Literal(LiteralType::Null) => Val::Null,68		Expr::Arr(n) => {69			if n.iter().any(|e| !is_trivial(e)) {70				return None;71			}72			Val::Arr(ArrValue::eager(73				n.iter()74					.map(|e| evaluate_trivial(&**e))75					.map(|e| e.expect("checked trivial"))76					.collect(),77			))78		}79		_ => return None,80	})81}8283pub fn evaluate_method(84	ctx: Context,85	name: IStr,86	params: ExprParams,87	body: Rc<Spanned<Expr>>,88) -> Val {89	Val::Func(FuncVal::Normal(Cc::new(FuncDesc {90		name,91		ctx,92		params,93		body,94	})))95}9697pub fn evaluate_field_name(ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {98	Ok(match field_name {99		FieldName::Fixed(n) => Some(n.clone()),100		FieldName::Dyn(expr) => in_frame(101			CallLocation::new(&expr.span()),102			|| "evaluating field name".to_string(),103			|| {104				let value = evaluate(ctx, expr)?;105				if matches!(value, Val::Null) {106					Ok(None)107				} else {108					Ok(Some(IStr::from_untyped(value)?))109				}110			},111		)?,112	})113}114115pub fn evaluate_comp(116	ctx: Context,117	specs: &[CompSpec],118	callback: &mut impl FnMut(Context) -> Result<()>,119) -> Result<()> {120	match specs.first() {121		None => callback(ctx)?,122		Some(CompSpec::IfSpec(IfSpecData(cond))) => {123			if bool::from_untyped(evaluate(ctx.clone(), cond)?)? {124				evaluate_comp(ctx, &specs[1..], callback)?;125			}126		}127		Some(CompSpec::ForSpec(ForSpecData(var, expr))) => match evaluate(ctx.clone(), expr)? {128			Val::Arr(list) => {129				for item in list.iter_lazy() {130					let fctx = Pending::new();131					let mut new_bindings = FxHashMap::with_capacity(var.binds_len());132					destruct(var, item, fctx.clone(), &mut new_bindings)?;133					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);134135					evaluate_comp(ctx, &specs[1..], callback)?;136				}137			}138			#[cfg(feature = "exp-object-iteration")]139			Val::Obj(obj) => {140				for field in obj.fields(141					// TODO: Should there be ability to preserve iteration order?142					#[cfg(feature = "exp-preserve-order")]143					false,144				) {145					let fctx = Pending::new();146					let mut new_bindings = FxHashMap::with_capacity(var.binds_len());147					let obj = obj.clone();148					let value = Thunk::evaluated(Val::Arr(ArrValue::lazy(vec![149						Thunk::evaluated(Val::string(field.clone())),150						Thunk!(move || obj.get(field).transpose().expect(151							"field exists, as field name was obtained from object.fields()",152						)),153					])));154					destruct(var, value, fctx.clone(), &mut new_bindings)?;155					let ctx = ctx.clone().extend_bindings(new_bindings).into_future(fctx);156157					evaluate_comp(ctx, &specs[1..], callback)?;158				}159			}160			_ => bail!(InComprehensionCanOnlyIterateOverArray),161		},162	}163	Ok(())164}165166trait CloneableUnbound<T>: Unbound<Bound = T> + Clone {}167impl<V, T> CloneableUnbound<T> for V where V: Unbound<Bound = T> + Clone {}168169fn evaluate_object_locals(170	fctx: Context,171	locals: Rc<Vec<BindSpec>>,172) -> impl CloneableUnbound<Context> {173	#[derive(Trace, Clone)]174	struct UnboundLocals {175		fctx: Context,176		locals: Rc<Vec<BindSpec>>,177	}178	impl Unbound for UnboundLocals {179		type Bound = Context;180181		fn bind(&self, sup_this: SupThis) -> Result<Context> {182			let fctx = Context::new_future();183			let mut new_bindings =184				FxHashMap::with_capacity(self.locals.iter().map(BindSpec::binds_len).sum());185			for b in self.locals.iter() {186				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;187			}188189			let ctx = self.fctx.clone();190191			let ctx = ctx192				.extend_bindings_sup_this(new_bindings, sup_this)193				.into_future(fctx);194195			Ok(ctx)196		}197	}198199	UnboundLocals { fctx, locals }200}201202pub fn evaluate_field_member<B: Unbound<Bound = Context> + Clone>(203	builder: &mut ObjValueBuilder,204	ctx: Context,205	uctx: B,206	field: &FieldMember,207) -> Result<()> {208	let name = evaluate_field_name(ctx, &field.name)?;209	let Some(name) = name else {210		return Ok(());211	};212213	match field {214		FieldMember {215			plus,216			params: None,217			visibility,218			value,219			..220		} => {221			#[derive(Trace)]222			struct UnboundValue<B: Trace> {223				uctx: B,224				value: Rc<Spanned<Expr>>,225				name: IStr,226			}227			impl<B: Unbound<Bound = Context>> Unbound for UnboundValue<B> {228				type Bound = Val;229				fn bind(&self, sup_this: SupThis) -> Result<Val> {230					evaluate_named(self.uctx.bind(sup_this)?, &self.value, self.name.clone())231				}232			}233234			builder235				.field(name.clone())236				.with_add(*plus)237				.with_visibility(*visibility)238				.with_location(value.span())239				.bindable(UnboundValue {240					uctx,241					value: value.clone(),242					name,243				})?;244		}245		FieldMember {246			params: Some(params),247			visibility,248			value,249			..250		} => {251			#[derive(Trace)]252			struct UnboundMethod<B: Trace> {253				uctx: B,254				value: Rc<Spanned<Expr>>,255				params: ExprParams,256				name: IStr,257			}258			impl<B: Unbound<Bound = Context>> Unbound for UnboundMethod<B> {259				type Bound = Val;260				fn bind(&self, sup_this: SupThis) -> Result<Val> {261					Ok(evaluate_method(262						self.uctx.bind(sup_this)?,263						self.name.clone(),264						self.params.clone(),265						self.value.clone(),266					))267				}268			}269270			builder271				.field(name.clone())272				.with_visibility(*visibility)273				.with_location(value.span())274				.bindable(UnboundMethod {275					uctx,276					value: value.clone(),277					params: params.clone(),278					name,279				})?;280		}281	}282	Ok(())283}284285#[allow(clippy::too_many_lines)]286pub fn evaluate_member_list_object(ctx: Context, members: &ObjMembers) -> Result<ObjValue> {287	let mut builder = ObjValueBuilder::new();288	let locals = members.locals.clone();289290	// We have single context for all fields, so we can cache binds291	let uctx = CachedUnbound::new(evaluate_object_locals(ctx.clone(), locals));292293	for field in &members.fields {294		evaluate_field_member(&mut builder, ctx.clone(), uctx.clone(), field)?;295	}296297	if !members.asserts.is_empty() {298		#[derive(Trace)]299		struct ObjectAssert<B: Trace> {300			uctx: B,301			asserts: Rc<Vec<AssertStmt>>,302		}303		impl<B: Unbound<Bound = Context>> ObjectAssertion for ObjectAssert<B> {304			fn run(&self, sup_this: SupThis) -> Result<()> {305				let ctx = self.uctx.bind(sup_this)?;306				for assert in &*self.asserts {307					evaluate_assert(ctx.clone(), assert)?;308				}309				Ok(())310			}311		}312		builder.assert(ObjectAssert {313			uctx,314			asserts: members.asserts.clone(),315		});316	}317318	Ok(builder.build())319}320321pub fn evaluate_object(ctx: Context, object: &ObjBody) -> Result<ObjValue> {322	Ok(match object {323		ObjBody::MemberList(members) => evaluate_member_list_object(ctx, members)?,324		ObjBody::ObjComp(obj) => {325			let mut builder = ObjValueBuilder::new();326			let locals = obj.locals.clone();327			evaluate_comp(ctx, &obj.compspecs, &mut |ctx| {328				let uctx = evaluate_object_locals(ctx.clone(), locals.clone());329330				evaluate_field_member(&mut builder, ctx, uctx, &obj.field)331			})?;332333			builder.build()334		}335	})336}337338pub fn evaluate_apply(339	ctx: Context,340	value: &Spanned<Expr>,341	args: &ArgsDesc,342	loc: CallLocation<'_>,343	tailstrict: bool,344) -> Result<Val> {345	let value = evaluate(ctx.clone(), value)?;346	Ok(match value {347		Val::Func(f) => {348			let body = || f.evaluate(ctx, loc, args, tailstrict);349			if tailstrict {350				body()?351			} else {352				in_frame(loc, || format!("function <{}> call", f.name()), body)?353			}354		}355		v => bail!(OnlyFunctionsCanBeCalledGot(v.value_type())),356	})357}358359pub fn evaluate_assert(ctx: Context, assertion: &AssertStmt) -> Result<()> {360	let value = &assertion.0;361	let msg = &assertion.1;362	let assertion_result = in_frame(363		CallLocation::new(&value.span()),364		|| "assertion condition".to_owned(),365		|| bool::from_untyped(evaluate(ctx.clone(), value)?),366	)?;367	if !assertion_result {368		in_frame(369			CallLocation::new(&value.span()),370			|| "assertion failure".to_owned(),371			|| {372				if let Some(msg) = msg {373					bail!(AssertionFailed(evaluate(ctx, msg)?.to_string()?));374				}375				bail!(AssertionFailed(Val::Null.to_string()?));376			},377		)?;378	}379	Ok(())380}381382pub fn evaluate_named_param(ctx: Context, expr: &Spanned<Expr>, name: ParamName) -> Result<Val> {383	match name {384		ParamName::Named(name) => evaluate_named(ctx, expr, name),385		ParamName::Unnamed => evaluate(ctx, expr),386	}387}388389pub fn evaluate_named(ctx: Context, expr: &Spanned<Expr>, name: IStr) -> Result<Val> {390	use Expr::*;391	Ok(match &**expr {392		Function(params, body) => evaluate_method(ctx, name, params.clone(), body.clone()),393		_ => evaluate(ctx, expr)?,394	})395}396397#[allow(clippy::too_many_lines)]398pub fn evaluate(ctx: Context, expr: &Expr) -> Result<Val> {399	use Expr::*;400401	if let Some(trivial) = evaluate_trivial(expr) {402		return Ok(trivial);403	}404	Ok(match expr {405		Literal(LiteralType::This) => Val::Obj(ctx.try_this()?),406		Literal(LiteralType::Super) => Val::Obj(ctx.try_sup_this()?.standalone_super()?),407		Literal(LiteralType::Dollar) => Val::Obj(ctx.try_dollar()?),408		Literal(LiteralType::True) => Val::Bool(true),409		Literal(LiteralType::False) => Val::Bool(false),410		Literal(LiteralType::Null) => Val::Null,411		Str(v) => Val::string(v.clone()),412		Num(v) => Val::try_num(*v)?,413		// I have tried to remove special behavior from super by implementing standalone-super414		// expresion, but looks like this case still needs special treatment.415		//416		// Note that other jsonnet implementations will fail on `if value in (super)` expression,417		// because the standalone super literal is not supported, that is because in other418		// implementations `in super` treated differently from `in smth_else`.419		BinaryOp(bin)420			if matches!(&*bin.rhs, Expr::Literal(LiteralType::Super))421				&& bin.op == BinaryOpType::In =>422		{423			let sup_this = ctx.try_sup_this()?;424			// In jsonnet, "field" in e is eager, LHS expression is always executed regardless of super existence.425			// In jrsonnet, however, this wasn't true, this was kept here for compatibility.426			if !sup_this.has_super() {427				return Ok(Val::Bool(false));428			}429			let field = evaluate(ctx, &bin.lhs)?;430			Val::Bool(sup_this.field_in_super(field.to_string()?))431		}432		BinaryOp(bin) => evaluate_binary_op_special(ctx, &bin.lhs, bin.op, &bin.rhs)?,433		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(ctx, v)?)?,434		Var(name) => in_frame(435			CallLocation::new(&name.span()),436			|| format!("local <{name}> access"),437			|| ctx.binding((**name).clone())?.evaluate(),438		)?,439		Index { indexable, parts } => ensure_sufficient_stack(|| {440			let mut parts = parts.iter();441			let mut indexable = if matches!(&***indexable, Expr::Literal(LiteralType::Super)) {442				let part = parts.next().expect("at least part should exist");443				// sup_this existence check might also be skipped here for null-coalesce...444				// But I believe this might cause errors.445				let sup_this = ctx.try_sup_this()?;446				if !sup_this.has_super() {447					#[cfg(feature = "exp-null-coaelse")]448					if part.null_coaelse {449						return Ok(Val::Null);450					}451					bail!(NoSuperFound)452				}453				let name = evaluate(ctx.clone(), &part.value)?;454455				let Val::Str(name) = name else {456					bail!(ValueIndexMustBeTypeGot(457						ValType::Obj,458						ValType::Str,459						name.value_type(),460					))461				};462463				let name = name.into_flat();464				match sup_this465					.get_super(name.clone())466					.with_description_src(&part.value, || format!("field <{name}> access"))?467				{468					Some(v) => v,469					#[cfg(feature = "exp-null-coaelse")]470					None if part.null_coaelse => return Ok(Val::Null),471					None => {472						let suggestions = suggest_object_fields(473							&sup_this.standalone_super().expect("super exists"),474							name.clone(),475						);476477						bail!(NoSuchField(name, suggestions))478					}479				}480			} else {481				evaluate(ctx.clone(), indexable)?482			};483484			for part in parts {485				indexable = match (indexable, evaluate(ctx.clone(), &part.value)?) {486					(Val::Obj(v), Val::Str(key)) => match v487						.get(key.clone().into_flat())488						.with_description_src(&part.value, || format!("field <{key}> access"))?489					{490						Some(v) => v,491						#[cfg(feature = "exp-null-coaelse")]492						None if part.null_coaelse => return Ok(Val::Null),493						None => {494							let suggestions = suggest_object_fields(&v, key.clone().into_flat());495496							return Err(Error::from(NoSuchField(497								key.clone().into_flat(),498								suggestions,499							)))500							.with_description_src(&part.value, || format!("field <{key}> access"));501						}502					},503					(Val::Obj(_), n) => bail!(ValueIndexMustBeTypeGot(504						ValType::Obj,505						ValType::Str,506						n.value_type(),507					)),508					(Val::Arr(v), Val::Num(n)) => {509						let n = n.get();510						if n.fract() > f64::EPSILON {511							bail!(FractionalIndex)512						}513						if n < 0.0 {514							bail!(ArrayBoundsError(n as isize, v.len()));515						}516						v.get(n as usize)?517							.ok_or_else(|| ArrayBoundsError(n as isize, v.len()))?518					}519					(Val::Arr(_), Val::Str(n)) => {520						bail!(AttemptedIndexAnArrayWithString(n.into_flat()))521					}522					(Val::Arr(_), n) => bail!(ValueIndexMustBeTypeGot(523						ValType::Arr,524						ValType::Num,525						n.value_type(),526					)),527528					(Val::Str(s), Val::Num(n)) => Val::Str({529						let n = n.get();530						if n.fract() > f64::EPSILON {531							bail!(FractionalIndex)532						}533						if n < 0.0 {534							bail!(ArrayBoundsError(n as isize, s.into_flat().chars().count()));535						}536						let v: IStr = s537							.clone()538							.into_flat()539							.chars()540							.skip(n as usize)541							.take(1)542							.collect::<String>()543							.into();544						if v.is_empty() {545							bail!(StringBoundsError(n as usize, s.into_flat().chars().count()))546						}547						StrValue::Flat(v)548					}),549					(Val::Str(_), n) => bail!(ValueIndexMustBeTypeGot(550						ValType::Str,551						ValType::Num,552						n.value_type(),553					)),554					#[cfg(feature = "exp-null-coaelse")]555					(Val::Null, _) if part.null_coaelse => return Ok(Val::Null),556					(v, _) => bail!(CantIndexInto(v.value_type())),557				};558			}559			Ok(indexable)560		})?,561		LocalExpr(bindings, returned) => {562			let mut new_bindings: FxHashMap<IStr, Thunk<Val>> =563				FxHashMap::with_capacity(bindings.iter().map(BindSpec::binds_len).sum());564			let fctx = Context::new_future();565			for b in bindings {566				evaluate_dest(b, fctx.clone(), &mut new_bindings)?;567			}568			let ctx = ctx.extend_bindings(new_bindings).into_future(fctx);569			evaluate(ctx, returned)?570		}571		Arr(items) => {572			if items.is_empty() {573				Val::Arr(ArrValue::empty())574			} else {575				Val::Arr(ArrValue::expr(ctx, items.clone()))576			}577		}578		ArrComp(expr, comp_specs) => {579			let mut out = Vec::new();580			evaluate_comp(ctx, comp_specs, &mut |ctx| {581				let expr = expr.clone();582				out.push(Thunk!(move || evaluate(ctx, &expr)));583				Ok(())584			})?;585			Val::Arr(ArrValue::lazy(out))586		}587		Obj(body) => Val::Obj(evaluate_object(ctx, body)?),588		ObjExtend(a, b) => evaluate_add_op(589			&evaluate(ctx.clone(), a)?,590			&Val::Obj(evaluate_object(ctx, b)?),591		)?,592		Apply(value, args, tailstrict) => ensure_sufficient_stack(|| {593			evaluate_apply(594				ctx,595				value,596				args,597				CallLocation::new(&args.span()),598				*tailstrict,599			)600		})?,601		Function(params, body) => {602			evaluate_method(ctx, "anonymous".into(), params.clone(), body.clone())603		}604		AssertExpr(assert) => {605			evaluate_assert(ctx.clone(), &assert.assert)?;606			evaluate(ctx, &assert.rest)?607		}608		ErrorStmt(e) => in_frame(609			CallLocation::new(&e.span()),610			|| "error statement".to_owned(),611			|| bail!(RuntimeError(evaluate(ctx, e)?.to_string()?,)),612		)?,613		IfElse(if_else) => {614			if in_frame(615				CallLocation::new(&if_else.cond.0.span()),616				|| "if condition".to_owned(),617				|| bool::from_untyped(evaluate(ctx.clone(), &if_else.cond.0)?),618			)? {619				evaluate(ctx, &if_else.cond_then)?620			} else {621				match &if_else.cond_else {622					Some(v) => evaluate(ctx, v)?,623					None => Val::Null,624				}625			}626		}627		Slice(slice) => {628			fn parse_idx<T: Typed + FromUntyped>(629				loc: CallLocation<'_>,630				ctx: Context,631				expr: Option<&Spanned<Expr>>,632				desc: &'static str,633			) -> Result<Option<T>> {634				if let Some(value) = expr {635					Ok(in_frame(636						loc,637						|| format!("slice {desc}"),638						|| <Option<T>>::from_untyped(evaluate(ctx, value)?),639					)?)640				} else {641					Ok(None)642				}643			}644645			let indexable = evaluate(ctx.clone(), &slice.value)?;646			let loc = CallLocation::new(&loc);647648			let start = parse_idx(loc, ctx.clone(), slice.slice.start.as_ref(), "start")?;649			let end = parse_idx(loc, ctx.clone(), slice.slice.end.as_ref(), "end")?;650			let step = parse_idx(loc, ctx, slice.slice.step.as_ref(), "step")?;651652			IndexableVal::into_untyped(indexable.into_indexable()?.slice(start, end, step)?)?653		}654		Import(kind, path) => {655			let Expr::Str(path) = &***path else {656				bail!("computed imports are not supported")657			};658			let tmp = loc.clone().0;659			with_state(|s| {660				let resolved_path = s.resolve_from(tmp.source_path(), path)?;661				Ok(match kind {662					ImportKind::Normal => in_frame(663						CallLocation::new(&loc),664						|| format!("import {:?}", path.clone()),665						|| s.import_resolved(resolved_path),666					)?,667					ImportKind::Str => Val::string(s.import_resolved_str(resolved_path)?),668					ImportKind::Bin => {669						Val::Arr(ArrValue::bytes(s.import_resolved_bin(resolved_path)?))670					}671				}) as Result<Val>672			})?673		}674	})675}
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, Expr, Spanned, UnaryOpType};
+use jrsonnet_ir::{BinaryOpType, Expr, Spanned, UnaryOpType};
 
 use crate::{
 	arr::ArrValue,
modifiedcrates/jrsonnet-evaluator/src/function/builtin.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/builtin.rs
+++ b/crates/jrsonnet-evaluator/src/function/builtin.rs
@@ -1,7 +1,7 @@
 use std::any::Any;
 
 use jrsonnet_gcmodule::{cc_dyn, Trace, TraceBox};
-use jrsonnet_parser::function::{FunctionSignature, ParamDefault, ParamName, ParamParse};
+use jrsonnet_ir::function::{FunctionSignature, ParamDefault, ParamName, ParamParse};
 
 use super::CallLocation;
 use crate::{Result, Thunk, Val};
modifiedcrates/jrsonnet-evaluator/src/function/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/mod.rs
+++ b/crates/jrsonnet-evaluator/src/function/mod.rs
@@ -4,7 +4,7 @@
 use jrsonnet_gcmodule::{Cc, Trace};
 use jrsonnet_interner::IStr;
 pub use jrsonnet_macros::builtin;
-use jrsonnet_parser::{ArgsDesc, Destruct, Expr, ExprParams, Span, Spanned};
+use jrsonnet_ir::{ArgsDesc, Destruct, Expr, ExprParams, Span, Spanned};
 
 use self::{
 	builtin::{Builtin, StaticBuiltin},
@@ -24,7 +24,7 @@
 pub use native::NativeFn;
 pub use prepared::PreparedFuncVal;
 
-pub use jrsonnet_parser::function::*;
+pub use jrsonnet_ir::function::*;
 
 /// Function callsite location.
 /// Either from other jsonnet code, specified by expression location, or from native (without location).
modifiedcrates/jrsonnet-evaluator/src/function/parse.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/parse.rs
+++ b/crates/jrsonnet-evaluator/src/function/parse.rs
@@ -1,6 +1,6 @@
 use std::rc::Rc;
 
-use jrsonnet_parser::{
+use jrsonnet_ir::{
 	function::{FunctionSignature, ParamName},
 	ArgsDesc, Expr, ExprParams, Spanned,
 };
modifiedcrates/jrsonnet-evaluator/src/function/prepared.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/function/prepared.rs
+++ b/crates/jrsonnet-evaluator/src/function/prepared.rs
@@ -1,8 +1,8 @@
 use std::rc::Rc;
 
 use jrsonnet_gcmodule::{Acyclic, Trace};
-use jrsonnet_parser::function::FunctionSignature;
-use jrsonnet_parser::{ExprParams, IStr};
+use jrsonnet_ir::function::FunctionSignature;
+use jrsonnet_ir::{ExprParams, IStr};
 use rustc_hash::{FxHashMap, FxHashSet};
 
 use crate::destructure::destruct;
modifiedcrates/jrsonnet-evaluator/src/import.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/import.rs
+++ b/crates/jrsonnet-evaluator/src/import.rs
@@ -10,7 +10,7 @@
 use fs::File;
 use jrsonnet_gcmodule::Acyclic;
 use jrsonnet_interner::IBytes;
-use jrsonnet_parser::{
+use jrsonnet_ir::{
 	IStr, SourceDefaultIgnoreJpath, SourceDirectory, SourceFifo, SourceFile, SourcePath,
 };
 
modifiedcrates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/lib.rs
+++ b/crates/jrsonnet-evaluator/src/lib.rs
@@ -44,8 +44,9 @@
 pub use jrsonnet_interner::{IBytes, IStr};
 #[doc(hidden)]
 pub use jrsonnet_macros;
-pub use jrsonnet_parser as parser;
-use jrsonnet_parser::{Expr, ParserSettings, Source, SourcePath, Spanned};
+pub use jrsonnet_ir as parser;
+use jrsonnet_ir::{Expr, Source, SourcePath, Spanned};
+use jrsonnet_peg_parser::ParserSettings;
 pub use obj::*;
 pub use rustc_hash;
 use rustc_hash::FxHashMap;
@@ -344,7 +345,7 @@
 		let file_name = Source::new(path.clone(), code.clone());
 		if file.parsed.is_none() {
 			file.parsed = Some(
-				jrsonnet_parser::parse(
+				jrsonnet_peg_parser::parse(
 					&code,
 					&ParserSettings {
 						source: file_name.clone(),
@@ -460,7 +461,7 @@
 	pub fn evaluate_snippet(&self, name: impl Into<IStr>, code: impl Into<IStr>) -> Result<Val> {
 		let code = code.into();
 		let source = Source::new_virtual(name.into(), code.clone());
-		let parsed = jrsonnet_parser::parse(
+		let parsed = jrsonnet_peg_parser::parse(
 			&code,
 			&ParserSettings {
 				source: source.clone(),
@@ -481,7 +482,7 @@
 	) -> Result<Val> {
 		let code = code.into();
 		let source = Source::new_virtual(name.into(), code.clone());
-		let parsed = jrsonnet_parser::parse(
+		let parsed = jrsonnet_peg_parser::parse(
 			&code,
 			&ParserSettings {
 				source: source.clone(),
modifiedcrates/jrsonnet-evaluator/src/obj/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/obj/mod.rs
+++ b/crates/jrsonnet-evaluator/src/obj/mod.rs
@@ -5,12 +5,12 @@
 use educe::Educe;
 use jrsonnet_gcmodule::{cc_dyn, Acyclic, Cc, Trace, Weak};
 use jrsonnet_interner::IStr;
-use jrsonnet_parser::Span;
+use jrsonnet_ir::Span;
 use rustc_hash::{FxHashMap, FxHashSet};
 
 mod oop;
 
-pub use jrsonnet_parser::Visibility;
+pub use jrsonnet_ir::Visibility;
 pub use oop::ObjValueBuilder;
 
 use crate::{
modifiedcrates/jrsonnet-evaluator/src/obj/oop.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/obj/oop.rs
+++ b/crates/jrsonnet-evaluator/src/obj/oop.rs
@@ -8,7 +8,7 @@
 	bail, error::ErrorKind::*, in_frame, CcUnbound, MaybeUnbound, Result, Thunk, Unbound, Val,
 };
 use jrsonnet_gcmodule::{Cc, Trace};
-use jrsonnet_parser::IStr;
+use jrsonnet_ir::IStr;
 use rustc_hash::{FxHashMap, FxHashSet};
 
 use super::ordering::{FieldIndex, SuperDepth};
modifiedcrates/jrsonnet-evaluator/src/tla.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/tla.rs
+++ b/crates/jrsonnet-evaluator/src/tla.rs
@@ -2,7 +2,7 @@
 
 use jrsonnet_gcmodule::Trace;
 use jrsonnet_interner::IStr;
-use jrsonnet_parser::{SourceFifo, SourcePath};
+use jrsonnet_ir::{SourceFifo, SourcePath};
 
 use crate::{
 	function::{CallLocation, PreparedFuncVal},
modifiedcrates/jrsonnet-evaluator/src/trace/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/trace/mod.rs
+++ b/crates/jrsonnet-evaluator/src/trace/mod.rs
@@ -6,9 +6,9 @@
 };
 
 use jrsonnet_gcmodule::Trace;
-use jrsonnet_parser::CodeLocation;
+use jrsonnet_ir::CodeLocation;
 #[cfg(feature = "explaining-traces")]
-use jrsonnet_parser::Span;
+use jrsonnet_ir::Span;
 
 use crate::{error::ErrorKind, Error};
 
modifiedcrates/jrsonnet-formatter/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-formatter/src/lib.rs
+++ b/crates/jrsonnet-formatter/src/lib.rs
@@ -855,7 +855,7 @@
 }
 
 pub struct FormatOptions {
-	// 0 for hard tabs
+	// 0 for hard tabs, otherwise number of spaces
 	pub indent: u8,
 }
 
addedcrates/jrsonnet-ir/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-ir/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "jrsonnet-ir"
+description = "jsonnet language parser and AST"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+version.workspace = true
+
+[features]
+default = []
+exp-destruct = []
+exp-null-coaelse = []
+
+[dependencies]
+jrsonnet-interner.workspace = true
+jrsonnet-gcmodule.workspace = true
+
+static_assertions.workspace = true
+
+peg.workspace = true
+
+[dev-dependencies]
+insta.workspace = true
addedcrates/jrsonnet-ir/README.adocdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-ir/README.adoc
@@ -0,0 +1,3 @@
+= jrsonnet-parser
+
+Parser for jsonnet language
addedcrates/jrsonnet-ir/src/expr.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-ir/src/expr.rs
@@ -0,0 +1,493 @@
+use std::{
+	fmt::{self, Debug, Display},
+	ops::Deref,
+	rc::Rc,
+};
+
+use jrsonnet_gcmodule::Acyclic;
+use jrsonnet_interner::IStr;
+
+use crate::{
+	function::{FunctionSignature, ParamDefault, ParamName, ParamParse},
+	source::Source,
+};
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub enum FieldName {
+	/// {fixed: 2}
+	Fixed(IStr),
+	/// {["dyn"+"amic"]: 3}
+	Dyn(Spanned<Expr>),
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Acyclic)]
+#[repr(u8)]
+pub enum Visibility {
+	/// :
+	Normal,
+	/// ::
+	Hidden,
+	/// :::
+	Unhide,
+}
+
+impl Visibility {
+	pub fn is_visible(&self) -> bool {
+		matches!(self, Self::Normal | Self::Unhide)
+	}
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct AssertStmt(pub Spanned<Expr>, pub Option<Spanned<Expr>>);
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct FieldMember {
+	pub name: FieldName,
+	pub plus: bool,
+	pub params: Option<ExprParams>,
+	pub visibility: Visibility,
+	pub value: Rc<Spanned<Expr>>,
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub enum Member {
+	Field(FieldMember),
+	BindStmt(BindSpec),
+	AssertStmt(AssertStmt),
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Acyclic)]
+pub enum UnaryOpType {
+	Plus,
+	Minus,
+	BitNot,
+	Not,
+}
+
+impl Display for UnaryOpType {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		use UnaryOpType::*;
+		write!(
+			f,
+			"{}",
+			match self {
+				Plus => "+",
+				Minus => "-",
+				BitNot => "~",
+				Not => "!",
+			}
+		)
+	}
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Acyclic)]
+pub enum BinaryOpType {
+	Mul,
+	Div,
+
+	/// Implemented as intrinsic, put here for completeness
+	Mod,
+
+	Add,
+	Sub,
+
+	Lhs,
+	Rhs,
+
+	Lt,
+	Gt,
+	Lte,
+	Gte,
+
+	BitAnd,
+	BitOr,
+	BitXor,
+
+	Eq,
+	Neq,
+
+	And,
+	Or,
+	#[cfg(feature = "exp-null-coaelse")]
+	NullCoaelse,
+
+	// Equialent to std.objectHasEx(a, b, true)
+	In,
+}
+
+impl Display for BinaryOpType {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		use BinaryOpType::*;
+		write!(
+			f,
+			"{}",
+			match self {
+				Mul => "*",
+				Div => "/",
+				Mod => "%",
+				Add => "+",
+				Sub => "-",
+				Lhs => "<<",
+				Rhs => ">>",
+				Lt => "<",
+				Gt => ">",
+				Lte => "<=",
+				Gte => ">=",
+				BitAnd => "&",
+				BitOr => "|",
+				BitXor => "^",
+				Eq => "==",
+				Neq => "!=",
+				And => "&&",
+				Or => "||",
+				In => "in",
+				#[cfg(feature = "exp-null-coaelse")]
+				NullCoaelse => "??",
+			}
+		)
+	}
+}
+
+/// name, default value
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct ExprParam {
+	pub destruct: Destruct,
+	pub default: Option<Rc<Spanned<Expr>>>,
+}
+
+/// Defined function parameters
+#[derive(Debug, Clone, PartialEq, Acyclic)]
+pub struct ExprParams {
+	pub exprs: Rc<Vec<ExprParam>>,
+	pub signature: FunctionSignature,
+	binds_len: usize,
+}
+impl ExprParams {
+	pub fn len(&self) -> usize {
+		self.exprs.len()
+	}
+	pub fn is_empty(&self) -> bool {
+		self.exprs.is_empty()
+	}
+
+	pub fn binds_len(&self) -> usize {
+		self.binds_len
+	}
+	pub fn new(exprs: Vec<ExprParam>) -> Self {
+		Self {
+			signature: FunctionSignature::new(
+				exprs
+					.iter()
+					.map(|p| {
+						ParamParse::new(
+							p.destruct.name(),
+							ParamDefault::exists(p.default.is_some()),
+						)
+					})
+					.collect(),
+			),
+			binds_len: exprs.iter().map(|v| v.destruct.binds_len()).sum(),
+			exprs: Rc::new(exprs),
+		}
+	}
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct ArgsDesc {
+	pub unnamed: Vec<Rc<Spanned<Expr>>>,
+	pub named: Vec<(IStr, Rc<Spanned<Expr>>)>,
+}
+impl ArgsDesc {
+	pub fn new(unnamed: Vec<Rc<Spanned<Expr>>>, named: Vec<(IStr, Rc<Spanned<Expr>>)>) -> Self {
+		Self { unnamed, named }
+	}
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Acyclic)]
+pub enum DestructRest {
+	/// ...rest
+	Keep(IStr),
+	/// ...
+	Drop,
+}
+
+#[derive(Debug, Clone, PartialEq, Acyclic)]
+pub enum Destruct {
+	Full(IStr),
+	#[cfg(feature = "exp-destruct")]
+	Skip,
+	#[cfg(feature = "exp-destruct")]
+	Array {
+		start: Vec<Destruct>,
+		rest: Option<DestructRest>,
+		end: Vec<Destruct>,
+	},
+	#[cfg(feature = "exp-destruct")]
+	Object {
+		#[allow(clippy::type_complexity)]
+		fields: Vec<(IStr, Option<Destruct>, Option<Rc<Spanned<Expr>>>)>,
+		rest: Option<DestructRest>,
+	},
+}
+impl Destruct {
+	/// Name of destructure, used for function parameter names
+	pub fn name(&self) -> ParamName {
+		match self {
+			Self::Full(name) => ParamName::Named(name.clone()),
+			#[cfg(feature = "exp-destruct")]
+			_ => ParamName::Unnamed,
+		}
+	}
+	pub fn binds_len(&self) -> usize {
+		#[cfg(feature = "exp-destruct")]
+		fn cap_rest(rest: &Option<DestructRest>) -> usize {
+			match rest {
+				Some(DestructRest::Keep(_)) => 1,
+				Some(DestructRest::Drop) => 0,
+				None => 0,
+			}
+		}
+		match self {
+			Self::Full(_) => 1,
+			#[cfg(feature = "exp-destruct")]
+			Self::Skip => 0,
+			#[cfg(feature = "exp-destruct")]
+			Self::Array { start, rest, end } => {
+				start.iter().map(Destruct::binds_len).sum::<usize>()
+					+ end.iter().map(Destruct::binds_len).sum::<usize>()
+					+ cap_rest(rest)
+			}
+			#[cfg(feature = "exp-destruct")]
+			Self::Object { fields, rest } => {
+				let mut out = 0;
+				for (_, into, _) in fields {
+					match into {
+						Some(v) => out += v.binds_len(),
+						// Field is destructured to default name
+						None => out += 1,
+					}
+				}
+				out + cap_rest(rest)
+			}
+		}
+	}
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub enum BindSpec {
+	Field {
+		into: Destruct,
+		value: Rc<Spanned<Expr>>,
+	},
+	Function {
+		name: IStr,
+		params: ExprParams,
+		value: Rc<Spanned<Expr>>,
+	},
+}
+impl BindSpec {
+	pub fn binds_len(&self) -> usize {
+		match self {
+			BindSpec::Field { into, .. } => into.binds_len(),
+			BindSpec::Function { .. } => 1,
+		}
+	}
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct IfSpecData(pub Spanned<Expr>);
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct ForSpecData(pub Destruct, pub Spanned<Expr>);
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub enum CompSpec {
+	IfSpec(IfSpecData),
+	ForSpec(ForSpecData),
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct ObjComp {
+	pub locals: Rc<Vec<BindSpec>>,
+	pub field: Rc<FieldMember>,
+	pub compspecs: Vec<CompSpec>,
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct ObjMembers {
+	pub locals: Rc<Vec<BindSpec>>,
+	pub asserts: Rc<Vec<AssertStmt>>,
+	pub fields: Vec<FieldMember>,
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub enum ObjBody {
+	MemberList(ObjMembers),
+	ObjComp(ObjComp),
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Acyclic)]
+pub enum LiteralType {
+	This,
+	Super,
+	Dollar,
+	Null,
+	True,
+	False,
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct SliceDesc {
+	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 {
+	Literal(LiteralType),
+
+	/// String value: "hello"
+	Str(IStr),
+	/// Number: 1, 2.0, 2e+20
+	Num(f64),
+	/// Variable name: test
+	Var(Spanned<IStr>),
+
+	/// Array of expressions: [1, 2, "Hello"]
+	Arr(Rc<Vec<Spanned<Expr>>>),
+	/// Array comprehension:
+	/// ```jsonnet
+	///  ingredients: [
+	///    { kind: kind, qty: 4 / 3 }
+	///    for kind in [
+	///      'Honey Syrup',
+	///      'Lemon Juice',
+	///      'Farmers Gin',
+	///    ]
+	///  ],
+	/// ```
+	ArrComp(Rc<Spanned<Expr>>, Vec<CompSpec>),
+
+	/// Object: {a: 2}
+	Obj(ObjBody),
+	/// Object extension: var1 {b: 2}
+	ObjExtend(Rc<Spanned<Expr>>, ObjBody),
+
+	/// -2
+	UnaryOp(UnaryOpType, Box<Spanned<Expr>>),
+	/// 2 - 2
+	BinaryOp(Box<BinaryOp>),
+	/// assert 2 == 2 : "Math is broken"
+	AssertExpr(Rc<AssertExpr>),
+	/// local a = 2; { b: a }
+	LocalExpr(Vec<BindSpec>, Box<Spanned<Expr>>),
+
+	/// import* "hello"
+	Import(ImportKind, Box<Spanned<Expr>>),
+	/// error "I'm broken"
+	ErrorStmt(Box<Spanned<Expr>>),
+	/// a(b, c)
+	Apply(Box<Spanned<Expr>>, Spanned<ArgsDesc>, bool),
+	/// a[b], a.b, a?.b
+	Index {
+		indexable: Box<Spanned<Expr>>,
+		parts: Vec<IndexPart>,
+	},
+	/// function(x) x
+	Function(ExprParams, Rc<Spanned<Expr>>),
+	/// if true == false then 1 else 2
+	IfElse(Box<IfElse>),
+	Slice(Box<Slice>),
+}
+
+#[derive(Debug, PartialEq, Acyclic)]
+pub struct IndexPart {
+	pub value: Spanned<Expr>,
+	#[cfg(feature = "exp-null-coaelse")]
+	pub null_coaelse: bool,
+}
+
+/// file, begin offset, end offset
+#[derive(Clone, PartialEq, Eq, Acyclic)]
+#[repr(C)]
+pub struct Span(pub Source, pub u32, pub u32);
+impl Span {
+	pub fn belongs_to(&self, other: &Span) -> bool {
+		other.0 == self.0 && other.1 <= self.1 && other.2 >= self.2
+	}
+}
+
+static_assertions::assert_eq_size!(Span, (usize, usize));
+
+impl Debug for Span {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "{:?}:{:?}-{:?}", self.0, self.1, self.2)
+	}
+}
+
+#[derive(Clone, PartialEq, Acyclic)]
+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 new(v: T, s: Span) -> Self {
+		Self(v, s)
+	}
+	#[inline]
+	pub fn span(&self) -> Span {
+		self.1.clone()
+	}
+}
+
+impl<T: Debug + Acyclic> Debug for Spanned<T> {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		let expr = &**self;
+		if f.alternate() {
+			write!(f, "{:#?}", expr)?;
+		} else {
+			write!(f, "{:?}", expr)?;
+		}
+		write!(f, " from {:?}", self.span())?;
+		Ok(())
+	}
+}
addedcrates/jrsonnet-ir/src/function.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-ir/src/function.rs
@@ -0,0 +1,132 @@
+use std::fmt;
+use std::ops::Deref;
+use std::rc::Rc;
+
+use jrsonnet_gcmodule::Acyclic;
+use jrsonnet_interner::IStr;
+
+#[derive(Clone, Acyclic, Debug, PartialEq, Eq)]
+pub enum ParamName {
+	Unnamed,
+	Named(IStr),
+}
+impl ParamName {
+	pub fn as_str(&self) -> Option<&str> {
+		match self {
+			ParamName::Unnamed => None,
+			ParamName::Named(istr) => Some(istr),
+		}
+	}
+	pub fn is_anonymous(&self) -> bool {
+		matches!(self, Self::Unnamed)
+	}
+	pub fn is_named(&self) -> bool {
+		matches!(self, Self::Named(_))
+	}
+}
+impl PartialEq<IStr> for ParamName {
+	fn eq(&self, other: &IStr) -> bool {
+		match self {
+			ParamName::Unnamed => false,
+			ParamName::Named(istr) => istr == other,
+		}
+	}
+}
+
+impl fmt::Display for ParamName {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		match &self {
+			Self::Named(v) => write!(f, "{v}"),
+			Self::Unnamed => write!(f, "<unnamed>"),
+		}
+	}
+}
+
+#[derive(Clone, Copy, Debug, Acyclic, PartialEq, Eq)]
+pub enum ParamDefault {
+	None,
+	Exists,
+	Literal(&'static str),
+}
+impl ParamDefault {
+	pub const fn exists(is_exists: bool) -> Self {
+		if is_exists {
+			Self::Exists
+		} else {
+			Self::None
+		}
+	}
+}
+impl fmt::Display for ParamDefault {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		match self {
+			ParamDefault::None => Ok(()),
+			ParamDefault::Exists => write!(f, " = <default>"),
+			ParamDefault::Literal(lit) => write!(f, " = {lit}"),
+		}
+	}
+}
+
+#[derive(Clone, Acyclic, Debug, PartialEq, Eq)]
+pub struct ParamParse {
+	name: ParamName,
+	default: ParamDefault,
+}
+impl ParamParse {
+	pub fn new(name: ParamName, default: ParamDefault) -> Self {
+		Self { name, default }
+	}
+	/// Parameter name for named call parsing
+	pub fn name(&self) -> &ParamName {
+		&self.name
+	}
+	pub fn default(&self) -> ParamDefault {
+		self.default
+	}
+	pub fn has_default(&self) -> bool {
+		!matches!(self.default, ParamDefault::None)
+	}
+}
+impl fmt::Display for ParamParse {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "{}{}", self.name, self.default)
+	}
+}
+
+#[derive(Debug, Clone, Acyclic, PartialEq, Eq)]
+pub struct FunctionSignature(Rc<[ParamParse]>);
+impl Deref for FunctionSignature {
+	type Target = [ParamParse];
+
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+
+thread_local! {
+	static EMPTY_SIGNATURE: FunctionSignature = FunctionSignature::new([].into());
+}
+
+impl FunctionSignature {
+	pub fn new(v: Rc<[ParamParse]>) -> Self {
+		Self(v)
+	}
+	pub fn empty() -> Self {
+		EMPTY_SIGNATURE.with(|p| p.clone())
+	}
+}
+impl fmt::Display for FunctionSignature {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		if self.0.is_empty() {
+			return write!(f, "(/*no arguments*/)");
+		}
+		write!(f, "(")?;
+		for (i, par) in self.0.iter().enumerate() {
+			if i != 0 {
+				write!(f, ", ")?;
+			}
+			write!(f, "{par}")?;
+		}
+		write!(f, ")")
+	}
+}
addedcrates/jrsonnet-ir/src/lib.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-ir/src/lib.rs
@@ -0,0 +1,15 @@
+#![allow(clippy::redundant_closure_call, clippy::derive_partial_eq_without_eq)]
+
+mod expr;
+pub use expr::*;
+pub use jrsonnet_interner::IStr;
+pub mod function;
+mod location;
+mod source;
+pub mod unescape;
+
+pub use location::CodeLocation;
+pub use source::{
+	Source, SourceDefaultIgnoreJpath, SourceDirectory, SourceFifo, SourceFile, SourcePath,
+	SourcePathT, SourceVirtual,
+};
addedcrates/jrsonnet-ir/src/location.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-ir/src/location.rs
@@ -0,0 +1,115 @@
+#[allow(clippy::module_name_repetitions)]
+#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
+pub struct CodeLocation {
+	pub offset: usize,
+
+	pub line: usize,
+	pub column: usize,
+
+	pub line_start_offset: usize,
+	pub line_end_offset: usize,
+}
+
+#[allow(clippy::module_name_repetitions)]
+pub fn location_to_offset(mut file: &str, mut line: usize, column: usize) -> Option<usize> {
+	let mut offset = 0;
+	while line > 1 {
+		let pos = file.find('\n')?;
+		offset += pos + 1;
+		file = &file[pos + 1..];
+		line -= 1;
+	}
+	offset += column - 1;
+	Some(offset)
+}
+
+#[allow(clippy::module_name_repetitions)]
+pub fn offset_to_location<const S: usize>(file: &str, offsets: &[u32; S]) -> [CodeLocation; S] {
+	if offsets.is_empty() {
+		return [CodeLocation::default(); S];
+	}
+	let mut line = 1;
+	let mut column = 1;
+	let max_offset = *offsets.iter().max().expect("offsets is not empty");
+
+	let mut offset_map = offsets
+		.iter()
+		.enumerate()
+		.map(|(pos, offset)| (*offset, pos))
+		.collect::<Vec<_>>();
+	offset_map.sort_by_key(|v| v.0);
+	offset_map.reverse();
+
+	let mut out = [CodeLocation::default(); S];
+	let mut with_no_known_line_ending = vec![];
+	let mut this_line_offset = 0;
+	for (pos, ch) in file
+		.chars()
+		.enumerate()
+		.chain(std::iter::once((file.len(), ' ')))
+	{
+		column += 1;
+		match offset_map.last() {
+			Some(x) if x.0 == pos as u32 => {
+				let out_idx = x.1;
+				with_no_known_line_ending.push(out_idx);
+				out[out_idx].offset = pos;
+				out[out_idx].line = line;
+				out[out_idx].column = column;
+				out[out_idx].line_start_offset = this_line_offset;
+				offset_map.pop();
+			}
+			_ => {}
+		}
+		if ch == '\n' {
+			line += 1;
+			column = 1;
+
+			for idx in with_no_known_line_ending.drain(..) {
+				out[idx].line_end_offset = pos;
+			}
+			this_line_offset = pos + 1;
+
+			if pos == max_offset as usize + 1 {
+				break;
+			}
+		}
+	}
+	let file_end = file.chars().count();
+	for idx in with_no_known_line_ending {
+		out[idx].line_end_offset = file_end;
+	}
+
+	out
+}
+
+#[cfg(test)]
+pub mod tests {
+	use super::{offset_to_location, CodeLocation};
+
+	#[test]
+	fn test() {
+		assert_eq!(
+			offset_to_location(
+				"hello world\n_______________________________________________________",
+				&[0, 14]
+			),
+			[
+				CodeLocation {
+					offset: 0,
+					line: 1,
+					column: 2,
+					line_start_offset: 0,
+					line_end_offset: 11,
+				},
+				CodeLocation {
+					offset: 14,
+					line: 2,
+					column: 4,
+					line_start_offset: 12,
+					line_end_offset: 67
+				}
+			]
+		)
+	}
+}
addedcrates/jrsonnet-ir/src/source.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-ir/src/source.rs
@@ -0,0 +1,307 @@
+use std::{
+	any::Any,
+	fmt::{self, Debug, Display},
+	hash::{Hash, Hasher},
+	path::{Path, PathBuf},
+	rc::Rc,
+};
+
+use jrsonnet_gcmodule::Acyclic;
+use jrsonnet_interner::{IBytes, IStr};
+
+use crate::location::{location_to_offset, offset_to_location, CodeLocation};
+
+macro_rules! any_ext_methods {
+	($T:ident) => {
+		fn as_any(&self) -> &dyn Any;
+		fn dyn_hash(&self, hasher: &mut dyn Hasher);
+		fn dyn_eq(&self, other: &dyn $T) -> bool;
+		fn dyn_debug(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
+	};
+}
+macro_rules! any_ext_impl {
+	($T:ident) => {
+		fn as_any(&self) -> &dyn Any {
+			self
+		}
+		fn dyn_hash(&self, mut hasher: &mut dyn Hasher) {
+			self.hash(&mut hasher)
+		}
+		fn dyn_eq(&self, other: &dyn $T) -> bool {
+			let Some(other) = other.as_any().downcast_ref::<Self>() else {
+				return false;
+			};
+			let this = <Self as $T>::as_any(self)
+				.downcast_ref::<Self>()
+				.expect("restricted by impl");
+			this == other
+		}
+		fn dyn_debug(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+			<Self as std::fmt::Debug>::fmt(self, fmt)
+		}
+	};
+}
+macro_rules! any_ext {
+	($T:ident) => {
+		impl Hash for dyn $T {
+			fn hash<H: Hasher>(&self, state: &mut H) {
+				self.dyn_hash(state)
+			}
+		}
+		impl PartialEq for dyn $T {
+			fn eq(&self, other: &Self) -> bool {
+				self.dyn_eq(other)
+			}
+		}
+		impl Eq for dyn $T {}
+	};
+}
+pub trait SourcePathT: Acyclic + Debug + Display {
+	/// This method should be checked by resolver before panicking with bad SourcePath input
+	/// if `true` - then resolver may threat this path as default, and default is usally a CWD
+	fn is_default(&self) -> bool;
+	fn path(&self) -> Option<&Path>;
+	any_ext_methods!(SourcePathT);
+}
+any_ext!(SourcePathT);
+
+/// Represents location of a file
+///
+/// Standard CLI only operates using
+/// - [`SourceFile`] - for any file
+/// - [`SourceDirectory`] - for resolution from CWD
+/// - [`SourceVirtual`] - for stdlib/ext-str
+/// - [`SourceFifo`] - for /dev/fd/X (This path may appear with `jrsonnet <(command_that_produces_jsonnet)`)
+///
+/// From all of those, only [`SourceVirtual`] may be constructed manually, any other path kind should be only obtained
+/// from assigned `ImportResolver`
+/// However, you should always check `is_default` method return, as it will return true for any paths, where default
+/// 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, Clone, Acyclic)]
+pub struct SourcePath(Rc<dyn SourcePathT>);
+impl SourcePath {
+	pub fn new(inner: impl SourcePathT) -> Self {
+		Self(Rc::new(inner))
+	}
+	pub fn downcast_ref<T: SourcePathT>(&self) -> Option<&T> {
+		self.0.as_any().downcast_ref()
+	}
+	pub fn is_default(&self) -> bool {
+		self.0.is_default()
+	}
+	pub fn path(&self) -> Option<&Path> {
+		self.0.path()
+	}
+}
+impl Hash for SourcePath {
+	fn hash<H: Hasher>(&self, state: &mut H) {
+		self.0.hash(state);
+	}
+}
+impl PartialEq for SourcePath {
+	#[allow(clippy::op_ref)]
+	fn eq(&self, other: &Self) -> bool {
+		&*self.0 == &*other.0
+	}
+}
+impl Display for SourcePath {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		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))
+	}
+}
+
+#[derive(Acyclic, Hash, PartialEq, Eq, Debug)]
+struct SourceDefault;
+impl Display for SourceDefault {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "<default>")
+	}
+}
+impl SourcePathT for SourceDefault {
+	fn is_default(&self) -> bool {
+		true
+	}
+	fn path(&self) -> Option<&Path> {
+		None
+	}
+	any_ext_impl!(SourcePathT);
+}
+
+#[derive(Acyclic, Hash, PartialEq, Eq, Debug)]
+pub struct SourceDefaultIgnoreJpath;
+impl Display for SourceDefaultIgnoreJpath {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "<default (ignoring jpath)>")
+	}
+}
+impl SourcePathT for SourceDefaultIgnoreJpath {
+	fn is_default(&self) -> bool {
+		true
+	}
+	fn path(&self) -> Option<&Path> {
+		None
+	}
+	any_ext_impl!(SourcePathT);
+}
+
+/// Represents path to the file on the disk
+/// Directories shouldn't be put here, as resolution for files differs from resolution for directories:
+///
+/// When `file` is being resolved from `SourceFile(a/b/c)`, it should be resolved to `SourceFile(a/b/file)`,
+/// however if it is being resolved from `SourceDirectory(a/b/c)`, then it should be resolved to `SourceDirectory(a/b/c/file)`
+#[derive(Acyclic, Hash, PartialEq, Eq, Debug)]
+pub struct SourceFile(PathBuf);
+impl SourceFile {
+	pub fn new(path: PathBuf) -> Self {
+		Self(path)
+	}
+	pub fn path(&self) -> &Path {
+		&self.0
+	}
+}
+impl Display for SourceFile {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "{}", self.0.display())
+	}
+}
+impl SourcePathT for SourceFile {
+	fn is_default(&self) -> bool {
+		false
+	}
+	fn path(&self) -> Option<&Path> {
+		Some(&self.0)
+	}
+	any_ext_impl!(SourcePathT);
+}
+
+/// Represents path to the directory on the disk
+///
+/// See also [`SourceFile`]
+#[derive(Acyclic, Hash, PartialEq, Eq, Debug)]
+pub struct SourceDirectory(PathBuf);
+impl SourceDirectory {
+	pub fn new(path: PathBuf) -> Self {
+		Self(path)
+	}
+	pub fn path(&self) -> &Path {
+		&self.0
+	}
+}
+impl Display for SourceDirectory {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "{}", self.0.display())
+	}
+}
+impl SourcePathT for SourceDirectory {
+	fn is_default(&self) -> bool {
+		false
+	}
+	fn path(&self) -> Option<&Path> {
+		Some(&self.0)
+	}
+	any_ext_impl!(SourcePathT);
+}
+
+/// Represents virtual file, whose are located in memory, and shouldn't be cached
+///
+/// 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, Clone)]
+pub struct SourceVirtual(pub IStr);
+impl Display for SourceVirtual {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		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
+	}
+	fn path(&self) -> Option<&Path> {
+		None
+	}
+	any_ext_impl!(SourcePathT);
+}
+
+/// Represents resolved FIFO file, those files may only be read once, and this type is only used for
+/// unix, where user might want to do `jrsonnet <(command_that_produces_jsonnet_source)`
+/// In most cases, user most probably want to use `jrsonnet -` instead of `jrsonnet /dev/stdin`
+/// for better cross-platform support.
+// PartialEq is limited to ptr equality
+#[allow(clippy::derived_hash_with_manual_eq)]
+#[derive(Acyclic, Debug, Hash)]
+pub struct SourceFifo(pub String, pub IBytes);
+impl PartialEq for SourceFifo {
+	fn eq(&self, other: &Self) -> bool {
+		std::ptr::eq(self, other)
+	}
+}
+impl fmt::Display for SourceFifo {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "fifo({:?})", self.0)
+	}
+}
+impl SourcePathT for SourceFifo {
+	fn is_default(&self) -> bool {
+		// In case of FD input, user won't expect relative paths to be resolved from /dev/fd/
+		true
+	}
+
+	fn path(&self) -> Option<&Path> {
+		None
+	}
+
+	any_ext_impl!(SourcePathT);
+}
+
+/// 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, Acyclic)]
+pub struct Source(pub Rc<(SourcePath, IStr)>);
+
+impl Source {
+	pub fn new(path: SourcePath, code: IStr) -> Self {
+		Self(Rc::new((path, code)))
+	}
+
+	pub fn new_virtual(name: IStr, code: IStr) -> Self {
+		Self::new(SourcePath::new(SourceVirtual(name)), code)
+	}
+
+	pub fn code(&self) -> &str {
+		&self.0 .1
+	}
+
+	pub fn source_path(&self) -> &SourcePath {
+		&self.0 .0
+	}
+
+	pub fn map_source_locations<const S: usize>(&self, locs: &[u32; S]) -> [CodeLocation; S] {
+		offset_to_location(&self.0 .1, locs)
+	}
+	pub fn map_from_source_location(&self, line: usize, column: usize) -> Option<usize> {
+		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)
+	}
+}
addedcrates/jrsonnet-ir/src/unescape.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-ir/src/unescape.rs
@@ -0,0 +1,55 @@
+use std::str::Chars;
+
+fn decode_unicode(chars: &mut Chars) -> Option<u16> {
+	IntoIterator::into_iter([chars.next()?, chars.next()?, chars.next()?, chars.next()?])
+		.map(|c| c.to_digit(16).map(|f| f as u16))
+		.try_fold(0u16, |acc, v| Some((acc << 4) | (v?)))
+}
+
+pub fn unescape(s: &str) -> Option<String> {
+	let mut chars = s.chars();
+	let mut out = String::with_capacity(s.len());
+
+	while let Some(c) = chars.next() {
+		if c != '\\' {
+			out.push(c);
+			continue;
+		}
+		match chars.next()? {
+			c @ ('\\' | '"' | '\'') => out.push(c),
+			'b' => out.push('\u{0008}'),
+			'f' => out.push('\u{000c}'),
+			'n' => out.push('\n'),
+			'r' => out.push('\r'),
+			't' => out.push('\t'),
+			'u' => match decode_unicode(&mut chars)? {
+				// May only be second byte
+				0xDC00..=0xDFFF => return None,
+				// Surrogate pair
+				n1 @ 0xD800..=0xDBFF => {
+					if chars.next() != Some('\\') {
+						return None;
+					}
+					if chars.next() != Some('u') {
+						return None;
+					}
+					let n2 = decode_unicode(&mut chars)?;
+					if !matches!(n2, 0xDC00..=0xDFFF) {
+						return None;
+					}
+					let n = (((n1 - 0xD800) as u32) << 10 | (n2 - 0xDC00) as u32) + 0x1_0000;
+					out.push(char::from_u32(n)?);
+				}
+				n => out.push(char::from_u32(n as u32)?),
+			},
+			'x' => {
+				let c = IntoIterator::into_iter([chars.next()?, chars.next()?])
+					.map(|c| c.to_digit(16))
+					.try_fold(0u32, |acc, v| Some((acc << 8) | (v?)))?;
+				out.push(char::from_u32(c)?)
+			}
+			_ => return None,
+		}
+	}
+	Some(out)
+}
deletedcrates/jrsonnet-parser/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-parser/Cargo.toml
+++ /dev/null
@@ -1,24 +0,0 @@
-[package]
-name = "jrsonnet-parser"
-description = "jsonnet language parser and AST"
-authors.workspace = true
-edition.workspace = true
-license.workspace = true
-repository.workspace = true
-version.workspace = true
-
-[features]
-default = []
-exp-destruct = []
-exp-null-coaelse = []
-
-[dependencies]
-jrsonnet-interner.workspace = true
-jrsonnet-gcmodule.workspace = true
-
-static_assertions.workspace = true
-
-peg.workspace = true
-
-[dev-dependencies]
-insta.workspace = true
deletedcrates/jrsonnet-parser/README.adocdiffbeforeafterboth
--- a/crates/jrsonnet-parser/README.adoc
+++ /dev/null
@@ -1,3 +0,0 @@
-= jrsonnet-parser
-
-Parser for jsonnet language
deletedcrates/jrsonnet-parser/src/expr.rsdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/expr.rs
+++ /dev/null
@@ -1,493 +0,0 @@
-use std::{
-	fmt::{self, Debug, Display},
-	ops::Deref,
-	rc::Rc,
-};
-
-use jrsonnet_gcmodule::Acyclic;
-use jrsonnet_interner::IStr;
-
-use crate::{
-	function::{FunctionSignature, ParamDefault, ParamName, ParamParse},
-	source::Source,
-};
-
-#[derive(Debug, PartialEq, Acyclic)]
-pub enum FieldName {
-	/// {fixed: 2}
-	Fixed(IStr),
-	/// {["dyn"+"amic"]: 3}
-	Dyn(Spanned<Expr>),
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Acyclic)]
-#[repr(u8)]
-pub enum Visibility {
-	/// :
-	Normal,
-	/// ::
-	Hidden,
-	/// :::
-	Unhide,
-}
-
-impl Visibility {
-	pub fn is_visible(&self) -> bool {
-		matches!(self, Self::Normal | Self::Unhide)
-	}
-}
-
-#[derive(Debug, PartialEq, Acyclic)]
-pub struct AssertStmt(pub Spanned<Expr>, pub Option<Spanned<Expr>>);
-
-#[derive(Debug, PartialEq, Acyclic)]
-pub struct FieldMember {
-	pub name: FieldName,
-	pub plus: bool,
-	pub params: Option<ExprParams>,
-	pub visibility: Visibility,
-	pub value: Rc<Spanned<Expr>>,
-}
-
-#[derive(Debug, PartialEq, Acyclic)]
-pub(crate) enum Member {
-	Field(FieldMember),
-	BindStmt(BindSpec),
-	AssertStmt(AssertStmt),
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Acyclic)]
-pub enum UnaryOpType {
-	Plus,
-	Minus,
-	BitNot,
-	Not,
-}
-
-impl Display for UnaryOpType {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		use UnaryOpType::*;
-		write!(
-			f,
-			"{}",
-			match self {
-				Plus => "+",
-				Minus => "-",
-				BitNot => "~",
-				Not => "!",
-			}
-		)
-	}
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Acyclic)]
-pub enum BinaryOpType {
-	Mul,
-	Div,
-
-	/// Implemented as intrinsic, put here for completeness
-	Mod,
-
-	Add,
-	Sub,
-
-	Lhs,
-	Rhs,
-
-	Lt,
-	Gt,
-	Lte,
-	Gte,
-
-	BitAnd,
-	BitOr,
-	BitXor,
-
-	Eq,
-	Neq,
-
-	And,
-	Or,
-	#[cfg(feature = "exp-null-coaelse")]
-	NullCoaelse,
-
-	// Equialent to std.objectHasEx(a, b, true)
-	In,
-}
-
-impl Display for BinaryOpType {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		use BinaryOpType::*;
-		write!(
-			f,
-			"{}",
-			match self {
-				Mul => "*",
-				Div => "/",
-				Mod => "%",
-				Add => "+",
-				Sub => "-",
-				Lhs => "<<",
-				Rhs => ">>",
-				Lt => "<",
-				Gt => ">",
-				Lte => "<=",
-				Gte => ">=",
-				BitAnd => "&",
-				BitOr => "|",
-				BitXor => "^",
-				Eq => "==",
-				Neq => "!=",
-				And => "&&",
-				Or => "||",
-				In => "in",
-				#[cfg(feature = "exp-null-coaelse")]
-				NullCoaelse => "??",
-			}
-		)
-	}
-}
-
-/// name, default value
-#[derive(Debug, PartialEq, Acyclic)]
-pub struct ExprParam {
-	pub destruct: Destruct,
-	pub default: Option<Rc<Spanned<Expr>>>,
-}
-
-/// Defined function parameters
-#[derive(Debug, Clone, PartialEq, Acyclic)]
-pub struct ExprParams {
-	pub exprs: Rc<Vec<ExprParam>>,
-	pub signature: FunctionSignature,
-	binds_len: usize,
-}
-impl ExprParams {
-	pub fn len(&self) -> usize {
-		self.exprs.len()
-	}
-	pub fn is_empty(&self) -> bool {
-		self.exprs.is_empty()
-	}
-
-	pub fn binds_len(&self) -> usize {
-		self.binds_len
-	}
-	pub fn new(exprs: Vec<ExprParam>) -> Self {
-		Self {
-			signature: FunctionSignature::new(
-				exprs
-					.iter()
-					.map(|p| {
-						ParamParse::new(
-							p.destruct.name(),
-							ParamDefault::exists(p.default.is_some()),
-						)
-					})
-					.collect(),
-			),
-			binds_len: exprs.iter().map(|v| v.destruct.binds_len()).sum(),
-			exprs: Rc::new(exprs),
-		}
-	}
-}
-
-#[derive(Debug, PartialEq, Acyclic)]
-pub struct ArgsDesc {
-	pub unnamed: Vec<Rc<Spanned<Expr>>>,
-	pub named: Vec<(IStr, Rc<Spanned<Expr>>)>,
-}
-impl ArgsDesc {
-	pub fn new(unnamed: Vec<Rc<Spanned<Expr>>>, named: Vec<(IStr, Rc<Spanned<Expr>>)>) -> Self {
-		Self { unnamed, named }
-	}
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Acyclic)]
-pub enum DestructRest {
-	/// ...rest
-	Keep(IStr),
-	/// ...
-	Drop,
-}
-
-#[derive(Debug, Clone, PartialEq, Acyclic)]
-pub enum Destruct {
-	Full(IStr),
-	#[cfg(feature = "exp-destruct")]
-	Skip,
-	#[cfg(feature = "exp-destruct")]
-	Array {
-		start: Vec<Destruct>,
-		rest: Option<DestructRest>,
-		end: Vec<Destruct>,
-	},
-	#[cfg(feature = "exp-destruct")]
-	Object {
-		#[allow(clippy::type_complexity)]
-		fields: Vec<(IStr, Option<Destruct>, Option<Rc<Spanned<Expr>>>)>,
-		rest: Option<DestructRest>,
-	},
-}
-impl Destruct {
-	/// Name of destructure, used for function parameter names
-	pub fn name(&self) -> ParamName {
-		match self {
-			Self::Full(name) => ParamName::Named(name.clone()),
-			#[cfg(feature = "exp-destruct")]
-			_ => ParamName::Unnamed,
-		}
-	}
-	pub fn binds_len(&self) -> usize {
-		#[cfg(feature = "exp-destruct")]
-		fn cap_rest(rest: &Option<DestructRest>) -> usize {
-			match rest {
-				Some(DestructRest::Keep(_)) => 1,
-				Some(DestructRest::Drop) => 0,
-				None => 0,
-			}
-		}
-		match self {
-			Self::Full(_) => 1,
-			#[cfg(feature = "exp-destruct")]
-			Self::Skip => 0,
-			#[cfg(feature = "exp-destruct")]
-			Self::Array { start, rest, end } => {
-				start.iter().map(Destruct::binds_len).sum::<usize>()
-					+ end.iter().map(Destruct::binds_len).sum::<usize>()
-					+ cap_rest(rest)
-			}
-			#[cfg(feature = "exp-destruct")]
-			Self::Object { fields, rest } => {
-				let mut out = 0;
-				for (_, into, _) in fields {
-					match into {
-						Some(v) => out += v.binds_len(),
-						// Field is destructured to default name
-						None => out += 1,
-					}
-				}
-				out + cap_rest(rest)
-			}
-		}
-	}
-}
-
-#[derive(Debug, PartialEq, Acyclic)]
-pub enum BindSpec {
-	Field {
-		into: Destruct,
-		value: Rc<Spanned<Expr>>,
-	},
-	Function {
-		name: IStr,
-		params: ExprParams,
-		value: Rc<Spanned<Expr>>,
-	},
-}
-impl BindSpec {
-	pub fn binds_len(&self) -> usize {
-		match self {
-			BindSpec::Field { into, .. } => into.binds_len(),
-			BindSpec::Function { .. } => 1,
-		}
-	}
-}
-
-#[derive(Debug, PartialEq, Acyclic)]
-pub struct IfSpecData(pub Spanned<Expr>);
-
-#[derive(Debug, PartialEq, Acyclic)]
-pub struct ForSpecData(pub Destruct, pub Spanned<Expr>);
-
-#[derive(Debug, PartialEq, Acyclic)]
-pub enum CompSpec {
-	IfSpec(IfSpecData),
-	ForSpec(ForSpecData),
-}
-
-#[derive(Debug, PartialEq, Acyclic)]
-pub struct ObjComp {
-	pub locals: Rc<Vec<BindSpec>>,
-	pub field: Rc<FieldMember>,
-	pub compspecs: Vec<CompSpec>,
-}
-
-#[derive(Debug, PartialEq, Acyclic)]
-pub struct ObjMembers {
-	pub locals: Rc<Vec<BindSpec>>,
-	pub asserts: Rc<Vec<AssertStmt>>,
-	pub fields: Vec<FieldMember>,
-}
-
-#[derive(Debug, PartialEq, Acyclic)]
-pub enum ObjBody {
-	MemberList(ObjMembers),
-	ObjComp(ObjComp),
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Acyclic)]
-pub enum LiteralType {
-	This,
-	Super,
-	Dollar,
-	Null,
-	True,
-	False,
-}
-
-#[derive(Debug, PartialEq, Acyclic)]
-pub struct SliceDesc {
-	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 {
-	Literal(LiteralType),
-
-	/// String value: "hello"
-	Str(IStr),
-	/// Number: 1, 2.0, 2e+20
-	Num(f64),
-	/// Variable name: test
-	Var(IStr),
-
-	/// Array of expressions: [1, 2, "Hello"]
-	Arr(Rc<Vec<Spanned<Expr>>>),
-	/// Array comprehension:
-	/// ```jsonnet
-	///  ingredients: [
-	///    { kind: kind, qty: 4 / 3 }
-	///    for kind in [
-	///      'Honey Syrup',
-	///      'Lemon Juice',
-	///      'Farmers Gin',
-	///    ]
-	///  ],
-	/// ```
-	ArrComp(Rc<Spanned<Expr>>, Vec<CompSpec>),
-
-	/// Object: {a: 2}
-	Obj(ObjBody),
-	/// Object extension: var1 {b: 2}
-	ObjExtend(Rc<Spanned<Expr>>, ObjBody),
-
-	/// -2
-	UnaryOp(UnaryOpType, Box<Spanned<Expr>>),
-	/// 2 - 2
-	BinaryOp(Box<BinaryOp>),
-	/// assert 2 == 2 : "Math is broken"
-	AssertExpr(Rc<AssertExpr>),
-	/// local a = 2; { b: a }
-	LocalExpr(Vec<BindSpec>, Box<Spanned<Expr>>),
-
-	/// import* "hello"
-	Import(ImportKind, Box<Spanned<Expr>>),
-	/// error "I'm broken"
-	ErrorStmt(Box<Spanned<Expr>>),
-	/// a(b, c)
-	Apply(Box<Spanned<Expr>>, ArgsDesc, bool),
-	/// a[b], a.b, a?.b
-	Index {
-		indexable: Box<Spanned<Expr>>,
-		parts: Vec<IndexPart>,
-	},
-	/// function(x) x
-	Function(ExprParams, Rc<Spanned<Expr>>),
-	/// if true == false then 1 else 2
-	IfElse(Box<IfElse>),
-	Slice(Box<Slice>),
-}
-
-#[derive(Debug, PartialEq, Acyclic)]
-pub struct IndexPart {
-	pub value: Spanned<Expr>,
-	#[cfg(feature = "exp-null-coaelse")]
-	pub null_coaelse: bool,
-}
-
-/// file, begin offset, end offset
-#[derive(Clone, PartialEq, Eq, Acyclic)]
-#[repr(C)]
-pub struct Span(pub Source, pub u32, pub u32);
-impl Span {
-	pub fn belongs_to(&self, other: &Span) -> bool {
-		other.0 == self.0 && other.1 <= self.1 && other.2 >= self.2
-	}
-}
-
-static_assertions::assert_eq_size!(Span, (usize, usize));
-
-impl Debug for Span {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		write!(f, "{:?}:{:?}-{:?}", self.0, self.1, self.2)
-	}
-}
-
-#[derive(Clone, PartialEq, Acyclic)]
-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 new(v: T, s: Span) -> Self {
-		Self(v, s)
-	}
-	#[inline]
-	pub fn span(&self) -> Span {
-		self.1.clone()
-	}
-}
-
-impl<T: Debug + Acyclic> Debug for Spanned<T> {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		let expr = &**self;
-		if f.alternate() {
-			write!(f, "{:#?}", expr)?;
-		} else {
-			write!(f, "{:?}", expr)?;
-		}
-		write!(f, " from {:?}", self.span())?;
-		Ok(())
-	}
-}
deletedcrates/jrsonnet-parser/src/function.rsdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/function.rs
+++ /dev/null
@@ -1,132 +0,0 @@
-use std::fmt;
-use std::ops::Deref;
-use std::rc::Rc;
-
-use jrsonnet_gcmodule::Acyclic;
-use jrsonnet_interner::IStr;
-
-#[derive(Clone, Acyclic, Debug, PartialEq, Eq)]
-pub enum ParamName {
-	Unnamed,
-	Named(IStr),
-}
-impl ParamName {
-	pub fn as_str(&self) -> Option<&str> {
-		match self {
-			ParamName::Unnamed => None,
-			ParamName::Named(istr) => Some(istr),
-		}
-	}
-	pub fn is_anonymous(&self) -> bool {
-		matches!(self, Self::Unnamed)
-	}
-	pub fn is_named(&self) -> bool {
-		matches!(self, Self::Named(_))
-	}
-}
-impl PartialEq<IStr> for ParamName {
-	fn eq(&self, other: &IStr) -> bool {
-		match self {
-			ParamName::Unnamed => false,
-			ParamName::Named(istr) => istr == other,
-		}
-	}
-}
-
-impl fmt::Display for ParamName {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		match &self {
-			Self::Named(v) => write!(f, "{v}"),
-			Self::Unnamed => write!(f, "<unnamed>"),
-		}
-	}
-}
-
-#[derive(Clone, Copy, Debug, Acyclic, PartialEq, Eq)]
-pub enum ParamDefault {
-	None,
-	Exists,
-	Literal(&'static str),
-}
-impl ParamDefault {
-	pub const fn exists(is_exists: bool) -> Self {
-		if is_exists {
-			Self::Exists
-		} else {
-			Self::None
-		}
-	}
-}
-impl fmt::Display for ParamDefault {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		match self {
-			ParamDefault::None => Ok(()),
-			ParamDefault::Exists => write!(f, " = <default>"),
-			ParamDefault::Literal(lit) => write!(f, " = {lit}"),
-		}
-	}
-}
-
-#[derive(Clone, Acyclic, Debug, PartialEq, Eq)]
-pub struct ParamParse {
-	name: ParamName,
-	default: ParamDefault,
-}
-impl ParamParse {
-	pub fn new(name: ParamName, default: ParamDefault) -> Self {
-		Self { name, default }
-	}
-	/// Parameter name for named call parsing
-	pub fn name(&self) -> &ParamName {
-		&self.name
-	}
-	pub fn default(&self) -> ParamDefault {
-		self.default
-	}
-	pub fn has_default(&self) -> bool {
-		!matches!(self.default, ParamDefault::None)
-	}
-}
-impl fmt::Display for ParamParse {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		write!(f, "{}{}", self.name, self.default)
-	}
-}
-
-#[derive(Debug, Clone, Acyclic, PartialEq, Eq)]
-pub struct FunctionSignature(Rc<[ParamParse]>);
-impl Deref for FunctionSignature {
-	type Target = [ParamParse];
-
-	fn deref(&self) -> &Self::Target {
-		&self.0
-	}
-}
-
-thread_local! {
-	static EMPTY_SIGNATURE: FunctionSignature = FunctionSignature::new([].into());
-}
-
-impl FunctionSignature {
-	pub fn new(v: Rc<[ParamParse]>) -> Self {
-		Self(v)
-	}
-	pub fn empty() -> Self {
-		EMPTY_SIGNATURE.with(|p| p.clone())
-	}
-}
-impl fmt::Display for FunctionSignature {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		if self.0.is_empty() {
-			return write!(f, "(/*no arguments*/)");
-		}
-		write!(f, "(")?;
-		for (i, par) in self.0.iter().enumerate() {
-			if i != 0 {
-				write!(f, ", ")?;
-			}
-			write!(f, "{par}")?;
-		}
-		write!(f, ")")
-	}
-}
deletedcrates/jrsonnet-parser/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/lib.rs
+++ /dev/null
@@ -1,553 +0,0 @@
-#![allow(clippy::redundant_closure_call, clippy::derive_partial_eq_without_eq)]
-
-use std::rc::Rc;
-
-use peg::parser;
-mod expr;
-pub use expr::*;
-pub use jrsonnet_interner::IStr;
-pub use peg;
-pub mod function;
-mod location;
-mod source;
-mod unescape;
-
-pub use location::CodeLocation;
-pub use source::{
-	Source, SourceDefaultIgnoreJpath, SourceDirectory, SourceFifo, SourceFile, SourcePath,
-	SourcePathT, SourceVirtual,
-};
-
-pub struct ParserSettings {
-	pub source: Source,
-}
-
-macro_rules! expr_bin {
-	($a:ident $op:ident $b:ident) => {
-		Expr::BinaryOp(Box::new(BinaryOp {
-			lhs: $a,
-			op: $op,
-			rhs: $b,
-		}))
-	};
-}
-macro_rules! expr_un {
-	($op:ident $a:ident) => {
-		Expr::UnaryOp($op, Box::new($a))
-	};
-}
-
-parser! {
-	grammar jsonnet_parser() for str {
-		use peg::ParseLiteral;
-
-		rule eof() = quiet!{![_]} / expected!("<eof>")
-		rule eol() = "\n" / eof()
-
-		/// Standard C-like comments
-		rule comment()
-			= "//" (!eol()[_])* eol()
-			/ "/*" (!("*/")[_])* "*/"
-			/ "#" (!eol()[_])* eol()
-
-		rule single_whitespace() = quiet!{([' ' | '\r' | '\n' | '\t'] / comment())} / expected!("<whitespace>")
-		rule _() = quiet!{([' ' | '\r' | '\n' | '\t']+) / comment()}* / expected!("<whitespace>")
-
-		/// For comma-delimited elements
-		rule comma() = quiet!{_ "," _} / expected!("<comma>")
-		rule alpha() -> char = c:$(['_' | 'a'..='z' | 'A'..='Z']) {c.chars().next().unwrap()}
-		rule digit() -> char = d:$(['0'..='9']) {d.chars().next().unwrap()}
-		rule end_of_ident() = !['0'..='9' | '_' | 'a'..='z' | 'A'..='Z']
-		/// Sequence of digits
-		rule uint_str() -> &'input str = a:$(digit()+ ("_" digit()+)*) { a }
-		/// Number in scientific notation format
-		rule number() -> f64 = quiet!{a:$(uint_str() ("." uint_str())? (['e'|'E'] (s:['+'|'-'])? uint_str())?) {? a.replace("_","").parse().map_err(|_| "<number>") }} / expected!("<number>")
-
-		/// Reserved word followed by any non-alphanumberic
-		rule reserved() = ("assert" / "else" / "error" / "false" / "for" / "function" / "if" / "import" / "importstr" / "importbin" / "in" / "local" / "null" / "tailstrict" / "then" / "self" / "super" / "true") end_of_ident()
-		rule id() -> IStr = v:$(quiet!{ !reserved() alpha() (alpha() / digit())*} / expected!("<identifier>")) { v.into() }
-
-		rule keyword(id: &'static str) -> ()
-			= ##parse_string_literal(id) end_of_ident()
-
-		pub rule param(s: &ParserSettings) -> expr::ExprParam = destruct:destruct(s) expr:(_ "=" _ expr:expr(s){expr})? { expr::ExprParam { destruct, default: expr.map(Rc::new) } }
-		pub rule params(s: &ParserSettings) -> expr::ExprParams
-			= params:param(s) ** comma() comma()? { expr::ExprParams::new(params) }
-			/ { expr::ExprParams::new(Vec::new()) }
-
-		pub rule arg(s: &ParserSettings) -> (Option<IStr>, Rc<Spanned<Expr>>)
-			= name:(quiet! { (s:id() _ "=" !['='] _ {s})? } / expected!("<argument name>")) expr:expr(s) {(name, Rc::new(expr))}
-
-		pub rule args(s: &ParserSettings) -> expr::ArgsDesc
-			= args:arg(s)**comma() comma()? {?
-				let unnamed_count = args.iter().take_while(|(n, _)| n.is_none()).count();
-				let mut unnamed = Vec::with_capacity(unnamed_count);
-				let mut named = Vec::with_capacity(args.len() - unnamed_count);
-				let mut named_started = false;
-				for (name, value) in args {
-					if let Some(name) = name {
-						named_started = true;
-						named.push((name, value));
-					} else {
-						if named_started {
-							return Err("<named argument>")
-						}
-						unnamed.push(value);
-					}
-				}
-				Ok(expr::ArgsDesc::new(unnamed, named))
-			}
-
-		pub rule destruct_rest() -> expr::DestructRest
-			= "..." into:(_ into:id() {into})? {if let Some(into) = into {
-				expr::DestructRest::Keep(into)
-			} else {expr::DestructRest::Drop}}
-		pub rule destruct_array(s: &ParserSettings) -> expr::Destruct
-			= "[" _ start:destruct(s)**comma() rest:(
-				comma() _ rest:destruct_rest()? end:(
-					comma() end:destruct(s)**comma() (_ comma())? {end}
-					/ comma()? {Vec::new()}
-				) {(rest, end)}
-				/ comma()? {(None, Vec::new())}
-			) _ "]" {?
-				#[cfg(feature = "exp-destruct")] return Ok(expr::Destruct::Array {
-					start,
-					rest: rest.0,
-					end: rest.1,
-				});
-				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")
-			}
-		pub rule destruct_object(s: &ParserSettings) -> expr::Destruct
-			= "{" _
-				fields:(name:id() into:(_ ":" _ into:destruct(s) {into})? default:(_ "=" _ v:expr(s) {v})? {(name, into, default.map(Rc::new))})**comma()
-				rest:(
-					comma() rest:destruct_rest()? {rest}
-					/ comma()? {None}
-				)
-			_ "}" {?
-				#[cfg(feature = "exp-destruct")] return Ok(expr::Destruct::Object {
-					fields,
-					rest,
-				});
-				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")
-			}
-		pub rule destruct(s: &ParserSettings) -> expr::Destruct
-			= v:id() {expr::Destruct::Full(v)}
-			/ "?" {?
-				#[cfg(feature = "exp-destruct")] return Ok(expr::Destruct::Skip);
-				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")
-			}
-			/ arr:destruct_array(s) {arr}
-			/ obj:destruct_object(s) {obj}
-
-		pub rule bind(s: &ParserSettings) -> expr::BindSpec
-			= into:destruct(s) _ "=" _ value:expr(s) {expr::BindSpec::Field{into, value: Rc::new(value)}}
-			/ name:id() _ "(" _ params:params(s) _ ")" _ "=" _ value:expr(s) {expr::BindSpec::Function{name, params, value: Rc::new(value)}}
-
-		pub rule assertion(s: &ParserSettings) -> expr::AssertStmt
-			= keyword("assert") _ cond:expr(s) msg:(_ ":" _ e:expr(s) {e})? { expr::AssertStmt(cond, msg) }
-
-		pub rule whole_line() -> &'input str
-			= str:$((!['\n'][_])* "\n") {str}
-		pub rule string_block() -> String
-			= "|||" chomped:"-"? (!['\n']single_whitespace())* "\n"
-			empty_lines:$(['\n']*)
-			prefix:[' ' | '\t']+ first_line:whole_line()
-			lines:("\n" {"\n"} / [' ' | '\t']*<{prefix.len()}> s:whole_line() {s})*
-			[' ' | '\t']*<, {prefix.len() - 1}> "|||"
-			{
-				let mut l = empty_lines.to_owned();
-				l.push_str(first_line);
-				l.extend(lines);
-				if chomped.is_some() {
-					debug_assert!(l.ends_with('\n'));
-					l.truncate(l.len() - 1);
-				}
-				l
-			}
-
-		rule hex_char()
-			= quiet! { ['0'..='9' | 'a'..='f' | 'A'..='F'] } / expected!("<hex char>")
-
-		rule string_char(c: rule<()>)
-			= (!['\\']!c()[_])+
-			/ "\\\\"
-			/ "\\u" hex_char() hex_char() hex_char() hex_char()
-			/ "\\x" hex_char() hex_char()
-			/ ['\\'] (quiet! { ['b' | 'f' | 'n' | 'r' | 't' | '"' | '\''] } / expected!("<escape character>"))
-		pub rule string() -> String
-			= ['"'] str:$(string_char(<"\"">)*) ['"'] {? unescape::unescape(str).ok_or("<escaped string>")}
-			/ ['\''] str:$(string_char(<"\'">)*) ['\''] {? unescape::unescape(str).ok_or("<escaped string>")}
-			/ quiet!{ "@'" str:$(("''" / (!['\''][_]))*) "'" {str.replace("''", "'")}
-			/ "@\"" str:$(("\"\"" / (!['"'][_]))*) "\"" {str.replace("\"\"", "\"")}
-			/ string_block() } / expected!("<string>")
-
-		pub rule field_name(s: &ParserSettings) -> expr::FieldName
-			= name:id() {expr::FieldName::Fixed(name)}
-			/ name:string() {expr::FieldName::Fixed(name.into())}
-			/ "[" _ expr:expr(s) _ "]" {expr::FieldName::Dyn(expr)}
-		pub rule visibility() -> expr::Visibility
-			= ":::" {expr::Visibility::Unhide}
-			/ "::" {expr::Visibility::Hidden}
-			/ ":" {expr::Visibility::Normal}
-		pub rule field(s: &ParserSettings) -> expr::FieldMember
-			= name:field_name(s) _ plus:"+"? _ visibility:visibility() _ value:expr(s) {expr::FieldMember{
-				name,
-				plus: plus.is_some(),
-				params: None,
-				visibility,
-				value: Rc::new(value),
-			}}
-			/ name:field_name(s) _ "(" _ params:params(s) _ ")" _ visibility:visibility() _ value:expr(s) {expr::FieldMember{
-				name,
-				plus: false,
-				params: Some(params),
-				visibility,
-				value: Rc::new(value),
-			}}
-		pub rule obj_local(s: &ParserSettings) -> BindSpec
-			= keyword("local") _ bind:bind(s) {bind}
-		pub rule member(s: &ParserSettings) -> expr::Member
-			= bind:obj_local(s) {expr::Member::BindStmt(bind)}
-			/ assertion:assertion(s) {expr::Member::AssertStmt(assertion)}
-			/ field:field(s) {expr::Member::Field(field)}
-		pub rule objinside(s: &ParserSettings) -> expr::ObjBody
-			= pre_locals:(b: obj_local(s) comma() {b})* &"[" field:field(s) post_locals:(comma() b:obj_local(s) {b})* _ ("," _)? forspec:forspec(s) others:(_ rest:compspec(s) {rest})? {
-				let mut compspecs = vec![CompSpec::ForSpec(forspec)];
-				compspecs.extend(others.unwrap_or_default());
-				let mut locals = pre_locals;
-				locals.extend(post_locals);
-				expr::ObjBody::ObjComp(expr::ObjComp{
-					locals: Rc::new(locals),
-					field: Rc::new(field),
-					compspecs,
-				})
-			}
-			/ members:(member(s) ** comma()) comma()? {
-				let mut locals = Vec::new();
-				let mut asserts = Vec::new();
-				let mut fields = Vec::new();
-				for member in members {
-					match member {
-						Member::Field(field_member) => fields.push(field_member),
-						Member::BindStmt(bind_spec) => locals.push(bind_spec),
-						Member::AssertStmt(assert_stmt) => asserts.push(assert_stmt),
-					}
-				}
-				expr::ObjBody::MemberList(ObjMembers {
-					locals: Rc::new(locals), asserts: Rc::new(asserts), fields
-				})
-			}
-		pub rule ifspec(s: &ParserSettings) -> IfSpecData
-			= keyword("if") _ expr:expr(s) {IfSpecData(expr)}
-		pub rule forspec(s: &ParserSettings) -> ForSpecData
-			= keyword("for") _ id:destruct(s) _ keyword("in") _ cond:expr(s) {ForSpecData(id, cond)}
-		pub rule compspec(s: &ParserSettings) -> Vec<expr::CompSpec>
-			= s:(i:ifspec(s) { expr::CompSpec::IfSpec(i) } / f:forspec(s) {expr::CompSpec::ForSpec(f)} ) ** _ {s}
-		pub rule local_expr(s: &ParserSettings) -> Expr
-			= keyword("local") _ binds:bind(s) ** comma() (_ ",")? _ ";" _ expr:expr(s) { Expr::LocalExpr(binds, Box::new(expr)) }
-		pub rule string_expr(s: &ParserSettings) -> Expr
-			= s:string() {Expr::Str(s.into())}
-		pub rule obj_expr(s: &ParserSettings) -> Expr
-			= "{" _ body:objinside(s) _ "}" {Expr::Obj(body)}
-		pub rule array_expr(s: &ParserSettings) -> Expr
-			= "[" _ elems:(expr(s) ** comma()) _ comma()? "]" {Expr::Arr(Rc::new(elems))}
-		pub rule array_comp_expr(s: &ParserSettings) -> Expr
-			= "[" _ expr:expr(s) _ comma()? _ forspec:forspec(s) _ others:(others: compspec(s) _ {others})? "]" {
-				let mut specs = vec![CompSpec::ForSpec(forspec)];
-				specs.extend(others.unwrap_or_default());
-				Expr::ArrComp(Rc::new(expr), specs)
-			}
-		pub rule number_expr(s: &ParserSettings) -> Expr
-			= n:number() {? if n.is_finite() {
-				Ok(expr::Expr::Num(n))
-			} else {
-				Err("!!!numbers are finite")
-			}}
-		pub rule var_expr(s: &ParserSettings) -> Expr
-			= n:id() { expr::Expr::Var(n) }
-		pub rule id_loc(s: &ParserSettings) -> Spanned<Expr>
-			= a:position!() n:id() b:position!() { Spanned::new(expr::Expr::Str(n), Span(s.source.clone(), a as u32,b as u32)) }
-		pub rule if_then_else_expr(s: &ParserSettings) -> Expr
-			= cond:ifspec(s) _ keyword("then") _ cond_then:expr(s) cond_else:(_ keyword("else") _ e:expr(s) {e})? {Expr::IfElse(Box::new(IfElse{
-				cond,
-				cond_then,
-				cond_else,
-			}))}
-
-		pub rule literal(s: &ParserSettings) -> Expr
-			= v:(
-				keyword("null") {LiteralType::Null}
-				/ keyword("true") {LiteralType::True}
-				/ keyword("false") {LiteralType::False}
-				/ keyword("self") {LiteralType::This}
-				/ keyword("$") {LiteralType::Dollar}
-				/ keyword("super") {LiteralType::Super}
-			) {Expr::Literal(v)}
-
-		rule import_kind() -> ImportKind
-			= keyword("importstr") { ImportKind::Str }
-			/ keyword("importbin") { ImportKind::Bin }
-			/ keyword("import") { ImportKind::Normal }
-
-		pub rule expr_basic(s: &ParserSettings) -> Expr
-			= literal(s)
-
-			/ string_expr(s) / number_expr(s)
-			/ array_expr(s)
-			/ obj_expr(s)
-			/ array_expr(s)
-			/ array_comp_expr(s)
-
-			/ kind:import_kind() _ path:expr(s) {Expr::Import(kind, Box::new(path))}
-
-			/ var_expr(s)
-			/ local_expr(s)
-			/ if_then_else_expr(s)
-
-			/ keyword("function") _ "(" _ params:params(s) _ ")" _ expr:expr(s) {Expr::Function(params, Rc::new(expr))}
-			/ assert:assertion(s) _ ";" _ rest:expr(s) { Expr::AssertExpr(Rc::new(AssertExpr{
-				assert, rest
-			})) }
-
-			/ keyword("error") _ expr:expr(s) { Expr::ErrorStmt(Box::new(expr)) }
-
-		rule slice_part(s: &ParserSettings) -> Option<Spanned<Expr>>
-			= _ e:(e:expr(s) _{e})? {e}
-		pub rule slice_desc(s: &ParserSettings) -> SliceDesc
-			= start:slice_part(s) ":" pair:(end:slice_part(s) step:(":" e:slice_part(s){e})? {(end, step.flatten())})? {
-				let (end, step) = if let Some((end, step)) = pair {
-					(end, step)
-				}else{
-					(None, None)
-				};
-
-				SliceDesc { start, end, step }
-			}
-
-		rule binop(x: rule<()>) -> ()
-			= quiet!{ x() } / expected!("<binary op>")
-		rule unaryop(x: rule<()>) -> ()
-			= quiet!{ x() } / expected!("<unary op>")
-
-		rule ensure_null_coaelse()
-			= "" {?
-				#[cfg(not(feature = "exp-null-coaelse"))] return Err("!!!experimental null coaelscing was not enabled");
-				#[cfg(feature = "exp-null-coaelse")] Ok(())
-			}
-		use BinaryOpType::*;
-		use UnaryOpType::*;
-		rule expr(s: &ParserSettings) -> Spanned<Expr>
-			= precedence! {
-				"(" _ e:expr(s) _ ")" {e}
-				start:position!() v:@ end:position!() { Spanned::new(v, Span(s.source.clone(), start as u32, end as u32)) }
-				--
-				a:(@) _ binop(<"||">) _ b:@ {expr_bin!(a Or b)}
-				a:(@) _ binop(<"??">) _ ensure_null_coaelse() b:@ {
-					#[cfg(feature = "exp-null-coaelse")] return expr_bin!(a NullCoaelse b);
-					unreachable!("ensure_null_coaelse will fail if feature is not enabled")
-				}
-				--
-				a:(@) _ binop(<"&&">) _ b:@ {expr_bin!(a And b)}
-				--
-				a:(@) _ binop(<"|">) _ b:@ {expr_bin!(a BitOr b)}
-				--
-				a:@ _ binop(<"^">) _ b:(@) {expr_bin!(a BitXor b)}
-				--
-				a:(@) _ binop(<"&">) _ b:@ {expr_bin!(a BitAnd b)}
-				--
-				a:(@) _ binop(<"==">) _ b:@ {expr_bin!(a Eq b)}
-				a:(@) _ binop(<"!=">) _ b:@ {expr_bin!(a Neq b)}
-				--
-				a:(@) _ binop(<"<">) _ b:@ {expr_bin!(a Lt b)}
-				a:(@) _ binop(<">">) _ b:@ {expr_bin!(a Gt b)}
-				a:(@) _ binop(<"<=">) _ b:@ {expr_bin!(a Lte b)}
-				a:(@) _ binop(<">=">) _ b:@ {expr_bin!(a Gte b)}
-				a:(@) _ binop(<keyword("in")>) _ b:@ {expr_bin!(a In b)}
-				--
-				a:(@) _ binop(<"<<">) _ b:@ {expr_bin!(a Lhs b)}
-				a:(@) _ binop(<">>">) _ b:@ {expr_bin!(a Rhs b)}
-				--
-				a:(@) _ binop(<"+">) _ b:@ {expr_bin!(a Add b)}
-				a:(@) _ binop(<"-">) _ b:@ {expr_bin!(a Sub b)}
-				--
-				a:(@) _ binop(<"*">) _ b:@ {expr_bin!(a Mul b)}
-				a:(@) _ binop(<"/">) _ b:@ {expr_bin!(a Div b)}
-				a:(@) _ binop(<"%">) _ b:@ {expr_bin!(a Mod b)}
-				--
-						unaryop(<"+">) _ b:@ {expr_un!(Plus b)}
-						unaryop(<"-">) _ b:@ {expr_un!(Minus b)}
-						unaryop(<"!">) _ b:@ {expr_un!(Not b)}
-						unaryop(<"~">) _ b:@ {expr_un!(BitNot b)}
-				--
-				value:(@) _ "[" _ slice:slice_desc(s) _ "]" {Expr::Slice(Box::new(Slice{value, slice}))}
-				indexable:(@) _ parts:index_part(s)+ {Expr::Index{indexable: Box::new(indexable), parts}}
-				a:(@) _ "(" _ args:args(s) _ ")" ts:(_ keyword("tailstrict"))? {Expr::Apply(Box::new(a), args, ts.is_some())}
-				a:(@) _ "{" _ body:objinside(s) _ "}" {Expr::ObjExtend(Rc::new(a), body)}
-				--
-				e:expr_basic(s) {e}
-			}
-		pub rule index_part(s: &ParserSettings) -> IndexPart
-		= n:("?" _ ensure_null_coaelse())? "." _ value:id_loc(s) {IndexPart {
-			value,
-			#[cfg(feature = "exp-null-coaelse")]
-			null_coaelse: n.is_some(),
-		}}
-		/ n:("?" _ "." _ ensure_null_coaelse())? "[" _ value:expr(s) _ "]" {IndexPart {
-			value,
-			#[cfg(feature = "exp-null-coaelse")]
-			null_coaelse: n.is_some(),
-		}}
-
-		pub rule jsonnet(s: &ParserSettings) -> Spanned<Expr> = _ e:expr(s) _ {e}
-	}
-}
-
-pub type ParseError = peg::error::ParseError<peg::str::LineCol>;
-pub fn parse(str: &str, settings: &ParserSettings) -> Result<Spanned<Expr>, ParseError> {
-	jsonnet_parser::jsonnet(str, settings)
-}
-/// Used for importstr values
-pub fn string_to_expr(str: IStr, settings: &ParserSettings) -> Spanned<Expr> {
-	let len = str.len();
-	Spanned::new(Expr::Str(str), Span(settings.source.clone(), 0, len as u32))
-}
-
-#[cfg(test)]
-pub mod tests {
-	use insta::assert_snapshot;
-	use jrsonnet_interner::IStr;
-
-	use super::parse;
-	use crate::{source::Source, ParserSettings};
-
-	fn parsep(s: &str) -> String {
-		let v = parse(
-			s,
-			&ParserSettings {
-				source: Source::new_virtual("<test>".into(), IStr::empty()),
-			},
-		)
-		.unwrap();
-		format!("{v:#?}")
-	}
-
-	macro_rules! parse {
-		($s:expr) => {
-			assert_snapshot!(parsep($s));
-		};
-	}
-
-	#[test]
-	fn multiline_string() {
-		parse!("|||\n    Hello world!\n     a\n|||");
-		parse!("|||\n  Hello world!\n   a\n|||");
-		parse!("|||\n\t\tHello world!\n\t\t\ta\n|||");
-		parse!("|||\n   Hello world!\n    a\n |||");
-	}
-
-	#[test]
-	fn slice() {
-		parse!("a[1:]");
-		parse!("a[1::]");
-		parse!("a[:1:]");
-		parse!("a[::1]");
-		parse!("str[:len - 1]");
-	}
-
-	#[test]
-	fn string_escaping() {
-		parse!(r#""Hello, \"world\"!""#);
-		parse!(r#"'Hello \'world\'!'"#);
-		parse!(r#"'\\\\'"#);
-	}
-
-	#[test]
-	fn string_unescaping() {
-		parse!(r#""Hello\nWorld""#);
-	}
-
-	#[test]
-	fn string_verbantim() {
-		parse!(r#"@"Hello\n""World""""#);
-	}
-
-	#[test]
-	fn imports() {
-		parse!("import \"hello\"");
-		parse!("importstr \"garnish.txt\"");
-		parse!("importbin \"garnish.bin\"");
-	}
-
-	#[test]
-	fn empty_object() {
-		parse!("{}");
-	}
-
-	#[test]
-	fn basic_math() {
-		parse!("2+2*2");
-		parse!("2	+ 	  2	  *	2   	");
-		parse!("2+(2+2*2)");
-		parse!("2//comment\n+//comment\n3/*test*/*/*test*/4");
-	}
-
-	#[test]
-	fn suffix() {
-		parse!("std.test");
-		parse!("std(2)");
-		parse!("std.test(2)");
-		parse!("a[b]");
-	}
-
-	#[test]
-	fn array_comp() {
-		parse!("[std.deepJoin(x) for x in arr]");
-	}
-
-	#[test]
-	fn reserved() {
-		parse!("null");
-		parse!("nulla");
-	}
-
-	#[test]
-	fn multiple_args_buf() {
-		parse!("a(b, null_fields)");
-	}
-
-	#[test]
-	fn infix_precedence() {
-		parse!("!a && !b");
-		parse!("!a / !b");
-	}
-
-	#[test]
-	fn double_negation() {
-		parse!("!!a");
-	}
-
-	#[test]
-	fn array_test_error() {
-		parse!("[a for a in b if c for e in f]");
-	}
-
-	#[test]
-	fn missing_newline_between_comment_and_eof() {
-		parse!(
-			"{a:1}
-
-			//+213"
-		);
-	}
-
-	#[test]
-	fn default_param_before_nondefault() {
-		parse!("local x(foo = 'foo', bar) = null; null");
-	}
-
-	#[test]
-	fn add_location_info_to_all_sub_expressions() {
-		parse!("{} { local x = 1, x: x } + {}");
-	}
-}
deletedcrates/jrsonnet-parser/src/location.rsdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/location.rs
+++ /dev/null
@@ -1,115 +0,0 @@
-#[allow(clippy::module_name_repetitions)]
-#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
-pub struct CodeLocation {
-	pub offset: usize,
-
-	pub line: usize,
-	pub column: usize,
-
-	pub line_start_offset: usize,
-	pub line_end_offset: usize,
-}
-
-#[allow(clippy::module_name_repetitions)]
-pub fn location_to_offset(mut file: &str, mut line: usize, column: usize) -> Option<usize> {
-	let mut offset = 0;
-	while line > 1 {
-		let pos = file.find('\n')?;
-		offset += pos + 1;
-		file = &file[pos + 1..];
-		line -= 1;
-	}
-	offset += column - 1;
-	Some(offset)
-}
-
-#[allow(clippy::module_name_repetitions)]
-pub fn offset_to_location<const S: usize>(file: &str, offsets: &[u32; S]) -> [CodeLocation; S] {
-	if offsets.is_empty() {
-		return [CodeLocation::default(); S];
-	}
-	let mut line = 1;
-	let mut column = 1;
-	let max_offset = *offsets.iter().max().expect("offsets is not empty");
-
-	let mut offset_map = offsets
-		.iter()
-		.enumerate()
-		.map(|(pos, offset)| (*offset, pos))
-		.collect::<Vec<_>>();
-	offset_map.sort_by_key(|v| v.0);
-	offset_map.reverse();
-
-	let mut out = [CodeLocation::default(); S];
-	let mut with_no_known_line_ending = vec![];
-	let mut this_line_offset = 0;
-	for (pos, ch) in file
-		.chars()
-		.enumerate()
-		.chain(std::iter::once((file.len(), ' ')))
-	{
-		column += 1;
-		match offset_map.last() {
-			Some(x) if x.0 == pos as u32 => {
-				let out_idx = x.1;
-				with_no_known_line_ending.push(out_idx);
-				out[out_idx].offset = pos;
-				out[out_idx].line = line;
-				out[out_idx].column = column;
-				out[out_idx].line_start_offset = this_line_offset;
-				offset_map.pop();
-			}
-			_ => {}
-		}
-		if ch == '\n' {
-			line += 1;
-			column = 1;
-
-			for idx in with_no_known_line_ending.drain(..) {
-				out[idx].line_end_offset = pos;
-			}
-			this_line_offset = pos + 1;
-
-			if pos == max_offset as usize + 1 {
-				break;
-			}
-		}
-	}
-	let file_end = file.chars().count();
-	for idx in with_no_known_line_ending {
-		out[idx].line_end_offset = file_end;
-	}
-
-	out
-}
-
-#[cfg(test)]
-pub mod tests {
-	use super::{offset_to_location, CodeLocation};
-
-	#[test]
-	fn test() {
-		assert_eq!(
-			offset_to_location(
-				"hello world\n_______________________________________________________",
-				&[0, 14]
-			),
-			[
-				CodeLocation {
-					offset: 0,
-					line: 1,
-					column: 2,
-					line_start_offset: 0,
-					line_end_offset: 11,
-				},
-				CodeLocation {
-					offset: 14,
-					line: 2,
-					column: 4,
-					line_start_offset: 12,
-					line_end_offset: 67
-				}
-			]
-		)
-	}
-}
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__add_location_info_to_all_sub_expressions.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__add_location_info_to_all_sub_expressions.snap
+++ /dev/null
@@ -1,57 +0,0 @@
----
-source: crates/jrsonnet-parser/src/lib.rs
-expression: "parsep(\"{} { local x = 1, x: x } + {}\")"
----
-BinaryOp(
-    BinaryOp {
-        lhs: ObjExtend(
-            Obj(
-                MemberList(
-                    ObjMembers {
-                        locals: [],
-                        asserts: [],
-                        fields: [],
-                    },
-                ),
-            ) from virtual:<test>:0-2,
-            MemberList(
-                ObjMembers {
-                    locals: [
-                        Field {
-                            into: Full(
-                                "x",
-                            ),
-                            value: Num(
-                                1.0,
-                            ) from virtual:<test>:15-16,
-                        },
-                    ],
-                    asserts: [],
-                    fields: [
-                        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(
-                ObjMembers {
-                    locals: [],
-                    asserts: [],
-                    fields: [],
-                },
-            ),
-        ) from virtual:<test>:27-29,
-    },
-) from virtual:<test>:0-29
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__array_comp.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__array_comp.snap
+++ /dev/null
@@ -1,41 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__array_test_error.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__array_test_error.snap
+++ /dev/null
@@ -1,38 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math-2.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math-2.snap
+++ /dev/null
@@ -1,23 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math-3.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math-3.snap
+++ /dev/null
@@ -1,31 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math-4.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math-4.snap
+++ /dev/null
@@ -1,23 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__basic_math.snap
+++ /dev/null
@@ -1,23 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__default_param_before_nondefault.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__default_param_before_nondefault.snap
+++ /dev/null
@@ -1,54 +0,0 @@
----
-source: crates/jrsonnet-parser/src/lib.rs
-expression: "parsep(\"local x(foo = 'foo', bar) = null; null\")"
----
-LocalExpr(
-    [
-        Function {
-            name: "x",
-            params: ExprParams {
-                exprs: [
-                    ExprParam {
-                        destruct: Full(
-                            "foo",
-                        ),
-                        default: Some(
-                            Str(
-                                "foo",
-                            ) from virtual:<test>:14-19,
-                        ),
-                    },
-                    ExprParam {
-                        destruct: Full(
-                            "bar",
-                        ),
-                        default: None,
-                    },
-                ],
-                signature: FunctionSignature(
-                    [
-                        ParamParse {
-                            name: Named(
-                                "foo",
-                            ),
-                            default: Exists,
-                        },
-                        ParamParse {
-                            name: Named(
-                                "bar",
-                            ),
-                            default: None,
-                        },
-                    ],
-                ),
-                binds_len: 2,
-            },
-            value: Literal(
-                Null,
-            ) from virtual:<test>:28-32,
-        },
-    ],
-    Literal(
-        Null,
-    ) from virtual:<test>:34-38,
-) from virtual:<test>:0-38
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__double_negation.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__double_negation.snap
+++ /dev/null
@@ -1,13 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__empty_object.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__empty_object.snap
+++ /dev/null
@@ -1,13 +0,0 @@
----
-source: crates/jrsonnet-parser/src/lib.rs
-expression: "parsep(\"{}\")"
----
-Obj(
-    MemberList(
-        ObjMembers {
-            locals: [],
-            asserts: [],
-            fields: [],
-        },
-    ),
-) from virtual:<test>:0-2
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__imports-2.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__imports-2.snap
+++ /dev/null
@@ -1,10 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__imports-3.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__imports-3.snap
+++ /dev/null
@@ -1,10 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__imports.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__imports.snap
+++ /dev/null
@@ -1,10 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__infix_precedence-2.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__infix_precedence-2.snap
+++ /dev/null
@@ -1,21 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__infix_precedence.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__infix_precedence.snap
+++ /dev/null
@@ -1,21 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__missing_newline_between_comment_and_eof.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__missing_newline_between_comment_and_eof.snap
+++ /dev/null
@@ -1,25 +0,0 @@
----
-source: crates/jrsonnet-parser/src/lib.rs
-expression: "parsep(\"{a:1}\n\n\t\t\t//+213\")"
----
-Obj(
-    MemberList(
-        ObjMembers {
-            locals: [],
-            asserts: [],
-            fields: [
-                FieldMember {
-                    name: Fixed(
-                        "a",
-                    ),
-                    plus: false,
-                    params: None,
-                    visibility: Normal,
-                    value: Num(
-                        1.0,
-                    ) from virtual:<test>:3-4,
-                },
-            ],
-        },
-    ),
-) from virtual:<test>:0-5
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string-2.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string-2.snap
+++ /dev/null
@@ -1,7 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string-3.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string-3.snap
+++ /dev/null
@@ -1,7 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string-4.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string-4.snap
+++ /dev/null
@@ -1,7 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiline_string.snap
+++ /dev/null
@@ -1,7 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiple_args_buf.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__multiple_args_buf.snap
+++ /dev/null
@@ -1,21 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__reserved-2.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__reserved-2.snap
+++ /dev/null
@@ -1,7 +0,0 @@
----
-source: crates/jrsonnet-parser/src/lib.rs
-expression: "parsep(\"nulla\")"
----
-Var(
-    "nulla",
-) from virtual:<test>:0-5
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__reserved.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__reserved.snap
+++ /dev/null
@@ -1,7 +0,0 @@
----
-source: crates/jrsonnet-parser/src/lib.rs
-expression: "parsep(\"null\")"
----
-Literal(
-    Null,
-) from virtual:<test>:0-4
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-2.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-2.snap
+++ /dev/null
@@ -1,20 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-3.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-3.snap
+++ /dev/null
@@ -1,20 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-4.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-4.snap
+++ /dev/null
@@ -1,20 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-5.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice-5.snap
+++ /dev/null
@@ -1,28 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__slice.snap
+++ /dev/null
@@ -1,20 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_escaping-2.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_escaping-2.snap
+++ /dev/null
@@ -1,7 +0,0 @@
----
-source: crates/jrsonnet-parser/src/lib.rs
-expression: "parsep(r#\"'Hello \\'world\\'!'\"#)"
----
-Str(
-    "Hello 'world'!",
-) from virtual:<test>:0-18
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_escaping-3.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_escaping-3.snap
+++ /dev/null
@@ -1,7 +0,0 @@
----
-source: crates/jrsonnet-parser/src/lib.rs
-expression: "parsep(r#\"'\\\\\\\\'\"#)"
----
-Str(
-    "\\\\",
-) from virtual:<test>:0-6
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_escaping.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_escaping.snap
+++ /dev/null
@@ -1,7 +0,0 @@
----
-source: crates/jrsonnet-parser/src/lib.rs
-expression: "parsep(r#\"\"Hello, \\\"world\\\"!\"\"#)"
----
-Str(
-    "Hello, \"world\"!",
-) from virtual:<test>:0-19
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_unescaping.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_unescaping.snap
+++ /dev/null
@@ -1,7 +0,0 @@
----
-source: crates/jrsonnet-parser/src/lib.rs
-expression: "parsep(r#\"\"Hello\\nWorld\"\"#)"
----
-Str(
-    "Hello\nWorld",
-) from virtual:<test>:0-14
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_verbantim.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__string_verbantim.snap
+++ /dev/null
@@ -1,7 +0,0 @@
----
-source: crates/jrsonnet-parser/src/lib.rs
-expression: "parsep(r#\"@\"Hello\\n\"\"World\"\"\"\"#)"
----
-Str(
-    "Hello\\n\"World\"",
-) from virtual:<test>:0-19
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix-2.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix-2.snap
+++ /dev/null
@@ -1,18 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix-3.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix-3.snap
+++ /dev/null
@@ -1,27 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix-4.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix-4.snap
+++ /dev/null
@@ -1,16 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix.snapdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/snapshots/jrsonnet_parser__tests__suffix.snap
+++ /dev/null
@@ -1,16 +0,0 @@
----
-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
deletedcrates/jrsonnet-parser/src/source.rsdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/source.rs
+++ /dev/null
@@ -1,307 +0,0 @@
-use std::{
-	any::Any,
-	fmt::{self, Debug, Display},
-	hash::{Hash, Hasher},
-	path::{Path, PathBuf},
-	rc::Rc,
-};
-
-use jrsonnet_gcmodule::Acyclic;
-use jrsonnet_interner::{IBytes, IStr};
-
-use crate::location::{location_to_offset, offset_to_location, CodeLocation};
-
-macro_rules! any_ext_methods {
-	($T:ident) => {
-		fn as_any(&self) -> &dyn Any;
-		fn dyn_hash(&self, hasher: &mut dyn Hasher);
-		fn dyn_eq(&self, other: &dyn $T) -> bool;
-		fn dyn_debug(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
-	};
-}
-macro_rules! any_ext_impl {
-	($T:ident) => {
-		fn as_any(&self) -> &dyn Any {
-			self
-		}
-		fn dyn_hash(&self, mut hasher: &mut dyn Hasher) {
-			self.hash(&mut hasher)
-		}
-		fn dyn_eq(&self, other: &dyn $T) -> bool {
-			let Some(other) = other.as_any().downcast_ref::<Self>() else {
-				return false;
-			};
-			let this = <Self as $T>::as_any(self)
-				.downcast_ref::<Self>()
-				.expect("restricted by impl");
-			this == other
-		}
-		fn dyn_debug(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-			<Self as std::fmt::Debug>::fmt(self, fmt)
-		}
-	};
-}
-macro_rules! any_ext {
-	($T:ident) => {
-		impl Hash for dyn $T {
-			fn hash<H: Hasher>(&self, state: &mut H) {
-				self.dyn_hash(state)
-			}
-		}
-		impl PartialEq for dyn $T {
-			fn eq(&self, other: &Self) -> bool {
-				self.dyn_eq(other)
-			}
-		}
-		impl Eq for dyn $T {}
-	};
-}
-pub trait SourcePathT: Acyclic + Debug + Display {
-	/// This method should be checked by resolver before panicking with bad SourcePath input
-	/// if `true` - then resolver may threat this path as default, and default is usally a CWD
-	fn is_default(&self) -> bool;
-	fn path(&self) -> Option<&Path>;
-	any_ext_methods!(SourcePathT);
-}
-any_ext!(SourcePathT);
-
-/// Represents location of a file
-///
-/// Standard CLI only operates using
-/// - [`SourceFile`] - for any file
-/// - [`SourceDirectory`] - for resolution from CWD
-/// - [`SourceVirtual`] - for stdlib/ext-str
-/// - [`SourceFifo`] - for /dev/fd/X (This path may appear with `jrsonnet <(command_that_produces_jsonnet)`)
-///
-/// From all of those, only [`SourceVirtual`] may be constructed manually, any other path kind should be only obtained
-/// from assigned `ImportResolver`
-/// However, you should always check `is_default` method return, as it will return true for any paths, where default
-/// 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, Clone, Acyclic)]
-pub struct SourcePath(Rc<dyn SourcePathT>);
-impl SourcePath {
-	pub fn new(inner: impl SourcePathT) -> Self {
-		Self(Rc::new(inner))
-	}
-	pub fn downcast_ref<T: SourcePathT>(&self) -> Option<&T> {
-		self.0.as_any().downcast_ref()
-	}
-	pub fn is_default(&self) -> bool {
-		self.0.is_default()
-	}
-	pub fn path(&self) -> Option<&Path> {
-		self.0.path()
-	}
-}
-impl Hash for SourcePath {
-	fn hash<H: Hasher>(&self, state: &mut H) {
-		self.0.hash(state);
-	}
-}
-impl PartialEq for SourcePath {
-	#[allow(clippy::op_ref)]
-	fn eq(&self, other: &Self) -> bool {
-		&*self.0 == &*other.0
-	}
-}
-impl Display for SourcePath {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		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))
-	}
-}
-
-#[derive(Acyclic, Hash, PartialEq, Eq, Debug)]
-struct SourceDefault;
-impl Display for SourceDefault {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		write!(f, "<default>")
-	}
-}
-impl SourcePathT for SourceDefault {
-	fn is_default(&self) -> bool {
-		true
-	}
-	fn path(&self) -> Option<&Path> {
-		None
-	}
-	any_ext_impl!(SourcePathT);
-}
-
-#[derive(Acyclic, Hash, PartialEq, Eq, Debug)]
-pub struct SourceDefaultIgnoreJpath;
-impl Display for SourceDefaultIgnoreJpath {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		write!(f, "<default (ignoring jpath)>")
-	}
-}
-impl SourcePathT for SourceDefaultIgnoreJpath {
-	fn is_default(&self) -> bool {
-		true
-	}
-	fn path(&self) -> Option<&Path> {
-		None
-	}
-	any_ext_impl!(SourcePathT);
-}
-
-/// Represents path to the file on the disk
-/// Directories shouldn't be put here, as resolution for files differs from resolution for directories:
-///
-/// When `file` is being resolved from `SourceFile(a/b/c)`, it should be resolved to `SourceFile(a/b/file)`,
-/// however if it is being resolved from `SourceDirectory(a/b/c)`, then it should be resolved to `SourceDirectory(a/b/c/file)`
-#[derive(Acyclic, Hash, PartialEq, Eq, Debug)]
-pub struct SourceFile(PathBuf);
-impl SourceFile {
-	pub fn new(path: PathBuf) -> Self {
-		Self(path)
-	}
-	pub fn path(&self) -> &Path {
-		&self.0
-	}
-}
-impl Display for SourceFile {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		write!(f, "{}", self.0.display())
-	}
-}
-impl SourcePathT for SourceFile {
-	fn is_default(&self) -> bool {
-		false
-	}
-	fn path(&self) -> Option<&Path> {
-		Some(&self.0)
-	}
-	any_ext_impl!(SourcePathT);
-}
-
-/// Represents path to the directory on the disk
-///
-/// See also [`SourceFile`]
-#[derive(Acyclic, Hash, PartialEq, Eq, Debug)]
-pub struct SourceDirectory(PathBuf);
-impl SourceDirectory {
-	pub fn new(path: PathBuf) -> Self {
-		Self(path)
-	}
-	pub fn path(&self) -> &Path {
-		&self.0
-	}
-}
-impl Display for SourceDirectory {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		write!(f, "{}", self.0.display())
-	}
-}
-impl SourcePathT for SourceDirectory {
-	fn is_default(&self) -> bool {
-		false
-	}
-	fn path(&self) -> Option<&Path> {
-		Some(&self.0)
-	}
-	any_ext_impl!(SourcePathT);
-}
-
-/// Represents virtual file, whose are located in memory, and shouldn't be cached
-///
-/// 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, Clone)]
-pub struct SourceVirtual(pub IStr);
-impl Display for SourceVirtual {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		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
-	}
-	fn path(&self) -> Option<&Path> {
-		None
-	}
-	any_ext_impl!(SourcePathT);
-}
-
-/// Represents resolved FIFO file, those files may only be read once, and this type is only used for
-/// unix, where user might want to do `jrsonnet <(command_that_produces_jsonnet_source)`
-/// In most cases, user most probably want to use `jrsonnet -` instead of `jrsonnet /dev/stdin`
-/// for better cross-platform support.
-// PartialEq is limited to ptr equality
-#[allow(clippy::derived_hash_with_manual_eq)]
-#[derive(Acyclic, Debug, Hash)]
-pub struct SourceFifo(pub String, pub IBytes);
-impl PartialEq for SourceFifo {
-	fn eq(&self, other: &Self) -> bool {
-		std::ptr::eq(self, other)
-	}
-}
-impl fmt::Display for SourceFifo {
-	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-		write!(f, "fifo({:?})", self.0)
-	}
-}
-impl SourcePathT for SourceFifo {
-	fn is_default(&self) -> bool {
-		// In case of FD input, user won't expect relative paths to be resolved from /dev/fd/
-		true
-	}
-
-	fn path(&self) -> Option<&Path> {
-		None
-	}
-
-	any_ext_impl!(SourcePathT);
-}
-
-/// 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, Acyclic)]
-pub struct Source(pub Rc<(SourcePath, IStr)>);
-
-impl Source {
-	pub fn new(path: SourcePath, code: IStr) -> Self {
-		Self(Rc::new((path, code)))
-	}
-
-	pub fn new_virtual(name: IStr, code: IStr) -> Self {
-		Self::new(SourcePath::new(SourceVirtual(name)), code)
-	}
-
-	pub fn code(&self) -> &str {
-		&self.0 .1
-	}
-
-	pub fn source_path(&self) -> &SourcePath {
-		&self.0 .0
-	}
-
-	pub fn map_source_locations<const S: usize>(&self, locs: &[u32; S]) -> [CodeLocation; S] {
-		offset_to_location(&self.0 .1, locs)
-	}
-	pub fn map_from_source_location(&self, line: usize, column: usize) -> Option<usize> {
-		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)
-	}
-}
deletedcrates/jrsonnet-parser/src/unescape.rsdiffbeforeafterboth
--- a/crates/jrsonnet-parser/src/unescape.rs
+++ /dev/null
@@ -1,55 +0,0 @@
-use std::str::Chars;
-
-fn decode_unicode(chars: &mut Chars) -> Option<u16> {
-	IntoIterator::into_iter([chars.next()?, chars.next()?, chars.next()?, chars.next()?])
-		.map(|c| c.to_digit(16).map(|f| f as u16))
-		.try_fold(0u16, |acc, v| Some((acc << 4) | (v?)))
-}
-
-pub fn unescape(s: &str) -> Option<String> {
-	let mut chars = s.chars();
-	let mut out = String::with_capacity(s.len());
-
-	while let Some(c) = chars.next() {
-		if c != '\\' {
-			out.push(c);
-			continue;
-		}
-		match chars.next()? {
-			c @ ('\\' | '"' | '\'') => out.push(c),
-			'b' => out.push('\u{0008}'),
-			'f' => out.push('\u{000c}'),
-			'n' => out.push('\n'),
-			'r' => out.push('\r'),
-			't' => out.push('\t'),
-			'u' => match decode_unicode(&mut chars)? {
-				// May only be second byte
-				0xDC00..=0xDFFF => return None,
-				// Surrogate pair
-				n1 @ 0xD800..=0xDBFF => {
-					if chars.next() != Some('\\') {
-						return None;
-					}
-					if chars.next() != Some('u') {
-						return None;
-					}
-					let n2 = decode_unicode(&mut chars)?;
-					if !matches!(n2, 0xDC00..=0xDFFF) {
-						return None;
-					}
-					let n = (((n1 - 0xD800) as u32) << 10 | (n2 - 0xDC00) as u32) + 0x1_0000;
-					out.push(char::from_u32(n)?);
-				}
-				n => out.push(char::from_u32(n as u32)?),
-			},
-			'x' => {
-				let c = IntoIterator::into_iter([chars.next()?, chars.next()?])
-					.map(|c| c.to_digit(16))
-					.try_fold(0u32, |acc, v| Some((acc << 8) | (v?)))?;
-				out.push(char::from_u32(c)?)
-			}
-			_ => return None,
-		}
-	}
-	Some(out)
-}
addedcrates/jrsonnet-peg-parser/Cargo.tomldiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "jrsonnet-peg-parser"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+version.workspace = true
+
+[dependencies]
+jrsonnet-ir.workspace = true
+peg.workspace = true
+
+[lints]
+workspace = true
+
+[dev-dependencies]
+insta.workspace = true
+
+[features]
+default = []
+exp-destruct = ["jrsonnet-ir/exp-destruct"]
+exp-null-coaelse = ["jrsonnet-ir/exp-null-coaelse"]
addedcrates/jrsonnet-peg-parser/src/lib.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/lib.rs
@@ -0,0 +1,556 @@
+use jrsonnet_ir::{
+	BinaryOp, Expr, ExprParams, IStr, IndexPart, Member, Slice, SliceDesc, Source, Span, Spanned,
+	ExprParam, ArgsDesc, AssertExpr, ImportKind, LiteralType, IfElse, CompSpec, ForSpecData, IfSpecData, ObjMembers, ObjBody,
+	ObjComp, FieldMember, Visibility, FieldName, unescape, AssertStmt, BindSpec, Destruct, DestructRest,
+};
+use peg::parser;
+use std::rc::Rc;
+
+pub struct ParserSettings {
+	pub source: Source,
+}
+
+macro_rules! expr_bin {
+	($a:ident $op:ident $b:ident) => {
+		Expr::BinaryOp(Box::new(BinaryOp {
+			lhs: $a,
+			op: $op,
+			rhs: $b,
+		}))
+	};
+}
+macro_rules! expr_un {
+	($op:ident $a:ident) => {
+		Expr::UnaryOp($op, Box::new($a))
+	};
+}
+
+parser! {
+	grammar jsonnet_parser() for str {
+		use peg::ParseLiteral;
+
+		rule eof() = quiet!{![_]} / expected!("<eof>")
+		rule eol() = "\n" / eof()
+
+		/// Standard C-like comments
+		rule comment()
+			= "//" (!eol()[_])* eol()
+			/ "/*" (!("*/")[_])* "*/"
+			/ "#" (!eol()[_])* eol()
+
+		rule single_whitespace() = quiet!{([' ' | '\r' | '\n' | '\t'] / comment())} / expected!("<whitespace>")
+		rule _() = quiet!{([' ' | '\r' | '\n' | '\t']+) / comment()}* / expected!("<whitespace>")
+
+		/// For comma-delimited elements
+		rule comma() = quiet!{_ "," _} / expected!("<comma>")
+		rule alpha() -> char = c:$(['_' | 'a'..='z' | 'A'..='Z']) {c.chars().next().unwrap()}
+		rule digit() -> char = d:$(['0'..='9']) {d.chars().next().unwrap()}
+		rule end_of_ident() = !['0'..='9' | '_' | 'a'..='z' | 'A'..='Z']
+		/// Sequence of digits
+		rule uint_str() -> &'input str = a:$(digit()+ ("_" digit()+)*) { a }
+		/// Number in scientific notation format
+		rule number() -> f64 = quiet!{a:$(uint_str() ("." uint_str())? (['e'|'E'] (s:['+'|'-'])? uint_str())?) {? a.replace("_","").parse().map_err(|_| "<number>") }} / expected!("<number>")
+
+		/// Reserved word followed by any non-alphanumberic
+		rule reserved() = ("assert" / "else" / "error" / "false" / "for" / "function" / "if" / "import" / "importstr" / "importbin" / "in" / "local" / "null" / "tailstrict" / "then" / "self" / "super" / "true") end_of_ident()
+		rule id() -> IStr = v:$(quiet!{ !reserved() alpha() (alpha() / digit())*} / expected!("<identifier>")) { v.into() }
+
+		rule keyword(id: &'static str) -> ()
+			= ##parse_string_literal(id) end_of_ident()
+
+		pub rule param(s: &ParserSettings) -> ExprParam = destruct:destruct(s) expr:(_ "=" _ expr:expr(s){expr})? { ExprParam { destruct, default: expr.map(Rc::new) } }
+		pub rule params(s: &ParserSettings) -> ExprParams
+			= params:param(s) ** comma() comma()? { ExprParams::new(params) }
+			/ { ExprParams::new(Vec::new()) }
+
+		pub rule arg(s: &ParserSettings) -> (Option<IStr>, Rc<Spanned<Expr>>)
+			= name:(quiet! { (s:id() _ "=" !['='] _ {s})? } / expected!("<argument name>")) expr:expr(s) {(name, Rc::new(expr))}
+
+		pub rule args(s: &ParserSettings) -> ArgsDesc
+			= args:arg(s)**comma() comma()? {?
+				let unnamed_count = args.iter().take_while(|(n, _)| n.is_none()).count();
+				let mut unnamed = Vec::with_capacity(unnamed_count);
+				let mut named = Vec::with_capacity(args.len() - unnamed_count);
+				let mut named_started = false;
+				for (name, value) in args {
+					if let Some(name) = name {
+						named_started = true;
+						named.push((name, value));
+					} else {
+						if named_started {
+							return Err("<named argument>")
+						}
+						unnamed.push(value);
+					}
+				}
+				Ok(ArgsDesc::new(unnamed, named))
+			}
+
+		pub rule destruct_rest() -> DestructRest
+			= "..." into:(_ into:id() {into})? {if let Some(into) = into {
+				DestructRest::Keep(into)
+			} else {DestructRest::Drop}}
+		pub rule destruct_array(s: &ParserSettings) -> Destruct
+			= "[" _ start:destruct(s)**comma() rest:(
+				comma() _ rest:destruct_rest()? end:(
+					comma() end:destruct(s)**comma() (_ comma())? {end}
+					/ comma()? {Vec::new()}
+				) {(rest, end)}
+				/ comma()? {(None, Vec::new())}
+			) _ "]" {?
+				#[cfg(feature = "exp-destruct")] return Ok(expr::Destruct::Array {
+					start,
+					rest: rest.0,
+					end: rest.1,
+				});
+				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")
+			}
+		pub rule destruct_object(s: &ParserSettings) -> Destruct
+			= "{" _
+				fields:(name:id() into:(_ ":" _ into:destruct(s) {into})? default:(_ "=" _ v:expr(s) {v})? {(name, into, default.map(Rc::new))})**comma()
+				rest:(
+					comma() rest:destruct_rest()? {rest}
+					/ comma()? {None}
+				)
+			_ "}" {?
+				#[cfg(feature = "exp-destruct")] return Ok(expr::Destruct::Object {
+					fields,
+					rest,
+				});
+				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")
+			}
+		pub rule destruct(s: &ParserSettings) -> Destruct
+			= v:id() {Destruct::Full(v)}
+			/ "?" {?
+				#[cfg(feature = "exp-destruct")] return Ok(expr::Destruct::Skip);
+				#[cfg(not(feature = "exp-destruct"))] Err("!!!experimental destructuring was not enabled")
+			}
+			/ arr:destruct_array(s) {arr}
+			/ obj:destruct_object(s) {obj}
+
+		pub rule bind(s: &ParserSettings) -> BindSpec
+			= into:destruct(s) _ "=" _ value:expr(s) {BindSpec::Field{into, value: Rc::new(value)}}
+			/ name:id() _ "(" _ params:params(s) _ ")" _ "=" _ value:expr(s) {BindSpec::Function{name, params, value: Rc::new(value)}}
+
+		pub rule assertion(s: &ParserSettings) -> AssertStmt
+			= keyword("assert") _ cond:expr(s) msg:(_ ":" _ e:expr(s) {e})? { AssertStmt(cond, msg) }
+
+		pub rule whole_line() -> &'input str
+			= str:$((!['\n'][_])* "\n") {str}
+		pub rule string_block() -> String
+			= "|||" chomped:"-"? (!['\n']single_whitespace())* "\n"
+			empty_lines:$(['\n']*)
+			prefix:[' ' | '\t']+ first_line:whole_line()
+			lines:("\n" {"\n"} / [' ' | '\t']*<{prefix.len()}> s:whole_line() {s})*
+			[' ' | '\t']*<, {prefix.len() - 1}> "|||"
+			{
+				let mut l = empty_lines.to_owned();
+				l.push_str(first_line);
+				l.extend(lines);
+				if chomped.is_some() {
+					debug_assert!(l.ends_with('\n'));
+					l.truncate(l.len() - 1);
+				}
+				l
+			}
+
+		rule hex_char()
+			= quiet! { ['0'..='9' | 'a'..='f' | 'A'..='F'] } / expected!("<hex char>")
+
+		rule string_char(c: rule<()>)
+			= (!['\\']!c()[_])+
+			/ "\\\\"
+			/ "\\u" hex_char() hex_char() hex_char() hex_char()
+			/ "\\x" hex_char() hex_char()
+			/ ['\\'] (quiet! { ['b' | 'f' | 'n' | 'r' | 't' | '"' | '\''] } / expected!("<escape character>"))
+		pub rule string() -> String
+			= ['"'] str:$(string_char(<"\"">)*) ['"'] {? unescape::unescape(str).ok_or("<escaped string>")}
+			/ ['\''] str:$(string_char(<"\'">)*) ['\''] {? unescape::unescape(str).ok_or("<escaped string>")}
+			/ quiet!{ "@'" str:$(("''" / (!['\''][_]))*) "'" {str.replace("''", "'")}
+			/ "@\"" str:$(("\"\"" / (!['"'][_]))*) "\"" {str.replace("\"\"", "\"")}
+			/ string_block() } / expected!("<string>")
+
+		pub rule field_name(s: &ParserSettings) -> FieldName
+			= name:id() {FieldName::Fixed(name)}
+			/ name:string() {FieldName::Fixed(name.into())}
+			/ "[" _ expr:expr(s) _ "]" {FieldName::Dyn(expr)}
+		pub rule visibility() -> Visibility
+			= ":::" {Visibility::Unhide}
+			/ "::" {Visibility::Hidden}
+			/ ":" {Visibility::Normal}
+		pub rule field(s: &ParserSettings) -> FieldMember
+			= name:field_name(s) _ plus:"+"? _ visibility:visibility() _ value:expr(s) {FieldMember{
+				name,
+				plus: plus.is_some(),
+				params: None,
+				visibility,
+				value: Rc::new(value),
+			}}
+			/ name:field_name(s) _ "(" _ params:params(s) _ ")" _ visibility:visibility() _ value:expr(s) {FieldMember{
+				name,
+				plus: false,
+				params: Some(params),
+				visibility,
+				value: Rc::new(value),
+			}}
+		pub rule obj_local(s: &ParserSettings) -> BindSpec
+			= keyword("local") _ bind:bind(s) {bind}
+		pub rule member(s: &ParserSettings) -> Member
+			= bind:obj_local(s) {Member::BindStmt(bind)}
+			/ assertion:assertion(s) {Member::AssertStmt(assertion)}
+			/ field:field(s) {Member::Field(field)}
+		pub rule objinside(s: &ParserSettings) -> ObjBody
+			=  members:(member(s) ** comma()) comma()? _ compspecs:compspecs(s)? {?
+				Ok(if let Some(compspecs) = compspecs {
+					let mut locals = Vec::new();
+					let mut field = None;
+					for member in members {
+						match member {
+							Member::Field(field_member) => if field.replace(field_member).is_some() {
+								return Err("<object comprehension can only contain one field>")
+							},
+							Member::BindStmt(bind_spec) => locals.push(bind_spec),
+							Member::AssertStmt(assert_stmt) => return Err("<asserts are unsupported in object comprehension>"),
+						}
+					}
+					ObjBody::ObjComp(ObjComp {
+						locals: Rc::new(locals),
+						field: field.map(Rc::new).ok_or("<missing object comprehension field>")?,
+						compspecs
+					})
+				} else {
+					let mut locals = Vec::new();
+					let mut asserts = Vec::new();
+					let mut fields = Vec::new();
+					for member in members {
+						match member {
+							Member::Field(field_member) => fields.push(field_member),
+							Member::BindStmt(bind_spec) => locals.push(bind_spec),
+							Member::AssertStmt(assert_stmt) => asserts.push(assert_stmt),
+						}
+					}
+					ObjBody::MemberList(ObjMembers {
+						locals: Rc::new(locals),
+						asserts: Rc::new(asserts),
+						fields
+					})
+				})
+			}
+		pub rule ifspec(s: &ParserSettings) -> IfSpecData
+			= keyword("if") _ expr:expr(s) {IfSpecData(expr)}
+		pub rule forspec(s: &ParserSettings) -> ForSpecData
+			= keyword("for") _ id:destruct(s) _ keyword("in") _ cond:expr(s) {ForSpecData(id, cond)}
+		rule compspec(s: &ParserSettings) -> CompSpec
+			= i:ifspec(s) { CompSpec::IfSpec(i) } / f:forspec(s) {CompSpec::ForSpec(f)}
+		pub rule compspecs(s: &ParserSettings) -> Vec<CompSpec>
+			= specs:compspec(s) ++ _ {?
+				if !matches!(specs[0], CompSpec::ForSpec(_)) {
+					return Err("<first compspec should be for>")
+				}
+				Ok(specs)
+			}
+		pub rule local_expr(s: &ParserSettings) -> Expr
+			= keyword("local") _ binds:bind(s) ** comma() (_ ",")? _ ";" _ expr:expr(s) { Expr::LocalExpr(binds, Box::new(expr)) }
+		pub rule string_expr(s: &ParserSettings) -> Expr
+			= s:string() {Expr::Str(s.into())}
+		pub rule obj_expr(s: &ParserSettings) -> Expr
+			= "{" _ body:objinside(s) _ "}" {Expr::Obj(body)}
+		pub rule array_expr(s: &ParserSettings) -> Expr
+			= "[" _ elems:(expr(s) ** comma()) _ comma()? "]" {Expr::Arr(Rc::new(elems))}
+		pub rule array_comp_expr(s: &ParserSettings) -> Expr
+			= "[" _ expr:expr(s) _ comma()? _ specs:(r: compspecs(s) _ {r}) "]" {
+				Expr::ArrComp(Rc::new(expr), specs)
+			}
+		pub rule number_expr(s: &ParserSettings) -> Expr
+			= n:number() {? if n.is_finite() {
+				Ok(Expr::Num(n))
+			} else {
+				Err("!!!numbers are finite")
+			}}
+		pub rule var_expr(s: &ParserSettings) -> Expr
+			= n:id() { Expr::Var(n) }
+		pub rule id_loc(s: &ParserSettings) -> Spanned<Expr>
+			= a:position!() n:id() b:position!() { Spanned::new(Expr::Str(n), Span(s.source.clone(), a as u32,b as u32)) }
+		pub rule if_then_else_expr(s: &ParserSettings) -> Expr
+			= cond:ifspec(s) _ keyword("then") _ cond_then:expr(s) cond_else:(_ keyword("else") _ e:expr(s) {e})? {Expr::IfElse(Box::new(IfElse{
+				cond,
+				cond_then,
+				cond_else,
+			}))}
+
+		pub rule literal(s: &ParserSettings) -> Expr
+			= v:(
+				keyword("null") {LiteralType::Null}
+				/ keyword("true") {LiteralType::True}
+				/ keyword("false") {LiteralType::False}
+				/ keyword("self") {LiteralType::This}
+				/ keyword("$") {LiteralType::Dollar}
+				/ keyword("super") {LiteralType::Super}
+			) {Expr::Literal(v)}
+
+		rule import_kind() -> ImportKind
+			= keyword("importstr") { ImportKind::Str }
+			/ keyword("importbin") { ImportKind::Bin }
+			/ keyword("import") { ImportKind::Normal }
+
+		pub rule expr_basic(s: &ParserSettings) -> Expr
+			= literal(s)
+
+			/ string_expr(s) / number_expr(s)
+			/ array_expr(s)
+			/ obj_expr(s)
+			/ array_expr(s)
+			/ array_comp_expr(s)
+
+			/ kind:import_kind() _ path:expr(s) {Expr::Import(kind, Box::new(path))}
+
+			/ var_expr(s)
+			/ local_expr(s)
+			/ if_then_else_expr(s)
+
+			/ keyword("function") _ "(" _ params:params(s) _ ")" _ expr:expr(s) {Expr::Function(params, Rc::new(expr))}
+			/ assert:assertion(s) _ ";" _ rest:expr(s) { Expr::AssertExpr(Rc::new(AssertExpr{
+				assert, rest
+			})) }
+
+			/ keyword("error") _ expr:expr(s) { Expr::ErrorStmt(Box::new(expr)) }
+
+		rule slice_part(s: &ParserSettings) -> Option<Spanned<Expr>>
+			= _ e:(e:expr(s) _{e})? {e}
+		pub rule slice_desc(s: &ParserSettings) -> SliceDesc
+			= start:slice_part(s) ":" pair:(end:slice_part(s) step:(":" e:slice_part(s){e})? {(end, step.flatten())})? {
+				let (end, step) = if let Some((end, step)) = pair {
+					(end, step)
+				}else{
+					(None, None)
+				};
+
+				SliceDesc { start, end, step }
+			}
+
+		rule binop(x: rule<()>) -> ()
+			= quiet!{ x() } / expected!("<binary op>")
+		rule unaryop(x: rule<()>) -> ()
+			= quiet!{ x() } / expected!("<unary op>")
+
+		rule ensure_null_coaelse()
+			= "" {?
+				#[cfg(not(feature = "exp-null-coaelse"))] return Err("!!!experimental null coaelscing was not enabled");
+				#[cfg(feature = "exp-null-coaelse")] Ok(())
+			}
+		use jrsonnet_ir::BinaryOpType::*;
+		use jrsonnet_ir::UnaryOpType::*;
+		rule expr(s: &ParserSettings) -> Spanned<Expr>
+			= precedence! {
+				"(" _ e:expr(s) _ ")" {e}
+				start:position!() v:@ end:position!() { Spanned::new(v, Span(s.source.clone(), start as u32, end as u32)) }
+				--
+				a:(@) _ binop(<"||">) _ b:@ {expr_bin!(a Or b)}
+				a:(@) _ binop(<"??">) _ ensure_null_coaelse() b:@ {
+					#[cfg(feature = "exp-null-coaelse")] return expr_bin!(a NullCoaelse b);
+					unreachable!("ensure_null_coaelse will fail if feature is not enabled")
+				}
+				--
+				a:(@) _ binop(<"&&">) _ b:@ {expr_bin!(a And b)}
+				--
+				a:(@) _ binop(<"|">) _ b:@ {expr_bin!(a BitOr b)}
+				--
+				a:@ _ binop(<"^">) _ b:(@) {expr_bin!(a BitXor b)}
+				--
+				a:(@) _ binop(<"&">) _ b:@ {expr_bin!(a BitAnd b)}
+				--
+				a:(@) _ binop(<"==">) _ b:@ {expr_bin!(a Eq b)}
+				a:(@) _ binop(<"!=">) _ b:@ {expr_bin!(a Neq b)}
+				--
+				a:(@) _ binop(<"<">) _ b:@ {expr_bin!(a Lt b)}
+				a:(@) _ binop(<">">) _ b:@ {expr_bin!(a Gt b)}
+				a:(@) _ binop(<"<=">) _ b:@ {expr_bin!(a Lte b)}
+				a:(@) _ binop(<">=">) _ b:@ {expr_bin!(a Gte b)}
+				a:(@) _ binop(<keyword("in")>) _ b:@ {expr_bin!(a In b)}
+				--
+				a:(@) _ binop(<"<<">) _ b:@ {expr_bin!(a Lhs b)}
+				a:(@) _ binop(<">>">) _ b:@ {expr_bin!(a Rhs b)}
+				--
+				a:(@) _ binop(<"+">) _ b:@ {expr_bin!(a Add b)}
+				a:(@) _ binop(<"-">) _ b:@ {expr_bin!(a Sub b)}
+				--
+				a:(@) _ binop(<"*">) _ b:@ {expr_bin!(a Mul b)}
+				a:(@) _ binop(<"/">) _ b:@ {expr_bin!(a Div b)}
+				a:(@) _ binop(<"%">) _ b:@ {expr_bin!(a Mod b)}
+				--
+						unaryop(<"+">) _ b:@ {expr_un!(Plus b)}
+						unaryop(<"-">) _ b:@ {expr_un!(Minus b)}
+						unaryop(<"!">) _ b:@ {expr_un!(Not b)}
+						unaryop(<"~">) _ b:@ {expr_un!(BitNot b)}
+				--
+				value:(@) _ "[" _ slice:slice_desc(s) _ "]" {Expr::Slice(Box::new(Slice{value, slice}))}
+				indexable:(@) _ parts:index_part(s)+ {Expr::Index{indexable: Box::new(indexable), parts}}
+				a:(@) _ "(" _ args:args(s) _ ")" ts:(_ keyword("tailstrict"))? {Expr::Apply(Box::new(a), args, ts.is_some())}
+				a:(@) _ "{" _ body:objinside(s) _ "}" {Expr::ObjExtend(Rc::new(a), body)}
+				--
+				e:expr_basic(s) {e}
+			}
+		pub rule index_part(s: &ParserSettings) -> IndexPart
+		= n:("?" _ ensure_null_coaelse())? "." _ value:id_loc(s) {IndexPart {
+			value,
+			#[cfg(feature = "exp-null-coaelse")]
+			null_coaelse: n.is_some(),
+		}}
+		/ n:("?" _ "." _ ensure_null_coaelse())? "[" _ value:expr(s) _ "]" {IndexPart {
+			value,
+			#[cfg(feature = "exp-null-coaelse")]
+			null_coaelse: n.is_some(),
+		}}
+
+		pub rule jsonnet(s: &ParserSettings) -> Spanned<Expr> = _ e:expr(s) _ {e}
+	}
+}
+
+pub type ParseError = peg::error::ParseError<peg::str::LineCol>;
+pub fn parse(str: &str, settings: &ParserSettings) -> Result<Spanned<Expr>, ParseError> {
+	jsonnet_parser::jsonnet(str, settings)
+}
+/// Used for importstr values
+pub fn string_to_expr(str: IStr, settings: &ParserSettings) -> Spanned<Expr> {
+	let len = str.len();
+	Spanned::new(Expr::Str(str), Span(settings.source.clone(), 0, len as u32))
+}
+
+#[cfg(test)]
+pub mod tests {
+	use insta::assert_snapshot;
+	use jrsonnet_ir::{IStr, Source};
+
+	use super::parse;
+	use crate::ParserSettings;
+
+	fn parsep(s: &str) -> String {
+		let v = parse(
+			s,
+			&ParserSettings {
+				source: Source::new_virtual("<test>".into(), IStr::empty()),
+			},
+		)
+		.unwrap();
+		format!("{v:#?}")
+	}
+
+	macro_rules! parse {
+		($s:expr) => {
+			assert_snapshot!(parsep($s));
+		};
+	}
+
+	#[test]
+	fn multiline_string() {
+		parse!("|||\n    Hello world!\n     a\n|||");
+		parse!("|||\n  Hello world!\n   a\n|||");
+		parse!("|||\n\t\tHello world!\n\t\t\ta\n|||");
+		parse!("|||\n   Hello world!\n    a\n |||");
+	}
+
+	#[test]
+	fn slice() {
+		parse!("a[1:]");
+		parse!("a[1::]");
+		parse!("a[:1:]");
+		parse!("a[::1]");
+		parse!("str[:len - 1]");
+	}
+
+	#[test]
+	fn string_escaping() {
+		parse!(r#""Hello, \"world\"!""#);
+		parse!(r#"'Hello \'world\'!'"#);
+		parse!(r#"'\\\\'"#);
+	}
+
+	#[test]
+	fn string_unescaping() {
+		parse!(r#""Hello\nWorld""#);
+	}
+
+	#[test]
+	fn string_verbantim() {
+		parse!(r#"@"Hello\n""World""""#);
+	}
+
+	#[test]
+	fn imports() {
+		parse!("import \"hello\"");
+		parse!("importstr \"garnish.txt\"");
+		parse!("importbin \"garnish.bin\"");
+	}
+
+	#[test]
+	fn empty_object() {
+		parse!("{}");
+	}
+
+	#[test]
+	fn basic_math() {
+		parse!("2+2*2");
+		parse!("2	+ 	  2	  *	2   	");
+		parse!("2+(2+2*2)");
+		parse!("2//comment\n+//comment\n3/*test*/*/*test*/4");
+	}
+
+	#[test]
+	fn suffix() {
+		parse!("std.test");
+		parse!("std(2)");
+		parse!("std.test(2)");
+		parse!("a[b]");
+	}
+
+	#[test]
+	fn array_comp() {
+		parse!("[std.deepJoin(x) for x in arr]");
+	}
+
+	#[test]
+	fn reserved() {
+		parse!("null");
+		parse!("nulla");
+	}
+
+	#[test]
+	fn multiple_args_buf() {
+		parse!("a(b, null_fields)");
+	}
+
+	#[test]
+	fn infix_precedence() {
+		parse!("!a && !b");
+		parse!("!a / !b");
+	}
+
+	#[test]
+	fn double_negation() {
+		parse!("!!a");
+	}
+
+	#[test]
+	fn array_test_error() {
+		parse!("[a for a in b if c for e in f]");
+	}
+
+	#[test]
+	fn missing_newline_between_comment_and_eof() {
+		parse!(
+			"{a:1}
+
+			//+213"
+		);
+	}
+
+	#[test]
+	fn default_param_before_nondefault() {
+		parse!("local x(foo = 'foo', bar) = null; null");
+	}
+
+	#[test]
+	fn add_location_info_to_all_sub_expressions() {
+		parse!("{} { local x = 1, x: x } + {}");
+	}
+}
addedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__add_location_info_to_all_sub_expressions.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__add_location_info_to_all_sub_expressions.snap
@@ -0,0 +1,57 @@
+---
+source: crates/jrsonnet-peg-parser/src/lib.rs
+expression: "parsep(\"{} { local x = 1, x: x } + {}\")"
+---
+BinaryOp(
+    BinaryOp {
+        lhs: ObjExtend(
+            Obj(
+                MemberList(
+                    ObjMembers {
+                        locals: [],
+                        asserts: [],
+                        fields: [],
+                    },
+                ),
+            ) from virtual:<test>:0-2,
+            MemberList(
+                ObjMembers {
+                    locals: [
+                        Field {
+                            into: Full(
+                                "x",
+                            ),
+                            value: Num(
+                                1.0,
+                            ) from virtual:<test>:15-16,
+                        },
+                    ],
+                    asserts: [],
+                    fields: [
+                        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(
+                ObjMembers {
+                    locals: [],
+                    asserts: [],
+                    fields: [],
+                },
+            ),
+        ) from virtual:<test>:27-29,
+    },
+) from virtual:<test>:0-29
addedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__array_comp.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__array_comp.snap
@@ -0,0 +1,41 @@
+---
+source: crates/jrsonnet-peg-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-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__array_test_error.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__array_test_error.snap
@@ -0,0 +1,38 @@
+---
+source: crates/jrsonnet-peg-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-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__basic_math.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__basic_math.snap
@@ -0,0 +1,23 @@
+---
+source: crates/jrsonnet-peg-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-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__default_param_before_nondefault.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__default_param_before_nondefault.snap
@@ -0,0 +1,54 @@
+---
+source: crates/jrsonnet-peg-parser/src/lib.rs
+expression: "parsep(\"local x(foo = 'foo', bar) = null; null\")"
+---
+LocalExpr(
+    [
+        Function {
+            name: "x",
+            params: ExprParams {
+                exprs: [
+                    ExprParam {
+                        destruct: Full(
+                            "foo",
+                        ),
+                        default: Some(
+                            Str(
+                                "foo",
+                            ) from virtual:<test>:14-19,
+                        ),
+                    },
+                    ExprParam {
+                        destruct: Full(
+                            "bar",
+                        ),
+                        default: None,
+                    },
+                ],
+                signature: FunctionSignature(
+                    [
+                        ParamParse {
+                            name: Named(
+                                "foo",
+                            ),
+                            default: Exists,
+                        },
+                        ParamParse {
+                            name: Named(
+                                "bar",
+                            ),
+                            default: None,
+                        },
+                    ],
+                ),
+                binds_len: 2,
+            },
+            value: Literal(
+                Null,
+            ) from virtual:<test>:28-32,
+        },
+    ],
+    Literal(
+        Null,
+    ) from virtual:<test>:34-38,
+) from virtual:<test>:0-38
addedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__double_negation.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__double_negation.snap
@@ -0,0 +1,13 @@
+---
+source: crates/jrsonnet-peg-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-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__empty_object.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__empty_object.snap
@@ -0,0 +1,13 @@
+---
+source: crates/jrsonnet-peg-parser/src/lib.rs
+expression: "parsep(\"{}\")"
+---
+Obj(
+    MemberList(
+        ObjMembers {
+            locals: [],
+            asserts: [],
+            fields: [],
+        },
+    ),
+) from virtual:<test>:0-2
addedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__imports.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__imports.snap
@@ -0,0 +1,10 @@
+---
+source: crates/jrsonnet-peg-parser/src/lib.rs
+expression: "parsep(\"import \\\"hello\\\"\")"
+---
+Import(
+    Normal,
+    Str(
+        "hello",
+    ) from virtual:<test>:7-14,
+) from virtual:<test>:0-14
addedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__infix_precedence.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__infix_precedence.snap
@@ -0,0 +1,21 @@
+---
+source: crates/jrsonnet-peg-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-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__missing_newline_between_comment_and_eof.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__missing_newline_between_comment_and_eof.snap
@@ -0,0 +1,25 @@
+---
+source: crates/jrsonnet-peg-parser/src/lib.rs
+expression: "parsep(\"{a:1}\n\n\t\t\t//+213\")"
+---
+Obj(
+    MemberList(
+        ObjMembers {
+            locals: [],
+            asserts: [],
+            fields: [
+                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-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__multiline_string.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__multiline_string.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-peg-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-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__multiple_args_buf.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__multiple_args_buf.snap
@@ -0,0 +1,21 @@
+---
+source: crates/jrsonnet-peg-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-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__reserved.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__reserved.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-peg-parser/src/lib.rs
+expression: "parsep(\"null\")"
+---
+Literal(
+    Null,
+) from virtual:<test>:0-4
addedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__slice.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__slice.snap
@@ -0,0 +1,20 @@
+---
+source: crates/jrsonnet-peg-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-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__string_escaping.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__string_escaping.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-peg-parser/src/lib.rs
+expression: "parsep(r#\"\"Hello, \\\"world\\\"!\"\"#)"
+---
+Str(
+    "Hello, \"world\"!",
+) from virtual:<test>:0-19
addedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__string_unescaping.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__string_unescaping.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-peg-parser/src/lib.rs
+expression: "parsep(r#\"\"Hello\\nWorld\"\"#)"
+---
+Str(
+    "Hello\nWorld",
+) from virtual:<test>:0-14
addedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__string_verbantim.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__string_verbantim.snap
@@ -0,0 +1,7 @@
+---
+source: crates/jrsonnet-peg-parser/src/lib.rs
+expression: "parsep(r#\"@\"Hello\\n\"\"World\"\"\"\"#)"
+---
+Str(
+    "Hello\\n\"World\"",
+) from virtual:<test>:0-19
addedcrates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__suffix.snapdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-peg-parser/src/snapshots/jrsonnet_peg_parser__tests__suffix.snap
@@ -0,0 +1,16 @@
+---
+source: crates/jrsonnet-peg-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-stdlib/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/Cargo.toml
+++ b/crates/jrsonnet-stdlib/Cargo.toml
@@ -16,14 +16,17 @@
 # Bigint type
 exp-bigint = ["dep:num-bigint", "jrsonnet-evaluator/exp-bigint"]
 
-exp-null-coaelse = ["jrsonnet-parser/exp-null-coaelse", "jrsonnet-evaluator/exp-null-coaelse"]
+exp-null-coaelse = [
+	"jrsonnet-ir/exp-null-coaelse",
+	"jrsonnet-evaluator/exp-null-coaelse",
+]
 # std.regexMatch and other helpers
 exp-regex = ["dep:regex", "dep:lru", "dep:rustc-hash"]
 
 [dependencies]
 jrsonnet-evaluator.workspace = true
 jrsonnet-macros.workspace = true
-jrsonnet-parser.workspace = true
+jrsonnet-ir.workspace = true
 jrsonnet-gcmodule.workspace = true
 
 # Used for std.parseJson/std.parseYaml
@@ -50,6 +53,3 @@
 regex = { workspace = true, optional = true }
 lru = { workspace = true, optional = true }
 rustc-hash = { workspace = true, optional = true }
-
-[build-dependencies]
-jrsonnet-parser.workspace = true
addedcrates/jrsonnet-stdlib/resultdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-stdlib/result
@@ -0,0 +1 @@
+/nix/store/2fxmjvz4f6c8g6gi37y35h356vj0n8k6-benchmarks
\ No newline at end of file
modifiedcrates/jrsonnet-stdlib/src/compat.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/compat.rs
+++ b/crates/jrsonnet-stdlib/src/compat.rs
@@ -8,7 +8,7 @@
 #[allow(non_snake_case)]
 pub fn builtin___compare(v1: Val, v2: Val) -> Result<i32> {
 	Ok(
-		match evaluate_compare_op(&v1, &v2, jrsonnet_parser::BinaryOpType::Lt)? {
+		match evaluate_compare_op(&v1, &v2, jrsonnet_ir::BinaryOpType::Lt)? {
 			Ordering::Less => -1,
 			Ordering::Equal => 0,
 			Ordering::Greater => 1,
@@ -30,7 +30,7 @@
 			let ordering = evaluate_compare_op(
 				&Val::Arr(arr1),
 				&Val::Arr(arr2),
-				jrsonnet_parser::BinaryOpType::Lt,
+				jrsonnet_ir::BinaryOpType::Lt,
 			)?;
 			Ok($operator.contains(&ordering))
 		}
modifiedcrates/jrsonnet-stdlib/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/lib.rs
+++ b/crates/jrsonnet-stdlib/src/lib.rs
@@ -20,7 +20,7 @@
 	ContextBuilder, IStr, ObjValue, ObjValueBuilder, Thunk, Val,
 };
 use jrsonnet_gcmodule::{Acyclic, Cc, Trace};
-use jrsonnet_parser::Source;
+use jrsonnet_ir::Source;
 pub use manifest::*;
 pub use math::*;
 pub use misc::*;
modifiedcrates/jrsonnet-stdlib/src/manifest/ini.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/manifest/ini.rs
+++ b/crates/jrsonnet-stdlib/src/manifest/ini.rs
@@ -4,8 +4,8 @@
 	manifest::{ManifestFormat, ToStringFormat},
 	typed::{FromUntyped, Typed},
 	ObjValue, Result, ResultExt, Val,
+	IStr,
 };
-use jrsonnet_parser::IStr;
 
 pub struct IniFormat {
 	#[cfg(feature = "exp-preserve-order")]
modifiedcrates/jrsonnet-stdlib/src/sets.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/sets.rs
+++ b/crates/jrsonnet-stdlib/src/sets.rs
@@ -3,7 +3,7 @@
 use jrsonnet_evaluator::{
 	function::builtin, operator::evaluate_compare_op, val::ArrValue, Result, Thunk, Val,
 };
-use jrsonnet_parser::BinaryOpType;
+use jrsonnet_ir::BinaryOpType;
 
 use crate::keyf::KeyF;
 
modifiedcrates/jrsonnet-stdlib/src/sort.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/sort.rs
+++ b/crates/jrsonnet-stdlib/src/sort.rs
@@ -9,7 +9,7 @@
 	val::{equals, ArrValue},
 	Result, Thunk, Val,
 };
-use jrsonnet_parser::BinaryOpType;
+use jrsonnet_ir::BinaryOpType;
 
 use crate::{eval_on_empty, keyf::KeyF};
 
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -160,6 +160,7 @@
               ]
               ++ lib.optionals (!stdenv.isDarwin) [
                 valgrind
+                kdePackages.kcachegrind
               ];
           };
         };
modifiedxtask/src/main.rsdiffbeforeafterboth
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -52,10 +52,9 @@
 		} => {
 			let out = sh.create_temp_dir()?;
 
-			// build-std
 			cmd!(
 				sh,
-				"cargo build -Zbuild-std --target={target} --profile releasedebug"
+				"cargo build --target={target} --profile releasedebug"
 			)
 			.run()?;
 			let built = format!("./target/{target}/releasedebug/jrsonnet");