difftreelog
refactor! implement stack size limit using thread local
in: master
18 files changed
bindings/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.
cmds/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)?;
crates/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) {
crates/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);
crates/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(());
crates/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());
crates/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
crates/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" }
crates/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 {
crates/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),
crates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth1//! 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}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}crates/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())),
crates/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));
+}
crates/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();
crates/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}"),
|| {
crates/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),
crates/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()),
)?};
crates/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()