git.delta.rocks / jrsonnet / refs/commits / 7eb771ff363d

difftreelog

feat allow both parsers at the same time

uwkkuzmuYaroslav Bolyukin2026-03-23parent: #b6f9e83.patch.diff
in: master

4 files changed

modifiedcrates/jrsonnet-evaluator/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/Cargo.toml
+++ b/crates/jrsonnet-evaluator/Cargo.toml
@@ -18,24 +18,26 @@
 explaining-traces = ["annotate-snippets", "hi-doc"]
 # Allows library authors to throw custom errors
 anyhow-error = ["anyhow"]
-# Use hand-written recursive descent parser instead of PEG parser
+# Use hand-written recursive descent parser
 ir-parser = ["dep:jrsonnet-ir-parser"]
+# Use PEG parser
+peg-parser = ["dep:jrsonnet-peg-parser"]
 
 # Allows to preserve field order in objects
 exp-preserve-order = []
 # Implements field destructuring
-exp-destruct = ["jrsonnet-peg-parser/exp-destruct"]
+exp-destruct = ["jrsonnet-peg-parser?/exp-destruct", "jrsonnet-ir-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-peg-parser/exp-null-coaelse", "jrsonnet-ir-parser?/exp-null-coaelse"]
+exp-null-coaelse = ["jrsonnet-peg-parser?/exp-null-coaelse", "jrsonnet-ir-parser?/exp-null-coaelse"]
 
 [dependencies]
 jrsonnet-interner.workspace = true
 jrsonnet-ir.workspace = true
-jrsonnet-peg-parser.workspace = true
+jrsonnet-peg-parser = { workspace = true, optional = true }
 jrsonnet-ir-parser = { workspace = true, optional = true }
 jrsonnet-types.workspace = true
 jrsonnet-macros.workspace = true
modifiedcrates/jrsonnet-evaluator/src/async_import.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/async_import.rs
+++ b/crates/jrsonnet-evaluator/src/async_import.rs
@@ -7,10 +7,6 @@
 	FieldMember, FieldName, ForSpecData, IfElse, IfSpecData, ImportKind, ObjBody, Slice, SliceDesc,
 	Source, SourcePath, Spanned,
 };
-#[cfg(feature = "ir-parser")]
-use jrsonnet_ir_parser::ParserSettings;
-#[cfg(not(feature = "ir-parser"))]
-use jrsonnet_peg_parser::ParserSettings;
 use rustc_hash::FxHashMap;
 
 use crate::{AsPathLike, FileData, ImportResolver, ResolvePathOwned, State};
@@ -326,7 +322,7 @@
 						};
 						let source = Source::new(path.clone(), code.clone());
 						// If failed - then skip import
-						file.parsed = crate::parse_jsonnet(&code, &ParserSettings { source })
+						file.parsed = crate::parse_jsonnet(&code, 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
@@ -14,6 +14,22 @@
 	ObjValue, ResolvePathOwned,
 };
 
+#[derive(Debug, Clone)]
+pub struct SyntaxErrorLocation {
+	pub offset: usize,
+}
+
+#[derive(Debug, Clone)]
+pub struct SyntaxError {
+	pub message: String,
+	pub location: SyntaxErrorLocation,
+}
+impl fmt::Display for SyntaxError {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "{}", self.message)
+	}
+}
+
 pub(crate) fn format_found(list: &[IStr], what: &str) -> String {
 	if list.is_empty() {
 		return String::new();
@@ -154,31 +170,11 @@
 	ImportNotSupported(SourcePath, ResolvePathOwned),
 	#[error("can't import from virtual file")]
 	CantImportFromVirtualFile,
-	#[cfg(not(feature = "ir-parser"))]
-	#[error(
-		"syntax error: {}",
-		// Peg has no fancier way to handle critical parsing errors https://github.com/kevinmehall/rust-peg/issues/225
-		{.error.expected.tokens().find(|t| t.starts_with("!!!")).map_or_else(|| {
-			format!(
-				"expected {}, got {:?}",
-				.error.expected,
-				.path.code().chars().nth(error.location.offset)
-				.map_or_else(|| "EOF".into(), |c| c.to_string())
-			)
-		}, |v| v[3..].into())}
-	)]
-	ImportSyntaxError {
-		path: Source,
-		#[trace(skip)]
-		error: Box<jrsonnet_peg_parser::ParseError>,
-	},
-
-	#[cfg(feature = "ir-parser")]
 	#[error("syntax error: {error}")]
 	ImportSyntaxError {
 		path: Source,
 		#[trace(skip)]
-		error: Box<jrsonnet_ir_parser::ParseError>,
+		error: Box<SyntaxError>,
 	},
 
 	#[error("runtime error: {}", format_empty_str(.0))]
modifiedcrates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/lib.rs
1//! jsonnet interpreter implementation2#![cfg_attr(nightly, feature(thread_local, type_alias_impl_trait))]34// For jrsonnet-macros5extern crate self as jrsonnet_evaluator;67mod arr;8// pub mod async_import;9mod ctx;10mod dynamic;11pub mod error;12mod evaluate;13pub mod function;14pub mod gc;15mod import;16mod integrations;17pub mod manifest;18mod map;19mod obj;20pub mod stack;21pub mod stdlib;22pub mod tla;23pub mod trace;24pub mod typed;25pub mod val;2627use std::{28	any::Any,29	cell::{RefCell, RefMut},30	clone::Clone,31	collections::hash_map::Entry,32	fmt::{self, Debug},33	marker::PhantomData,34	rc::Rc,35};3637pub use ctx::*;38pub use dynamic::*;39pub use error::{Error, ErrorKind::*, Result, ResultExt};40pub use evaluate::*;41use function::CallLocation;42pub use import::*;43use jrsonnet_gcmodule::{cc_dyn, Cc, Trace};44pub use jrsonnet_interner::{IBytes, IStr};45pub use jrsonnet_ir as parser;46use jrsonnet_ir::{Expr, Source, SourcePath};47#[doc(hidden)]48pub use jrsonnet_macros;49#[cfg(feature = "ir-parser")]50use jrsonnet_ir_parser::ParserSettings;51#[cfg(not(feature = "ir-parser"))]52use jrsonnet_peg_parser::ParserSettings;53pub use obj::*;54pub use rustc_hash;55use rustc_hash::FxHashMap;56use stack::check_depth;57pub use tla::apply_tla;58pub use val::{Thunk, Val};5960use crate::gc::WithCapacityExt as _;6162#[cfg(feature = "ir-parser")]63pub(crate) fn parse_jsonnet(64	code: &str,65	settings: &ParserSettings,66) -> Result<Expr, jrsonnet_ir_parser::ParseError> {67	jrsonnet_ir_parser::parse(code, settings)68}6970#[cfg(not(feature = "ir-parser"))]71pub(crate) fn parse_jsonnet(72	code: &str,73	settings: &ParserSettings,74) -> Result<Expr, jrsonnet_peg_parser::ParseError> {75	jrsonnet_peg_parser::parse(code, settings)76}7778cc_dyn!(79	#[derive(Clone)]80	CcUnbound<V>,81	Unbound<Bound = V>82);8384/// Thunk without bound `super`/`this`85/// object inheritance may be overriden multiple times, and will be fixed only on field read86pub trait Unbound: Trace {87	/// Type of value after object context is bound88	type Bound;89	/// Create value bound to specified object context90	fn bind(&self, sup_this: SupThis) -> Result<Self::Bound>;91}9293/// Object fields may, or may not depend on `this`/`super`, this enum allows cheaper reuse of object-independent fields for native code94/// Standard jsonnet fields are always unbound95#[derive(Clone, Trace)]96pub enum MaybeUnbound {97	/// Value needs to be bound to `this`/`super`98	Unbound(CcUnbound<Val>),99	/// Value is object-independent100	Bound(Thunk<Val>),101}102103impl Debug for MaybeUnbound {104	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {105		write!(f, "MaybeUnbound")106	}107}108impl MaybeUnbound {109	/// Attach object context to value, if required110	pub fn evaluate(&self, sup_this: SupThis) -> Result<Val> {111		match self {112			Self::Unbound(v) => v.0.bind(sup_this),113			Self::Bound(v) => Ok(v.evaluate()?),114		}115	}116}117118cc_dyn!(CcContextInitializer, ContextInitializer);119120/// During import, this trait will be called to create initial context for file.121/// It may initialize global variables, stdlib for example.122pub trait ContextInitializer: Trace {123	/// For which size the builder should be preallocated124	fn reserve_vars(&self) -> usize {125		0126	}127	/// Initialize default file context.128	/// Has default implementation, which calls `populate`.129	/// Prefer to always implement `populate` instead.130	fn initialize(&self, for_file: Source) -> Context {131		let mut builder = ContextBuilder::with_capacity(self.reserve_vars());132		self.populate(for_file, &mut builder);133		builder.build()134	}135	/// For composability: extend builder. May panic if this initialization is not supported,136	/// and the context may only be created via `initialize`.137	fn populate(&self, for_file: Source, builder: &mut ContextBuilder);138	/// Allows upcasting from abstract to concrete context initializer.139	/// jrsonnet by itself doesn't use this method, it is allowed for it to panic.140	fn as_any(&self) -> &dyn Any;141}142143/// Context initializer which adds nothing.144impl ContextInitializer for () {145	fn populate(&self, _for_file: Source, _builder: &mut ContextBuilder) {}146	fn as_any(&self) -> &dyn Any {147		self148	}149}150151impl<T> ContextInitializer for Option<T>152where153	T: ContextInitializer,154{155	fn initialize(&self, for_file: Source) -> Context {156		if let Some(ctx) = self {157			ctx.initialize(for_file)158		} else {159			().initialize(for_file)160		}161	}162163	fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {164		if let Some(ctx) = self {165			ctx.populate(for_file, builder);166		}167	}168169	fn as_any(&self) -> &dyn Any {170		self171	}172}173174macro_rules! impl_context_initializer {175	($($gen:ident)*) => {176		#[allow(non_snake_case)]177		impl<$($gen: ContextInitializer + Trace,)*> ContextInitializer for ($($gen,)*) {178			fn reserve_vars(&self) -> usize {179				let mut out = 0;180				let ($($gen,)*) = self;181				$(out += $gen.reserve_vars();)*182				out183			}184			fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {185				let ($($gen,)*) = self;186				$($gen.populate(for_file.clone(), builder);)*187			}188			fn as_any(&self) -> &dyn Any {189				self190			}191		}192	};193	($($cur:ident)* @ $c:ident $($rest:ident)*) => {194		impl_context_initializer!($($cur)*);195		impl_context_initializer!($($cur)* $c @ $($rest)*);196	};197	($($cur:ident)* @) => {198		impl_context_initializer!($($cur)*);199	}200}201impl_context_initializer! {202	A @ B C D E F G203}204205#[derive(Trace)]206struct FileData {207	string: Option<IStr>,208	bytes: Option<IBytes>,209	parsed: Option<Rc<Expr>>,210	evaluated: Option<Val>,211212	evaluating: bool,213}214impl FileData {215	fn new_string(data: IStr) -> Self {216		Self {217			string: Some(data),218			bytes: None,219			parsed: None,220			evaluated: None,221			evaluating: false,222		}223	}224	fn new_bytes(data: IBytes) -> Self {225		Self {226			string: None,227			bytes: Some(data),228			parsed: None,229			evaluated: None,230			evaluating: false,231		}232	}233	pub(crate) fn get_string(&mut self) -> Option<IStr> {234		if self.string.is_none() {235			self.string = Some(236				self.bytes237					.as_ref()238					.expect("either string or bytes should be set")239					.clone()240					.cast_str()?,241			);242		}243		Some(self.string.clone().expect("just set"))244	}245}246247#[derive(Trace)]248pub struct EvaluationStateInternals {249	/// Internal state250	file_cache: RefCell<FxHashMap<SourcePath, FileData>>,251	/// Context initializer, which will be used for imports and everything252	/// [`NoopContextInitializer`] is used by default, most likely you want to have `jrsonnet-stdlib`253	context_initializer: CcContextInitializer,254	/// Used to resolve file locations/contents255	import_resolver: Rc<dyn ImportResolver>,256}257258/// Maintains stack trace and import resolution259#[derive(Clone, Trace)]260pub struct State(Cc<EvaluationStateInternals>);261262thread_local! {263	pub static DEFAULT_STATE: State = State::builder().build();264	pub static STATE: RefCell<Option<State>> = const {RefCell::new(None)};265}266pub struct StateEnterGuard(PhantomData<()>);267impl Drop for StateEnterGuard {268	fn drop(&mut self) {269		STATE.with_borrow_mut(|v| *v = None);270	}271}272273pub fn with_state<V>(v: impl FnOnce(State) -> V) -> V {274	if let Some(state) = STATE.with_borrow(Clone::clone) {275		v(state)276	} else {277		let s = DEFAULT_STATE.with(Clone::clone);278		v(s)279	}280}281282impl State {283	pub fn enter(&self) -> StateEnterGuard {284		self.try_enter().expect("entered state already exists")285	}286	pub fn try_enter(&self) -> Option<StateEnterGuard> {287		STATE.with_borrow_mut(|v| {288			if v.is_none() {289				*v = Some(self.clone());290				Some(StateEnterGuard(PhantomData))291			} else {292				None293			}294		})295	}296	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise297	pub fn import_resolved_str(&self, path: SourcePath) -> Result<IStr> {298		let mut file_cache = self.file_cache();299		let mut file = file_cache.entry(path.clone());300301		let file = match file {302			Entry::Occupied(ref mut d) => d.get_mut(),303			Entry::Vacant(v) => {304				let data = self.import_resolver().load_file_contents(&path)?;305				v.insert(FileData::new_string(306					std::str::from_utf8(&data)307						.map_err(|_| ImportBadFileUtf8(path.clone()))?308						.into(),309				))310			}311		};312		Ok(file313			.get_string()314			.ok_or_else(|| ImportBadFileUtf8(path.clone()))?)315	}316	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise317	pub fn import_resolved_bin(&self, path: SourcePath) -> Result<IBytes> {318		let mut file_cache = self.file_cache();319		let mut file = file_cache.entry(path.clone());320321		let file = match file {322			Entry::Occupied(ref mut d) => d.get_mut(),323			Entry::Vacant(v) => {324				let data = self.import_resolver().load_file_contents(&path)?;325				v.insert(FileData::new_bytes(data.as_slice().into()))326			}327		};328		if let Some(str) = &file.bytes {329			return Ok(str.clone());330		}331		if file.bytes.is_none() {332			file.bytes = Some(333				file.string334					.as_ref()335					.expect("either string or bytes should be set")336					.clone()337					.cast_bytes(),338			);339		}340		Ok(file.bytes.as_ref().expect("just set").clone())341	}342	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise343	pub fn import_resolved(&self, path: SourcePath) -> Result<Val> {344		let mut file_cache = self.file_cache();345		let mut file = file_cache.entry(path.clone());346347		let file = match file {348			Entry::Occupied(ref mut d) => d.get_mut(),349			Entry::Vacant(v) => {350				let data = self.import_resolver().load_file_contents(&path)?;351				v.insert(FileData::new_string(352					std::str::from_utf8(&data)353						.map_err(|_| ImportBadFileUtf8(path.clone()))?354						.into(),355				))356			}357		};358		if let Some(val) = &file.evaluated {359			return Ok(val.clone());360		}361		let code = file362			.get_string()363			.ok_or_else(|| ImportBadFileUtf8(path.clone()))?;364		let file_name = Source::new(path.clone(), code.clone());365		if file.parsed.is_none() {366			file.parsed = Some(367				parse_jsonnet(368					&code,369					&ParserSettings {370						source: file_name.clone(),371					},372				)373				.map(Rc::new)374				.map_err(|e| ImportSyntaxError {375					path: file_name.clone(),376					error: Box::new(e),377				})?,378			);379		}380		let parsed = file.parsed.as_ref().expect("just set").clone();381		if file.evaluating {382			bail!(InfiniteRecursionDetected)383		}384		file.evaluating = true;385		// Dropping file cache guard here, as evaluation may use this map too386		drop(file_cache);387		let res = evaluate(self.create_default_context(file_name), &parsed);388389		let mut file_cache = self.file_cache();390		let mut file = file_cache.entry(path);391392		let Entry::Occupied(file) = &mut file else {393			unreachable!("this file was just here")394		};395		let file = file.get_mut();396		file.evaluating = false;397		match res {398			Ok(v) => {399				file.evaluated = Some(v.clone());400				Ok(v)401			}402			Err(e) => Err(e),403		}404	}405406	/// Has same semantics as `import 'path'` called from `from` file407	pub fn import_from(&self, from: &SourcePath, path: impl AsPathLike) -> Result<Val> {408		let resolved = self.resolve_from(from, &path)?;409		self.import_resolved(resolved)410	}411	pub fn import(&self, path: impl AsPathLike) -> Result<Val> {412		let resolved = self.resolve_from_default(&path)?;413		self.import_resolved(resolved)414	}415416	/// Creates context with all passed global variables417	pub fn create_default_context(&self, source: Source) -> Context {418		self.context_initializer().initialize(source)419	}420421	/// Creates context with all passed global variables, calling custom modifier422	pub fn create_default_context_with(423		&self,424		source: Source,425		context_initializer: impl ContextInitializer,426	) -> Context {427		let default_initializer = self.context_initializer();428		let mut builder = ContextBuilder::with_capacity(429			default_initializer.reserve_vars() + context_initializer.reserve_vars(),430		);431		default_initializer.populate(source.clone(), &mut builder);432		context_initializer.populate(source, &mut builder);433434		builder.build()435	}436}437438/// Internals439impl State {440	fn file_cache(&self) -> RefMut<'_, FxHashMap<SourcePath, FileData>> {441		self.0.file_cache.borrow_mut()442	}443}444/// Executes code creating a new stack frame, to be replaced with try{}445pub fn in_frame<T>(446	e: CallLocation<'_>,447	frame_desc: impl FnOnce() -> String,448	f: impl FnOnce() -> Result<T>,449) -> Result<T> {450	let _guard = check_depth()?;451452	f().with_description_src(e, frame_desc)453}454455/// Executes code creating a new stack frame, to be replaced with try{}456pub fn in_description_frame<T>(457	frame_desc: impl FnOnce() -> String,458	f: impl FnOnce() -> Result<T>,459) -> Result<T> {460	let _guard = check_depth()?;461462	f().with_description(frame_desc)463}464465#[derive(Trace)]466pub struct InitialUnderscore(pub Thunk<Val>);467impl ContextInitializer for InitialUnderscore {468	fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {469		builder.bind("_", self.0.clone());470	}471472	fn as_any(&self) -> &dyn Any {473		self474	}475}476477/// Raw methods evaluate passed values but don't perform TLA execution478impl State {479	/// Parses and evaluates the given snippet480	pub fn evaluate_snippet(&self, name: impl Into<IStr>, code: impl Into<IStr>) -> Result<Val> {481		let code = code.into();482		let source = Source::new_virtual(name.into(), code.clone());483		let parsed = parse_jsonnet(484			&code,485			&ParserSettings {486				source: source.clone(),487			},488		)489		.map_err(|e| ImportSyntaxError {490			path: source.clone(),491			error: Box::new(e),492		})?;493		evaluate(self.create_default_context(source), &parsed)494	}495	/// Parses and evaluates the given snippet with custom context modifier496	pub fn evaluate_snippet_with(497		&self,498		name: impl Into<IStr>,499		code: impl Into<IStr>,500		context_initializer: impl ContextInitializer,501	) -> Result<Val> {502		let code = code.into();503		let source = Source::new_virtual(name.into(), code.clone());504		let parsed = parse_jsonnet(505			&code,506			&ParserSettings {507				source: source.clone(),508			},509		)510		.map_err(|e| ImportSyntaxError {511			path: source.clone(),512			error: Box::new(e),513		})?;514		evaluate(515			self.create_default_context_with(source, context_initializer),516			&parsed,517		)518	}519}520521/// Settings utilities522impl State {523	// Only panics in case of [`ImportResolver`] contract violation524	#[allow(clippy::missing_panics_doc)]525	pub fn resolve_from(&self, from: &SourcePath, path: &dyn AsPathLike) -> Result<SourcePath> {526		self.import_resolver().resolve_from(from, path)527	}528	#[allow(clippy::missing_panics_doc)]529	pub fn resolve_from_default(&self, path: &dyn AsPathLike) -> Result<SourcePath> {530		self.import_resolver().resolve_from_default(path)531	}532	pub fn import_resolver(&self) -> &dyn ImportResolver {533		&*self.0.import_resolver534	}535	pub fn context_initializer(&self) -> &dyn ContextInitializer {536		&*self.0.context_initializer.0537	}538}539540impl State {541	pub fn builder() -> StateBuilder {542		StateBuilder::default()543	}544}545546impl Default for State {547	fn default() -> Self {548		Self::builder().build()549	}550}551552#[derive(Default)]553pub struct StateBuilder {554	import_resolver: Option<Rc<dyn ImportResolver>>,555	context_initializer: Option<CcContextInitializer>,556}557impl StateBuilder {558	pub fn import_resolver(&mut self, import_resolver: impl ImportResolver) -> &mut Self {559		let _ = self.import_resolver.insert(Rc::new(import_resolver));560		self561	}562	pub fn context_initializer(563		&mut self,564		context_initializer: impl ContextInitializer,565	) -> &mut Self {566		let _ = self567			.context_initializer568			.insert(CcContextInitializer::new(context_initializer));569		self570	}571	pub fn build(mut self) -> State {572		State(Cc::new(EvaluationStateInternals {573			file_cache: RefCell::new(FxHashMap::new()),574			context_initializer: self575				.context_initializer576				.take()577				.unwrap_or_else(|| CcContextInitializer::new(())),578			import_resolver: self579				.import_resolver580				.take()581				.unwrap_or_else(|| Rc::new(DummyImportResolver)),582		}))583	}584}
after · crates/jrsonnet-evaluator/src/lib.rs
1//! jsonnet interpreter implementation2#![cfg_attr(nightly, feature(thread_local, type_alias_impl_trait))]34// For jrsonnet-macros5extern crate self as jrsonnet_evaluator;67mod arr;8// pub mod async_import;9mod ctx;10mod dynamic;11pub mod error;12mod evaluate;13pub mod function;14pub mod gc;15mod import;16mod integrations;17pub mod manifest;18mod map;19mod obj;20pub mod stack;21pub mod stdlib;22pub mod tla;23pub mod trace;24pub mod typed;25pub mod val;2627use std::{28	any::Any,29	cell::{RefCell, RefMut},30	clone::Clone,31	collections::hash_map::Entry,32	fmt::{self, Debug},33	marker::PhantomData,34	rc::Rc,35};3637pub use ctx::*;38pub use dynamic::*;39pub use error::{Error, ErrorKind::*, Result, ResultExt};40pub use evaluate::*;41use function::CallLocation;42pub use import::*;43use jrsonnet_gcmodule::{cc_dyn, Cc, Trace};44pub use jrsonnet_interner::{IBytes, IStr};45pub use jrsonnet_ir as parser;46use jrsonnet_ir::{Expr, Source, SourcePath};47#[doc(hidden)]48pub use jrsonnet_macros;4950#[cfg(not(any(feature = "ir-parser", feature = "peg-parser")))]51compile_error!("at least one of `ir-parser` or `peg-parser` features must be enabled");5253pub use error::{SyntaxError, SyntaxErrorLocation};54pub use obj::*;55pub use rustc_hash;56use rustc_hash::FxHashMap;57use stack::check_depth;58pub use tla::apply_tla;59pub use val::{Thunk, Val};6061use crate::gc::WithCapacityExt as _;6263pub(crate) fn parse_jsonnet(code: &str, source: Source) -> Result<Expr, SyntaxError> {64	#[cfg(all(feature = "ir-parser", feature = "peg-parser"))]65	{66		if std::env::var_os("JRSONNET_LEGACY_PARSER").is_some() {67			return parse_peg(code, source);68		}69		return parse_ir(code, source);70	}71	#[cfg(all(feature = "ir-parser", not(feature = "peg-parser")))]72	{73		return parse_ir(code, source);74	}75	#[cfg(all(feature = "peg-parser", not(feature = "ir-parser")))]76	{77		return parse_peg(code, source);78	}79}8081#[cfg(feature = "ir-parser")]82fn parse_ir(code: &str, source: Source) -> Result<Expr, SyntaxError> {83	jrsonnet_ir_parser::parse(code, &jrsonnet_ir_parser::ParserSettings { source }).map_err(84		|e| SyntaxError {85			message: e.message,86			location: SyntaxErrorLocation {87				offset: e.location.offset,88			},89		},90	)91}9293#[cfg(feature = "peg-parser")]94fn parse_peg(code: &str, source: Source) -> Result<Expr, SyntaxError> {95	jrsonnet_peg_parser::parse(code, &jrsonnet_peg_parser::ParserSettings { source }).map_err(96		|e| {97			let message = e98				.expected99				.tokens()100				.find(|t| t.starts_with("!!!"))101				.map_or_else(102					|| {103						format!(104							"expected {}, got {:?}",105							e.expected,106							code.chars()107								.nth(e.location.offset)108								.map_or_else(|| "EOF".into(), |c: char| c.to_string())109						)110					},111					|v| v[3..].into(),112				);113			SyntaxError {114				message,115				location: SyntaxErrorLocation {116					offset: e.location.offset,117				},118			}119		},120	)121}122123cc_dyn!(124	#[derive(Clone)]125	CcUnbound<V>,126	Unbound<Bound = V>127);128129/// Thunk without bound `super`/`this`130/// object inheritance may be overriden multiple times, and will be fixed only on field read131pub trait Unbound: Trace {132	/// Type of value after object context is bound133	type Bound;134	/// Create value bound to specified object context135	fn bind(&self, sup_this: SupThis) -> Result<Self::Bound>;136}137138/// Object fields may, or may not depend on `this`/`super`, this enum allows cheaper reuse of object-independent fields for native code139/// Standard jsonnet fields are always unbound140#[derive(Clone, Trace)]141pub enum MaybeUnbound {142	/// Value needs to be bound to `this`/`super`143	Unbound(CcUnbound<Val>),144	/// Value is object-independent145	Bound(Thunk<Val>),146}147148impl Debug for MaybeUnbound {149	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {150		write!(f, "MaybeUnbound")151	}152}153impl MaybeUnbound {154	/// Attach object context to value, if required155	pub fn evaluate(&self, sup_this: SupThis) -> Result<Val> {156		match self {157			Self::Unbound(v) => v.0.bind(sup_this),158			Self::Bound(v) => Ok(v.evaluate()?),159		}160	}161}162163cc_dyn!(CcContextInitializer, ContextInitializer);164165/// During import, this trait will be called to create initial context for file.166/// It may initialize global variables, stdlib for example.167pub trait ContextInitializer: Trace {168	/// For which size the builder should be preallocated169	fn reserve_vars(&self) -> usize {170		0171	}172	/// Initialize default file context.173	/// Has default implementation, which calls `populate`.174	/// Prefer to always implement `populate` instead.175	fn initialize(&self, for_file: Source) -> Context {176		let mut builder = ContextBuilder::with_capacity(self.reserve_vars());177		self.populate(for_file, &mut builder);178		builder.build()179	}180	/// For composability: extend builder. May panic if this initialization is not supported,181	/// and the context may only be created via `initialize`.182	fn populate(&self, for_file: Source, builder: &mut ContextBuilder);183	/// Allows upcasting from abstract to concrete context initializer.184	/// jrsonnet by itself doesn't use this method, it is allowed for it to panic.185	fn as_any(&self) -> &dyn Any;186}187188/// Context initializer which adds nothing.189impl ContextInitializer for () {190	fn populate(&self, _for_file: Source, _builder: &mut ContextBuilder) {}191	fn as_any(&self) -> &dyn Any {192		self193	}194}195196impl<T> ContextInitializer for Option<T>197where198	T: ContextInitializer,199{200	fn initialize(&self, for_file: Source) -> Context {201		if let Some(ctx) = self {202			ctx.initialize(for_file)203		} else {204			().initialize(for_file)205		}206	}207208	fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {209		if let Some(ctx) = self {210			ctx.populate(for_file, builder);211		}212	}213214	fn as_any(&self) -> &dyn Any {215		self216	}217}218219macro_rules! impl_context_initializer {220	($($gen:ident)*) => {221		#[allow(non_snake_case)]222		impl<$($gen: ContextInitializer + Trace,)*> ContextInitializer for ($($gen,)*) {223			fn reserve_vars(&self) -> usize {224				let mut out = 0;225				let ($($gen,)*) = self;226				$(out += $gen.reserve_vars();)*227				out228			}229			fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {230				let ($($gen,)*) = self;231				$($gen.populate(for_file.clone(), builder);)*232			}233			fn as_any(&self) -> &dyn Any {234				self235			}236		}237	};238	($($cur:ident)* @ $c:ident $($rest:ident)*) => {239		impl_context_initializer!($($cur)*);240		impl_context_initializer!($($cur)* $c @ $($rest)*);241	};242	($($cur:ident)* @) => {243		impl_context_initializer!($($cur)*);244	}245}246impl_context_initializer! {247	A @ B C D E F G248}249250#[derive(Trace)]251struct FileData {252	string: Option<IStr>,253	bytes: Option<IBytes>,254	parsed: Option<Rc<Expr>>,255	evaluated: Option<Val>,256257	evaluating: bool,258}259impl FileData {260	fn new_string(data: IStr) -> Self {261		Self {262			string: Some(data),263			bytes: None,264			parsed: None,265			evaluated: None,266			evaluating: false,267		}268	}269	fn new_bytes(data: IBytes) -> Self {270		Self {271			string: None,272			bytes: Some(data),273			parsed: None,274			evaluated: None,275			evaluating: false,276		}277	}278	pub(crate) fn get_string(&mut self) -> Option<IStr> {279		if self.string.is_none() {280			self.string = Some(281				self.bytes282					.as_ref()283					.expect("either string or bytes should be set")284					.clone()285					.cast_str()?,286			);287		}288		Some(self.string.clone().expect("just set"))289	}290}291292#[derive(Trace)]293pub struct EvaluationStateInternals {294	/// Internal state295	file_cache: RefCell<FxHashMap<SourcePath, FileData>>,296	/// Context initializer, which will be used for imports and everything297	/// [`NoopContextInitializer`] is used by default, most likely you want to have `jrsonnet-stdlib`298	context_initializer: CcContextInitializer,299	/// Used to resolve file locations/contents300	import_resolver: Rc<dyn ImportResolver>,301}302303/// Maintains stack trace and import resolution304#[derive(Clone, Trace)]305pub struct State(Cc<EvaluationStateInternals>);306307thread_local! {308	pub static DEFAULT_STATE: State = State::builder().build();309	pub static STATE: RefCell<Option<State>> = const {RefCell::new(None)};310}311pub struct StateEnterGuard(PhantomData<()>);312impl Drop for StateEnterGuard {313	fn drop(&mut self) {314		STATE.with_borrow_mut(|v| *v = None);315	}316}317318pub fn with_state<V>(v: impl FnOnce(State) -> V) -> V {319	if let Some(state) = STATE.with_borrow(Clone::clone) {320		v(state)321	} else {322		let s = DEFAULT_STATE.with(Clone::clone);323		v(s)324	}325}326327impl State {328	pub fn enter(&self) -> StateEnterGuard {329		self.try_enter().expect("entered state already exists")330	}331	pub fn try_enter(&self) -> Option<StateEnterGuard> {332		STATE.with_borrow_mut(|v| {333			if v.is_none() {334				*v = Some(self.clone());335				Some(StateEnterGuard(PhantomData))336			} else {337				None338			}339		})340	}341	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise342	pub fn import_resolved_str(&self, path: SourcePath) -> Result<IStr> {343		let mut file_cache = self.file_cache();344		let mut file = file_cache.entry(path.clone());345346		let file = match file {347			Entry::Occupied(ref mut d) => d.get_mut(),348			Entry::Vacant(v) => {349				let data = self.import_resolver().load_file_contents(&path)?;350				v.insert(FileData::new_string(351					std::str::from_utf8(&data)352						.map_err(|_| ImportBadFileUtf8(path.clone()))?353						.into(),354				))355			}356		};357		Ok(file358			.get_string()359			.ok_or_else(|| ImportBadFileUtf8(path.clone()))?)360	}361	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise362	pub fn import_resolved_bin(&self, path: SourcePath) -> Result<IBytes> {363		let mut file_cache = self.file_cache();364		let mut file = file_cache.entry(path.clone());365366		let file = match file {367			Entry::Occupied(ref mut d) => d.get_mut(),368			Entry::Vacant(v) => {369				let data = self.import_resolver().load_file_contents(&path)?;370				v.insert(FileData::new_bytes(data.as_slice().into()))371			}372		};373		if let Some(str) = &file.bytes {374			return Ok(str.clone());375		}376		if file.bytes.is_none() {377			file.bytes = Some(378				file.string379					.as_ref()380					.expect("either string or bytes should be set")381					.clone()382					.cast_bytes(),383			);384		}385		Ok(file.bytes.as_ref().expect("just set").clone())386	}387	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise388	pub fn import_resolved(&self, path: SourcePath) -> Result<Val> {389		let mut file_cache = self.file_cache();390		let mut file = file_cache.entry(path.clone());391392		let file = match file {393			Entry::Occupied(ref mut d) => d.get_mut(),394			Entry::Vacant(v) => {395				let data = self.import_resolver().load_file_contents(&path)?;396				v.insert(FileData::new_string(397					std::str::from_utf8(&data)398						.map_err(|_| ImportBadFileUtf8(path.clone()))?399						.into(),400				))401			}402		};403		if let Some(val) = &file.evaluated {404			return Ok(val.clone());405		}406		let code = file407			.get_string()408			.ok_or_else(|| ImportBadFileUtf8(path.clone()))?;409		let file_name = Source::new(path.clone(), code.clone());410		if file.parsed.is_none() {411			file.parsed = Some(412				parse_jsonnet(&code, file_name.clone())413				.map(Rc::new)414				.map_err(|e| ImportSyntaxError {415					path: file_name.clone(),416					error: Box::new(e),417				})?,418			);419		}420		let parsed = file.parsed.as_ref().expect("just set").clone();421		if file.evaluating {422			bail!(InfiniteRecursionDetected)423		}424		file.evaluating = true;425		// Dropping file cache guard here, as evaluation may use this map too426		drop(file_cache);427		let res = evaluate(self.create_default_context(file_name), &parsed);428429		let mut file_cache = self.file_cache();430		let mut file = file_cache.entry(path);431432		let Entry::Occupied(file) = &mut file else {433			unreachable!("this file was just here")434		};435		let file = file.get_mut();436		file.evaluating = false;437		match res {438			Ok(v) => {439				file.evaluated = Some(v.clone());440				Ok(v)441			}442			Err(e) => Err(e),443		}444	}445446	/// Has same semantics as `import 'path'` called from `from` file447	pub fn import_from(&self, from: &SourcePath, path: impl AsPathLike) -> Result<Val> {448		let resolved = self.resolve_from(from, &path)?;449		self.import_resolved(resolved)450	}451	pub fn import(&self, path: impl AsPathLike) -> Result<Val> {452		let resolved = self.resolve_from_default(&path)?;453		self.import_resolved(resolved)454	}455456	/// Creates context with all passed global variables457	pub fn create_default_context(&self, source: Source) -> Context {458		self.context_initializer().initialize(source)459	}460461	/// Creates context with all passed global variables, calling custom modifier462	pub fn create_default_context_with(463		&self,464		source: Source,465		context_initializer: impl ContextInitializer,466	) -> Context {467		let default_initializer = self.context_initializer();468		let mut builder = ContextBuilder::with_capacity(469			default_initializer.reserve_vars() + context_initializer.reserve_vars(),470		);471		default_initializer.populate(source.clone(), &mut builder);472		context_initializer.populate(source, &mut builder);473474		builder.build()475	}476}477478/// Internals479impl State {480	fn file_cache(&self) -> RefMut<'_, FxHashMap<SourcePath, FileData>> {481		self.0.file_cache.borrow_mut()482	}483}484/// Executes code creating a new stack frame, to be replaced with try{}485pub fn in_frame<T>(486	e: CallLocation<'_>,487	frame_desc: impl FnOnce() -> String,488	f: impl FnOnce() -> Result<T>,489) -> Result<T> {490	let _guard = check_depth()?;491492	f().with_description_src(e, frame_desc)493}494495/// Executes code creating a new stack frame, to be replaced with try{}496pub fn in_description_frame<T>(497	frame_desc: impl FnOnce() -> String,498	f: impl FnOnce() -> Result<T>,499) -> Result<T> {500	let _guard = check_depth()?;501502	f().with_description(frame_desc)503}504505#[derive(Trace)]506pub struct InitialUnderscore(pub Thunk<Val>);507impl ContextInitializer for InitialUnderscore {508	fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {509		builder.bind("_", self.0.clone());510	}511512	fn as_any(&self) -> &dyn Any {513		self514	}515}516517/// Raw methods evaluate passed values but don't perform TLA execution518impl State {519	/// Parses and evaluates the given snippet520	pub fn evaluate_snippet(&self, name: impl Into<IStr>, code: impl Into<IStr>) -> Result<Val> {521		let code = code.into();522		let source = Source::new_virtual(name.into(), code.clone());523		let parsed = parse_jsonnet(&code, source.clone())524		.map_err(|e| ImportSyntaxError {525			path: source.clone(),526			error: Box::new(e),527		})?;528		evaluate(self.create_default_context(source), &parsed)529	}530	/// Parses and evaluates the given snippet with custom context modifier531	pub fn evaluate_snippet_with(532		&self,533		name: impl Into<IStr>,534		code: impl Into<IStr>,535		context_initializer: impl ContextInitializer,536	) -> Result<Val> {537		let code = code.into();538		let source = Source::new_virtual(name.into(), code.clone());539		let parsed = parse_jsonnet(&code, source.clone())540		.map_err(|e| ImportSyntaxError {541			path: source.clone(),542			error: Box::new(e),543		})?;544		evaluate(545			self.create_default_context_with(source, context_initializer),546			&parsed,547		)548	}549}550551/// Settings utilities552impl State {553	// Only panics in case of [`ImportResolver`] contract violation554	#[allow(clippy::missing_panics_doc)]555	pub fn resolve_from(&self, from: &SourcePath, path: &dyn AsPathLike) -> Result<SourcePath> {556		self.import_resolver().resolve_from(from, path)557	}558	#[allow(clippy::missing_panics_doc)]559	pub fn resolve_from_default(&self, path: &dyn AsPathLike) -> Result<SourcePath> {560		self.import_resolver().resolve_from_default(path)561	}562	pub fn import_resolver(&self) -> &dyn ImportResolver {563		&*self.0.import_resolver564	}565	pub fn context_initializer(&self) -> &dyn ContextInitializer {566		&*self.0.context_initializer.0567	}568}569570impl State {571	pub fn builder() -> StateBuilder {572		StateBuilder::default()573	}574}575576impl Default for State {577	fn default() -> Self {578		Self::builder().build()579	}580}581582#[derive(Default)]583pub struct StateBuilder {584	import_resolver: Option<Rc<dyn ImportResolver>>,585	context_initializer: Option<CcContextInitializer>,586}587impl StateBuilder {588	pub fn import_resolver(&mut self, import_resolver: impl ImportResolver) -> &mut Self {589		let _ = self.import_resolver.insert(Rc::new(import_resolver));590		self591	}592	pub fn context_initializer(593		&mut self,594		context_initializer: impl ContextInitializer,595	) -> &mut Self {596		let _ = self597			.context_initializer598			.insert(CcContextInitializer::new(context_initializer));599		self600	}601	pub fn build(mut self) -> State {602		State(Cc::new(EvaluationStateInternals {603			file_cache: RefCell::new(FxHashMap::new()),604			context_initializer: self605				.context_initializer606				.take()607				.unwrap_or_else(|| CcContextInitializer::new(())),608			import_resolver: self609				.import_resolver610				.take()611				.unwrap_or_else(|| Rc::new(DummyImportResolver)),612		}))613	}614}