--- 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. --- 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)?; --- 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; } #[derive(Parser)] @@ -44,7 +48,8 @@ jpath: Vec, } impl ConfigureState for MiscOpts { - fn configure(&self, s: &State) -> Result<()> { + type Guards = StackDepthLimitOverrideGuard; + fn configure(&self, s: &State) -> Result { 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 = ( + ::Guards, + ::Guards, + ); + fn configure(&self, s: &State) -> Result { // 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, Option) { +impl ConfigureState for GcOpts { + type Guards = (Option, Option); + + fn configure(&self, _s: &State) -> Result { // 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) { --- 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); --- a/crates/jrsonnet-cli/src/stdlib.rs +++ b/crates/jrsonnet-cli/src/stdlib.rs @@ -106,6 +106,7 @@ ext_code_file: Vec, } impl ConfigureState for StdOpts { + type Guards = (); fn configure(&self, s: &State) -> Result<()> { if self.no_stdlib { return Ok(()); --- a/crates/jrsonnet-cli/src/tla.rs +++ b/crates/jrsonnet-cli/src/tla.rs @@ -47,6 +47,7 @@ tla_code_file: Vec, } 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()); --- 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 --- 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" } --- 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; +} +impl ErrorSource for &LocExpr { + fn to_location(self) -> Option { + Some(self.1.clone()) + } +} +impl ErrorSource for &ExprLocation { + fn to_location(self) -> Option { + Some(self.clone()) + } +} +impl ErrorSource for CallLocation<'_> { + fn to_location(self) -> Option { + self.0.cloned() + } +} + pub type Result = std::result::Result; +pub trait ResultExt: Sized { + #[must_use] + fn with_description>(self, msg: impl FnOnce() -> O) -> Self; + #[must_use] + fn description(self, msg: &str) -> Self { + self.with_description(|| msg) + } + + #[must_use] + fn with_description_src>( + 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 ResultExt for Result { + fn with_description>(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>( + 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 { --- 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> { 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> { 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), --- a/crates/jrsonnet-evaluator/src/lib.rs +++ b/crates/jrsonnet-evaluator/src/lib.rs @@ -1,5 +1,5 @@ //! jsonnet interpreter implementation - +#![cfg_attr(feature = "nightly", feature(thread_local))] #![deny(unsafe_op_in_unsafe_fn)] #![warn( clippy::all, @@ -51,6 +51,7 @@ mod integrations; mod map; mod obj; +pub mod stack; pub mod stdlib; pub mod trace; pub mod typed; @@ -67,7 +68,7 @@ pub use ctx::*; pub use dynamic::*; -use error::{Error::*, LocError, Result, StackTraceElement}; +use error::{Error::*, LocError, Result, ResultExt}; pub use evaluate::*; use function::{CallLocation, TlaArg}; use gc::{GcHashMap, TraceBox}; @@ -78,6 +79,7 @@ pub use jrsonnet_parser as parser; use jrsonnet_parser::*; pub use obj::*; +use stack::check_depth; use trace::{CompactFormat, TraceFormat}; pub use val::{ManifestFormat, Thunk, Val}; @@ -143,8 +145,6 @@ /// Dynamically reconfigurable evaluation settings pub struct EvaluationSettings { - /// Limits recursion by limiting the number of stack frames - pub max_stack: usize, /// Limits amount of stack trace items preserved pub max_trace: usize, /// TLA vars @@ -162,7 +162,6 @@ impl Default for EvaluationSettings { fn default() -> Self { Self { - max_stack: 200, max_trace: 20, context_initializer: Box::new(DummyContextInitializer), tla_vars: HashMap::default(), @@ -179,19 +178,7 @@ } } } - -#[derive(Default)] -struct EvaluationData { - /// Used for stack overflow detection, stacktrace is populated on unwind - stack_depth: usize, - /// Updated every time stack entry is popt - stack_generation: usize, - breakpoints: Breakpoints, - - /// Contains file source codes and evaluation results for imports and pretty-printed stacktraces - files: GcHashMap, -} struct FileData { string: Option, bytes: Option, @@ -217,46 +204,14 @@ parsed: None, evaluated: None, evaluating: false, - } - } -} - -#[allow(clippy::type_complexity)] -pub struct Breakpoint { - loc: ExprLocation, - collected: RefCell>)>>, -} -#[derive(Default)] -struct Breakpoints(Vec>); -impl Breakpoints { - fn insert( - &self, - stack_depth: usize, - stack_generation: usize, - loc: &ExprLocation, - result: Result, - ) -> Result { - if self.0.is_empty() { - return result; - } - for item in &self.0 { - if item.loc.belongs_to(loc) { - let mut collected = item.collected.borrow_mut(); - let (depth, vals) = collected.entry(stack_generation).or_default(); - if stack_depth > *depth { - vals.clear(); - } - vals.push(result.clone()); - } } - result } } #[derive(Default)] pub struct EvaluationStateInternals { /// Internal state - data: RefCell, + file_cache: RefCell>, /// Settings, safe to change at runtime settings: RefCell, } @@ -268,8 +223,8 @@ impl State { /// Should only be called with path retrieved from [`resolve_path`], may panic otherwise pub fn import_resolved_str(&self, path: SourcePath) -> Result { - let mut data = self.data_mut(); - let mut file = data.files.raw_entry_mut().from_key(&path); + let mut file_cache = self.file_cache(); + let mut file = file_cache.raw_entry_mut().from_key(&path); let file = match file { RawEntryMut::Occupied(ref mut d) => d.get_mut(), @@ -303,8 +258,8 @@ } /// Should only be called with path retrieved from [`resolve_path`], may panic otherwise pub fn import_resolved_bin(&self, path: SourcePath) -> Result { - let mut data = self.data_mut(); - let mut file = data.files.raw_entry_mut().from_key(&path); + let mut file_cache = self.file_cache(); + let mut file = file_cache.raw_entry_mut().from_key(&path); let file = match file { RawEntryMut::Occupied(ref mut d) => d.get_mut(), @@ -330,8 +285,8 @@ } /// Should only be called with path retrieved from [`resolve_path`], may panic otherwise pub fn import_resolved(&self, path: SourcePath) -> Result { - let mut data = self.data_mut(); - let mut file = data.files.raw_entry_mut().from_key(&path); + let mut file_cache = self.file_cache(); + let mut file = file_cache.raw_entry_mut().from_key(&path); let file = match file { RawEntryMut::Occupied(ref mut d) => d.get_mut(), @@ -383,16 +338,16 @@ throw!(InfiniteRecursionDetected) } file.evaluating = true; - // Dropping file here, as it borrows data, which may be used in evaluation - drop(data); + // Dropping file cache guard here, as evaluation may use this map too + drop(file_cache); let res = evaluate( self.clone(), self.create_default_context(file_name), &parsed, ); - let mut data = self.data_mut(); - let mut file = data.files.raw_entry_mut().from_key(&path); + let mut file_cache = self.file_cache(); + let mut file = file_cache.raw_entry_mut().from_key(&path); let file = match file { RawEntryMut::Occupied(ref mut d) => d.get_mut(), @@ -426,35 +381,13 @@ /// Executes code creating a new stack frame pub fn push( - &self, e: CallLocation<'_>, frame_desc: impl FnOnce() -> String, f: impl FnOnce() -> Result, ) -> Result { - { - let mut data = self.data_mut(); - let stack_depth = &mut data.stack_depth; - if *stack_depth > self.max_stack() { - // Error creation uses data, so i drop guard here - drop(data); - throw!(StackOverflow); - } - *stack_depth += 1; - } - let result = f(); - { - let mut data = self.data_mut(); - data.stack_depth -= 1; - data.stack_generation += 1; - } - if let Err(mut err) = result { - err.trace_mut().0.push(StackTraceElement { - location: e.0.cloned(), - desc: frame_desc(), - }); - return Err(err); - } - result + let _guard = check_depth()?; + + f().with_description_src(e, frame_desc) } /// Executes code creating a new stack frame @@ -464,64 +397,18 @@ frame_desc: impl FnOnce() -> String, f: impl FnOnce() -> Result, ) -> Result { - { - let mut data = self.data_mut(); - let stack_depth = &mut data.stack_depth; - if *stack_depth > self.max_stack() { - // Error creation uses data, so i drop guard here - drop(data); - throw!(StackOverflow); - } - *stack_depth += 1; - } - let mut result = f(); - { - let mut data = self.data_mut(); - data.stack_depth -= 1; - data.stack_generation += 1; - result = data - .breakpoints - .insert(data.stack_depth, data.stack_generation, e, result); - } - if let Err(mut err) = result { - err.trace_mut().0.push(StackTraceElement { - location: Some(e.clone()), - desc: frame_desc(), - }); - return Err(err); - } - result + let _guard = check_depth()?; + + f().with_description_src(e, frame_desc) } /// Executes code creating a new stack frame pub fn push_description( - &self, frame_desc: impl FnOnce() -> String, f: impl FnOnce() -> Result, ) -> Result { - { - let mut data = self.data_mut(); - let stack_depth = &mut data.stack_depth; - if *stack_depth > self.max_stack() { - // Error creation uses data, so i drop guard here - drop(data); - throw!(StackOverflow); - } - *stack_depth += 1; - } - let result = f(); - { - let mut data = self.data_mut(); - data.stack_depth -= 1; - data.stack_generation += 1; - } - if let Err(mut err) = result { - err.trace_mut().0.push(StackTraceElement { - location: None, - desc: frame_desc(), - }); - return Err(err); - } - result + let _guard = check_depth()?; + + f().with_description(frame_desc) } /// # Panics @@ -536,7 +423,7 @@ } pub fn manifest(&self, val: Val) -> Result { - self.push_description( + Self::push_description( || "manifestification".to_string(), || val.manifest(self.clone(), &self.manifest_format()), ) @@ -551,7 +438,7 @@ /// If passed value is function then call with set TLA pub fn with_tla(&self, val: Val) -> Result { Ok(match val { - Val::Func(func) => self.push_description( + Val::Func(func) => State::push_description( || "during TLA call".to_owned(), || { func.evaluate( @@ -573,8 +460,8 @@ /// Internals impl State { - fn data_mut(&self) -> RefMut<'_, EvaluationData> { - self.0.data.borrow_mut() + fn file_cache(&self) -> RefMut<'_, GcHashMap> { + self.0.file_cache.borrow_mut() } pub fn settings(&self) -> Ref<'_, EvaluationSettings> { self.0.settings.borrow() @@ -675,12 +562,5 @@ } pub fn set_max_trace(&self, trace: usize) { self.settings_mut().max_trace = trace; - } - - pub fn max_stack(&self) -> usize { - self.settings().max_stack - } - pub fn set_max_stack(&self, trace: usize) { - self.settings_mut().max_stack = trace; } } --- 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> { - 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>>, - ) -> 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) -> Result<()> { + self.binding(MaybeUnbound::Bound(value)) + } + pub fn bindable(self, bindable: TraceBox>>) -> 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())), --- /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, + current_depth: Cell, +} + +#[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 for Error { + fn from(_: StackOverflowError) -> Self { + Error::StackOverflow + } +} +impl From 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 { + fn internal(limit: &StackLimit) -> Result { + 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)); +} --- 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(); --- 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 { - s.push( + State::push( CallLocation::native(), || format!("std.format of {str}"), || { --- 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), --- 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(""); - 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()), )?}; --- 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()