git.delta.rocks / jrsonnet / refs/commits / 284612bfbf1c

difftreelog

refactor! implement stack size limit using thread local

Yaroslav Bolyukin2022-10-25parent: #7b8b823.patch.diff
in: master

18 files changed

modifiedbindings/jsonnet/src/lib.rsdiffbeforeafterboth
--- a/bindings/jsonnet/src/lib.rs
+++ b/bindings/jsonnet/src/lib.rs
@@ -85,8 +85,8 @@
 
 /// Set the maximum stack depth.
 #[no_mangle]
-pub extern "C" fn jsonnet_max_stack(vm: &State, v: c_uint) {
-	vm.settings_mut().max_stack = v as usize;
+pub extern "C" fn jsonnet_max_stack(_vm: &State, _v: c_uint) {
+	todo!()
 }
 
 /// Set the number of objects required before a garbage collection cycle is allowed.
modifiedcmds/jrsonnet/src/main.rsdiffbeforeafterboth
--- a/cmds/jrsonnet/src/main.rs
+++ b/cmds/jrsonnet/src/main.rs
@@ -5,7 +5,7 @@
 
 use clap::{AppSettings, IntoApp, Parser};
 use clap_complete::Shell;
-use jrsonnet_cli::{ConfigureState, GcOpts, GeneralOpts, ManifestOpts, OutputOpts};
+use jrsonnet_cli::{ConfigureState, GeneralOpts, ManifestOpts, OutputOpts};
 use jrsonnet_evaluator::{error::LocError, State};
 
 #[cfg(feature = "mimalloc")]
@@ -60,8 +60,6 @@
 	output: OutputOpts,
 	#[clap(flatten)]
 	debug: DebugOpts,
-	#[clap(flatten)]
-	gc: GcOpts,
 }
 
 fn main() {
@@ -113,7 +111,6 @@
 }
 
 fn main_catch(opts: Opts) -> bool {
-	let _printer = opts.gc.stats_printer();
 	let s = State::default();
 	if let Err(e) = main_real(&s, opts) {
 		if let Error::Evaluation(e) = e {
@@ -127,7 +124,7 @@
 }
 
 fn main_real(s: &State, opts: Opts) -> Result<(), Error> {
-	opts.general.configure(s)?;
+	let _guards = opts.general.configure(s)?;
 	opts.manifest.configure(s)?;
 
 	let input = opts.input.input.ok_or(Error::MissingInputArgument)?;
modifiedcrates/jrsonnet-cli/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-cli/src/lib.rs
+++ b/crates/jrsonnet-cli/src/lib.rs
@@ -3,10 +3,12 @@
 mod tla;
 mod trace;
 
-use std::{env, path::PathBuf};
+use std::{env, marker::PhantomData, path::PathBuf};
 
 use clap::Parser;
-use jrsonnet_evaluator::{error::Result, FileImportResolver, State};
+use jrsonnet_evaluator::{
+	error::Result, stack::StackDepthLimitOverrideGuard, FileImportResolver, State,
+};
 use jrsonnet_gcmodule::with_thread_object_space;
 pub use manifest::*;
 pub use stdlib::*;
@@ -14,7 +16,9 @@
 pub use trace::*;
 
 pub trait ConfigureState {
-	fn configure(&self, s: &State) -> Result<()>;
+	type Guards;
+
+	fn configure(&self, s: &State) -> Result<Self::Guards>;
 }
 
 #[derive(Parser)]
@@ -44,7 +48,8 @@
 	jpath: Vec<PathBuf>,
 }
 impl ConfigureState for MiscOpts {
-	fn configure(&self, s: &State) -> Result<()> {
+	type Guards = StackDepthLimitOverrideGuard;
+	fn configure(&self, s: &State) -> Result<Self::Guards> {
 		let mut library_paths = self.jpath.clone();
 		library_paths.reverse();
 		if let Some(path) = env::var_os("JSONNET_PATH") {
@@ -53,8 +58,8 @@
 
 		s.set_import_resolver(Box::new(FileImportResolver::new(library_paths)));
 
-		s.set_max_stack(self.max_stack);
-		Ok(())
+		let _depth_limit = jrsonnet_evaluator::stack::limit_stack_depth(self.max_stack);
+		Ok(_depth_limit)
 	}
 }
 
@@ -72,16 +77,24 @@
 
 	#[clap(flatten)]
 	trace: TraceOpts,
+
+	#[clap(flatten)]
+	gc: GcOpts,
 }
 
 impl ConfigureState for GeneralOpts {
-	fn configure(&self, s: &State) -> Result<()> {
+	type Guards = (
+		<MiscOpts as ConfigureState>::Guards,
+		<GcOpts as ConfigureState>::Guards,
+	);
+	fn configure(&self, s: &State) -> Result<Self::Guards> {
 		// Configure trace first, because tla-code/ext-code can throw
 		self.trace.configure(s)?;
-		self.misc.configure(s)?;
+		let misc_guards = self.misc.configure(s)?;
 		self.tla.configure(s)?;
 		self.std.configure(s)?;
-		Ok(())
+		let gc_guards = self.gc.configure(s)?;
+		Ok((misc_guards, gc_guards))
 	}
 }
 
@@ -100,20 +113,22 @@
 	#[clap(long)]
 	gc_collect_before_printing_stats: bool,
 }
-impl GcOpts {
-	pub fn stats_printer(&self) -> (Option<GcStatsPrinter>, Option<LeakSpace>) {
+impl ConfigureState for GcOpts {
+	type Guards = (Option<GcStatsPrinter>, Option<LeakSpace>);
+
+	fn configure(&self, _s: &State) -> Result<Self::Guards> {
 		// Constructed structs have side-effects in Drop impl
 		#[allow(clippy::unnecessary_lazy_evaluations)]
-		(
+		Ok((
 			self.gc_print_stats.then(|| GcStatsPrinter {
 				collect_before_printing_stats: self.gc_collect_before_printing_stats,
 			}),
-			(!self.gc_collect_on_exit).then(|| LeakSpace {}),
-		)
+			(!self.gc_collect_on_exit).then(|| LeakSpace(PhantomData)),
+		))
 	}
 }
 
-pub struct LeakSpace {}
+pub struct LeakSpace(PhantomData<()>);
 
 impl Drop for LeakSpace {
 	fn drop(&mut self) {
modifiedcrates/jrsonnet-cli/src/manifest.rsdiffbeforeafterboth
--- a/crates/jrsonnet-cli/src/manifest.rs
+++ b/crates/jrsonnet-cli/src/manifest.rs
@@ -47,6 +47,7 @@
 	exp_preserve_order: bool,
 }
 impl ConfigureState for ManifestOpts {
+	type Guards = ();
 	fn configure(&self, s: &State) -> Result<()> {
 		if self.string {
 			s.set_manifest_format(ManifestFormat::String);
modifiedcrates/jrsonnet-cli/src/stdlib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-cli/src/stdlib.rs
+++ b/crates/jrsonnet-cli/src/stdlib.rs
@@ -106,6 +106,7 @@
 	ext_code_file: Vec<ExtFile>,
 }
 impl ConfigureState for StdOpts {
+	type Guards = ();
 	fn configure(&self, s: &State) -> Result<()> {
 		if self.no_stdlib {
 			return Ok(());
modifiedcrates/jrsonnet-cli/src/tla.rsdiffbeforeafterboth
--- a/crates/jrsonnet-cli/src/tla.rs
+++ b/crates/jrsonnet-cli/src/tla.rs
@@ -47,6 +47,7 @@
 	tla_code_file: Vec<ExtFile>,
 }
 impl ConfigureState for TLAOpts {
+	type Guards = ();
 	fn configure(&self, s: &State) -> Result<()> {
 		for tla in self.tla_str.iter() {
 			s.add_tla_str((&tla.name as &str).into(), (&tla.value as &str).into());
modifiedcrates/jrsonnet-cli/src/trace.rsdiffbeforeafterboth
--- a/crates/jrsonnet-cli/src/trace.rs
+++ b/crates/jrsonnet-cli/src/trace.rs
@@ -41,6 +41,7 @@
 	max_trace: usize,
 }
 impl ConfigureState for TraceOpts {
+	type Guards = ();
 	fn configure(&self, s: &State) -> Result<()> {
 		let resolver = PathResolver::new_cwd_fallback();
 		match self
modifiedcrates/jrsonnet-evaluator/Cargo.tomldiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/Cargo.toml
+++ b/crates/jrsonnet-evaluator/Cargo.toml
@@ -20,8 +20,8 @@
 exp-preserve-order = []
 # Implements field destructuring
 exp-destruct = ["jrsonnet-parser/exp-destruct"]
-# Provide Typed for conversions to/from serde_json::Value type
-serde_json = ["dep:serde_json"]
+# Improves performance, and implements some useful things using nightly-only features
+nightly = []
 
 [dependencies]
 jrsonnet-interner = { path = "../jrsonnet-interner", version = "0.4.2" }
modifiedcrates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -2,11 +2,11 @@
 
 use jrsonnet_gcmodule::Trace;
 use jrsonnet_interner::IStr;
-use jrsonnet_parser::{BinaryOpType, ExprLocation, Source, SourcePath, UnaryOpType};
+use jrsonnet_parser::{BinaryOpType, ExprLocation, LocExpr, Source, SourcePath, UnaryOpType};
 use jrsonnet_types::ValType;
 use thiserror::Error;
 
-use crate::{stdlib::format::FormatError, typed::TypeLocError};
+use crate::{function::CallLocation, stdlib::format::FormatError, typed::TypeLocError};
 
 fn format_found(list: &[IStr], what: &str) -> String {
 	if list.is_empty() {
@@ -262,7 +262,72 @@
 	}
 }
 
+pub trait ErrorSource {
+	fn to_location(self) -> Option<ExprLocation>;
+}
+impl ErrorSource for &LocExpr {
+	fn to_location(self) -> Option<ExprLocation> {
+		Some(self.1.clone())
+	}
+}
+impl ErrorSource for &ExprLocation {
+	fn to_location(self) -> Option<ExprLocation> {
+		Some(self.clone())
+	}
+}
+impl ErrorSource for CallLocation<'_> {
+	fn to_location(self) -> Option<ExprLocation> {
+		self.0.cloned()
+	}
+}
+
 pub type Result<V, E = LocError> = std::result::Result<V, E>;
+pub trait ResultExt: Sized {
+	#[must_use]
+	fn with_description<O: Into<String>>(self, msg: impl FnOnce() -> O) -> Self;
+	#[must_use]
+	fn description(self, msg: &str) -> Self {
+		self.with_description(|| msg)
+	}
+
+	#[must_use]
+	fn with_description_src<O: Into<String>>(
+		self,
+		src: impl ErrorSource,
+		msg: impl FnOnce() -> O,
+	) -> Self;
+	#[must_use]
+	fn description_src(self, src: impl ErrorSource, msg: &str) -> Self {
+		self.with_description_src(src, || msg)
+	}
+}
+impl<T> ResultExt for Result<T, LocError> {
+	fn with_description<O: Into<String>>(mut self, msg: impl FnOnce() -> O) -> Self {
+		if let Err(e) = &mut self {
+			let trace = e.trace_mut();
+			trace.0.push(StackTraceElement {
+				location: None,
+				desc: msg().into(),
+			});
+		}
+		self
+	}
+
+	fn with_description_src<O: Into<String>>(
+		mut self,
+		src: impl ErrorSource,
+		msg: impl FnOnce() -> O,
+	) -> Self {
+		if let Err(e) = &mut self {
+			let trace = e.trace_mut();
+			trace.0.push(StackTraceElement {
+				location: src.to_location(),
+				desc: msg().into(),
+			});
+		}
+		self
+	}
+}
 
 #[macro_export]
 macro_rules! throw {
modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/evaluate/mod.rs
+++ b/crates/jrsonnet-evaluator/src/evaluate/mod.rs
@@ -34,7 +34,7 @@
 pub fn evaluate_field_name(s: State, ctx: Context, field_name: &FieldName) -> Result<Option<IStr>> {
 	Ok(match field_name {
 		FieldName::Fixed(n) => Some(n.clone()),
-		FieldName::Dyn(expr) => s.push(
+		FieldName::Dyn(expr) => State::push(
 			CallLocation::new(&expr.1),
 			|| "evaluating field name".to_string(),
 			|| {
@@ -184,14 +184,11 @@
 					.with_add(*plus)
 					.with_visibility(*visibility)
 					.with_location(value.1.clone())
-					.bindable(
-						s.clone(),
-						tb!(UnboundValue {
-							uctx: uctx.clone(),
-							value: value.clone(),
-							name: name.clone()
-						}),
-					)?;
+					.bindable(tb!(UnboundValue {
+						uctx: uctx.clone(),
+						value: value.clone(),
+						name: name.clone()
+					}))?;
 			}
 			Member::Field(FieldMember {
 				name,
@@ -233,15 +230,12 @@
 					.member(name.clone())
 					.hide()
 					.with_location(value.1.clone())
-					.bindable(
-						s.clone(),
-						tb!(UnboundMethod {
-							uctx: uctx.clone(),
-							value: value.clone(),
-							params: params.clone(),
-							name: name.clone()
-						}),
-					)?;
+					.bindable(tb!(UnboundMethod {
+						uctx: uctx.clone(),
+						value: value.clone(),
+						params: params.clone(),
+						name: name.clone()
+					}))?;
 			}
 			Member::BindStmt(_) => {}
 			Member::AssertStmt(stmt) => {
@@ -324,13 +318,10 @@
 							.member(n)
 							.with_location(obj.value.1.clone())
 							.with_add(obj.plus)
-							.bindable(
-								s.clone(),
-								tb!(UnboundValue {
-									uctx,
-									value: obj.value.clone(),
-								}),
-							)?;
+							.bindable(tb!(UnboundValue {
+								uctx,
+								value: obj.value.clone(),
+							}))?;
 					}
 					v => throw!(FieldMustBeStringGot(v.value_type())),
 				}
@@ -364,7 +355,7 @@
 			if tailstrict {
 				body()?
 			} else {
-				s.push(loc, || format!("function <{}> call", f.name()), body)?
+				State::push(loc, || format!("function <{}> call", f.name()), body)?
 			}
 		}
 		v => throw!(OnlyFunctionsCanBeCalledGot(v.value_type())),
@@ -374,13 +365,13 @@
 pub fn evaluate_assert(s: State, ctx: Context, assertion: &AssertStmt) -> Result<()> {
 	let value = &assertion.0;
 	let msg = &assertion.1;
-	let assertion_result = s.push(
+	let assertion_result = State::push(
 		CallLocation::new(&value.1),
 		|| "assertion condition".to_owned(),
 		|| bool::from_untyped(evaluate(s.clone(), ctx.clone(), value)?, s.clone()),
 	)?;
 	if !assertion_result {
-		s.push(
+		State::push(
 			CallLocation::new(&value.1),
 			|| "assertion failure".to_owned(),
 			|| {
@@ -432,7 +423,7 @@
 		Num(v) => Val::new_checked_num(*v)?,
 		BinaryOp(v1, o, v2) => evaluate_binary_op_special(s, ctx, v1, *o, v2)?,
 		UnaryOp(o, v) => evaluate_unary_op(*o, &evaluate(s, ctx, v)?)?,
-		Var(name) => s.push(
+		Var(name) => State::push(
 			CallLocation::new(loc),
 			|| format!("variable <{name}> access"),
 			|| ctx.binding(name.clone())?.evaluate(s.clone()),
@@ -442,7 +433,7 @@
 				evaluate(s.clone(), ctx.clone(), value)?,
 				evaluate(s.clone(), ctx, index)?,
 			) {
-				(Val::Obj(v), Val::Str(key)) => s.push(
+				(Val::Obj(v), Val::Str(key)) => State::push(
 					CallLocation::new(loc),
 					|| format!("field <{key}> access"),
 					|| match v.get(s.clone(), key.clone()) {
@@ -571,7 +562,7 @@
 			evaluate_assert(s.clone(), ctx.clone(), assert)?;
 			evaluate(s, ctx, returned)?
 		}
-		ErrorStmt(e) => s.push(
+		ErrorStmt(e) => State::push(
 			CallLocation::new(loc),
 			|| "error statement".to_owned(),
 			|| {
@@ -585,7 +576,7 @@
 			cond_then,
 			cond_else,
 		} => {
-			if s.push(
+			if State::push(
 				CallLocation::new(loc),
 				|| "if condition".to_owned(),
 				|| bool::from_untyped(evaluate(s.clone(), ctx.clone(), &cond.0)?, s.clone()),
@@ -607,7 +598,7 @@
 				desc: &'static str,
 			) -> Result<Option<T>> {
 				if let Some(value) = expr {
-					Ok(Some(s.push(
+					Ok(Some(State::push(
 						loc,
 						|| format!("slice {desc}"),
 						|| T::from_untyped(evaluate(s.clone(), ctx.clone(), value)?, s.clone()),
@@ -630,7 +621,7 @@
 			let tmp = loc.clone().0;
 			let resolved_path = s.resolve_from(tmp.source_path(), path as &str)?;
 			match i {
-				Import(_) => s.push(
+				Import(_) => State::push(
 					CallLocation::new(loc),
 					|| format!("import {:?}", path.clone()),
 					|| s.import_resolved(resolved_path),
modifiedcrates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/lib.rs
1//! jsonnet interpreter implementation23#![deny(unsafe_op_in_unsafe_fn)]4#![warn(5	clippy::all,6	clippy::nursery,7	clippy::pedantic,8	// missing_docs,9	elided_lifetimes_in_paths,10	explicit_outlives_requirements,11	noop_method_call,12	single_use_lifetimes,13	variant_size_differences,14	rustdoc::all15)]16#![allow(17	macro_expanded_macro_exports_accessed_by_absolute_paths,18	clippy::ptr_arg,19	// Too verbose20	clippy::must_use_candidate,21	// A lot of functions pass around errors thrown by code22	clippy::missing_errors_doc,23	// A lot of pointers have interior Rc24	clippy::needless_pass_by_value,25	// Its fine26	clippy::wildcard_imports,27	clippy::enum_glob_use,28	clippy::module_name_repetitions,29	// TODO: fix individual issues, however this works as intended almost everywhere30	clippy::cast_precision_loss,31	clippy::cast_possible_wrap,32	clippy::cast_possible_truncation,33	clippy::cast_sign_loss,34	// False positives35	// https://github.com/rust-lang/rust-clippy/issues/690236	clippy::use_self,37	// https://github.com/rust-lang/rust-clippy/issues/853938	clippy::iter_with_drain,39)]4041// For jrsonnet-macros42extern crate self as jrsonnet_evaluator;4344mod ctx;45mod dynamic;46pub mod error;47mod evaluate;48pub mod function;49pub mod gc;50mod import;51mod integrations;52mod map;53mod obj;54pub mod stdlib;55pub mod trace;56pub mod typed;57pub mod val;5859use std::{60	any::Any,61	cell::{Ref, RefCell, RefMut},62	collections::HashMap,63	fmt::{self, Debug},64	path::Path,65	rc::Rc,66};6768pub use ctx::*;69pub use dynamic::*;70use error::{Error::*, LocError, Result, StackTraceElement};71pub use evaluate::*;72use function::{CallLocation, TlaArg};73use gc::{GcHashMap, TraceBox};74use hashbrown::hash_map::RawEntryMut;75pub use import::*;76use jrsonnet_gcmodule::{Cc, Trace};77pub use jrsonnet_interner::{IBytes, IStr};78pub use jrsonnet_parser as parser;79use jrsonnet_parser::*;80pub use obj::*;81use trace::{CompactFormat, TraceFormat};82pub use val::{ManifestFormat, Thunk, Val};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, s: State, sup: Option<ObjValue>, this: Option<ObjValue>) -> 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(Cc<TraceBox<dyn Unbound<Bound = Thunk<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(111		&self,112		s: State,113		sup: Option<ObjValue>,114		this: Option<ObjValue>,115	) -> Result<Thunk<Val>> {116		match self {117			Self::Unbound(v) => v.bind(s, sup, this),118			Self::Bound(v) => Ok(v.clone()),119		}120	}121}122123/// During import, this trait will be called to create initial context for file.124/// It may initialize global variables, stdlib for example.125pub trait ContextInitializer {126	/// Initialize default file context.127	fn initialize(&self, state: State, for_file: Source) -> Context;128	/// Allows upcasting from abstract to concrete context initializer.129	/// jrsonnet by itself doesn't use this method, it is allowed for it to panic.130	fn as_any(&self) -> &dyn Any;131}132133/// Context initializer which adds nothing.134pub struct DummyContextInitializer;135impl ContextInitializer for DummyContextInitializer {136	fn initialize(&self, _state: State, _for_file: Source) -> Context {137		Context::default()138	}139	fn as_any(&self) -> &dyn Any {140		self141	}142}143144/// Dynamically reconfigurable evaluation settings145pub struct EvaluationSettings {146	/// Limits recursion by limiting the number of stack frames147	pub max_stack: usize,148	/// Limits amount of stack trace items preserved149	pub max_trace: usize,150	/// TLA vars151	pub tla_vars: HashMap<IStr, TlaArg>,152	/// Context initializer, which will be used for imports and everything153	/// [`NoopContextInitializer`] is used by default, most likely you want to have `jrsonnet-stdlib`154	pub context_initializer: Box<dyn ContextInitializer>,155	/// Used to resolve file locations/contents156	pub import_resolver: Box<dyn ImportResolver>,157	/// Used in manifestification functions158	pub manifest_format: ManifestFormat,159	/// Used for bindings160	pub trace_format: Box<dyn TraceFormat>,161}162impl Default for EvaluationSettings {163	fn default() -> Self {164		Self {165			max_stack: 200,166			max_trace: 20,167			context_initializer: Box::new(DummyContextInitializer),168			tla_vars: HashMap::default(),169			import_resolver: Box::new(DummyImportResolver),170			manifest_format: ManifestFormat::Json {171				padding: 4,172				#[cfg(feature = "exp-preserve-order")]173				preserve_order: false,174			},175			trace_format: Box::new(CompactFormat {176				padding: 4,177				resolver: trace::PathResolver::Absolute,178			}),179		}180	}181}182183#[derive(Default)]184struct EvaluationData {185	/// Used for stack overflow detection, stacktrace is populated on unwind186	stack_depth: usize,187	/// Updated every time stack entry is popt188	stack_generation: usize,189190	breakpoints: Breakpoints,191192	/// Contains file source codes and evaluation results for imports and pretty-printed stacktraces193	files: GcHashMap<SourcePath, FileData>,194}195struct FileData {196	string: Option<IStr>,197	bytes: Option<IBytes>,198	parsed: Option<LocExpr>,199	evaluated: Option<Val>,200201	evaluating: bool,202}203impl FileData {204	fn new_string(data: IStr) -> Self {205		Self {206			string: Some(data),207			bytes: None,208			parsed: None,209			evaluated: None,210			evaluating: false,211		}212	}213	fn new_bytes(data: IBytes) -> Self {214		Self {215			string: None,216			bytes: Some(data),217			parsed: None,218			evaluated: None,219			evaluating: false,220		}221	}222}223224#[allow(clippy::type_complexity)]225pub struct Breakpoint {226	loc: ExprLocation,227	collected: RefCell<HashMap<usize, (usize, Vec<Result<Val>>)>>,228}229#[derive(Default)]230struct Breakpoints(Vec<Rc<Breakpoint>>);231impl Breakpoints {232	fn insert(233		&self,234		stack_depth: usize,235		stack_generation: usize,236		loc: &ExprLocation,237		result: Result<Val>,238	) -> Result<Val> {239		if self.0.is_empty() {240			return result;241		}242		for item in &self.0 {243			if item.loc.belongs_to(loc) {244				let mut collected = item.collected.borrow_mut();245				let (depth, vals) = collected.entry(stack_generation).or_default();246				if stack_depth > *depth {247					vals.clear();248				}249				vals.push(result.clone());250			}251		}252		result253	}254}255256#[derive(Default)]257pub struct EvaluationStateInternals {258	/// Internal state259	data: RefCell<EvaluationData>,260	/// Settings, safe to change at runtime261	settings: RefCell<EvaluationSettings>,262}263264/// Maintains stack trace and import resolution265#[derive(Default, Clone)]266pub struct State(Rc<EvaluationStateInternals>);267268impl State {269	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise270	pub fn import_resolved_str(&self, path: SourcePath) -> Result<IStr> {271		let mut data = self.data_mut();272		let mut file = data.files.raw_entry_mut().from_key(&path);273274		let file = match file {275			RawEntryMut::Occupied(ref mut d) => d.get_mut(),276			RawEntryMut::Vacant(v) => {277				let data = self.settings().import_resolver.load_file_contents(&path)?;278				v.insert(279					path.clone(),280					FileData::new_string(281						std::str::from_utf8(&data)282							.map_err(|_| ImportBadFileUtf8(path.clone()))?283							.into(),284					),285				)286				.1287			}288		};289		if let Some(str) = &file.string {290			return Ok(str.clone());291		}292		if file.string.is_none() {293			file.string = Some(294				file.bytes295					.as_ref()296					.expect("either string or bytes should be set")297					.clone()298					.cast_str()299					.ok_or_else(|| ImportBadFileUtf8(path.clone()))?,300			);301		}302		Ok(file.string.as_ref().expect("just set").clone())303	}304	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise305	pub fn import_resolved_bin(&self, path: SourcePath) -> Result<IBytes> {306		let mut data = self.data_mut();307		let mut file = data.files.raw_entry_mut().from_key(&path);308309		let file = match file {310			RawEntryMut::Occupied(ref mut d) => d.get_mut(),311			RawEntryMut::Vacant(v) => {312				let data = self.settings().import_resolver.load_file_contents(&path)?;313				v.insert(path.clone(), FileData::new_bytes(data.as_slice().into()))314					.1315			}316		};317		if let Some(str) = &file.bytes {318			return Ok(str.clone());319		}320		if file.bytes.is_none() {321			file.bytes = Some(322				file.string323					.as_ref()324					.expect("either string or bytes should be set")325					.clone()326					.cast_bytes(),327			);328		}329		Ok(file.bytes.as_ref().expect("just set").clone())330	}331	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise332	pub fn import_resolved(&self, path: SourcePath) -> Result<Val> {333		let mut data = self.data_mut();334		let mut file = data.files.raw_entry_mut().from_key(&path);335336		let file = match file {337			RawEntryMut::Occupied(ref mut d) => d.get_mut(),338			RawEntryMut::Vacant(v) => {339				let data = self.settings().import_resolver.load_file_contents(&path)?;340				v.insert(341					path.clone(),342					FileData::new_string(343						std::str::from_utf8(&data)344							.map_err(|_| ImportBadFileUtf8(path.clone()))?345							.into(),346					),347				)348				.1349			}350		};351		if let Some(val) = &file.evaluated {352			return Ok(val.clone());353		}354		if file.string.is_none() {355			file.string = Some(356				std::str::from_utf8(357					file.bytes358						.as_ref()359						.expect("either string or bytes should be set"),360				)361				.map_err(|_| ImportBadFileUtf8(path.clone()))?362				.into(),363			);364		}365		let code = file.string.as_ref().expect("just set");366		let file_name = Source::new(path.clone(), code.clone());367		if file.parsed.is_none() {368			file.parsed = Some(369				jrsonnet_parser::parse(370					code,371					&ParserSettings {372						file_name: file_name.clone(),373					},374				)375				.map_err(|e| ImportSyntaxError {376					path: file_name.clone(),377					error: Box::new(e),378				})?,379			);380		}381		let parsed = file.parsed.as_ref().expect("just set").clone();382		if file.evaluating {383			throw!(InfiniteRecursionDetected)384		}385		file.evaluating = true;386		// Dropping file here, as it borrows data, which may be used in evaluation387		drop(data);388		let res = evaluate(389			self.clone(),390			self.create_default_context(file_name),391			&parsed,392		);393394		let mut data = self.data_mut();395		let mut file = data.files.raw_entry_mut().from_key(&path);396397		let file = match file {398			RawEntryMut::Occupied(ref mut d) => d.get_mut(),399			RawEntryMut::Vacant(_) => unreachable!("this file was just here!"),400		};401		file.evaluating = false;402		match res {403			Ok(v) => {404				file.evaluated = Some(v.clone());405				Ok(v)406			}407			Err(e) => Err(e),408		}409	}410411	/// Has same semantics as `import 'path'` called from `from` file412	pub fn import_from(&self, from: &SourcePath, path: &str) -> Result<Val> {413		let resolved = self.resolve_from(from, path)?;414		self.import_resolved(resolved)415	}416	pub fn import(&self, path: impl AsRef<Path>) -> Result<Val> {417		let resolved = self.resolve(path)?;418		self.import_resolved(resolved)419	}420421	/// Creates context with all passed global variables422	pub fn create_default_context(&self, source: Source) -> Context {423		let context_initializer = &self.settings().context_initializer;424		context_initializer.initialize(self.clone(), source)425	}426427	/// Executes code creating a new stack frame428	pub fn push<T>(429		&self,430		e: CallLocation<'_>,431		frame_desc: impl FnOnce() -> String,432		f: impl FnOnce() -> Result<T>,433	) -> Result<T> {434		{435			let mut data = self.data_mut();436			let stack_depth = &mut data.stack_depth;437			if *stack_depth > self.max_stack() {438				// Error creation uses data, so i drop guard here439				drop(data);440				throw!(StackOverflow);441			}442			*stack_depth += 1;443		}444		let result = f();445		{446			let mut data = self.data_mut();447			data.stack_depth -= 1;448			data.stack_generation += 1;449		}450		if let Err(mut err) = result {451			err.trace_mut().0.push(StackTraceElement {452				location: e.0.cloned(),453				desc: frame_desc(),454			});455			return Err(err);456		}457		result458	}459460	/// Executes code creating a new stack frame461	pub fn push_val(462		&self,463		e: &ExprLocation,464		frame_desc: impl FnOnce() -> String,465		f: impl FnOnce() -> Result<Val>,466	) -> Result<Val> {467		{468			let mut data = self.data_mut();469			let stack_depth = &mut data.stack_depth;470			if *stack_depth > self.max_stack() {471				// Error creation uses data, so i drop guard here472				drop(data);473				throw!(StackOverflow);474			}475			*stack_depth += 1;476		}477		let mut result = f();478		{479			let mut data = self.data_mut();480			data.stack_depth -= 1;481			data.stack_generation += 1;482			result = data483				.breakpoints484				.insert(data.stack_depth, data.stack_generation, e, result);485		}486		if let Err(mut err) = result {487			err.trace_mut().0.push(StackTraceElement {488				location: Some(e.clone()),489				desc: frame_desc(),490			});491			return Err(err);492		}493		result494	}495	/// Executes code creating a new stack frame496	pub fn push_description<T>(497		&self,498		frame_desc: impl FnOnce() -> String,499		f: impl FnOnce() -> Result<T>,500	) -> Result<T> {501		{502			let mut data = self.data_mut();503			let stack_depth = &mut data.stack_depth;504			if *stack_depth > self.max_stack() {505				// Error creation uses data, so i drop guard here506				drop(data);507				throw!(StackOverflow);508			}509			*stack_depth += 1;510		}511		let result = f();512		{513			let mut data = self.data_mut();514			data.stack_depth -= 1;515			data.stack_generation += 1;516		}517		if let Err(mut err) = result {518			err.trace_mut().0.push(StackTraceElement {519				location: None,520				desc: frame_desc(),521			});522			return Err(err);523		}524		result525	}526527	/// # Panics528	/// In case of formatting failure529	pub fn stringify_err(&self, e: &LocError) -> String {530		let mut out = String::new();531		self.settings()532			.trace_format533			.write_trace(&mut out, self, e)534			.unwrap();535		out536	}537538	pub fn manifest(&self, val: Val) -> Result<IStr> {539		self.push_description(540			|| "manifestification".to_string(),541			|| val.manifest(self.clone(), &self.manifest_format()),542		)543	}544	pub fn manifest_multi(&self, val: Val) -> Result<Vec<(IStr, IStr)>> {545		val.manifest_multi(self.clone(), &self.manifest_format())546	}547	pub fn manifest_stream(&self, val: Val) -> Result<Vec<IStr>> {548		val.manifest_stream(self.clone(), &self.manifest_format())549	}550551	/// If passed value is function then call with set TLA552	pub fn with_tla(&self, val: Val) -> Result<Val> {553		Ok(match val {554			Val::Func(func) => self.push_description(555				|| "during TLA call".to_owned(),556				|| {557					func.evaluate(558						self.clone(),559						self.create_default_context(Source::new_virtual(560							"<tla>".into(),561							IStr::empty(),562						)),563						CallLocation::native(),564						&self.settings().tla_vars,565						true,566					)567				},568			)?,569			v => v,570		})571	}572}573574/// Internals575impl State {576	fn data_mut(&self) -> RefMut<'_, EvaluationData> {577		self.0.data.borrow_mut()578	}579	pub fn settings(&self) -> Ref<'_, EvaluationSettings> {580		self.0.settings.borrow()581	}582	pub fn settings_mut(&self) -> RefMut<'_, EvaluationSettings> {583		self.0.settings.borrow_mut()584	}585}586587/// Raw methods evaluate passed values but don't perform TLA execution588impl State {589	/// Parses and evaluates the given snippet590	pub fn evaluate_snippet(&self, name: impl Into<IStr>, code: impl Into<IStr>) -> Result<Val> {591		let code = code.into();592		let source = Source::new_virtual(name.into(), code.clone());593		let parsed = jrsonnet_parser::parse(594			&code,595			&ParserSettings {596				file_name: source.clone(),597			},598		)599		.map_err(|e| ImportSyntaxError {600			path: source.clone(),601			error: Box::new(e),602		})?;603		evaluate(self.clone(), self.create_default_context(source), &parsed)604	}605}606607/// Settings utilities608impl State {609	pub fn add_tla(&self, name: IStr, value: Val) {610		self.settings_mut()611			.tla_vars612			.insert(name, TlaArg::Val(value));613	}614	pub fn add_tla_str(&self, name: IStr, value: IStr) {615		self.settings_mut()616			.tla_vars617			.insert(name, TlaArg::String(value));618	}619	pub fn add_tla_code(&self, name: IStr, code: &str) -> Result<()> {620		let source_name = format!("<top-level-arg:{name}>");621		let source = Source::new_virtual(source_name.into(), code.into());622		let parsed = jrsonnet_parser::parse(623			code,624			&ParserSettings {625				file_name: source.clone(),626			},627		)628		.map_err(|e| ImportSyntaxError {629			path: source,630			error: Box::new(e),631		})?;632		self.settings_mut()633			.tla_vars634			.insert(name, TlaArg::Code(parsed));635		Ok(())636	}637638	// Only panics in case of [`ImportResolver`] contract violation639	#[allow(clippy::missing_panics_doc)]640	pub fn resolve_from(&self, from: &SourcePath, path: &str) -> Result<SourcePath> {641		self.import_resolver().resolve_from(from, path.as_ref())642	}643644	// Only panics in case of [`ImportResolver`] contract violation645	#[allow(clippy::missing_panics_doc)]646	pub fn resolve(&self, path: impl AsRef<Path>) -> Result<SourcePath> {647		self.import_resolver().resolve(path.as_ref())648	}649	pub fn import_resolver(&self) -> Ref<'_, dyn ImportResolver> {650		Ref::map(self.settings(), |s| &*s.import_resolver)651	}652	pub fn set_import_resolver(&self, resolver: Box<dyn ImportResolver>) {653		self.settings_mut().import_resolver = resolver;654	}655	pub fn context_initializer(&self) -> Ref<'_, dyn ContextInitializer> {656		Ref::map(self.settings(), |s| &*s.context_initializer)657	}658659	pub fn manifest_format(&self) -> ManifestFormat {660		self.settings().manifest_format.clone()661	}662	pub fn set_manifest_format(&self, format: ManifestFormat) {663		self.settings_mut().manifest_format = format;664	}665666	pub fn trace_format(&self) -> Ref<'_, dyn TraceFormat> {667		Ref::map(self.settings(), |s| &*s.trace_format)668	}669	pub fn set_trace_format(&self, format: Box<dyn TraceFormat>) {670		self.settings_mut().trace_format = format;671	}672673	pub fn max_trace(&self) -> usize {674		self.settings().max_trace675	}676	pub fn set_max_trace(&self, trace: usize) {677		self.settings_mut().max_trace = trace;678	}679680	pub fn max_stack(&self) -> usize {681		self.settings().max_stack682	}683	pub fn set_max_stack(&self, trace: usize) {684		self.settings_mut().max_stack = trace;685	}686}
after · crates/jrsonnet-evaluator/src/lib.rs
1//! jsonnet interpreter implementation2#![cfg_attr(feature = "nightly", feature(thread_local))]3#![deny(unsafe_op_in_unsafe_fn)]4#![warn(5	clippy::all,6	clippy::nursery,7	clippy::pedantic,8	// missing_docs,9	elided_lifetimes_in_paths,10	explicit_outlives_requirements,11	noop_method_call,12	single_use_lifetimes,13	variant_size_differences,14	rustdoc::all15)]16#![allow(17	macro_expanded_macro_exports_accessed_by_absolute_paths,18	clippy::ptr_arg,19	// Too verbose20	clippy::must_use_candidate,21	// A lot of functions pass around errors thrown by code22	clippy::missing_errors_doc,23	// A lot of pointers have interior Rc24	clippy::needless_pass_by_value,25	// Its fine26	clippy::wildcard_imports,27	clippy::enum_glob_use,28	clippy::module_name_repetitions,29	// TODO: fix individual issues, however this works as intended almost everywhere30	clippy::cast_precision_loss,31	clippy::cast_possible_wrap,32	clippy::cast_possible_truncation,33	clippy::cast_sign_loss,34	// False positives35	// https://github.com/rust-lang/rust-clippy/issues/690236	clippy::use_self,37	// https://github.com/rust-lang/rust-clippy/issues/853938	clippy::iter_with_drain,39)]4041// For jrsonnet-macros42extern crate self as jrsonnet_evaluator;4344mod ctx;45mod dynamic;46pub mod error;47mod evaluate;48pub mod function;49pub mod gc;50mod import;51mod integrations;52mod map;53mod obj;54pub mod stack;55pub mod stdlib;56pub mod trace;57pub mod typed;58pub mod val;5960use std::{61	any::Any,62	cell::{Ref, RefCell, RefMut},63	collections::HashMap,64	fmt::{self, Debug},65	path::Path,66	rc::Rc,67};6869pub use ctx::*;70pub use dynamic::*;71use error::{Error::*, LocError, Result, ResultExt};72pub use evaluate::*;73use function::{CallLocation, TlaArg};74use gc::{GcHashMap, TraceBox};75use hashbrown::hash_map::RawEntryMut;76pub use import::*;77use jrsonnet_gcmodule::{Cc, Trace};78pub use jrsonnet_interner::{IBytes, IStr};79pub use jrsonnet_parser as parser;80use jrsonnet_parser::*;81pub use obj::*;82use stack::check_depth;83use trace::{CompactFormat, TraceFormat};84pub use val::{ManifestFormat, Thunk, Val};8586/// Thunk without bound `super`/`this`87/// object inheritance may be overriden multiple times, and will be fixed only on field read88pub trait Unbound: Trace {89	/// Type of value after object context is bound90	type Bound;91	/// Create value bound to specified object context92	fn bind(&self, s: State, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<Self::Bound>;93}9495/// Object fields may, or may not depend on `this`/`super`, this enum allows cheaper reuse of object-independent fields for native code96/// Standard jsonnet fields are always unbound97#[derive(Clone, Trace)]98pub enum MaybeUnbound {99	/// Value needs to be bound to `this`/`super`100	Unbound(Cc<TraceBox<dyn Unbound<Bound = Thunk<Val>>>>),101	/// Value is object-independent102	Bound(Thunk<Val>),103}104105impl Debug for MaybeUnbound {106	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {107		write!(f, "MaybeUnbound")108	}109}110impl MaybeUnbound {111	/// Attach object context to value, if required112	pub fn evaluate(113		&self,114		s: State,115		sup: Option<ObjValue>,116		this: Option<ObjValue>,117	) -> Result<Thunk<Val>> {118		match self {119			Self::Unbound(v) => v.bind(s, sup, this),120			Self::Bound(v) => Ok(v.clone()),121		}122	}123}124125/// During import, this trait will be called to create initial context for file.126/// It may initialize global variables, stdlib for example.127pub trait ContextInitializer {128	/// Initialize default file context.129	fn initialize(&self, state: State, for_file: Source) -> Context;130	/// Allows upcasting from abstract to concrete context initializer.131	/// jrsonnet by itself doesn't use this method, it is allowed for it to panic.132	fn as_any(&self) -> &dyn Any;133}134135/// Context initializer which adds nothing.136pub struct DummyContextInitializer;137impl ContextInitializer for DummyContextInitializer {138	fn initialize(&self, _state: State, _for_file: Source) -> Context {139		Context::default()140	}141	fn as_any(&self) -> &dyn Any {142		self143	}144}145146/// Dynamically reconfigurable evaluation settings147pub struct EvaluationSettings {148	/// Limits amount of stack trace items preserved149	pub max_trace: usize,150	/// TLA vars151	pub tla_vars: HashMap<IStr, TlaArg>,152	/// Context initializer, which will be used for imports and everything153	/// [`NoopContextInitializer`] is used by default, most likely you want to have `jrsonnet-stdlib`154	pub context_initializer: Box<dyn ContextInitializer>,155	/// Used to resolve file locations/contents156	pub import_resolver: Box<dyn ImportResolver>,157	/// Used in manifestification functions158	pub manifest_format: ManifestFormat,159	/// Used for bindings160	pub trace_format: Box<dyn TraceFormat>,161}162impl Default for EvaluationSettings {163	fn default() -> Self {164		Self {165			max_trace: 20,166			context_initializer: Box::new(DummyContextInitializer),167			tla_vars: HashMap::default(),168			import_resolver: Box::new(DummyImportResolver),169			manifest_format: ManifestFormat::Json {170				padding: 4,171				#[cfg(feature = "exp-preserve-order")]172				preserve_order: false,173			},174			trace_format: Box::new(CompactFormat {175				padding: 4,176				resolver: trace::PathResolver::Absolute,177			}),178		}179	}180}181182struct FileData {183	string: Option<IStr>,184	bytes: Option<IBytes>,185	parsed: Option<LocExpr>,186	evaluated: Option<Val>,187188	evaluating: bool,189}190impl FileData {191	fn new_string(data: IStr) -> Self {192		Self {193			string: Some(data),194			bytes: None,195			parsed: None,196			evaluated: None,197			evaluating: false,198		}199	}200	fn new_bytes(data: IBytes) -> Self {201		Self {202			string: None,203			bytes: Some(data),204			parsed: None,205			evaluated: None,206			evaluating: false,207		}208	}209}210211#[derive(Default)]212pub struct EvaluationStateInternals {213	/// Internal state214	file_cache: RefCell<GcHashMap<SourcePath, FileData>>,215	/// Settings, safe to change at runtime216	settings: RefCell<EvaluationSettings>,217}218219/// Maintains stack trace and import resolution220#[derive(Default, Clone)]221pub struct State(Rc<EvaluationStateInternals>);222223impl State {224	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise225	pub fn import_resolved_str(&self, path: SourcePath) -> Result<IStr> {226		let mut file_cache = self.file_cache();227		let mut file = file_cache.raw_entry_mut().from_key(&path);228229		let file = match file {230			RawEntryMut::Occupied(ref mut d) => d.get_mut(),231			RawEntryMut::Vacant(v) => {232				let data = self.settings().import_resolver.load_file_contents(&path)?;233				v.insert(234					path.clone(),235					FileData::new_string(236						std::str::from_utf8(&data)237							.map_err(|_| ImportBadFileUtf8(path.clone()))?238							.into(),239					),240				)241				.1242			}243		};244		if let Some(str) = &file.string {245			return Ok(str.clone());246		}247		if file.string.is_none() {248			file.string = Some(249				file.bytes250					.as_ref()251					.expect("either string or bytes should be set")252					.clone()253					.cast_str()254					.ok_or_else(|| ImportBadFileUtf8(path.clone()))?,255			);256		}257		Ok(file.string.as_ref().expect("just set").clone())258	}259	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise260	pub fn import_resolved_bin(&self, path: SourcePath) -> Result<IBytes> {261		let mut file_cache = self.file_cache();262		let mut file = file_cache.raw_entry_mut().from_key(&path);263264		let file = match file {265			RawEntryMut::Occupied(ref mut d) => d.get_mut(),266			RawEntryMut::Vacant(v) => {267				let data = self.settings().import_resolver.load_file_contents(&path)?;268				v.insert(path.clone(), FileData::new_bytes(data.as_slice().into()))269					.1270			}271		};272		if let Some(str) = &file.bytes {273			return Ok(str.clone());274		}275		if file.bytes.is_none() {276			file.bytes = Some(277				file.string278					.as_ref()279					.expect("either string or bytes should be set")280					.clone()281					.cast_bytes(),282			);283		}284		Ok(file.bytes.as_ref().expect("just set").clone())285	}286	/// Should only be called with path retrieved from [`resolve_path`], may panic otherwise287	pub fn import_resolved(&self, path: SourcePath) -> Result<Val> {288		let mut file_cache = self.file_cache();289		let mut file = file_cache.raw_entry_mut().from_key(&path);290291		let file = match file {292			RawEntryMut::Occupied(ref mut d) => d.get_mut(),293			RawEntryMut::Vacant(v) => {294				let data = self.settings().import_resolver.load_file_contents(&path)?;295				v.insert(296					path.clone(),297					FileData::new_string(298						std::str::from_utf8(&data)299							.map_err(|_| ImportBadFileUtf8(path.clone()))?300							.into(),301					),302				)303				.1304			}305		};306		if let Some(val) = &file.evaluated {307			return Ok(val.clone());308		}309		if file.string.is_none() {310			file.string = Some(311				std::str::from_utf8(312					file.bytes313						.as_ref()314						.expect("either string or bytes should be set"),315				)316				.map_err(|_| ImportBadFileUtf8(path.clone()))?317				.into(),318			);319		}320		let code = file.string.as_ref().expect("just set");321		let file_name = Source::new(path.clone(), code.clone());322		if file.parsed.is_none() {323			file.parsed = Some(324				jrsonnet_parser::parse(325					code,326					&ParserSettings {327						file_name: file_name.clone(),328					},329				)330				.map_err(|e| ImportSyntaxError {331					path: file_name.clone(),332					error: Box::new(e),333				})?,334			);335		}336		let parsed = file.parsed.as_ref().expect("just set").clone();337		if file.evaluating {338			throw!(InfiniteRecursionDetected)339		}340		file.evaluating = true;341		// Dropping file cache guard here, as evaluation may use this map too342		drop(file_cache);343		let res = evaluate(344			self.clone(),345			self.create_default_context(file_name),346			&parsed,347		);348349		let mut file_cache = self.file_cache();350		let mut file = file_cache.raw_entry_mut().from_key(&path);351352		let file = match file {353			RawEntryMut::Occupied(ref mut d) => d.get_mut(),354			RawEntryMut::Vacant(_) => unreachable!("this file was just here!"),355		};356		file.evaluating = false;357		match res {358			Ok(v) => {359				file.evaluated = Some(v.clone());360				Ok(v)361			}362			Err(e) => Err(e),363		}364	}365366	/// Has same semantics as `import 'path'` called from `from` file367	pub fn import_from(&self, from: &SourcePath, path: &str) -> Result<Val> {368		let resolved = self.resolve_from(from, path)?;369		self.import_resolved(resolved)370	}371	pub fn import(&self, path: impl AsRef<Path>) -> Result<Val> {372		let resolved = self.resolve(path)?;373		self.import_resolved(resolved)374	}375376	/// Creates context with all passed global variables377	pub fn create_default_context(&self, source: Source) -> Context {378		let context_initializer = &self.settings().context_initializer;379		context_initializer.initialize(self.clone(), source)380	}381382	/// Executes code creating a new stack frame383	pub fn push<T>(384		e: CallLocation<'_>,385		frame_desc: impl FnOnce() -> String,386		f: impl FnOnce() -> Result<T>,387	) -> Result<T> {388		let _guard = check_depth()?;389390		f().with_description_src(e, frame_desc)391	}392393	/// Executes code creating a new stack frame394	pub fn push_val(395		&self,396		e: &ExprLocation,397		frame_desc: impl FnOnce() -> String,398		f: impl FnOnce() -> Result<Val>,399	) -> Result<Val> {400		let _guard = check_depth()?;401402		f().with_description_src(e, frame_desc)403	}404	/// Executes code creating a new stack frame405	pub fn push_description<T>(406		frame_desc: impl FnOnce() -> String,407		f: impl FnOnce() -> Result<T>,408	) -> Result<T> {409		let _guard = check_depth()?;410411		f().with_description(frame_desc)412	}413414	/// # Panics415	/// In case of formatting failure416	pub fn stringify_err(&self, e: &LocError) -> String {417		let mut out = String::new();418		self.settings()419			.trace_format420			.write_trace(&mut out, self, e)421			.unwrap();422		out423	}424425	pub fn manifest(&self, val: Val) -> Result<IStr> {426		Self::push_description(427			|| "manifestification".to_string(),428			|| val.manifest(self.clone(), &self.manifest_format()),429		)430	}431	pub fn manifest_multi(&self, val: Val) -> Result<Vec<(IStr, IStr)>> {432		val.manifest_multi(self.clone(), &self.manifest_format())433	}434	pub fn manifest_stream(&self, val: Val) -> Result<Vec<IStr>> {435		val.manifest_stream(self.clone(), &self.manifest_format())436	}437438	/// If passed value is function then call with set TLA439	pub fn with_tla(&self, val: Val) -> Result<Val> {440		Ok(match val {441			Val::Func(func) => State::push_description(442				|| "during TLA call".to_owned(),443				|| {444					func.evaluate(445						self.clone(),446						self.create_default_context(Source::new_virtual(447							"<tla>".into(),448							IStr::empty(),449						)),450						CallLocation::native(),451						&self.settings().tla_vars,452						true,453					)454				},455			)?,456			v => v,457		})458	}459}460461/// Internals462impl State {463	fn file_cache(&self) -> RefMut<'_, GcHashMap<SourcePath, FileData>> {464		self.0.file_cache.borrow_mut()465	}466	pub fn settings(&self) -> Ref<'_, EvaluationSettings> {467		self.0.settings.borrow()468	}469	pub fn settings_mut(&self) -> RefMut<'_, EvaluationSettings> {470		self.0.settings.borrow_mut()471	}472}473474/// Raw methods evaluate passed values but don't perform TLA execution475impl State {476	/// Parses and evaluates the given snippet477	pub fn evaluate_snippet(&self, name: impl Into<IStr>, code: impl Into<IStr>) -> Result<Val> {478		let code = code.into();479		let source = Source::new_virtual(name.into(), code.clone());480		let parsed = jrsonnet_parser::parse(481			&code,482			&ParserSettings {483				file_name: source.clone(),484			},485		)486		.map_err(|e| ImportSyntaxError {487			path: source.clone(),488			error: Box::new(e),489		})?;490		evaluate(self.clone(), self.create_default_context(source), &parsed)491	}492}493494/// Settings utilities495impl State {496	pub fn add_tla(&self, name: IStr, value: Val) {497		self.settings_mut()498			.tla_vars499			.insert(name, TlaArg::Val(value));500	}501	pub fn add_tla_str(&self, name: IStr, value: IStr) {502		self.settings_mut()503			.tla_vars504			.insert(name, TlaArg::String(value));505	}506	pub fn add_tla_code(&self, name: IStr, code: &str) -> Result<()> {507		let source_name = format!("<top-level-arg:{name}>");508		let source = Source::new_virtual(source_name.into(), code.into());509		let parsed = jrsonnet_parser::parse(510			code,511			&ParserSettings {512				file_name: source.clone(),513			},514		)515		.map_err(|e| ImportSyntaxError {516			path: source,517			error: Box::new(e),518		})?;519		self.settings_mut()520			.tla_vars521			.insert(name, TlaArg::Code(parsed));522		Ok(())523	}524525	// Only panics in case of [`ImportResolver`] contract violation526	#[allow(clippy::missing_panics_doc)]527	pub fn resolve_from(&self, from: &SourcePath, path: &str) -> Result<SourcePath> {528		self.import_resolver().resolve_from(from, path.as_ref())529	}530531	// Only panics in case of [`ImportResolver`] contract violation532	#[allow(clippy::missing_panics_doc)]533	pub fn resolve(&self, path: impl AsRef<Path>) -> Result<SourcePath> {534		self.import_resolver().resolve(path.as_ref())535	}536	pub fn import_resolver(&self) -> Ref<'_, dyn ImportResolver> {537		Ref::map(self.settings(), |s| &*s.import_resolver)538	}539	pub fn set_import_resolver(&self, resolver: Box<dyn ImportResolver>) {540		self.settings_mut().import_resolver = resolver;541	}542	pub fn context_initializer(&self) -> Ref<'_, dyn ContextInitializer> {543		Ref::map(self.settings(), |s| &*s.context_initializer)544	}545546	pub fn manifest_format(&self) -> ManifestFormat {547		self.settings().manifest_format.clone()548	}549	pub fn set_manifest_format(&self, format: ManifestFormat) {550		self.settings_mut().manifest_format = format;551	}552553	pub fn trace_format(&self) -> Ref<'_, dyn TraceFormat> {554		Ref::map(self.settings(), |s| &*s.trace_format)555	}556	pub fn set_trace_format(&self, format: Box<dyn TraceFormat>) {557		self.settings_mut().trace_format = format;558	}559560	pub fn max_trace(&self) -> usize {561		self.settings().max_trace562	}563	pub fn set_max_trace(&self, trace: usize) {564		self.settings_mut().max_trace = trace;565	}566}
modifiedcrates/jrsonnet-evaluator/src/obj.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/obj.rs
+++ b/crates/jrsonnet-evaluator/src/obj.rs
@@ -577,22 +577,29 @@
 
 pub struct ValueBuilder<'v>(&'v mut ObjValueBuilder);
 impl ObjMemberBuilder<ValueBuilder<'_>> {
-	pub fn value(self, s: State, value: Val) -> Result<()> {
-		self.binding(s, MaybeUnbound::Bound(Thunk::evaluated(value)))
+	/// Inserts value, replacing if it is already defined
+	pub fn value_unchecked(self, value: Val) {
+		let (receiver, name, member) =
+			self.build_member(MaybeUnbound::Bound(Thunk::evaluated(value)));
+		let entry = receiver.0.map.entry(name);
+		entry.insert(member);
 	}
-	pub fn bindable(
-		self,
-		s: State,
-		bindable: TraceBox<dyn Unbound<Bound = Thunk<Val>>>,
-	) -> Result<()> {
-		self.binding(s, MaybeUnbound::Unbound(Cc::new(bindable)))
+
+	pub fn value(self, value: Val) -> Result<()> {
+		self.thunk(Thunk::evaluated(value))
+	}
+	pub fn thunk(self, value: Thunk<Val>) -> Result<()> {
+		self.binding(MaybeUnbound::Bound(value))
+	}
+	pub fn bindable(self, bindable: TraceBox<dyn Unbound<Bound = Thunk<Val>>>) -> Result<()> {
+		self.binding(MaybeUnbound::Unbound(Cc::new(bindable)))
 	}
-	pub fn binding(self, s: State, binding: MaybeUnbound) -> Result<()> {
+	pub fn binding(self, binding: MaybeUnbound) -> Result<()> {
 		let (receiver, name, member) = self.build_member(binding);
 		let location = member.location.clone();
 		let old = receiver.0.map.insert(name.clone(), member);
 		if old.is_some() {
-			s.push(
+			State::push(
 				CallLocation(location.as_ref()),
 				|| format!("field <{}> initializtion", name.clone()),
 				|| throw!(DuplicateFieldName(name.clone())),
addedcrates/jrsonnet-evaluator/src/stack.rsdiffbeforeafterboth
--- /dev/null
+++ b/crates/jrsonnet-evaluator/src/stack.rs
@@ -0,0 +1,109 @@
+use std::{cell::Cell, marker::PhantomData};
+
+use crate::error::{Error, LocError};
+
+struct StackLimit {
+	max_stack_size: Cell<usize>,
+	current_depth: Cell<usize>,
+}
+
+#[cfg(feature = "nightly")]
+#[thread_local]
+static STACK_LIMIT: StackLimit = StackLimit {
+	max_stack_size: Cell::new(200),
+	current_depth: Cell::new(0),
+};
+#[cfg(not(feature = "nightly"))]
+thread_local! {
+	static STACK_LIMIT: StackLimit = StackLimit {
+		max_stack_size: Cell::new(200),
+		current_depth: Cell::new(0),
+	};
+}
+
+pub struct StackOverflowError;
+impl From<StackOverflowError> for Error {
+	fn from(_: StackOverflowError) -> Self {
+		Error::StackOverflow
+	}
+}
+impl From<StackOverflowError> for LocError {
+	fn from(_: StackOverflowError) -> Self {
+		Error::StackOverflow.into()
+	}
+}
+
+/// Used to implement stack depth limitation
+pub struct StackDepthGuard(PhantomData<()>);
+impl Drop for StackDepthGuard {
+	#[cfg(feature = "nightly")]
+	fn drop(&mut self) {
+		STACK_LIMIT
+			.current_depth
+			.set(STACK_LIMIT.current_depth.get() - 1)
+	}
+	#[cfg(not(feature = "nightly"))]
+	fn drop(&mut self) {
+		STACK_LIMIT.with(|limit| limit.current_depth.set(limit.current_depth.get() - 1));
+	}
+}
+
+// #[cfg(feature = "nightly")]
+pub fn check_depth() -> Result<StackDepthGuard, StackOverflowError> {
+	fn internal(limit: &StackLimit) -> Result<StackDepthGuard, StackOverflowError> {
+		let current = limit.current_depth.get();
+		if current < limit.max_stack_size.get() {
+			limit.current_depth.set(current + 1);
+			Ok(StackDepthGuard(PhantomData))
+		} else {
+			Err(StackOverflowError)
+		}
+	}
+	#[cfg(feature = "nightly")]
+	{
+		internal(&STACK_LIMIT)
+	}
+	#[cfg(not(feature = "nightly"))]
+	{
+		STACK_LIMIT.with(internal)
+	}
+}
+
+pub struct StackDepthLimitOverrideGuard {
+	old_limit: usize,
+}
+impl Drop for StackDepthLimitOverrideGuard {
+	#[cfg(feature = "nightly")]
+	fn drop(&mut self) {
+		STACK_LIMIT.max_stack_size.set(self.old_limit)
+	}
+	#[cfg(not(feature = "nightly"))]
+	fn drop(&mut self) {
+		STACK_LIMIT.with(|limit| limit.max_stack_size.set(self.old_limit));
+	}
+}
+
+pub fn limit_stack_depth(depth_limit: usize) -> StackDepthLimitOverrideGuard {
+	fn internal(limit: &StackLimit, depth_limit: usize) -> StackDepthLimitOverrideGuard {
+		let old_limit = limit.max_stack_size.get();
+		let current_depth = limit.current_depth.get();
+
+		limit.max_stack_size.set(current_depth + depth_limit);
+		StackDepthLimitOverrideGuard { old_limit }
+	}
+	#[cfg(feature = "nightly")]
+	{
+		internal(&STACK_LIMIT, depth_limit)
+	}
+	#[cfg(not(feature = "nightly"))]
+	{
+		STACK_LIMIT.with(|limit| internal(limit, depth_limit))
+	}
+}
+
+/// Like [`limit_stack_depth`], but set depth is not guarded, and will be kept
+///
+/// Used to implement `set_max_stack` in C api, prefer to use [`limit_stack_depth`] instead
+pub fn set_stack_depth_limit(depth_limit: usize) {
+	std::mem::forget(limit_stack_depth(depth_limit));
+}
modifiedcrates/jrsonnet-evaluator/src/stdlib/manifest.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/stdlib/manifest.rs
+++ b/crates/jrsonnet-evaluator/src/stdlib/manifest.rs
@@ -111,7 +111,7 @@
 					buf.push_str(cur_padding);
 					escape_string_json_buf(&field, buf);
 					buf.push_str(options.key_val_sep);
-					s.push_description(
+					State::push_description(
 						|| format!("field <{}> manifestification", field.clone()),
 						|| {
 							let value = obj.get(s.clone(), field.clone())?.unwrap();
modifiedcrates/jrsonnet-evaluator/src/stdlib/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/stdlib/mod.rs
+++ b/crates/jrsonnet-evaluator/src/stdlib/mod.rs
@@ -10,7 +10,7 @@
 pub mod manifest;
 
 pub fn std_format(s: State, str: IStr, vals: Val) -> Result<String> {
-	s.push(
+	State::push(
 		CallLocation::native(),
 		|| format!("std.format of {str}"),
 		|| {
modifiedcrates/jrsonnet-evaluator/src/typed/mod.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/typed/mod.rs
+++ b/crates/jrsonnet-evaluator/src/typed/mod.rs
@@ -85,12 +85,11 @@
 }
 
 fn push_type_description(
-	s: State,
 	error_reason: impl Fn() -> String,
 	path: impl Fn() -> ValuePathItem,
 	item: impl Fn() -> Result<()>,
 ) -> Result<()> {
-	s.push_description(error_reason, || match item() {
+	State::push_description(error_reason, || match item() {
 		Ok(_) => Ok(()),
 		Err(mut e) => {
 			if let Error::TypeError(e) = &mut e.error_mut() {
@@ -170,7 +169,6 @@
 				Val::Arr(a) => {
 					for (i, item) in a.iter(s.clone()).enumerate() {
 						push_type_description(
-							s.clone(),
 							|| format!("array index {i}"),
 							|| ValuePathItem::Index(i as u64),
 							|| elem_type.check(s.clone(), &item.clone()?),
@@ -184,7 +182,6 @@
 				Val::Arr(a) => {
 					for (i, item) in a.iter(s.clone()).enumerate() {
 						push_type_description(
-							s.clone(),
 							|| format!("array index {i}"),
 							|| ValuePathItem::Index(i as u64),
 							|| elem_type.check(s.clone(), &item.clone()?),
@@ -199,7 +196,6 @@
 					for (k, v) in elems.iter() {
 						if let Some(got_v) = obj.get(s.clone(), (*k).into())? {
 							push_type_description(
-								s.clone(),
 								|| format!("property {k}"),
 								|| ValuePathItem::Field((*k).into()),
 								|| v.check(s.clone(), &got_v),
modifiedcrates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-macros/src/lib.rs
+++ b/crates/jrsonnet-macros/src/lib.rs
@@ -299,7 +299,7 @@
 				cfg_attrs,
 			} => {
 				let name = name.as_ref().map(|v| v.as_str()).unwrap_or("<unnamed>");
-				let eval = quote! {s.push_description(
+				let eval = quote! {jrsonnet_evaluator::State::push_description(
 					|| format!("argument <{}> evaluation", #name),
 					|| <#ty>::from_untyped(value.evaluate(s.clone())?, s.clone()),
 				)?};
modifiedcrates/jrsonnet-stdlib/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/lib.rs
+++ b/crates/jrsonnet-stdlib/src/lib.rs
@@ -137,43 +137,36 @@
 		builder
 			.member(name.into())
 			.hide()
-			.value(s.clone(), Val::Func(FuncVal::StaticBuiltin(builtin)))
+			.value(Val::Func(FuncVal::StaticBuiltin(builtin)))
 			.expect("no conflict");
 	}
 
 	builder
 		.member("extVar".into())
 		.hide()
-		.value(
-			s.clone(),
-			Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_ext_var {
-				settings: settings.clone()
-			})))),
-		)
+		.value(Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_ext_var {
+			settings: settings.clone()
+		})))))
 		.expect("no conflict");
 	builder
 		.member("native".into())
 		.hide()
-		.value(
-			s.clone(),
-			Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_native {
-				settings: settings.clone()
-			})))),
-		)
+		.value(Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_native {
+			settings: settings.clone()
+		})))))
 		.expect("no conflict");
 	builder
 		.member("trace".into())
 		.hide()
-		.value(
-			s.clone(),
-			Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_trace { settings })))),
-		)
+		.value(Val::Func(FuncVal::Builtin(Cc::new(tb!(builtin_trace {
+			settings
+		})))))
 		.expect("no conflict");
 
 	builder
 		.member("id".into())
 		.hide()
-		.value(s, Val::Func(FuncVal::Id))
+		.value(Val::Func(FuncVal::Id))
 		.expect("no conflict");
 
 	builder.build()