git.delta.rocks / jrsonnet / refs/commits / 1c69f1ac7855

difftreelog

source

crates/jrsonnet-evaluator/src/lib.rs13.9 KiBsourcehistory
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;8pub 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;22mod tla;23pub mod trace;24pub mod typed;25pub mod val;2627use std::{28	any::Any,29	cell::{RefCell, RefMut},30	collections::hash_map::Entry,31	fmt::{self, Debug},32	rc::Rc,33};3435pub use ctx::*;36pub use dynamic::*;37pub use error::{Error, ErrorKind::*, Result, ResultExt};38pub use evaluate::*;39use function::CallLocation;40pub use import::*;41use jrsonnet_gcmodule::{cc_dyn, Cc, Trace};42pub use jrsonnet_interner::{IBytes, IStr};43#[doc(hidden)]44pub use jrsonnet_macros;45pub use jrsonnet_parser as parser;46use jrsonnet_parser::{LocExpr, ParserSettings, Source, SourcePath};47pub use obj::*;48pub use rustc_hash;49use rustc_hash::FxHashMap;50use stack::check_depth;51pub use tla::apply_tla;52pub use val::{Thunk, Val};5354use crate::gc::WithCapacityExt as _;5556cc_dyn!(57	#[derive(Clone)]58	CcUnbound<V>,59	Unbound<Bound = V>60);6162/// Thunk without bound `super`/`this`63/// object inheritance may be overriden multiple times, and will be fixed only on field read64pub trait Unbound: Trace {65	/// Type of value after object context is bound66	type Bound;67	/// Create value bound to specified object context68	fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Self::Bound>;69}7071/// Object fields may, or may not depend on `this`/`super`, this enum allows cheaper reuse of object-independent fields for native code72/// Standard jsonnet fields are always unbound73#[derive(Clone, Trace)]74pub enum MaybeUnbound {75	/// Value needs to be bound to `this`/`super`76	Unbound(CcUnbound<Val>),77	/// Value is object-independent78	Bound(Thunk<Val>),79}8081impl Debug for MaybeUnbound {82	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {83		write!(f, "MaybeUnbound")84	}85}86impl MaybeUnbound {87	/// Attach object context to value, if required88	pub fn evaluate(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Val> {89		match self {90			Self::Unbound(v) => v.0.bind(sup, this),91			Self::Bound(v) => Ok(v.evaluate()?),92		}93	}94}9596cc_dyn!(CcContextInitializer, ContextInitializer);9798/// During import, this trait will be called to create initial context for file.99/// It may initialize global variables, stdlib for example.100pub trait ContextInitializer: Trace {101	/// For which size the builder should be preallocated102	fn reserve_vars(&self) -> usize {103		0104	}105	/// Initialize default file context.106	/// Has default implementation, which calls `populate`.107	/// Prefer to always implement `populate` instead.108	fn initialize(&self, state: State, for_file: Source) -> Context {109		let mut builder = ContextBuilder::with_capacity(state, self.reserve_vars());110		self.populate(for_file, &mut builder);111		builder.build()112	}113	/// For composability: extend builder. May panic if this initialization is not supported,114	/// and the context may only be created via `initialize`.115	fn populate(&self, for_file: Source, builder: &mut ContextBuilder);116	/// Allows upcasting from abstract to concrete context initializer.117	/// jrsonnet by itself doesn't use this method, it is allowed for it to panic.118	fn as_any(&self) -> &dyn Any;119}120121/// Context initializer which adds nothing.122impl ContextInitializer for () {123	fn populate(&self, _for_file: Source, _builder: &mut ContextBuilder) {}124	fn as_any(&self) -> &dyn Any {125		self126	}127}128129impl<T> ContextInitializer for Option<T>130where131	T: ContextInitializer,132{133	fn initialize(&self, state: State, for_file: Source) -> Context {134		if let Some(ctx) = self {135			ctx.initialize(state, for_file)136		} else {137			().initialize(state, for_file)138		}139	}140141	fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {142		if let Some(ctx) = self {143			ctx.populate(for_file, builder);144		}145	}146147	fn as_any(&self) -> &dyn Any {148		self149	}150}151152macro_rules! impl_context_initializer {153	($($gen:ident)*) => {154		#[allow(non_snake_case)]155		impl<$($gen: ContextInitializer + Trace,)*> ContextInitializer for ($($gen,)*) {156			fn reserve_vars(&self) -> usize {157				let mut out = 0;158				let ($($gen,)*) = self;159				$(out += $gen.reserve_vars();)*160				out161			}162			fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {163				let ($($gen,)*) = self;164				$($gen.populate(for_file.clone(), builder);)*165			}166			fn as_any(&self) -> &dyn Any {167				self168			}169		}170	};171	($($cur:ident)* @ $c:ident $($rest:ident)*) => {172		impl_context_initializer!($($cur)*);173		impl_context_initializer!($($cur)* $c @ $($rest)*);174	};175	($($cur:ident)* @) => {176		impl_context_initializer!($($cur)*);177	}178}179impl_context_initializer! {180	A @ B C D E F G181}182183#[derive(Trace)]184struct FileData {185	string: Option<IStr>,186	bytes: Option<IBytes>,187	parsed: Option<LocExpr>,188	evaluated: Option<Val>,189190	evaluating: bool,191}192impl FileData {193	fn new_string(data: IStr) -> Self {194		Self {195			string: Some(data),196			bytes: None,197			parsed: None,198			evaluated: None,199			evaluating: false,200		}201	}202	fn new_bytes(data: IBytes) -> Self {203		Self {204			string: None,205			bytes: Some(data),206			parsed: None,207			evaluated: None,208			evaluating: false,209		}210	}211	pub(crate) fn get_string(&mut self) -> Option<IStr> {212		if self.string.is_none() {213			self.string = Some(214				self.bytes215					.as_ref()216					.expect("either string or bytes should be set")217					.clone()218					.cast_str()?,219			);220		}221		Some(self.string.clone().expect("just set"))222	}223}224225#[derive(Trace)]226pub struct EvaluationStateInternals {227	/// Internal state228	file_cache: RefCell<FxHashMap<SourcePath, FileData>>,229	/// Context initializer, which will be used for imports and everything230	/// [`NoopContextInitializer`] is used by default, most likely you want to have `jrsonnet-stdlib`231	context_initializer: CcContextInitializer,232	/// Used to resolve file locations/contents233	import_resolver: Rc<dyn ImportResolver>,234}235236/// Maintains stack trace and import resolution237#[derive(Clone, Trace)]238pub struct State(Cc<EvaluationStateInternals>);239240impl State {241	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise242	pub fn import_resolved_str(&self, path: SourcePath) -> Result<IStr> {243		let mut file_cache = self.file_cache();244		let mut file = file_cache.entry(path.clone());245246		let file = match file {247			Entry::Occupied(ref mut d) => d.get_mut(),248			Entry::Vacant(v) => {249				let data = self.import_resolver().load_file_contents(&path)?;250				v.insert(FileData::new_string(251					std::str::from_utf8(&data)252						.map_err(|_| ImportBadFileUtf8(path.clone()))?253						.into(),254				))255			}256		};257		Ok(file258			.get_string()259			.ok_or_else(|| ImportBadFileUtf8(path.clone()))?)260	}261	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise262	pub fn import_resolved_bin(&self, path: SourcePath) -> Result<IBytes> {263		let mut file_cache = self.file_cache();264		let mut file = file_cache.entry(path.clone());265266		let file = match file {267			Entry::Occupied(ref mut d) => d.get_mut(),268			Entry::Vacant(v) => {269				let data = self.import_resolver().load_file_contents(&path)?;270				v.insert(FileData::new_bytes(data.as_slice().into()))271			}272		};273		if let Some(str) = &file.bytes {274			return Ok(str.clone());275		}276		if file.bytes.is_none() {277			file.bytes = Some(278				file.string279					.as_ref()280					.expect("either string or bytes should be set")281					.clone()282					.cast_bytes(),283			);284		}285		Ok(file.bytes.as_ref().expect("just set").clone())286	}287	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise288	pub fn import_resolved(&self, path: SourcePath) -> Result<Val> {289		let mut file_cache = self.file_cache();290		let mut file = file_cache.entry(path.clone());291292		let file = match file {293			Entry::Occupied(ref mut d) => d.get_mut(),294			Entry::Vacant(v) => {295				let data = self.import_resolver().load_file_contents(&path)?;296				v.insert(FileData::new_string(297					std::str::from_utf8(&data)298						.map_err(|_| ImportBadFileUtf8(path.clone()))?299						.into(),300				))301			}302		};303		if let Some(val) = &file.evaluated {304			return Ok(val.clone());305		}306		let code = file307			.get_string()308			.ok_or_else(|| ImportBadFileUtf8(path.clone()))?;309		let file_name = Source::new(path.clone(), code.clone());310		if file.parsed.is_none() {311			file.parsed = Some(312				jrsonnet_parser::parse(313					&code,314					&ParserSettings {315						source: file_name.clone(),316					},317				)318				.map_err(|e| ImportSyntaxError {319					path: file_name.clone(),320					error: Box::new(e),321				})?,322			);323		}324		let parsed = file.parsed.as_ref().expect("just set").clone();325		if file.evaluating {326			bail!(InfiniteRecursionDetected)327		}328		file.evaluating = true;329		// Dropping file cache guard here, as evaluation may use this map too330		drop(file_cache);331		let res = evaluate(self.create_default_context(file_name), &parsed);332333		let mut file_cache = self.file_cache();334		let mut file = file_cache.entry(path.clone());335336		let Entry::Occupied(file) = &mut file else {337			unreachable!("this file was just here")338		};339		let file = file.get_mut();340		file.evaluating = false;341		match res {342			Ok(v) => {343				file.evaluated = Some(v.clone());344				Ok(v)345			}346			Err(e) => Err(e),347		}348	}349350	/// Has same semantics as `import 'path'` called from `from` file351	pub fn import_from(&self, from: &SourcePath, path: impl AsPathLike) -> Result<Val> {352		let resolved = self.resolve_from(from, &path)?;353		self.import_resolved(resolved)354	}355	pub fn import(&self, path: impl AsPathLike) -> Result<Val> {356		let resolved = self.resolve_from_default(&path)?;357		self.import_resolved(resolved)358	}359360	/// Creates context with all passed global variables361	pub fn create_default_context(&self, source: Source) -> Context {362		self.context_initializer().initialize(self.clone(), source)363	}364365	/// Creates context with all passed global variables, calling custom modifier366	pub fn create_default_context_with(367		&self,368		source: Source,369		context_initializer: impl ContextInitializer,370	) -> Context {371		let default_initializer = self.context_initializer();372		let mut builder = ContextBuilder::with_capacity(373			self.clone(),374			default_initializer.reserve_vars() + context_initializer.reserve_vars(),375		);376		default_initializer.populate(source.clone(), &mut builder);377		context_initializer.populate(source, &mut builder);378379		builder.build()380	}381}382383/// Internals384impl State {385	fn file_cache(&self) -> RefMut<'_, FxHashMap<SourcePath, FileData>> {386		self.0.file_cache.borrow_mut()387	}388}389/// Executes code creating a new stack frame, to be replaced with try{}390pub fn in_frame<T>(391	e: CallLocation<'_>,392	frame_desc: impl FnOnce() -> String,393	f: impl FnOnce() -> Result<T>,394) -> Result<T> {395	let _guard = check_depth()?;396397	f().with_description_src(e, frame_desc)398}399400/// Executes code creating a new stack frame, to be replaced with try{}401pub fn in_description_frame<T>(402	frame_desc: impl FnOnce() -> String,403	f: impl FnOnce() -> Result<T>,404) -> Result<T> {405	let _guard = check_depth()?;406407	f().with_description(frame_desc)408}409410#[derive(Trace)]411pub struct InitialUnderscore(pub Thunk<Val>);412impl ContextInitializer for InitialUnderscore {413	fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {414		builder.bind("_", self.0.clone());415	}416417	fn as_any(&self) -> &dyn Any {418		self419	}420}421422/// Raw methods evaluate passed values but don't perform TLA execution423impl State {424	/// Parses and evaluates the given snippet425	pub fn evaluate_snippet(&self, name: impl Into<IStr>, code: impl Into<IStr>) -> Result<Val> {426		let code = code.into();427		let source = Source::new_virtual(name.into(), code.clone());428		let parsed = jrsonnet_parser::parse(429			&code,430			&ParserSettings {431				source: source.clone(),432			},433		)434		.map_err(|e| ImportSyntaxError {435			path: source.clone(),436			error: Box::new(e),437		})?;438		evaluate(self.create_default_context(source), &parsed)439	}440	/// Parses and evaluates the given snippet with custom context modifier441	pub fn evaluate_snippet_with(442		&self,443		name: impl Into<IStr>,444		code: impl Into<IStr>,445		context_initializer: impl ContextInitializer,446	) -> Result<Val> {447		let code = code.into();448		let source = Source::new_virtual(name.into(), code.clone());449		let parsed = jrsonnet_parser::parse(450			&code,451			&ParserSettings {452				source: source.clone(),453			},454		)455		.map_err(|e| ImportSyntaxError {456			path: source.clone(),457			error: Box::new(e),458		})?;459		evaluate(460			self.create_default_context_with(source, context_initializer),461			&parsed,462		)463	}464}465466/// Settings utilities467impl State {468	// Only panics in case of [`ImportResolver`] contract violation469	#[allow(clippy::missing_panics_doc)]470	pub fn resolve_from(&self, from: &SourcePath, path: &dyn AsPathLike) -> Result<SourcePath> {471		self.import_resolver().resolve_from(from, path)472	}473	#[allow(clippy::missing_panics_doc)]474	pub fn resolve_from_default(&self, path: &dyn AsPathLike) -> Result<SourcePath> {475		self.import_resolver().resolve_from_default(path)476	}477	pub fn import_resolver(&self) -> &dyn ImportResolver {478		&*self.0.import_resolver479	}480	pub fn context_initializer(&self) -> &dyn ContextInitializer {481		&*self.0.context_initializer.0482	}483}484485impl State {486	pub fn builder() -> StateBuilder {487		StateBuilder::default()488	}489}490491impl Default for State {492	fn default() -> Self {493		Self::builder().build()494	}495}496497#[derive(Default)]498pub struct StateBuilder {499	import_resolver: Option<Rc<dyn ImportResolver>>,500	context_initializer: Option<CcContextInitializer>,501}502impl StateBuilder {503	pub fn import_resolver(&mut self, import_resolver: impl ImportResolver) -> &mut Self {504		let _ = self.import_resolver.insert(Rc::new(import_resolver));505		self506	}507	pub fn context_initializer(508		&mut self,509		context_initializer: impl ContextInitializer,510	) -> &mut Self {511		let _ = self512			.context_initializer513			.insert(CcContextInitializer::new(context_initializer));514		self515	}516	pub fn build(mut self) -> State {517		State(Cc::new(EvaluationStateInternals {518			file_cache: RefCell::new(FxHashMap::new()),519			context_initializer: self520				.context_initializer521				.take()522				.unwrap_or_else(|| CcContextInitializer::new(())),523			import_resolver: self524				.import_resolver525				.take()526				.unwrap_or_else(|| Rc::new(DummyImportResolver)),527		}))528	}529}