difftreelog
feat allow both parsers at the same time
in: master
4 files changed
crates/jrsonnet-evaluator/Cargo.tomldiffbeforeafterboth--- a/crates/jrsonnet-evaluator/Cargo.toml
+++ b/crates/jrsonnet-evaluator/Cargo.toml
@@ -18,24 +18,26 @@
explaining-traces = ["annotate-snippets", "hi-doc"]
# Allows library authors to throw custom errors
anyhow-error = ["anyhow"]
-# Use hand-written recursive descent parser instead of PEG parser
+# Use hand-written recursive descent parser
ir-parser = ["dep:jrsonnet-ir-parser"]
+# Use PEG parser
+peg-parser = ["dep:jrsonnet-peg-parser"]
# Allows to preserve field order in objects
exp-preserve-order = []
# Implements field destructuring
-exp-destruct = ["jrsonnet-peg-parser/exp-destruct"]
+exp-destruct = ["jrsonnet-peg-parser?/exp-destruct", "jrsonnet-ir-parser?/exp-destruct"]
# Iteration over objects yields [key, value] elements
exp-object-iteration = []
# Bigint type
exp-bigint = ["num-bigint", "jrsonnet-types/exp-bigint"]
# obj?.field, obj?.['field']
-exp-null-coaelse = ["jrsonnet-peg-parser/exp-null-coaelse", "jrsonnet-ir-parser?/exp-null-coaelse"]
+exp-null-coaelse = ["jrsonnet-peg-parser?/exp-null-coaelse", "jrsonnet-ir-parser?/exp-null-coaelse"]
[dependencies]
jrsonnet-interner.workspace = true
jrsonnet-ir.workspace = true
-jrsonnet-peg-parser.workspace = true
+jrsonnet-peg-parser = { workspace = true, optional = true }
jrsonnet-ir-parser = { workspace = true, optional = true }
jrsonnet-types.workspace = true
jrsonnet-macros.workspace = true
crates/jrsonnet-evaluator/src/async_import.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/async_import.rs
+++ b/crates/jrsonnet-evaluator/src/async_import.rs
@@ -7,10 +7,6 @@
FieldMember, FieldName, ForSpecData, IfElse, IfSpecData, ImportKind, ObjBody, Slice, SliceDesc,
Source, SourcePath, Spanned,
};
-#[cfg(feature = "ir-parser")]
-use jrsonnet_ir_parser::ParserSettings;
-#[cfg(not(feature = "ir-parser"))]
-use jrsonnet_peg_parser::ParserSettings;
use rustc_hash::FxHashMap;
use crate::{AsPathLike, FileData, ImportResolver, ResolvePathOwned, State};
@@ -326,7 +322,7 @@
};
let source = Source::new(path.clone(), code.clone());
// If failed - then skip import
- file.parsed = crate::parse_jsonnet(&code, &ParserSettings { source })
+ file.parsed = crate::parse_jsonnet(&code, source)
.map(Rc::new)
.ok();
if let Some(parsed) = &file.parsed {
crates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -14,6 +14,22 @@
ObjValue, ResolvePathOwned,
};
+#[derive(Debug, Clone)]
+pub struct SyntaxErrorLocation {
+ pub offset: usize,
+}
+
+#[derive(Debug, Clone)]
+pub struct SyntaxError {
+ pub message: String,
+ pub location: SyntaxErrorLocation,
+}
+impl fmt::Display for SyntaxError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.message)
+ }
+}
+
pub(crate) fn format_found(list: &[IStr], what: &str) -> String {
if list.is_empty() {
return String::new();
@@ -154,31 +170,11 @@
ImportNotSupported(SourcePath, ResolvePathOwned),
#[error("can't import from virtual file")]
CantImportFromVirtualFile,
- #[cfg(not(feature = "ir-parser"))]
- #[error(
- "syntax error: {}",
- // Peg has no fancier way to handle critical parsing errors https://github.com/kevinmehall/rust-peg/issues/225
- {.error.expected.tokens().find(|t| t.starts_with("!!!")).map_or_else(|| {
- format!(
- "expected {}, got {:?}",
- .error.expected,
- .path.code().chars().nth(error.location.offset)
- .map_or_else(|| "EOF".into(), |c| c.to_string())
- )
- }, |v| v[3..].into())}
- )]
- ImportSyntaxError {
- path: Source,
- #[trace(skip)]
- error: Box<jrsonnet_peg_parser::ParseError>,
- },
-
- #[cfg(feature = "ir-parser")]
#[error("syntax error: {error}")]
ImportSyntaxError {
path: Source,
#[trace(skip)]
- error: Box<jrsonnet_ir_parser::ParseError>,
+ error: Box<SyntaxError>,
},
#[error("runtime error: {}", format_empty_str(.0))]
crates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth1//! jsonnet interpreter implementation2#![cfg_attr(nightly, feature(thread_local, type_alias_impl_trait))]34// For jrsonnet-macros5extern crate self as jrsonnet_evaluator;67mod arr;8// pub mod async_import;9mod ctx;10mod dynamic;11pub mod error;12mod evaluate;13pub mod function;14pub mod gc;15mod import;16mod integrations;17pub mod manifest;18mod map;19mod obj;20pub mod stack;21pub mod stdlib;22pub mod tla;23pub mod trace;24pub mod typed;25pub mod val;2627use std::{28 any::Any,29 cell::{RefCell, RefMut},30 clone::Clone,31 collections::hash_map::Entry,32 fmt::{self, Debug},33 marker::PhantomData,34 rc::Rc,35};3637pub use ctx::*;38pub use dynamic::*;39pub use error::{Error, ErrorKind::*, Result, ResultExt};40pub use evaluate::*;41use function::CallLocation;42pub use import::*;43use jrsonnet_gcmodule::{cc_dyn, Cc, Trace};44pub use jrsonnet_interner::{IBytes, IStr};45pub use jrsonnet_ir as parser;46use jrsonnet_ir::{Expr, Source, SourcePath};47#[doc(hidden)]48pub use jrsonnet_macros;49#[cfg(feature = "ir-parser")]50use jrsonnet_ir_parser::ParserSettings;51#[cfg(not(feature = "ir-parser"))]52use jrsonnet_peg_parser::ParserSettings;53pub use obj::*;54pub use rustc_hash;55use rustc_hash::FxHashMap;56use stack::check_depth;57pub use tla::apply_tla;58pub use val::{Thunk, Val};5960use crate::gc::WithCapacityExt as _;6162#[cfg(feature = "ir-parser")]63pub(crate) fn parse_jsonnet(64 code: &str,65 settings: &ParserSettings,66) -> Result<Expr, jrsonnet_ir_parser::ParseError> {67 jrsonnet_ir_parser::parse(code, settings)68}6970#[cfg(not(feature = "ir-parser"))]71pub(crate) fn parse_jsonnet(72 code: &str,73 settings: &ParserSettings,74) -> Result<Expr, jrsonnet_peg_parser::ParseError> {75 jrsonnet_peg_parser::parse(code, settings)76}7778cc_dyn!(79 #[derive(Clone)]80 CcUnbound<V>,81 Unbound<Bound = V>82);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, sup_this: SupThis) -> 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(CcUnbound<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(&self, sup_this: SupThis) -> Result<Val> {111 match self {112 Self::Unbound(v) => v.0.bind(sup_this),113 Self::Bound(v) => Ok(v.evaluate()?),114 }115 }116}117118cc_dyn!(CcContextInitializer, ContextInitializer);119120/// During import, this trait will be called to create initial context for file.121/// It may initialize global variables, stdlib for example.122pub trait ContextInitializer: Trace {123 /// For which size the builder should be preallocated124 fn reserve_vars(&self) -> usize {125 0126 }127 /// Initialize default file context.128 /// Has default implementation, which calls `populate`.129 /// Prefer to always implement `populate` instead.130 fn initialize(&self, for_file: Source) -> Context {131 let mut builder = ContextBuilder::with_capacity(self.reserve_vars());132 self.populate(for_file, &mut builder);133 builder.build()134 }135 /// For composability: extend builder. May panic if this initialization is not supported,136 /// and the context may only be created via `initialize`.137 fn populate(&self, for_file: Source, builder: &mut ContextBuilder);138 /// Allows upcasting from abstract to concrete context initializer.139 /// jrsonnet by itself doesn't use this method, it is allowed for it to panic.140 fn as_any(&self) -> &dyn Any;141}142143/// Context initializer which adds nothing.144impl ContextInitializer for () {145 fn populate(&self, _for_file: Source, _builder: &mut ContextBuilder) {}146 fn as_any(&self) -> &dyn Any {147 self148 }149}150151impl<T> ContextInitializer for Option<T>152where153 T: ContextInitializer,154{155 fn initialize(&self, for_file: Source) -> Context {156 if let Some(ctx) = self {157 ctx.initialize(for_file)158 } else {159 ().initialize(for_file)160 }161 }162163 fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {164 if let Some(ctx) = self {165 ctx.populate(for_file, builder);166 }167 }168169 fn as_any(&self) -> &dyn Any {170 self171 }172}173174macro_rules! impl_context_initializer {175 ($($gen:ident)*) => {176 #[allow(non_snake_case)]177 impl<$($gen: ContextInitializer + Trace,)*> ContextInitializer for ($($gen,)*) {178 fn reserve_vars(&self) -> usize {179 let mut out = 0;180 let ($($gen,)*) = self;181 $(out += $gen.reserve_vars();)*182 out183 }184 fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {185 let ($($gen,)*) = self;186 $($gen.populate(for_file.clone(), builder);)*187 }188 fn as_any(&self) -> &dyn Any {189 self190 }191 }192 };193 ($($cur:ident)* @ $c:ident $($rest:ident)*) => {194 impl_context_initializer!($($cur)*);195 impl_context_initializer!($($cur)* $c @ $($rest)*);196 };197 ($($cur:ident)* @) => {198 impl_context_initializer!($($cur)*);199 }200}201impl_context_initializer! {202 A @ B C D E F G203}204205#[derive(Trace)]206struct FileData {207 string: Option<IStr>,208 bytes: Option<IBytes>,209 parsed: Option<Rc<Expr>>,210 evaluated: Option<Val>,211212 evaluating: bool,213}214impl FileData {215 fn new_string(data: IStr) -> Self {216 Self {217 string: Some(data),218 bytes: None,219 parsed: None,220 evaluated: None,221 evaluating: false,222 }223 }224 fn new_bytes(data: IBytes) -> Self {225 Self {226 string: None,227 bytes: Some(data),228 parsed: None,229 evaluated: None,230 evaluating: false,231 }232 }233 pub(crate) fn get_string(&mut self) -> Option<IStr> {234 if self.string.is_none() {235 self.string = Some(236 self.bytes237 .as_ref()238 .expect("either string or bytes should be set")239 .clone()240 .cast_str()?,241 );242 }243 Some(self.string.clone().expect("just set"))244 }245}246247#[derive(Trace)]248pub struct EvaluationStateInternals {249 /// Internal state250 file_cache: RefCell<FxHashMap<SourcePath, FileData>>,251 /// Context initializer, which will be used for imports and everything252 /// [`NoopContextInitializer`] is used by default, most likely you want to have `jrsonnet-stdlib`253 context_initializer: CcContextInitializer,254 /// Used to resolve file locations/contents255 import_resolver: Rc<dyn ImportResolver>,256}257258/// Maintains stack trace and import resolution259#[derive(Clone, Trace)]260pub struct State(Cc<EvaluationStateInternals>);261262thread_local! {263 pub static DEFAULT_STATE: State = State::builder().build();264 pub static STATE: RefCell<Option<State>> = const {RefCell::new(None)};265}266pub struct StateEnterGuard(PhantomData<()>);267impl Drop for StateEnterGuard {268 fn drop(&mut self) {269 STATE.with_borrow_mut(|v| *v = None);270 }271}272273pub fn with_state<V>(v: impl FnOnce(State) -> V) -> V {274 if let Some(state) = STATE.with_borrow(Clone::clone) {275 v(state)276 } else {277 let s = DEFAULT_STATE.with(Clone::clone);278 v(s)279 }280}281282impl State {283 pub fn enter(&self) -> StateEnterGuard {284 self.try_enter().expect("entered state already exists")285 }286 pub fn try_enter(&self) -> Option<StateEnterGuard> {287 STATE.with_borrow_mut(|v| {288 if v.is_none() {289 *v = Some(self.clone());290 Some(StateEnterGuard(PhantomData))291 } else {292 None293 }294 })295 }296 /// Should only be called with path retrieved from [`resolve_path`], may panic otherwise297 pub fn import_resolved_str(&self, path: SourcePath) -> Result<IStr> {298 let mut file_cache = self.file_cache();299 let mut file = file_cache.entry(path.clone());300301 let file = match file {302 Entry::Occupied(ref mut d) => d.get_mut(),303 Entry::Vacant(v) => {304 let data = self.import_resolver().load_file_contents(&path)?;305 v.insert(FileData::new_string(306 std::str::from_utf8(&data)307 .map_err(|_| ImportBadFileUtf8(path.clone()))?308 .into(),309 ))310 }311 };312 Ok(file313 .get_string()314 .ok_or_else(|| ImportBadFileUtf8(path.clone()))?)315 }316 /// Should only be called with path retrieved from [`resolve_path`], may panic otherwise317 pub fn import_resolved_bin(&self, path: SourcePath) -> Result<IBytes> {318 let mut file_cache = self.file_cache();319 let mut file = file_cache.entry(path.clone());320321 let file = match file {322 Entry::Occupied(ref mut d) => d.get_mut(),323 Entry::Vacant(v) => {324 let data = self.import_resolver().load_file_contents(&path)?;325 v.insert(FileData::new_bytes(data.as_slice().into()))326 }327 };328 if let Some(str) = &file.bytes {329 return Ok(str.clone());330 }331 if file.bytes.is_none() {332 file.bytes = Some(333 file.string334 .as_ref()335 .expect("either string or bytes should be set")336 .clone()337 .cast_bytes(),338 );339 }340 Ok(file.bytes.as_ref().expect("just set").clone())341 }342 /// Should only be called with path retrieved from [`resolve_path`], may panic otherwise343 pub fn import_resolved(&self, path: SourcePath) -> Result<Val> {344 let mut file_cache = self.file_cache();345 let mut file = file_cache.entry(path.clone());346347 let file = match file {348 Entry::Occupied(ref mut d) => d.get_mut(),349 Entry::Vacant(v) => {350 let data = self.import_resolver().load_file_contents(&path)?;351 v.insert(FileData::new_string(352 std::str::from_utf8(&data)353 .map_err(|_| ImportBadFileUtf8(path.clone()))?354 .into(),355 ))356 }357 };358 if let Some(val) = &file.evaluated {359 return Ok(val.clone());360 }361 let code = file362 .get_string()363 .ok_or_else(|| ImportBadFileUtf8(path.clone()))?;364 let file_name = Source::new(path.clone(), code.clone());365 if file.parsed.is_none() {366 file.parsed = Some(367 parse_jsonnet(368 &code,369 &ParserSettings {370 source: file_name.clone(),371 },372 )373 .map(Rc::new)374 .map_err(|e| ImportSyntaxError {375 path: file_name.clone(),376 error: Box::new(e),377 })?,378 );379 }380 let parsed = file.parsed.as_ref().expect("just set").clone();381 if file.evaluating {382 bail!(InfiniteRecursionDetected)383 }384 file.evaluating = true;385 // Dropping file cache guard here, as evaluation may use this map too386 drop(file_cache);387 let res = evaluate(self.create_default_context(file_name), &parsed);388389 let mut file_cache = self.file_cache();390 let mut file = file_cache.entry(path);391392 let Entry::Occupied(file) = &mut file else {393 unreachable!("this file was just here")394 };395 let file = file.get_mut();396 file.evaluating = false;397 match res {398 Ok(v) => {399 file.evaluated = Some(v.clone());400 Ok(v)401 }402 Err(e) => Err(e),403 }404 }405406 /// Has same semantics as `import 'path'` called from `from` file407 pub fn import_from(&self, from: &SourcePath, path: impl AsPathLike) -> Result<Val> {408 let resolved = self.resolve_from(from, &path)?;409 self.import_resolved(resolved)410 }411 pub fn import(&self, path: impl AsPathLike) -> Result<Val> {412 let resolved = self.resolve_from_default(&path)?;413 self.import_resolved(resolved)414 }415416 /// Creates context with all passed global variables417 pub fn create_default_context(&self, source: Source) -> Context {418 self.context_initializer().initialize(source)419 }420421 /// Creates context with all passed global variables, calling custom modifier422 pub fn create_default_context_with(423 &self,424 source: Source,425 context_initializer: impl ContextInitializer,426 ) -> Context {427 let default_initializer = self.context_initializer();428 let mut builder = ContextBuilder::with_capacity(429 default_initializer.reserve_vars() + context_initializer.reserve_vars(),430 );431 default_initializer.populate(source.clone(), &mut builder);432 context_initializer.populate(source, &mut builder);433434 builder.build()435 }436}437438/// Internals439impl State {440 fn file_cache(&self) -> RefMut<'_, FxHashMap<SourcePath, FileData>> {441 self.0.file_cache.borrow_mut()442 }443}444/// Executes code creating a new stack frame, to be replaced with try{}445pub fn in_frame<T>(446 e: CallLocation<'_>,447 frame_desc: impl FnOnce() -> String,448 f: impl FnOnce() -> Result<T>,449) -> Result<T> {450 let _guard = check_depth()?;451452 f().with_description_src(e, frame_desc)453}454455/// Executes code creating a new stack frame, to be replaced with try{}456pub fn in_description_frame<T>(457 frame_desc: impl FnOnce() -> String,458 f: impl FnOnce() -> Result<T>,459) -> Result<T> {460 let _guard = check_depth()?;461462 f().with_description(frame_desc)463}464465#[derive(Trace)]466pub struct InitialUnderscore(pub Thunk<Val>);467impl ContextInitializer for InitialUnderscore {468 fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {469 builder.bind("_", self.0.clone());470 }471472 fn as_any(&self) -> &dyn Any {473 self474 }475}476477/// Raw methods evaluate passed values but don't perform TLA execution478impl State {479 /// Parses and evaluates the given snippet480 pub fn evaluate_snippet(&self, name: impl Into<IStr>, code: impl Into<IStr>) -> Result<Val> {481 let code = code.into();482 let source = Source::new_virtual(name.into(), code.clone());483 let parsed = parse_jsonnet(484 &code,485 &ParserSettings {486 source: source.clone(),487 },488 )489 .map_err(|e| ImportSyntaxError {490 path: source.clone(),491 error: Box::new(e),492 })?;493 evaluate(self.create_default_context(source), &parsed)494 }495 /// Parses and evaluates the given snippet with custom context modifier496 pub fn evaluate_snippet_with(497 &self,498 name: impl Into<IStr>,499 code: impl Into<IStr>,500 context_initializer: impl ContextInitializer,501 ) -> Result<Val> {502 let code = code.into();503 let source = Source::new_virtual(name.into(), code.clone());504 let parsed = parse_jsonnet(505 &code,506 &ParserSettings {507 source: source.clone(),508 },509 )510 .map_err(|e| ImportSyntaxError {511 path: source.clone(),512 error: Box::new(e),513 })?;514 evaluate(515 self.create_default_context_with(source, context_initializer),516 &parsed,517 )518 }519}520521/// Settings utilities522impl State {523 // Only panics in case of [`ImportResolver`] contract violation524 #[allow(clippy::missing_panics_doc)]525 pub fn resolve_from(&self, from: &SourcePath, path: &dyn AsPathLike) -> Result<SourcePath> {526 self.import_resolver().resolve_from(from, path)527 }528 #[allow(clippy::missing_panics_doc)]529 pub fn resolve_from_default(&self, path: &dyn AsPathLike) -> Result<SourcePath> {530 self.import_resolver().resolve_from_default(path)531 }532 pub fn import_resolver(&self) -> &dyn ImportResolver {533 &*self.0.import_resolver534 }535 pub fn context_initializer(&self) -> &dyn ContextInitializer {536 &*self.0.context_initializer.0537 }538}539540impl State {541 pub fn builder() -> StateBuilder {542 StateBuilder::default()543 }544}545546impl Default for State {547 fn default() -> Self {548 Self::builder().build()549 }550}551552#[derive(Default)]553pub struct StateBuilder {554 import_resolver: Option<Rc<dyn ImportResolver>>,555 context_initializer: Option<CcContextInitializer>,556}557impl StateBuilder {558 pub fn import_resolver(&mut self, import_resolver: impl ImportResolver) -> &mut Self {559 let _ = self.import_resolver.insert(Rc::new(import_resolver));560 self561 }562 pub fn context_initializer(563 &mut self,564 context_initializer: impl ContextInitializer,565 ) -> &mut Self {566 let _ = self567 .context_initializer568 .insert(CcContextInitializer::new(context_initializer));569 self570 }571 pub fn build(mut self) -> State {572 State(Cc::new(EvaluationStateInternals {573 file_cache: RefCell::new(FxHashMap::new()),574 context_initializer: self575 .context_initializer576 .take()577 .unwrap_or_else(|| CcContextInitializer::new(())),578 import_resolver: self579 .import_resolver580 .take()581 .unwrap_or_else(|| Rc::new(DummyImportResolver)),582 }))583 }584}1//! jsonnet interpreter implementation2#![cfg_attr(nightly, feature(thread_local, type_alias_impl_trait))]34// For jrsonnet-macros5extern crate self as jrsonnet_evaluator;67mod arr;8// pub mod async_import;9mod ctx;10mod dynamic;11pub mod error;12mod evaluate;13pub mod function;14pub mod gc;15mod import;16mod integrations;17pub mod manifest;18mod map;19mod obj;20pub mod stack;21pub mod stdlib;22pub mod tla;23pub mod trace;24pub mod typed;25pub mod val;2627use std::{28 any::Any,29 cell::{RefCell, RefMut},30 clone::Clone,31 collections::hash_map::Entry,32 fmt::{self, Debug},33 marker::PhantomData,34 rc::Rc,35};3637pub use ctx::*;38pub use dynamic::*;39pub use error::{Error, ErrorKind::*, Result, ResultExt};40pub use evaluate::*;41use function::CallLocation;42pub use import::*;43use jrsonnet_gcmodule::{cc_dyn, Cc, Trace};44pub use jrsonnet_interner::{IBytes, IStr};45pub use jrsonnet_ir as parser;46use jrsonnet_ir::{Expr, Source, SourcePath};47#[doc(hidden)]48pub use jrsonnet_macros;4950#[cfg(not(any(feature = "ir-parser", feature = "peg-parser")))]51compile_error!("at least one of `ir-parser` or `peg-parser` features must be enabled");5253pub use error::{SyntaxError, SyntaxErrorLocation};54pub use obj::*;55pub use rustc_hash;56use rustc_hash::FxHashMap;57use stack::check_depth;58pub use tla::apply_tla;59pub use val::{Thunk, Val};6061use crate::gc::WithCapacityExt as _;6263pub(crate) fn parse_jsonnet(code: &str, source: Source) -> Result<Expr, SyntaxError> {64 #[cfg(all(feature = "ir-parser", feature = "peg-parser"))]65 {66 if std::env::var_os("JRSONNET_LEGACY_PARSER").is_some() {67 return parse_peg(code, source);68 }69 return parse_ir(code, source);70 }71 #[cfg(all(feature = "ir-parser", not(feature = "peg-parser")))]72 {73 return parse_ir(code, source);74 }75 #[cfg(all(feature = "peg-parser", not(feature = "ir-parser")))]76 {77 return parse_peg(code, source);78 }79}8081#[cfg(feature = "ir-parser")]82fn parse_ir(code: &str, source: Source) -> Result<Expr, SyntaxError> {83 jrsonnet_ir_parser::parse(code, &jrsonnet_ir_parser::ParserSettings { source }).map_err(84 |e| SyntaxError {85 message: e.message,86 location: SyntaxErrorLocation {87 offset: e.location.offset,88 },89 },90 )91}9293#[cfg(feature = "peg-parser")]94fn parse_peg(code: &str, source: Source) -> Result<Expr, SyntaxError> {95 jrsonnet_peg_parser::parse(code, &jrsonnet_peg_parser::ParserSettings { source }).map_err(96 |e| {97 let message = e98 .expected99 .tokens()100 .find(|t| t.starts_with("!!!"))101 .map_or_else(102 || {103 format!(104 "expected {}, got {:?}",105 e.expected,106 code.chars()107 .nth(e.location.offset)108 .map_or_else(|| "EOF".into(), |c: char| c.to_string())109 )110 },111 |v| v[3..].into(),112 );113 SyntaxError {114 message,115 location: SyntaxErrorLocation {116 offset: e.location.offset,117 },118 }119 },120 )121}122123cc_dyn!(124 #[derive(Clone)]125 CcUnbound<V>,126 Unbound<Bound = V>127);128129/// Thunk without bound `super`/`this`130/// object inheritance may be overriden multiple times, and will be fixed only on field read131pub trait Unbound: Trace {132 /// Type of value after object context is bound133 type Bound;134 /// Create value bound to specified object context135 fn bind(&self, sup_this: SupThis) -> Result<Self::Bound>;136}137138/// Object fields may, or may not depend on `this`/`super`, this enum allows cheaper reuse of object-independent fields for native code139/// Standard jsonnet fields are always unbound140#[derive(Clone, Trace)]141pub enum MaybeUnbound {142 /// Value needs to be bound to `this`/`super`143 Unbound(CcUnbound<Val>),144 /// Value is object-independent145 Bound(Thunk<Val>),146}147148impl Debug for MaybeUnbound {149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {150 write!(f, "MaybeUnbound")151 }152}153impl MaybeUnbound {154 /// Attach object context to value, if required155 pub fn evaluate(&self, sup_this: SupThis) -> Result<Val> {156 match self {157 Self::Unbound(v) => v.0.bind(sup_this),158 Self::Bound(v) => Ok(v.evaluate()?),159 }160 }161}162163cc_dyn!(CcContextInitializer, ContextInitializer);164165/// During import, this trait will be called to create initial context for file.166/// It may initialize global variables, stdlib for example.167pub trait ContextInitializer: Trace {168 /// For which size the builder should be preallocated169 fn reserve_vars(&self) -> usize {170 0171 }172 /// Initialize default file context.173 /// Has default implementation, which calls `populate`.174 /// Prefer to always implement `populate` instead.175 fn initialize(&self, for_file: Source) -> Context {176 let mut builder = ContextBuilder::with_capacity(self.reserve_vars());177 self.populate(for_file, &mut builder);178 builder.build()179 }180 /// For composability: extend builder. May panic if this initialization is not supported,181 /// and the context may only be created via `initialize`.182 fn populate(&self, for_file: Source, builder: &mut ContextBuilder);183 /// Allows upcasting from abstract to concrete context initializer.184 /// jrsonnet by itself doesn't use this method, it is allowed for it to panic.185 fn as_any(&self) -> &dyn Any;186}187188/// Context initializer which adds nothing.189impl ContextInitializer for () {190 fn populate(&self, _for_file: Source, _builder: &mut ContextBuilder) {}191 fn as_any(&self) -> &dyn Any {192 self193 }194}195196impl<T> ContextInitializer for Option<T>197where198 T: ContextInitializer,199{200 fn initialize(&self, for_file: Source) -> Context {201 if let Some(ctx) = self {202 ctx.initialize(for_file)203 } else {204 ().initialize(for_file)205 }206 }207208 fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {209 if let Some(ctx) = self {210 ctx.populate(for_file, builder);211 }212 }213214 fn as_any(&self) -> &dyn Any {215 self216 }217}218219macro_rules! impl_context_initializer {220 ($($gen:ident)*) => {221 #[allow(non_snake_case)]222 impl<$($gen: ContextInitializer + Trace,)*> ContextInitializer for ($($gen,)*) {223 fn reserve_vars(&self) -> usize {224 let mut out = 0;225 let ($($gen,)*) = self;226 $(out += $gen.reserve_vars();)*227 out228 }229 fn populate(&self, for_file: Source, builder: &mut ContextBuilder) {230 let ($($gen,)*) = self;231 $($gen.populate(for_file.clone(), builder);)*232 }233 fn as_any(&self) -> &dyn Any {234 self235 }236 }237 };238 ($($cur:ident)* @ $c:ident $($rest:ident)*) => {239 impl_context_initializer!($($cur)*);240 impl_context_initializer!($($cur)* $c @ $($rest)*);241 };242 ($($cur:ident)* @) => {243 impl_context_initializer!($($cur)*);244 }245}246impl_context_initializer! {247 A @ B C D E F G248}249250#[derive(Trace)]251struct FileData {252 string: Option<IStr>,253 bytes: Option<IBytes>,254 parsed: Option<Rc<Expr>>,255 evaluated: Option<Val>,256257 evaluating: bool,258}259impl FileData {260 fn new_string(data: IStr) -> Self {261 Self {262 string: Some(data),263 bytes: None,264 parsed: None,265 evaluated: None,266 evaluating: false,267 }268 }269 fn new_bytes(data: IBytes) -> Self {270 Self {271 string: None,272 bytes: Some(data),273 parsed: None,274 evaluated: None,275 evaluating: false,276 }277 }278 pub(crate) fn get_string(&mut self) -> Option<IStr> {279 if self.string.is_none() {280 self.string = Some(281 self.bytes282 .as_ref()283 .expect("either string or bytes should be set")284 .clone()285 .cast_str()?,286 );287 }288 Some(self.string.clone().expect("just set"))289 }290}291292#[derive(Trace)]293pub struct EvaluationStateInternals {294 /// Internal state295 file_cache: RefCell<FxHashMap<SourcePath, FileData>>,296 /// Context initializer, which will be used for imports and everything297 /// [`NoopContextInitializer`] is used by default, most likely you want to have `jrsonnet-stdlib`298 context_initializer: CcContextInitializer,299 /// Used to resolve file locations/contents300 import_resolver: Rc<dyn ImportResolver>,301}302303/// Maintains stack trace and import resolution304#[derive(Clone, Trace)]305pub struct State(Cc<EvaluationStateInternals>);306307thread_local! {308 pub static DEFAULT_STATE: State = State::builder().build();309 pub static STATE: RefCell<Option<State>> = const {RefCell::new(None)};310}311pub struct StateEnterGuard(PhantomData<()>);312impl Drop for StateEnterGuard {313 fn drop(&mut self) {314 STATE.with_borrow_mut(|v| *v = None);315 }316}317318pub fn with_state<V>(v: impl FnOnce(State) -> V) -> V {319 if let Some(state) = STATE.with_borrow(Clone::clone) {320 v(state)321 } else {322 let s = DEFAULT_STATE.with(Clone::clone);323 v(s)324 }325}326327impl State {328 pub fn enter(&self) -> StateEnterGuard {329 self.try_enter().expect("entered state already exists")330 }331 pub fn try_enter(&self) -> Option<StateEnterGuard> {332 STATE.with_borrow_mut(|v| {333 if v.is_none() {334 *v = Some(self.clone());335 Some(StateEnterGuard(PhantomData))336 } else {337 None338 }339 })340 }341 /// Should only be called with path retrieved from [`resolve_path`], may panic otherwise342 pub fn import_resolved_str(&self, path: SourcePath) -> Result<IStr> {343 let mut file_cache = self.file_cache();344 let mut file = file_cache.entry(path.clone());345346 let file = match file {347 Entry::Occupied(ref mut d) => d.get_mut(),348 Entry::Vacant(v) => {349 let data = self.import_resolver().load_file_contents(&path)?;350 v.insert(FileData::new_string(351 std::str::from_utf8(&data)352 .map_err(|_| ImportBadFileUtf8(path.clone()))?353 .into(),354 ))355 }356 };357 Ok(file358 .get_string()359 .ok_or_else(|| ImportBadFileUtf8(path.clone()))?)360 }361 /// Should only be called with path retrieved from [`resolve_path`], may panic otherwise362 pub fn import_resolved_bin(&self, path: SourcePath) -> Result<IBytes> {363 let mut file_cache = self.file_cache();364 let mut file = file_cache.entry(path.clone());365366 let file = match file {367 Entry::Occupied(ref mut d) => d.get_mut(),368 Entry::Vacant(v) => {369 let data = self.import_resolver().load_file_contents(&path)?;370 v.insert(FileData::new_bytes(data.as_slice().into()))371 }372 };373 if let Some(str) = &file.bytes {374 return Ok(str.clone());375 }376 if file.bytes.is_none() {377 file.bytes = Some(378 file.string379 .as_ref()380 .expect("either string or bytes should be set")381 .clone()382 .cast_bytes(),383 );384 }385 Ok(file.bytes.as_ref().expect("just set").clone())386 }387 /// Should only be called with path retrieved from [`resolve_path`], may panic otherwise388 pub fn import_resolved(&self, path: SourcePath) -> Result<Val> {389 let mut file_cache = self.file_cache();390 let mut file = file_cache.entry(path.clone());391392 let file = match file {393 Entry::Occupied(ref mut d) => d.get_mut(),394 Entry::Vacant(v) => {395 let data = self.import_resolver().load_file_contents(&path)?;396 v.insert(FileData::new_string(397 std::str::from_utf8(&data)398 .map_err(|_| ImportBadFileUtf8(path.clone()))?399 .into(),400 ))401 }402 };403 if let Some(val) = &file.evaluated {404 return Ok(val.clone());405 }406 let code = file407 .get_string()408 .ok_or_else(|| ImportBadFileUtf8(path.clone()))?;409 let file_name = Source::new(path.clone(), code.clone());410 if file.parsed.is_none() {411 file.parsed = Some(412 parse_jsonnet(&code, file_name.clone())413 .map(Rc::new)414 .map_err(|e| ImportSyntaxError {415 path: file_name.clone(),416 error: Box::new(e),417 })?,418 );419 }420 let parsed = file.parsed.as_ref().expect("just set").clone();421 if file.evaluating {422 bail!(InfiniteRecursionDetected)423 }424 file.evaluating = true;425 // Dropping file cache guard here, as evaluation may use this map too426 drop(file_cache);427 let res = evaluate(self.create_default_context(file_name), &parsed);428429 let mut file_cache = self.file_cache();430 let mut file = file_cache.entry(path);431432 let Entry::Occupied(file) = &mut file else {433 unreachable!("this file was just here")434 };435 let file = file.get_mut();436 file.evaluating = false;437 match res {438 Ok(v) => {439 file.evaluated = Some(v.clone());440 Ok(v)441 }442 Err(e) => Err(e),443 }444 }445446 /// Has same semantics as `import 'path'` called from `from` file447 pub fn import_from(&self, from: &SourcePath, path: impl AsPathLike) -> Result<Val> {448 let resolved = self.resolve_from(from, &path)?;449 self.import_resolved(resolved)450 }451 pub fn import(&self, path: impl AsPathLike) -> Result<Val> {452 let resolved = self.resolve_from_default(&path)?;453 self.import_resolved(resolved)454 }455456 /// Creates context with all passed global variables457 pub fn create_default_context(&self, source: Source) -> Context {458 self.context_initializer().initialize(source)459 }460461 /// Creates context with all passed global variables, calling custom modifier462 pub fn create_default_context_with(463 &self,464 source: Source,465 context_initializer: impl ContextInitializer,466 ) -> Context {467 let default_initializer = self.context_initializer();468 let mut builder = ContextBuilder::with_capacity(469 default_initializer.reserve_vars() + context_initializer.reserve_vars(),470 );471 default_initializer.populate(source.clone(), &mut builder);472 context_initializer.populate(source, &mut builder);473474 builder.build()475 }476}477478/// Internals479impl State {480 fn file_cache(&self) -> RefMut<'_, FxHashMap<SourcePath, FileData>> {481 self.0.file_cache.borrow_mut()482 }483}484/// Executes code creating a new stack frame, to be replaced with try{}485pub fn in_frame<T>(486 e: CallLocation<'_>,487 frame_desc: impl FnOnce() -> String,488 f: impl FnOnce() -> Result<T>,489) -> Result<T> {490 let _guard = check_depth()?;491492 f().with_description_src(e, frame_desc)493}494495/// Executes code creating a new stack frame, to be replaced with try{}496pub fn in_description_frame<T>(497 frame_desc: impl FnOnce() -> String,498 f: impl FnOnce() -> Result<T>,499) -> Result<T> {500 let _guard = check_depth()?;501502 f().with_description(frame_desc)503}504505#[derive(Trace)]506pub struct InitialUnderscore(pub Thunk<Val>);507impl ContextInitializer for InitialUnderscore {508 fn populate(&self, _for_file: Source, builder: &mut ContextBuilder) {509 builder.bind("_", self.0.clone());510 }511512 fn as_any(&self) -> &dyn Any {513 self514 }515}516517/// Raw methods evaluate passed values but don't perform TLA execution518impl State {519 /// Parses and evaluates the given snippet520 pub fn evaluate_snippet(&self, name: impl Into<IStr>, code: impl Into<IStr>) -> Result<Val> {521 let code = code.into();522 let source = Source::new_virtual(name.into(), code.clone());523 let parsed = parse_jsonnet(&code, source.clone())524 .map_err(|e| ImportSyntaxError {525 path: source.clone(),526 error: Box::new(e),527 })?;528 evaluate(self.create_default_context(source), &parsed)529 }530 /// Parses and evaluates the given snippet with custom context modifier531 pub fn evaluate_snippet_with(532 &self,533 name: impl Into<IStr>,534 code: impl Into<IStr>,535 context_initializer: impl ContextInitializer,536 ) -> Result<Val> {537 let code = code.into();538 let source = Source::new_virtual(name.into(), code.clone());539 let parsed = parse_jsonnet(&code, source.clone())540 .map_err(|e| ImportSyntaxError {541 path: source.clone(),542 error: Box::new(e),543 })?;544 evaluate(545 self.create_default_context_with(source, context_initializer),546 &parsed,547 )548 }549}550551/// Settings utilities552impl State {553 // Only panics in case of [`ImportResolver`] contract violation554 #[allow(clippy::missing_panics_doc)]555 pub fn resolve_from(&self, from: &SourcePath, path: &dyn AsPathLike) -> Result<SourcePath> {556 self.import_resolver().resolve_from(from, path)557 }558 #[allow(clippy::missing_panics_doc)]559 pub fn resolve_from_default(&self, path: &dyn AsPathLike) -> Result<SourcePath> {560 self.import_resolver().resolve_from_default(path)561 }562 pub fn import_resolver(&self) -> &dyn ImportResolver {563 &*self.0.import_resolver564 }565 pub fn context_initializer(&self) -> &dyn ContextInitializer {566 &*self.0.context_initializer.0567 }568}569570impl State {571 pub fn builder() -> StateBuilder {572 StateBuilder::default()573 }574}575576impl Default for State {577 fn default() -> Self {578 Self::builder().build()579 }580}581582#[derive(Default)]583pub struct StateBuilder {584 import_resolver: Option<Rc<dyn ImportResolver>>,585 context_initializer: Option<CcContextInitializer>,586}587impl StateBuilder {588 pub fn import_resolver(&mut self, import_resolver: impl ImportResolver) -> &mut Self {589 let _ = self.import_resolver.insert(Rc::new(import_resolver));590 self591 }592 pub fn context_initializer(593 &mut self,594 context_initializer: impl ContextInitializer,595 ) -> &mut Self {596 let _ = self597 .context_initializer598 .insert(CcContextInitializer::new(context_initializer));599 self600 }601 pub fn build(mut self) -> State {602 State(Cc::new(EvaluationStateInternals {603 file_cache: RefCell::new(FxHashMap::new()),604 context_initializer: self605 .context_initializer606 .take()607 .unwrap_or_else(|| CcContextInitializer::new(())),608 import_resolver: self609 .import_resolver610 .take()611 .unwrap_or_else(|| Rc::new(DummyImportResolver)),612 }))613 }614}