jrsonnet_evaluator/
lib.rs

1//! jsonnet interpreter implementation
2#![cfg_attr(feature = "nightly", feature(thread_local, type_alias_impl_trait))]
3
4// For jrsonnet-macros
5extern crate self as jrsonnet_evaluator;
6
7mod arr;
8#[cfg(feature = "async-import")]
9pub mod async_import;
10mod ctx;
11mod dynamic;
12pub mod error;
13mod evaluate;
14pub mod function;
15pub mod gc;
16mod import;
17mod integrations;
18pub mod manifest;
19mod map;
20mod obj;
21pub mod stack;
22pub mod stdlib;
23mod tla;
24pub mod trace;
25pub mod typed;
26pub mod val;
27
28use std::{
29	any::Any,
30	cell::{RefCell, RefMut},
31	fmt::{self, Debug},
32	path::Path,
33};
34
35pub use ctx::*;
36pub use dynamic::*;
37pub use error::{Error, ErrorKind::*, Result, ResultExt};
38pub use evaluate::*;
39use function::CallLocation;
40use gc::{GcHashMap, TraceBox};
41use hashbrown::hash_map::RawEntryMut;
42pub use import::*;
43use jrsonnet_gcmodule::{Cc, Trace};
44pub use jrsonnet_interner::{IBytes, IStr};
45#[doc(hidden)]
46pub use jrsonnet_macros;
47pub use jrsonnet_parser as parser;
48use jrsonnet_parser::{LocExpr, ParserSettings, Source, SourcePath};
49pub use obj::*;
50use stack::check_depth;
51pub use tla::apply_tla;
52pub use val::{Thunk, Val};
53
54/// Thunk without bound `super`/`this`
55/// object inheritance may be overriden multiple times, and will be fixed only on field read
56pub trait Unbound: Trace {
57	/// Type of value after object context is bound
58	type Bound;
59	/// Create value bound to specified object context
60	fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Self::Bound>;
61}
62
63/// Object fields may, or may not depend on `this`/`super`, this enum allows cheaper reuse of object-independent fields for native code
64/// Standard jsonnet fields are always unbound
65#[derive(Clone, Trace)]
66pub enum MaybeUnbound {
67	/// Value needs to be bound to `this`/`super`
68	Unbound(Cc<TraceBox<dyn Unbound<Bound = Val>>>),
69	/// Value is object-independent
70	Bound(Thunk<Val>),
71}
72
73impl Debug for MaybeUnbound {
74	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75		write!(f, "MaybeUnbound")
76	}
77}
78impl MaybeUnbound {
79	/// Attach object context to value, if required
80	pub fn evaluate(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {
81		match self {
82			Self::Unbound(v) => v.bind(sup, this),
83			Self::Bound(v) => Ok(v.evaluate()?),
84		}
85	}
86}
87
88/// During import, this trait will be called to create initial context for file.
89/// It may initialize global variables, stdlib for example.
90pub trait ContextInitializer: Trace {
91	/// For which size the builder should be preallocated
92	fn reserve_vars(&self) -> usize {
93		0
94	}
95	/// Initialize default file context.
96	/// Has default implementation, which calls `populate`.
97	/// Prefer to always implement `populate` instead.
98	fn initialize(&self, state: State, for_file: Source) -> Context {
99		let mut builder = ContextBuilder::with_capacity(state, self.reserve_vars());
100		self.populate(for_file, &mut builder);
101		builder.build()
102	}
103	/// For composability: extend builder. May panic if this initialization is not supported,
104	/// and the context may only be created via `initialize`.
105	fn populate(&self, for_file: Source, builder: &mut ContextBuilder);
106	/// Allows upcasting from abstract to concrete context initializer.
107	/// jrsonnet by itself doesn't use this method, it is allowed for it to panic.
108	fn as_any(&self) -> &dyn Any;
109}
110
111/// Context initializer which adds nothing.
112impl ContextInitializer for () {
113	fn populate(&self, _for_file: Source, _builder: &mut ContextBuilder) {}
114	fn as_any(&self) -> &dyn Any {
115		self
116	}
117}
118
119impl<T> ContextInitializer for Option<T>
120where
121	T: ContextInitializer,
122{
123	fn initialize(&self, state: State, for_file: Source) -> Context {
124		if let Some(ctx) = self {
125			ctx.initialize(state, for_file)
126		} else {
127			().initialize(state, for_file)
128		}
129	}
130
131	fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {
132		if let Some(ctx) = self {
133			ctx.populate(for_file, builder);
134		}
135	}
136
137	fn as_any(&self) -> &dyn Any {
138		self
139	}
140}
141
142macro_rules! impl_context_initializer {
143	($($gen:ident)*) => {
144		#[allow(non_snake_case)]
145		impl<$($gen: ContextInitializer + Trace,)*> ContextInitializer for ($($gen,)*) {
146			fn reserve_vars(&self) -> usize {
147				let mut out = 0;
148				let ($($gen,)*) = self;
149				$(out += $gen.reserve_vars();)*
150				out
151			}
152			fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {
153				let ($($gen,)*) = self;
154				$($gen.populate(for_file.clone(), builder);)*
155			}
156			fn as_any(&self) -> &dyn Any {
157				self
158			}
159		}
160	};
161	($($cur:ident)* @ $c:ident $($rest:ident)*) => {
162		impl_context_initializer!($($cur)*);
163		impl_context_initializer!($($cur)* $c @ $($rest)*);
164	};
165	($($cur:ident)* @) => {
166		impl_context_initializer!($($cur)*);
167	}
168}
169impl_context_initializer! {
170	A @ B C D E F G
171}
172
173#[derive(Trace)]
174struct FileData {
175	string: Option<IStr>,
176	bytes: Option<IBytes>,
177	parsed: Option<LocExpr>,
178	evaluated: Option<Val>,
179
180	evaluating: bool,
181}
182impl FileData {
183	fn new_string(data: IStr) -> Self {
184		Self {
185			string: Some(data),
186			bytes: None,
187			parsed: None,
188			evaluated: None,
189			evaluating: false,
190		}
191	}
192	fn new_bytes(data: IBytes) -> Self {
193		Self {
194			string: None,
195			bytes: Some(data),
196			parsed: None,
197			evaluated: None,
198			evaluating: false,
199		}
200	}
201	pub(crate) fn get_string(&mut self) -> Option<IStr> {
202		if self.string.is_none() {
203			self.string = Some(
204				self.bytes
205					.as_ref()
206					.expect("either string or bytes should be set")
207					.clone()
208					.cast_str()?,
209			);
210		}
211		Some(self.string.clone().expect("just set"))
212	}
213}
214
215#[derive(Trace)]
216pub struct EvaluationStateInternals {
217	/// Internal state
218	file_cache: RefCell<GcHashMap<SourcePath, FileData>>,
219	/// Context initializer, which will be used for imports and everything
220	/// [`NoopContextInitializer`] is used by default, most likely you want to have `jrsonnet-stdlib`
221	context_initializer: TraceBox<dyn ContextInitializer>,
222	/// Used to resolve file locations/contents
223	import_resolver: TraceBox<dyn ImportResolver>,
224}
225
226/// Maintains stack trace and import resolution
227#[derive(Clone, Trace)]
228pub struct State(Cc<EvaluationStateInternals>);
229
230impl State {
231	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise
232	pub fn import_resolved_str(&self, path: SourcePath) -> Result<IStr> {
233		let mut file_cache = self.file_cache();
234		let mut file = file_cache.raw_entry_mut().from_key(&path);
235
236		let file = match file {
237			RawEntryMut::Occupied(ref mut d) => d.get_mut(),
238			RawEntryMut::Vacant(v) => {
239				let data = self.import_resolver().load_file_contents(&path)?;
240				v.insert(
241					path.clone(),
242					FileData::new_string(
243						std::str::from_utf8(&data)
244							.map_err(|_| ImportBadFileUtf8(path.clone()))?
245							.into(),
246					),
247				)
248				.1
249			}
250		};
251		Ok(file
252			.get_string()
253			.ok_or_else(|| ImportBadFileUtf8(path.clone()))?)
254	}
255	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise
256	pub fn import_resolved_bin(&self, path: SourcePath) -> Result<IBytes> {
257		let mut file_cache = self.file_cache();
258		let mut file = file_cache.raw_entry_mut().from_key(&path);
259
260		let file = match file {
261			RawEntryMut::Occupied(ref mut d) => d.get_mut(),
262			RawEntryMut::Vacant(v) => {
263				let data = self.import_resolver().load_file_contents(&path)?;
264				v.insert(path.clone(), FileData::new_bytes(data.as_slice().into()))
265					.1
266			}
267		};
268		if let Some(str) = &file.bytes {
269			return Ok(str.clone());
270		}
271		if file.bytes.is_none() {
272			file.bytes = Some(
273				file.string
274					.as_ref()
275					.expect("either string or bytes should be set")
276					.clone()
277					.cast_bytes(),
278			);
279		}
280		Ok(file.bytes.as_ref().expect("just set").clone())
281	}
282	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise
283	pub fn import_resolved(&self, path: SourcePath) -> Result<Val> {
284		let mut file_cache = self.file_cache();
285		let mut file = file_cache.raw_entry_mut().from_key(&path);
286
287		let file = match file {
288			RawEntryMut::Occupied(ref mut d) => d.get_mut(),
289			RawEntryMut::Vacant(v) => {
290				let data = self.import_resolver().load_file_contents(&path)?;
291				v.insert(
292					path.clone(),
293					FileData::new_string(
294						std::str::from_utf8(&data)
295							.map_err(|_| ImportBadFileUtf8(path.clone()))?
296							.into(),
297					),
298				)
299				.1
300			}
301		};
302		if let Some(val) = &file.evaluated {
303			return Ok(val.clone());
304		}
305		let code = file
306			.get_string()
307			.ok_or_else(|| ImportBadFileUtf8(path.clone()))?;
308		let file_name = Source::new(path.clone(), code.clone());
309		if file.parsed.is_none() {
310			file.parsed = Some(
311				jrsonnet_parser::parse(
312					&code,
313					&ParserSettings {
314						source: file_name.clone(),
315					},
316				)
317				.map_err(|e| ImportSyntaxError {
318					path: file_name.clone(),
319					error: Box::new(e),
320				})?,
321			);
322		}
323		let parsed = file.parsed.as_ref().expect("just set").clone();
324		if file.evaluating {
325			bail!(InfiniteRecursionDetected)
326		}
327		file.evaluating = true;
328		// Dropping file cache guard here, as evaluation may use this map too
329		drop(file_cache);
330		let res = evaluate(self.create_default_context(file_name), &parsed);
331
332		let mut file_cache = self.file_cache();
333		let mut file = file_cache.raw_entry_mut().from_key(&path);
334
335		let RawEntryMut::Occupied(file) = &mut file else {
336			unreachable!("this file was just here!")
337		};
338		let file = file.get_mut();
339		file.evaluating = false;
340		match res {
341			Ok(v) => {
342				file.evaluated = Some(v.clone());
343				Ok(v)
344			}
345			Err(e) => Err(e),
346		}
347	}
348
349	/// Has same semantics as `import 'path'` called from `from` file
350	pub fn import_from(&self, from: &SourcePath, path: &str) -> Result<Val> {
351		let resolved = self.resolve_from(from, path)?;
352		self.import_resolved(resolved)
353	}
354	pub fn import(&self, path: impl AsRef<Path>) -> Result<Val> {
355		let resolved = self.resolve(path)?;
356		self.import_resolved(resolved)
357	}
358
359	/// Creates context with all passed global variables
360	pub fn create_default_context(&self, source: Source) -> Context {
361		self.context_initializer().initialize(self.clone(), source)
362	}
363
364	/// Creates context with all passed global variables, calling custom modifier
365	pub fn create_default_context_with(
366		&self,
367		source: Source,
368		context_initializer: impl ContextInitializer,
369	) -> Context {
370		let default_initializer = self.context_initializer();
371		let mut builder = ContextBuilder::with_capacity(
372			self.clone(),
373			default_initializer.reserve_vars() + context_initializer.reserve_vars(),
374		);
375		default_initializer.populate(source.clone(), &mut builder);
376		context_initializer.populate(source, &mut builder);
377
378		builder.build()
379	}
380}
381
382/// Internals
383impl State {
384	fn file_cache(&self) -> RefMut<'_, GcHashMap<SourcePath, FileData>> {
385		self.0.file_cache.borrow_mut()
386	}
387}
388/// Executes code creating a new stack frame, to be replaced with try{}
389pub fn in_frame<T>(
390	e: CallLocation<'_>,
391	frame_desc: impl FnOnce() -> String,
392	f: impl FnOnce() -> Result<T>,
393) -> Result<T> {
394	let _guard = check_depth()?;
395
396	f().with_description_src(e, frame_desc)
397}
398
399/// Executes code creating a new stack frame, to be replaced with try{}
400pub fn in_description_frame<T>(
401	frame_desc: impl FnOnce() -> String,
402	f: impl FnOnce() -> Result<T>,
403) -> Result<T> {
404	let _guard = check_depth()?;
405
406	f().with_description(frame_desc)
407}
408
409#[derive(Trace)]
410pub struct InitialUnderscore(pub Thunk<Val>);
411impl ContextInitializer for InitialUnderscore {
412	fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {
413		builder.bind("_", self.0.clone());
414	}
415
416	fn as_any(&self) -> &dyn Any {
417		self
418	}
419}
420
421/// Raw methods evaluate passed values but don't perform TLA execution
422impl State {
423	/// Parses and evaluates the given snippet
424	pub fn evaluate_snippet(&self, name: impl Into<IStr>, code: impl Into<IStr>) -> Result<Val> {
425		let code = code.into();
426		let source = Source::new_virtual(name.into(), code.clone());
427		let parsed = jrsonnet_parser::parse(
428			&code,
429			&ParserSettings {
430				source: source.clone(),
431			},
432		)
433		.map_err(|e| ImportSyntaxError {
434			path: source.clone(),
435			error: Box::new(e),
436		})?;
437		evaluate(self.create_default_context(source), &parsed)
438	}
439	/// Parses and evaluates the given snippet with custom context modifier
440	pub fn evaluate_snippet_with(
441		&self,
442		name: impl Into<IStr>,
443		code: impl Into<IStr>,
444		context_initializer: impl ContextInitializer,
445	) -> Result<Val> {
446		let code = code.into();
447		let source = Source::new_virtual(name.into(), code.clone());
448		let parsed = jrsonnet_parser::parse(
449			&code,
450			&ParserSettings {
451				source: source.clone(),
452			},
453		)
454		.map_err(|e| ImportSyntaxError {
455			path: source.clone(),
456			error: Box::new(e),
457		})?;
458		evaluate(
459			self.create_default_context_with(source, context_initializer),
460			&parsed,
461		)
462	}
463}
464
465/// Settings utilities
466impl State {
467	// Only panics in case of [`ImportResolver`] contract violation
468	#[allow(clippy::missing_panics_doc)]
469	pub fn resolve_from(&self, from: &SourcePath, path: &str) -> Result<SourcePath> {
470		self.import_resolver().resolve_from(from, path.as_ref())
471	}
472
473	// Only panics in case of [`ImportResolver`] contract violation
474	#[allow(clippy::missing_panics_doc)]
475	pub fn resolve(&self, path: impl AsRef<Path>) -> Result<SourcePath> {
476		self.import_resolver().resolve(path.as_ref())
477	}
478	pub fn import_resolver(&self) -> &dyn ImportResolver {
479		&*self.0.import_resolver
480	}
481	pub fn context_initializer(&self) -> &dyn ContextInitializer {
482		&*self.0.context_initializer
483	}
484}
485
486impl State {
487	pub fn builder() -> StateBuilder {
488		StateBuilder::default()
489	}
490}
491
492impl Default for State {
493	fn default() -> Self {
494		Self::builder().build()
495	}
496}
497
498#[derive(Default)]
499pub struct StateBuilder {
500	import_resolver: Option<TraceBox<dyn ImportResolver>>,
501	context_initializer: Option<TraceBox<dyn ContextInitializer>>,
502}
503impl StateBuilder {
504	pub fn import_resolver(&mut self, import_resolver: impl ImportResolver) -> &mut Self {
505		let _ = self.import_resolver.insert(tb!(import_resolver));
506		self
507	}
508	pub fn context_initializer(
509		&mut self,
510		context_initializer: impl ContextInitializer,
511	) -> &mut Self {
512		let _ = self.context_initializer.insert(tb!(context_initializer));
513		self
514	}
515	pub fn build(mut self) -> State {
516		State(Cc::new(EvaluationStateInternals {
517			file_cache: RefCell::new(GcHashMap::new()),
518			context_initializer: self.context_initializer.take().unwrap_or_else(|| tb!(())),
519			import_resolver: self
520				.import_resolver
521				.take()
522				.unwrap_or_else(|| tb!(DummyImportResolver)),
523		}))
524	}
525}