difftreelog
feat experimental object field order preservation
in: master
15 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -492,6 +492,7 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
dependencies = [
+ "indexmap",
"itoa",
"ryu",
"serde",
bindings/jsonnet/Cargo.tomldiffbeforeafterboth--- a/bindings/jsonnet/Cargo.toml
+++ b/bindings/jsonnet/Cargo.toml
@@ -17,3 +17,4 @@
[features]
interop = []
+exp-preserve-order = ["jrsonnet-evaluator/exp-preserve-order"]
bindings/jsonnet/src/lib.rsdiffbeforeafterboth--- a/bindings/jsonnet/src/lib.rs
+++ b/bindings/jsonnet/src/lib.rs
@@ -58,7 +58,11 @@
pub extern "C" fn jsonnet_string_output(vm: &EvaluationState, v: c_int) {
match v {
1 => vm.set_manifest_format(ManifestFormat::String),
- 0 => vm.set_manifest_format(ManifestFormat::Json(4)),
+ 0 => vm.set_manifest_format(ManifestFormat::Json {
+ padding: 4,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order: false,
+ }),
_ => panic!("incorrect output format"),
}
}
bindings/jsonnet/src/val_modify.rsdiffbeforeafterboth--- a/bindings/jsonnet/src/val_modify.rs
+++ b/bindings/jsonnet/src/val_modify.rs
@@ -5,8 +5,7 @@
use std::{ffi::CStr, os::raw::c_char};
use gcmodule::Cc;
-use jrsonnet_evaluator::{val::ArrValue, EvaluationState, LazyBinding, LazyVal, ObjMember, Val};
-use jrsonnet_parser::Visibility;
+use jrsonnet_evaluator::{val::ArrValue, EvaluationState, LazyVal, Val};
/// # Safety
///
@@ -41,19 +40,9 @@
val: &Val,
) {
match obj {
- Val::Obj(old) => {
- let new_obj = old.clone().extend_with_field(
- CStr::from_ptr(name).to_str().unwrap().into(),
- ObjMember {
- add: false,
- visibility: Visibility::Normal,
- invoke: LazyBinding::Bound(LazyVal::new_resolved(val.clone())),
- location: None,
- },
- );
-
- *obj = Val::Obj(new_obj);
- }
+ Val::Obj(old) => old
+ .extend_field(CStr::from_ptr(name).to_str().unwrap().into())
+ .value(val.clone()),
_ => panic!("should receive object"),
}
}
cmds/jrsonnet/Cargo.tomldiffbeforeafterboth--- a/cmds/jrsonnet/Cargo.toml
+++ b/cmds/jrsonnet/Cargo.toml
@@ -9,6 +9,12 @@
[features]
# Use mimalloc as allocator
mimalloc = ["mimallocator"]
+# Experimental feature, which allows to preserve order of object fields
+exp-preserve-order = [
+ "jrsonnet-evaluator/exp-preserve-order",
+ "jrsonnet-evaluator/exp-serde-preserve-order",
+ "jrsonnet-cli/exp-preserve-order",
+]
[dependencies]
jrsonnet-evaluator = { path = "../../crates/jrsonnet-evaluator", version = "0.4.2" }
crates/jrsonnet-cli/Cargo.tomldiffbeforeafterboth--- a/crates/jrsonnet-cli/Cargo.toml
+++ b/crates/jrsonnet-cli/Cargo.toml
@@ -6,6 +6,9 @@
license = "MIT"
edition = "2021"
+[features]
+exp-preserve-order = ["jrsonnet-evaluator/exp-preserve-order"]
+
[dependencies]
jrsonnet-evaluator = { path = "../../crates/jrsonnet-evaluator", version = "0.4.2", features = [
"explaining-traces",
crates/jrsonnet-cli/src/manifest.rsdiffbeforeafterboth--- a/crates/jrsonnet-cli/src/manifest.rs
+++ b/crates/jrsonnet-cli/src/manifest.rs
@@ -43,20 +43,30 @@
/// `0` for hard tabs, `-1` for single line output [default: 3 for json, 2 for yaml]
#[clap(long)]
line_padding: Option<usize>,
+ /// Preserve order in object manifestification
+ #[cfg(feature = "exp-preserve-order")]
+ #[clap(long)]
+ exp_preserve_order: bool,
}
impl ConfigureState for ManifestOpts {
fn configure(&self, state: &EvaluationState) -> Result<()> {
if self.string {
state.set_manifest_format(ManifestFormat::String);
} else {
+ #[cfg(feature = "exp-preserve-order")]
+ let preserve_order = self.exp_preserve_order;
match self.format {
ManifestFormatName::String => state.set_manifest_format(ManifestFormat::String),
- ManifestFormatName::Json => {
- state.set_manifest_format(ManifestFormat::Json(self.line_padding.unwrap_or(3)))
- }
- ManifestFormatName::Yaml => {
- state.set_manifest_format(ManifestFormat::Yaml(self.line_padding.unwrap_or(2)))
- }
+ ManifestFormatName::Json => state.set_manifest_format(ManifestFormat::Json {
+ padding: self.line_padding.unwrap_or(3),
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ }),
+ ManifestFormatName::Yaml => state.set_manifest_format(ManifestFormat::Yaml {
+ padding: self.line_padding.unwrap_or(2),
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ }),
}
}
if self.yaml_stream {
crates/jrsonnet-evaluator/Cargo.tomldiffbeforeafterboth--- a/crates/jrsonnet-evaluator/Cargo.toml
+++ b/crates/jrsonnet-evaluator/Cargo.toml
@@ -15,6 +15,10 @@
# Allows library authors to throw custom errors
anyhow-error = ["anyhow"]
+# Allows to preserve field order in objects
+exp-preserve-order = []
+exp-serde-preserve-order = ["serde_json/preserve_order"]
+
[dependencies]
jrsonnet-interner = { path = "../jrsonnet-interner", version = "0.4.2" }
jrsonnet-parser = { path = "../jrsonnet-parser", version = "0.4.2" }
crates/jrsonnet-evaluator/src/builtin/manifest.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/builtin/manifest.rs
+++ b/crates/jrsonnet-evaluator/src/builtin/manifest.rs
@@ -21,6 +21,8 @@
pub mtype: ManifestType,
pub newline: &'s str,
pub key_val_sep: &'s str,
+ #[cfg(feature = "exp-preserve-order")]
+ pub preserve_order: bool,
}
pub fn manifest_json_ex(val: &Val, options: &ManifestJsonOptions<'_>) -> Result<String> {
@@ -85,7 +87,10 @@
Val::Obj(obj) => {
obj.run_assertions()?;
buf.push('{');
- let fields = obj.fields();
+ let fields = obj.fields(
+ #[cfg(feature = "exp-preserve-order")]
+ options.preserve_order,
+ );
if !fields.is_empty() {
if mtype != ManifestType::ToString && mtype != ManifestType::Minify {
buf.push_str(options.newline);
@@ -182,6 +187,10 @@
/// safe_key: 1
/// ```
pub quote_keys: bool,
+ /// If true - then order of fields is preserved as written,
+ /// instead of sorting alphabetically
+ #[cfg(feature = "exp-preserve-order")]
+ pub preserve_order: bool,
}
/// From https://github.com/chyh1990/yaml-rust/blob/da52a68615f2ecdd6b7e4567019f280c433c1521/src/emitter.rs#L289
@@ -287,7 +296,14 @@
if o.is_empty() {
buf.push_str("{}");
} else {
- for (i, key) in o.fields().iter().enumerate() {
+ for (i, key) in o
+ .fields(
+ #[cfg(feature = "exp-preserve-order")]
+ options.preserve_order,
+ )
+ .iter()
+ .enumerate()
+ {
if i != 0 {
buf.push('\n');
buf.push_str(cur_padding);
crates/jrsonnet-evaluator/src/builtin/mod.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/builtin/mod.rs
+++ b/crates/jrsonnet-evaluator/src/builtin/mod.rs
@@ -162,11 +162,7 @@
Ok(match x {
A(x) => x.chars().count(),
B(x) => x.len(),
- C(x) => x
- .fields_visibility()
- .into_iter()
- .filter(|(_k, v)| *v)
- .count(),
+ C(x) => x.len(),
D(f) => f.args_len(),
})
}
@@ -191,8 +187,20 @@
}
#[jrsonnet_macros::builtin]
-fn builtin_object_fields_ex(obj: ObjValue, inc_hidden: bool) -> Result<VecVal> {
- let out = obj.fields_ex(inc_hidden);
+fn builtin_object_fields_ex(
+ obj: ObjValue,
+ inc_hidden: bool,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: Option<bool>,
+) -> Result<VecVal> {
+ #[cfg(not(feature = "exp-preserve-order"))]
+ let preserve_order = false;
+ #[cfg(feature = "exp-preserve-order")]
+ let preserve_order = preserve_order.unwrap_or(false);
+ let out = obj.fields_ex(
+ inc_hidden,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ );
Ok(VecVal(Cc::new(
out.into_iter().map(Val::Str).collect::<Vec<_>>(),
)))
@@ -586,6 +594,7 @@
indent: IStr,
newline: Option<IStr>,
key_val_sep: Option<IStr>,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: Option<bool>,
) -> Result<String> {
let newline = newline.as_deref().unwrap_or("\n");
let key_val_sep = key_val_sep.as_deref().unwrap_or(": ");
@@ -596,6 +605,8 @@
mtype: ManifestType::Std,
newline,
key_val_sep,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order: preserve_order.unwrap_or(false),
},
)
}
@@ -605,6 +616,7 @@
value: Any,
indent_array_in_object: Option<bool>,
quote_keys: Option<bool>,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: Option<bool>,
) -> Result<String> {
manifest_yaml_ex(
&value.0,
@@ -616,6 +628,8 @@
""
},
quote_keys: quote_keys.unwrap_or(true),
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order: preserve_order.unwrap_or(false),
},
)
}
crates/jrsonnet-evaluator/src/integrations/serde.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/integrations/serde.rs
+++ b/crates/jrsonnet-evaluator/src/integrations/serde.rs
@@ -28,7 +28,10 @@
}
Val::Obj(o) => {
let mut out = Map::new();
- for key in o.fields() {
+ for key in o.fields(
+ #[cfg(feature = "exp-preserve-order")]
+ cfg!(feature = "exp-serde-preserve-order"),
+ ) {
out.insert(
(&key as &str).into(),
o.get(key)?
crates/jrsonnet-evaluator/src/lib.rsdiffbeforeafterboth1#![warn(clippy::all, clippy::nursery)]2#![allow(3 macro_expanded_macro_exports_accessed_by_absolute_paths,4 clippy::ptr_arg5)]67// For jrsonnet-macros8extern crate self as jrsonnet_evaluator;910mod builtin;11mod ctx;12mod dynamic;13pub mod error;14mod evaluate;15pub mod function;16pub mod gc;17mod import;18mod integrations;19mod map;20pub mod native;21mod obj;22pub mod trace;23pub mod typed;24pub mod val;2526use std::{27 cell::{Ref, RefCell, RefMut},28 collections::HashMap,29 fmt::Debug,30 path::{Path, PathBuf},31 rc::Rc,32};3334pub use ctx::*;35pub use dynamic::*;36use error::{Error::*, LocError, Result, StackTraceElement};37pub use evaluate::*;38use function::{Builtin, CallLocation, TlaArg};39use gc::{GcHashMap, TraceBox};40use gcmodule::{Cc, Trace, Weak};41pub use import::*;42pub use jrsonnet_interner::IStr;43pub use jrsonnet_parser as parser;44use jrsonnet_parser::*;45pub use obj::*;46use trace::{location_to_offset, offset_to_location, CodeLocation, CompactFormat, TraceFormat};47pub use val::{LazyVal, ManifestFormat, Val};4849pub trait Bindable: Trace + 'static {50 fn bind(&self, this: Option<ObjValue>, super_obj: Option<ObjValue>) -> Result<LazyVal>;51}5253#[derive(Clone, Trace)]54pub enum LazyBinding {55 Bindable(Cc<TraceBox<dyn Bindable>>),56 Bound(LazyVal),57}5859impl Debug for LazyBinding {60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {61 write!(f, "LazyBinding")62 }63}64impl LazyBinding {65 pub fn evaluate(&self, this: Option<ObjValue>, super_obj: Option<ObjValue>) -> Result<LazyVal> {66 match self {67 Self::Bindable(v) => v.bind(this, super_obj),68 Self::Bound(v) => Ok(v.clone()),69 }70 }71}7273pub struct EvaluationSettings {74 /// Limits recursion by limiting the number of stack frames75 pub max_stack: usize,76 /// Limits amount of stack trace items preserved77 pub max_trace: usize,78 /// Used for s`td.extVar`79 pub ext_vars: HashMap<IStr, Val>,80 /// Used for ext.native81 pub ext_natives: HashMap<IStr, Cc<TraceBox<dyn Builtin>>>,82 /// TLA vars83 pub tla_vars: HashMap<IStr, TlaArg>,84 /// Global variables are inserted in default context85 pub globals: HashMap<IStr, Val>,86 /// Used to resolve file locations/contents87 pub import_resolver: Box<dyn ImportResolver>,88 /// Used in manifestification functions89 pub manifest_format: ManifestFormat,90 /// Used for bindings91 pub trace_format: Box<dyn TraceFormat>,92}93impl Default for EvaluationSettings {94 fn default() -> Self {95 Self {96 max_stack: 200,97 max_trace: 20,98 globals: Default::default(),99 ext_vars: Default::default(),100 ext_natives: Default::default(),101 tla_vars: Default::default(),102 import_resolver: Box::new(DummyImportResolver),103 manifest_format: ManifestFormat::Json {104 padding: 4,105 #[cfg(feature = "exp-preserve-order")]106 preserve_order: false,107 },108 trace_format: Box::new(CompactFormat {109 padding: 4,110 resolver: trace::PathResolver::Absolute,111 }),112 }113 }114}115116#[derive(Default)]117struct EvaluationData {118 /// Used for stack overflow detection, stacktrace is populated on unwind119 stack_depth: usize,120 /// Updated every time stack entry is popt121 stack_generation: usize,122123 breakpoints: Breakpoints,124 /// Contains file source codes and evaluation results for imports and pretty-printed stacktraces125 files: GcHashMap<Rc<Path>, FileData>,126 str_files: GcHashMap<Rc<Path>, IStr>,127 bin_files: GcHashMap<Rc<Path>, Rc<[u8]>>,128}129130pub struct FileData {131 source_code: IStr,132 parsed: LocExpr,133 evaluated: Option<Val>,134}135136#[allow(clippy::type_complexity)]137pub struct Breakpoint {138 loc: ExprLocation,139 collected: RefCell<HashMap<usize, (usize, Vec<Result<Val>>)>>,140}141#[derive(Default)]142struct Breakpoints(Vec<Rc<Breakpoint>>);143impl Breakpoints {144 fn insert(145 &self,146 stack_depth: usize,147 stack_generation: usize,148 loc: &ExprLocation,149 result: Result<Val>,150 ) -> Result<Val> {151 if self.0.is_empty() {152 return result;153 }154 for item in self.0.iter() {155 if item.loc.belongs_to(loc) {156 let mut collected = item.collected.borrow_mut();157 let (depth, vals) = collected.entry(stack_generation).or_default();158 if stack_depth > *depth {159 vals.clear();160 }161 vals.push(result.clone());162 }163 }164 result165 }166}167168#[derive(Default)]169pub struct EvaluationStateInternals {170 /// Internal state171 data: RefCell<EvaluationData>,172 /// Settings, safe to change at runtime173 settings: RefCell<EvaluationSettings>,174}175176thread_local! {177 /// Contains the state for a currently executed file.178 /// Global state is fine here.179 pub(crate) static EVAL_STATE: RefCell<Option<EvaluationState>> = RefCell::new(None)180}181182pub(crate) fn with_state<T>(f: impl FnOnce(&EvaluationState) -> T) -> T {183 EVAL_STATE.with(|s| {184 f(s.borrow().as_ref().expect(185 "missing evaluation state, some functions should be called inside of run_in_state call",186 ))187 })188}189pub fn push_frame<T>(190 e: CallLocation,191 frame_desc: impl FnOnce() -> String,192 f: impl FnOnce() -> Result<T>,193) -> Result<T> {194 with_state(|s| s.push(e, frame_desc, f))195}196197#[allow(dead_code)]198pub fn push_val_frame(199 e: &ExprLocation,200 frame_desc: impl FnOnce() -> String,201 f: impl FnOnce() -> Result<Val>,202) -> Result<Val> {203 with_state(|s| s.push_val(e, frame_desc, f))204}205#[allow(dead_code)]206pub fn push_description_frame<T>(207 frame_desc: impl FnOnce() -> String,208 f: impl FnOnce() -> Result<T>,209) -> Result<T> {210 with_state(|s| s.push_description(frame_desc, f))211}212213/// Maintains stack trace and import resolution214#[derive(Default, Clone)]215pub struct EvaluationState(Rc<EvaluationStateInternals>);216217impl EvaluationState {218 /// Parses and adds file as loaded219 pub fn add_file(&self, path: Rc<Path>, source_code: IStr) -> Result<LocExpr> {220 let parsed = parse(221 &source_code,222 &ParserSettings {223 file_name: path.clone(),224 },225 )226 .map_err(|error| ImportSyntaxError {227 error: Box::new(error),228 path: path.to_owned(),229 source_code: source_code.clone(),230 })?;231 self.add_parsed_file(path, source_code, parsed.clone())?;232233 Ok(parsed)234 }235236 pub fn reset_evaluation_state(&self, name: &Path) {237 self.data_mut()238 .files239 .get_mut(name)240 .unwrap()241 .evaluated242 .take();243 }244245 /// Adds file by source code and parsed expr246 pub fn add_parsed_file(247 &self,248 name: Rc<Path>,249 source_code: IStr,250 parsed: LocExpr,251 ) -> Result<()> {252 self.data_mut().files.insert(253 name,254 FileData {255 source_code,256 parsed,257 evaluated: None,258 },259 );260261 Ok(())262 }263 pub fn get_source(&self, name: &Path) -> Option<IStr> {264 let ro_map = &self.data().files;265 ro_map.get(name).map(|value| value.source_code.clone())266 }267 pub fn map_source_locations(&self, file: &Path, locs: &[usize]) -> Vec<CodeLocation> {268 offset_to_location(&self.get_source(file).unwrap_or_else(|| "".into()), locs)269 }270 pub fn map_from_source_location(271 &self,272 file: &Path,273 line: usize,274 column: usize,275 ) -> Option<usize> {276 location_to_offset(&self.get_source(file).unwrap(), line, column)277 }278 pub fn import_file(&self, from: &Path, path: &Path) -> Result<Val> {279 let file_path = self.resolve_file(from, path)?;280 {281 let data = self.data();282 let files = &data.files;283 if files.contains_key(&file_path as &Path) {284 drop(data);285 return self.evaluate_loaded_file_raw(&file_path);286 }287 }288 let contents = self.load_file_str(&file_path)?;289 self.add_file(file_path.clone(), contents)?;290 self.evaluate_loaded_file_raw(&file_path)291 }292 pub(crate) fn import_file_str(&self, from: &Path, path: &Path) -> Result<IStr> {293 let path = self.resolve_file(from, path)?;294 if !self.data().str_files.contains_key(&path) {295 let file_str = self.load_file_str(&path)?;296 self.data_mut().str_files.insert(path.clone(), file_str);297 }298 Ok(self.data().str_files.get(&path).cloned().unwrap())299 }300 pub(crate) fn import_file_bin(&self, from: &Path, path: &Path) -> Result<Rc<[u8]>> {301 let path = self.resolve_file(from, path)?;302 if !self.data().bin_files.contains_key(&path) {303 let file_bin = self.load_file_bin(&path)?;304 self.data_mut().bin_files.insert(path.clone(), file_bin);305 }306 Ok(self.data().bin_files.get(&path).cloned().unwrap())307 }308309 fn evaluate_loaded_file_raw(&self, name: &Path) -> Result<Val> {310 let expr: LocExpr = {311 let ro_map = &self.data().files;312 let value = ro_map313 .get(name)314 .unwrap_or_else(|| panic!("file not added: {:?}", name));315 if let Some(ref evaluated) = value.evaluated {316 return Ok(evaluated.clone());317 }318 value.parsed.clone()319 };320 let value = evaluate(self.create_default_context(), &expr)?;321 {322 self.data_mut()323 .files324 .get_mut(name)325 .unwrap()326 .evaluated327 .replace(value.clone());328 }329 Ok(value)330 }331332 /// Adds standard library global variable (std) to this evaluator333 pub fn with_stdlib(&self) -> &Self {334 use jrsonnet_stdlib::STDLIB_STR;335 let std_path: Rc<Path> = PathBuf::from("std.jsonnet").into();336 self.run_in_state(|| {337 self.add_parsed_file(338 std_path.clone(),339 STDLIB_STR.to_owned().into(),340 builtin::get_parsed_stdlib(),341 )342 .unwrap();343 let val = self.evaluate_loaded_file_raw(&std_path).unwrap();344 self.settings_mut().globals.insert("std".into(), val);345 });346 self347 }348349 /// Creates context with all passed global variables350 pub fn create_default_context(&self) -> Context {351 let globals = &self.settings().globals;352 let mut new_bindings = GcHashMap::with_capacity(globals.len());353 for (name, value) in globals.iter() {354 new_bindings.insert(name.clone(), LazyVal::new_resolved(value.clone()));355 }356 Context::new().extend_bound(new_bindings)357 }358359 /// Executes code creating a new stack frame360 pub fn push<T>(361 &self,362 e: CallLocation,363 frame_desc: impl FnOnce() -> String,364 f: impl FnOnce() -> Result<T>,365 ) -> Result<T> {366 {367 let mut data = self.data_mut();368 let stack_depth = &mut data.stack_depth;369 if *stack_depth > self.max_stack() {370 // Error creation uses data, so i drop guard here371 drop(data);372 throw!(StackOverflow);373 } else {374 *stack_depth += 1;375 }376 }377 let result = f();378 {379 let mut data = self.data_mut();380 data.stack_depth -= 1;381 data.stack_generation += 1;382 }383 if let Err(mut err) = result {384 err.trace_mut().0.push(StackTraceElement {385 location: e.0.cloned(),386 desc: frame_desc(),387 });388 return Err(err);389 }390 result391 }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 {401 let mut data = self.data_mut();402 let stack_depth = &mut data.stack_depth;403 if *stack_depth > self.max_stack() {404 // Error creation uses data, so i drop guard here405 drop(data);406 throw!(StackOverflow);407 } else {408 *stack_depth += 1;409 }410 }411 let mut result = f();412 {413 let mut data = self.data_mut();414 data.stack_depth -= 1;415 data.stack_generation += 1;416 result = data417 .breakpoints418 .insert(data.stack_depth, data.stack_generation, e, result);419 }420 if let Err(mut err) = result {421 err.trace_mut().0.push(StackTraceElement {422 location: Some(e.clone()),423 desc: frame_desc(),424 });425 return Err(err);426 }427 result428 }429 /// Executes code creating a new stack frame430 pub fn push_description<T>(431 &self,432 frame_desc: impl FnOnce() -> String,433 f: impl FnOnce() -> Result<T>,434 ) -> Result<T> {435 {436 let mut data = self.data_mut();437 let stack_depth = &mut data.stack_depth;438 if *stack_depth > self.max_stack() {439 // Error creation uses data, so i drop guard here440 drop(data);441 throw!(StackOverflow);442 } else {443 *stack_depth += 1;444 }445 }446 let result = f();447 {448 let mut data = self.data_mut();449 data.stack_depth -= 1;450 data.stack_generation += 1;451 }452 if let Err(mut err) = result {453 err.trace_mut().0.push(StackTraceElement {454 location: None,455 desc: frame_desc(),456 });457 return Err(err);458 }459 result460 }461462 /// Runs passed function in state (required if function needs to modify stack trace)463 pub fn run_in_state<T>(&self, f: impl FnOnce() -> T) -> T {464 EVAL_STATE.with(|v| {465 let has_state = v.borrow().is_some();466 if !has_state {467 v.borrow_mut().replace(self.clone());468 }469 let result = f();470 if !has_state {471 v.borrow_mut().take();472 }473 result474 })475 }476 pub fn run_in_state_with_breakpoint(477 &self,478 bp: Rc<Breakpoint>,479 f: impl FnOnce() -> Result<()>,480 ) -> Result<()> {481 {482 let mut data = self.data_mut();483 data.breakpoints.0.push(bp);484 }485486 let result = self.run_in_state(f);487488 {489 let mut data = self.data_mut();490 data.breakpoints.0.pop();491 }492493 result494 }495496 pub fn stringify_err(&self, e: &LocError) -> String {497 let mut out = String::new();498 self.settings()499 .trace_format500 .write_trace(&mut out, self, e)501 .unwrap();502 out503 }504505 pub fn manifest(&self, val: Val) -> Result<IStr> {506 self.run_in_state(|| {507 push_description_frame(508 || "manifestification".to_string(),509 || val.manifest(&self.manifest_format()),510 )511 })512 }513 pub fn manifest_multi(&self, val: Val) -> Result<Vec<(IStr, IStr)>> {514 self.run_in_state(|| val.manifest_multi(&self.manifest_format()))515 }516 pub fn manifest_stream(&self, val: Val) -> Result<Vec<IStr>> {517 self.run_in_state(|| val.manifest_stream(&self.manifest_format()))518 }519520 /// If passed value is function then call with set TLA521 pub fn with_tla(&self, val: Val) -> Result<Val> {522 self.run_in_state(|| {523 Ok(match val {524 Val::Func(func) => push_description_frame(525 || "during TLA call".to_owned(),526 || {527 func.evaluate(528 self.create_default_context(),529 CallLocation::native(),530 &self.settings().tla_vars,531 true,532 )533 },534 )?,535 v => v,536 })537 })538 }539}540541/// Internals542impl EvaluationState {543 fn data(&self) -> Ref<EvaluationData> {544 self.0.data.borrow()545 }546 fn data_mut(&self) -> RefMut<EvaluationData> {547 self.0.data.borrow_mut()548 }549 pub fn settings(&self) -> Ref<EvaluationSettings> {550 self.0.settings.borrow()551 }552 pub fn settings_mut(&self) -> RefMut<EvaluationSettings> {553 self.0.settings.borrow_mut()554 }555}556557/// Raw methods evaluate passed values but don't perform TLA execution558impl EvaluationState {559 pub fn evaluate_file_raw(&self, name: &Path) -> Result<Val> {560 self.run_in_state(|| self.import_file(&std::env::current_dir().expect("cwd"), name))561 }562 pub fn evaluate_file_raw_nocwd(&self, name: &Path) -> Result<Val> {563 self.run_in_state(|| self.import_file(&PathBuf::from("."), name))564 }565 /// Parses and evaluates the given snippet566 pub fn evaluate_snippet_raw(&self, source: Rc<Path>, code: IStr) -> Result<Val> {567 let parsed = parse(568 &code,569 &ParserSettings {570 file_name: source.clone(),571 },572 )573 .map_err(|e| ImportSyntaxError {574 path: source.clone(),575 source_code: code.clone(),576 error: Box::new(e),577 })?;578 self.add_parsed_file(source, code, parsed.clone())?;579 self.evaluate_expr_raw(parsed)580 }581 /// Evaluates the parsed expression582 pub fn evaluate_expr_raw(&self, code: LocExpr) -> Result<Val> {583 self.run_in_state(|| evaluate(self.create_default_context(), &code))584 }585}586587/// Settings utilities588impl EvaluationState {589 pub fn add_ext_var(&self, name: IStr, value: Val) {590 self.settings_mut().ext_vars.insert(name, value);591 }592 pub fn add_ext_str(&self, name: IStr, value: IStr) {593 self.add_ext_var(name, Val::Str(value));594 }595 pub fn add_ext_code(&self, name: IStr, code: IStr) -> Result<()> {596 let value =597 self.evaluate_snippet_raw(PathBuf::from(format!("ext_code {}", name)).into(), code)?;598 self.add_ext_var(name, value);599 Ok(())600 }601602 pub fn add_tla(&self, name: IStr, value: Val) {603 self.settings_mut()604 .tla_vars605 .insert(name, TlaArg::Val(value));606 }607 pub fn add_tla_str(&self, name: IStr, value: IStr) {608 self.settings_mut()609 .tla_vars610 .insert(name, TlaArg::String(value));611 }612 pub fn add_tla_code(&self, name: IStr, code: IStr) -> Result<()> {613 let parsed = self.add_file(PathBuf::from(format!("tla_code {}", name)).into(), code)?;614 self.settings_mut()615 .tla_vars616 .insert(name, TlaArg::Code(parsed));617 Ok(())618 }619620 pub fn resolve_file(&self, from: &Path, path: &Path) -> Result<Rc<Path>> {621 self.settings().import_resolver.resolve_file(from, path)622 }623 pub fn load_file_str(&self, path: &Path) -> Result<IStr> {624 self.settings().import_resolver.load_file_str(path)625 }626 pub fn load_file_bin(&self, path: &Path) -> Result<Rc<[u8]>> {627 self.settings().import_resolver.load_file_bin(path)628 }629630 pub fn import_resolver(&self) -> Ref<dyn ImportResolver> {631 Ref::map(self.settings(), |s| &*s.import_resolver)632 }633 pub fn set_import_resolver(&self, resolver: Box<dyn ImportResolver>) {634 self.settings_mut().import_resolver = resolver;635 }636637 pub fn add_native(&self, name: IStr, cb: Cc<TraceBox<dyn Builtin>>) {638 self.settings_mut().ext_natives.insert(name, cb);639 }640641 pub fn manifest_format(&self) -> ManifestFormat {642 self.settings().manifest_format.clone()643 }644 pub fn set_manifest_format(&self, format: ManifestFormat) {645 self.settings_mut().manifest_format = format;646 }647648 pub fn trace_format(&self) -> Ref<dyn TraceFormat> {649 Ref::map(self.settings(), |s| &*s.trace_format)650 }651 pub fn set_trace_format(&self, format: Box<dyn TraceFormat>) {652 self.settings_mut().trace_format = format;653 }654655 pub fn max_trace(&self) -> usize {656 self.settings().max_trace657 }658 pub fn set_max_trace(&self, trace: usize) {659 self.settings_mut().max_trace = trace;660 }661662 pub fn max_stack(&self) -> usize {663 self.settings().max_stack664 }665 pub fn set_max_stack(&self, trace: usize) {666 self.settings_mut().max_stack = trace;667 }668}669670pub fn cc_ptr_eq<T>(a: &Cc<T>, b: &Cc<T>) -> bool {671 let a = a as &T;672 let b = b as &T;673 std::ptr::eq(a, b)674}675676fn weak_raw<T>(a: Weak<T>) -> *const () {677 unsafe { std::mem::transmute(a) }678}679fn weak_ptr_eq<T>(a: Weak<T>, b: Weak<T>) -> bool {680 std::ptr::eq(weak_raw(a), weak_raw(b))681}682683#[test]684fn weak_unsafe() {685 let a = Cc::new(1);686 let b = Cc::new(2);687688 let aw1 = a.clone().downgrade();689 let aw2 = a.clone().downgrade();690 let aw3 = a.clone().downgrade();691692 let bw = b.clone().downgrade();693694 assert!(weak_ptr_eq(aw1, aw2));695 assert!(!weak_ptr_eq(aw3, bw));696}697698#[cfg(test)]699pub mod tests {700 use std::{701 path::{Path, PathBuf},702 rc::Rc,703 };704705 use gcmodule::{Cc, Trace};706 use jrsonnet_parser::*;707708 use super::Val;709 use crate::{710 error::Error::*,711 function::{BuiltinParam, CallLocation},712 gc::TraceBox,713 native::NativeCallbackHandler,714 val::primitive_equals,715 EvaluationState,716 };717718 #[test]719 #[should_panic]720 fn eval_state_stacktrace() {721 let state = EvaluationState::default();722 state.run_in_state(|| {723 state724 .push(725 CallLocation::new(&ExprLocation(PathBuf::from("test1.jsonnet").into(), 10, 20)),726 || "outer".to_owned(),727 || {728 state.push(729 CallLocation::new(&ExprLocation(730 PathBuf::from("test2.jsonnet").into(),731 30,732 40,733 )),734 || "inner".to_owned(),735 || Err(RuntimeError("".into()).into()),736 )?;737 Ok(Val::Null)738 },739 )740 .unwrap();741 });742 }743744 #[test]745 fn eval_state_standard() {746 let state = EvaluationState::default();747 state.with_stdlib();748 assert!(primitive_equals(749 &state750 .evaluate_snippet_raw(751 PathBuf::from("raw.jsonnet").into(),752 r#"std.assertEqual(std.base64("test"), "dGVzdA==")"#.into()753 )754 .unwrap(),755 &Val::Bool(true),756 )757 .unwrap());758 }759760 macro_rules! eval {761 ($str: expr) => {{762 let evaluator = EvaluationState::default();763 evaluator.with_stdlib();764 evaluator.run_in_state(|| {765 evaluator766 .evaluate_snippet_raw(PathBuf::from("raw.jsonnet").into(), $str.into())767 .unwrap()768 })769 }};770 }771 macro_rules! eval_json {772 ($str: expr) => {{773 let evaluator = EvaluationState::default();774 evaluator.with_stdlib();775 evaluator.run_in_state(|| {776 evaluator777 .evaluate_snippet_raw(PathBuf::from("raw.jsonnet").into(), $str.into())778 .unwrap()779 .to_json(0)780 .unwrap()781 .replace("\n", "")782 })783 }};784 }785786 /// Asserts given code returns `true`787 macro_rules! assert_eval {788 ($str: expr) => {789 assert!(primitive_equals(&eval!($str), &Val::Bool(true)).unwrap())790 };791 }792793 /// Asserts given code returns `false`794 macro_rules! assert_eval_neg {795 ($str: expr) => {796 assert!(primitive_equals(&eval!($str), &Val::Bool(false)).unwrap())797 };798 }799 macro_rules! assert_json {800 ($str: expr, $out: expr) => {801 assert_eq!(eval_json!($str), $out.replace("\t", ""))802 };803 }804805 /// Sanity checking, before trusting to another tests806 #[test]807 fn equality_operator() {808 assert_eval!("2 == 2");809 assert_eval_neg!("2 != 2");810 assert_eval!("2 != 3");811 assert_eval_neg!("2 == 3");812 assert_eval!("'Hello' == 'Hello'");813 assert_eval_neg!("'Hello' != 'Hello'");814 assert_eval!("'Hello' != 'World'");815 assert_eval_neg!("'Hello' == 'World'");816 }817818 #[test]819 fn math_evaluation() {820 assert_eval!("2 + 2 * 2 == 6");821 assert_eval!("3 + (2 + 2 * 2) == 9");822 }823824 #[test]825 fn string_concat() {826 assert_eval!("'Hello' + 'World' == 'HelloWorld'");827 assert_eval!("'Hello' * 3 == 'HelloHelloHello'");828 assert_eval!("'Hello' + 'World' * 3 == 'HelloWorldWorldWorld'");829 }830831 #[test]832 fn faster_join() {833 assert_eval!("std.join([0,0], [[1,2],[3,4],[5,6]]) == [1,2,0,0,3,4,0,0,5,6]");834 assert_eval!("std.join(',', ['1','2','3','4']) == '1,2,3,4'");835 }836837 #[test]838 fn function_contexts() {839 assert_eval!(840 r#"841 local k = {842 t(name = self.h): [self.h, name],843 h: 3,844 };845 local f = {846 t: k.t(),847 h: 4,848 };849 f.t[0] == f.t[1]850 "#851 );852 }853854 #[test]855 fn local() {856 assert_eval!("local a = 2; local b = 3; a + b == 5");857 assert_eval!("local a = 1, b = a + 1; a + b == 3");858 assert_eval!("local a = 1; local a = 2; a == 2");859 }860861 #[test]862 fn object_lazyness() {863 assert_json!("local a = {a:error 'test'}; {}", r#"{}"#);864 }865866 #[test]867 fn object_inheritance() {868 assert_json!("{a: self.b} + {b:3}", r#"{"a": 3,"b": 3}"#);869 }870871 #[test]872 fn object_assertion_success() {873 eval!("{assert \"a\" in self} + {a:2}");874 }875876 #[test]877 fn object_assertion_error() {878 eval!("{assert \"a\" in self}");879 }880881 #[test]882 fn lazy_args() {883 eval!("local test(a) = 2; test(error '3')");884 }885886 #[test]887 #[should_panic]888 fn tailstrict_args() {889 eval!("local test(a) = 2; test(error '3') tailstrict");890 }891892 #[test]893 #[should_panic]894 fn no_binding_error() {895 eval!("a");896 }897898 #[test]899 fn test_object() {900 assert_json!("{a:2}", r#"{"a": 2}"#);901 assert_json!("{a:2+2}", r#"{"a": 4}"#);902 assert_json!("{a:2}+{b:2}", r#"{"a": 2,"b": 2}"#);903 assert_json!("{b:3}+{b:2}", r#"{"b": 2}"#);904 assert_json!("{b:3}+{b+:2}", r#"{"b": 5}"#);905 assert_json!("local test='a'; {[test]:2}", r#"{"a": 2}"#);906 assert_json!(907 r#"908 {909 name: "Alice",910 welcome: "Hello " + self.name + "!",911 }912 "#,913 r#"{"name": "Alice","welcome": "Hello Alice!"}"#914 );915 assert_json!(916 r#"917 {918 name: "Alice",919 welcome: "Hello " + self.name + "!",920 } + {921 name: "Bob"922 }923 "#,924 r#"{"name": "Bob","welcome": "Hello Bob!"}"#925 );926 }927928 #[test]929 fn functions() {930 assert_json!(r#"local a = function(b, c = 2) b + c; a(2)"#, "4");931 assert_json!(932 r#"local a = function(b, c = "Dear") b + c + d, d = "World"; a("Hello")"#,933 r#""HelloDearWorld""#934 );935 }936937 #[test]938 fn local_methods() {939 assert_json!(r#"local a(b, c = 2) = b + c; a(2)"#, "4");940 assert_json!(941 r#"local a(b, c = "Dear") = b + c + d, d = "World"; a("Hello")"#,942 r#""HelloDearWorld""#943 );944 }945946 #[test]947 fn object_locals() {948 assert_json!(r#"{local a = 3, b: a}"#, r#"{"b": 3}"#);949 assert_json!(r#"{local a = 3, local c = a, b: c}"#, r#"{"b": 3}"#);950 assert_json!(951 r#"{local a = function (b) {[b]:4}, test: a("test")}"#,952 r#"{"test": {"test": 4}}"#953 );954 }955956 #[test]957 fn object_comp() {958 assert_json!(959 r#"{local t = "a", ["h"+i+"_"+z]: if "h"+(i-1)+"_"+z in self then t+1 else 0+t for i in [1,2,3] for z in [2,3,4] if z != i}"#,960 "{\"h1_2\": \"0a\",\"h1_3\": \"0a\",\"h1_4\": \"0a\",\"h2_3\": \"a1\",\"h2_4\": \"a1\",\"h3_2\": \"0a\",\"h3_4\": \"a1\"}"961 )962 }963964 #[test]965 fn direct_self() {966 println!(967 "{:#?}",968 eval!(969 r#"970 {971 local me = self,972 a: 3,973 b(): me.a,974 }975 "#976 )977 );978 }979980 #[test]981 fn indirect_self() {982 // `self` assigned to `me` was lost when being983 // referenced from field984 eval!(985 r#"{986 local me = self,987 a: 3,988 b: me.a,989 }.b"#990 );991 }992993 // We can't trust other tests (And official jsonnet testsuite), if assert is not working correctly994 #[test]995 fn std_assert_ok() {996 eval!("std.assertEqual(4.5 << 2, 16)");997 }998999 #[test]1000 #[should_panic]1001 fn std_assert_failure() {1002 eval!("std.assertEqual(4.5 << 2, 15)");1003 }10041005 #[test]1006 fn string_is_string() {1007 assert!(primitive_equals(1008 &eval!("local arr = 'hello'; (!std.isArray(arr)) && (!std.isString(arr))"),1009 &Val::Bool(false),1010 )1011 .unwrap());1012 }10131014 #[test]1015 fn base64_works() {1016 assert_json!(r#"std.base64("test")"#, r#""dGVzdA==""#);1017 }10181019 #[test]1020 fn utf8_chars() {1021 assert_json!(1022 r#"local c="😎";{c:std.codepoint(c),l:std.length(c)}"#,1023 r#"{"c": 128526,"l": 1}"#1024 )1025 }10261027 #[test]1028 fn json() {1029 assert_json!(1030 r#"std.manifestJsonEx({a:3, b:4, c:6},"")"#,1031 r#""{\n\"a\": 3,\n\"b\": 4,\n\"c\": 6\n}""#1032 );1033 }10341035 #[test]1036 fn json_minified() {1037 assert_json!(1038 r#"std.manifestJsonMinified({a:3, b:4, c:6})"#,1039 r#""{\"a\":3,\"b\":4,\"c\":6}""#1040 );1041 }10421043 #[test]1044 fn parse_json() {1045 assert_json!(1046 r#"std.parseJson('{"a": -1,"b": 1,"c": 3.141,"d": []}')"#,1047 r#"{"a": -1,"b": 1,"c": 3.141,"d": []}"#1048 );1049 }10501051 #[test]1052 fn test() {1053 assert_json!(1054 r#"[[a, b] for a in [1,2,3] for b in [4,5,6]]"#,1055 "[[1,4],[1,5],[1,6],[2,4],[2,5],[2,6],[3,4],[3,5],[3,6]]"1056 );1057 }10581059 #[test]1060 fn sjsonnet() {1061 eval!(1062 r#"1063 local x0 = {k: 1};1064 local x1 = {k: x0.k + x0.k};1065 local x2 = {k: x1.k + x1.k};1066 local x3 = {k: x2.k + x2.k};1067 local x4 = {k: x3.k + x3.k};1068 local x5 = {k: x4.k + x4.k};1069 local x6 = {k: x5.k + x5.k};1070 local x7 = {k: x6.k + x6.k};1071 local x8 = {k: x7.k + x7.k};1072 local x9 = {k: x8.k + x8.k};1073 local x10 = {k: x9.k + x9.k};1074 local x11 = {k: x10.k + x10.k};1075 local x12 = {k: x11.k + x11.k};1076 local x13 = {k: x12.k + x12.k};1077 local x14 = {k: x13.k + x13.k};1078 local x15 = {k: x14.k + x14.k};1079 local x16 = {k: x15.k + x15.k};1080 local x17 = {k: x16.k + x16.k};1081 local x18 = {k: x17.k + x17.k};1082 local x19 = {k: x18.k + x18.k};1083 local x20 = {k: x19.k + x19.k};1084 local x21 = {k: x20.k + x20.k};1085 x21.k1086 "#1087 );1088 }10891090 // This test is commented out by default, because of huge compilation slowdown1091 // #[bench]1092 // fn bench_codegen(b: &mut Bencher) {1093 // b.iter(|| {1094 // #[allow(clippy::all)]1095 // let stdlib = {1096 // use jrsonnet_parser::*;1097 // include!(concat!(env!("OUT_DIR"), "/stdlib.rs"))1098 // };1099 // stdlib1100 // })1101 // }11021103 /*1104 #[bench]1105 fn bench_serialize(b: &mut Bencher) {1106 b.iter(|| {1107 bincode::deserialize::<jrsonnet_parser::LocExpr>(include_bytes!(concat!(1108 env!("OUT_DIR"),1109 "/stdlib.bincode"1110 )))1111 .expect("deserialize stdlib")1112 })1113 }11141115 #[bench]1116 fn bench_parse(b: &mut Bencher) {1117 b.iter(|| {1118 jrsonnet_parser::parse(1119 jrsonnet_stdlib::STDLIB_STR,1120 &jrsonnet_parser::ParserSettings {1121 loc_data: true,1122 file_name: Rc::new(PathBuf::from("std.jsonnet")),1123 },1124 )1125 })1126 }1127 */11281129 #[test]1130 fn equality() {1131 println!(1132 "{:?}",1133 jrsonnet_parser::parse(1134 "{ x: 1, y: 2 } == { x: 1, y: 2 }",1135 &ParserSettings {1136 file_name: PathBuf::from("equality").into(),1137 }1138 )1139 );1140 assert_eval!("{ x: 1, y: 2 } == { x: 1, y: 2 }")1141 }11421143 #[test]1144 fn native_ext() -> crate::error::Result<()> {1145 use super::native::NativeCallback;1146 let evaluator = EvaluationState::default();11471148 evaluator.with_stdlib();11491150 #[derive(Trace)]1151 struct NativeAdd;1152 impl NativeCallbackHandler for NativeAdd {1153 fn call(&self, from: Option<Rc<Path>>, args: &[Val]) -> crate::error::Result<Val> {1154 assert_eq!(1155 &from.unwrap() as &Path,1156 &PathBuf::from("native_caller.jsonnet")1157 );1158 match (&args[0], &args[1]) {1159 (Val::Num(a), Val::Num(b)) => Ok(Val::Num(a + b)),1160 (_, _) => unreachable!(),1161 }1162 }1163 }1164 evaluator.settings_mut().ext_natives.insert(1165 "native_add".into(),1166 #[allow(deprecated)]1167 Cc::new(TraceBox(Box::new(NativeCallback::new(1168 vec![1169 BuiltinParam {1170 name: "a".into(),1171 has_default: false,1172 },1173 BuiltinParam {1174 name: "b".into(),1175 has_default: false,1176 },1177 ],1178 TraceBox(Box::new(NativeAdd)),1179 )))),1180 );1181 evaluator.evaluate_snippet_raw(1182 PathBuf::from("native_caller.jsonnet").into(),1183 "std.assertEqual(std.native(\"native_add\")(1, 2), 3)".into(),1184 )?;1185 Ok(())1186 }11871188 #[test]1189 fn constant_intrinsic() -> crate::error::Result<()> {1190 assert_eval!(1191 "local std2 = std; local std = std2 { primitiveEquals(a, b):: false }; 1 == 1"1192 );1193 Ok(())1194 }11951196 #[test]1197 fn standalone_super() -> crate::error::Result<()> {1198 assert_eval!(1199 r#"1200 local obj = {1201 a: 1,1202 b: 2,1203 c: 3,1204 };1205 local test = obj + {1206 fields: std.objectFields(super),1207 d: 5,1208 };1209 test.fields == ['a', 'b', 'c']1210 "#1211 );1212 Ok(())1213 }12141215 #[test]1216 fn comp_self() -> crate::error::Result<()> {1217 assert_eval!(1218 r#"1219 std.objectFields({1220 a:{1221 [name]: name for name in std.objectFields(self)1222 },1223 b: 2,1224 c: 3,1225 }.a) == ['a', 'b', 'c']1226 "#1227 );12281229 Ok(())1230 }12311232 struct TestImportResolver(Vec<u8>);1233 impl crate::import::ImportResolver for TestImportResolver {1234 fn resolve_file(&self, _: &Path, _: &Path) -> crate::error::Result<Rc<Path>> {1235 Ok(PathBuf::from("/test").into())1236 }12371238 fn load_file_contents(&self, _: &Path) -> crate::error::Result<Vec<u8>> {1239 Ok(self.0.clone())1240 }12411242 unsafe fn as_any(&self) -> &dyn std::any::Any {1243 panic!()1244 }12451246 fn load_file_bin(&self, _resolved: &Path) -> crate::error::Result<Rc<[u8]>> {1247 panic!()1248 }1249 }12501251 #[test]1252 fn issue_23() {1253 let state = EvaluationState::default();1254 state.set_import_resolver(Box::new(TestImportResolver(r#"import "/test""#.into())));1255 let _ = state.evaluate_file_raw(&PathBuf::from("/test"));1256 }12571258 #[test]1259 fn issue_40() {1260 let state = EvaluationState::default();1261 state.with_stdlib();12621263 let error = state1264 .evaluate_snippet_raw(1265 PathBuf::from("issue40.jsonnet").into(),1266 r#"1267 local conf = {1268 n: ""1269 };12701271 local result = conf + {1272 assert std.isNumber(self.n): "is number"1273 };12741275 std.manifestJsonEx(result, "")1276 "#1277 .into(),1278 )1279 .unwrap_err();1280 assert_eq!(error.error().to_string(), "assert failed: is number");1281 }12821283 #[test]1284 fn test_ascii_upper_lower() {1285 assert_eval!(r#"std.assertEqual(std.asciiUpper("aBc😀"), "ABC😀")"#);1286 assert_eval!(r#"std.assertEqual(std.asciiLower("aBc😀"), "abc😀")"#);1287 }12881289 #[test]1290 fn test_member() {1291 assert_eval!(r#"!std.member("", "")"#);1292 assert_eval!(r#"std.member("abc", "a")"#);1293 assert_eval!(r#"!std.member("abc", "d")"#);1294 assert_eval!(r#"!std.member([], "")"#);1295 assert_eval!(r#"std.member(["a", "b", "c"], "a")"#);1296 assert_eval!(r#"!std.member(["a", "b", "c"], "d")"#);1297 }12981299 #[test]1300 fn test_count() {1301 assert_eval!(r#"std.assertEqual(std.count([], ""), 0)"#);1302 assert_eval!(r#"std.assertEqual(std.count(["a", "b", "a"], "d"), 0)"#);1303 assert_eval!(r#"std.assertEqual(std.count(["a", "b", "a"], "a"), 2)"#);1304 }13051306 mod derive_typed {1307 use std::path::PathBuf;13081309 use crate::{typed::Typed, EvaluationState};13101311 #[derive(PartialEq, Debug, Typed)]1312 struct MyTyped {1313 a: u32,1314 #[typed(rename = "b")]1315 c: String,1316 }13171318 #[test]1319 fn test() {1320 let es = EvaluationState::default();1321 let val = eval!("{a: 14, b: 'Hello, world!'}");1322 let typed = es.run_in_state(|| MyTyped::try_from(val).unwrap());13231324 assert_eq!(1325 typed,1326 MyTyped {1327 a: 14,1328 c: "Hello, world!".to_string()1329 }1330 );1331 es.settings_mut().globals.insert(1332 "mytyped".into(),1333 es.run_in_state(|| typed.try_into()).unwrap(),1334 );13351336 let v = es1337 .evaluate_snippet_raw(1338 PathBuf::from("raw.jsonnet").into(),1339 "1340 mytyped == {a: 14, b: 'Hello, world!'}1341 "1342 .into(),1343 )1344 .unwrap()1345 .as_bool()1346 .unwrap();1347 assert!(v)1348 }1349 }1350}crates/jrsonnet-evaluator/src/obj.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/obj.rs
+++ b/crates/jrsonnet-evaluator/src/obj.rs
@@ -18,10 +18,82 @@
push_frame, throw, weak_ptr_eq, weak_raw, Bindable, LazyBinding, LazyVal, Result, Val,
};
+#[cfg(not(feature = "exp-preserve-order"))]
+pub(crate) mod ordering {
+ use gcmodule::Trace;
+
+ #[derive(Clone, Copy, Default, Debug, Trace)]
+ pub struct FieldIndex;
+ impl FieldIndex {
+ pub fn next(self) -> Self {
+ Self
+ }
+ }
+
+ #[derive(Clone, Copy, Default, Debug, Trace)]
+ pub struct SuperDepth;
+ impl SuperDepth {
+ pub fn deeper(self) -> Self {
+ Self
+ }
+ }
+
+ #[derive(Clone, Copy)]
+ pub struct FieldSortKey;
+ impl FieldSortKey {
+ pub fn new(_: SuperDepth, _: FieldIndex) -> Self {
+ Self
+ }
+ }
+}
+
+#[cfg(feature = "exp-preserve-order")]
+mod ordering {
+ use std::cmp::Reverse;
+
+ use gcmodule::Trace;
+
+ #[derive(Clone, Copy, Default, Debug, Trace, PartialEq, Eq, PartialOrd, Ord)]
+ pub struct FieldIndex(u32);
+ impl FieldIndex {
+ pub fn next(self) -> Self {
+ Self(self.0 + 1)
+ }
+ }
+
+ #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)]
+ pub struct SuperDepth(u32);
+ impl SuperDepth {
+ pub fn deeper(self) -> Self {
+ Self(self.0 + 1)
+ }
+ }
+
+ #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
+ pub struct FieldSortKey(Reverse<SuperDepth>, FieldIndex);
+ impl FieldSortKey {
+ pub fn new(depth: SuperDepth, index: FieldIndex) -> Self {
+ Self(Reverse(depth), index)
+ }
+ pub fn collide(self, other: Self) -> Self {
+ if self.0 .0 > other.0 .0 {
+ self
+ } else if self.0 .0 < other.0 .0 {
+ other
+ } else {
+ unreachable!("object can't have two fields with same name")
+ }
+ }
+ }
+}
+
+pub(crate) use ordering::*;
+
#[derive(Debug, Trace)]
pub struct ObjMember {
pub add: bool,
pub visibility: Visibility,
+ original_index: FieldIndex,
pub invoke: LazyBinding,
pub location: Option<ExprLocation>,
}
@@ -120,6 +192,14 @@
),
}
}
+ pub(crate) fn extend_with_raw_member(self, key: IStr, value: ObjMember) -> Self {
+ let mut new = GcHashMap::with_capacity(1);
+ new.insert(key, value);
+ Self::new(Some(self), Cc::new(new), Cc::new(Vec::new()))
+ }
+ pub fn extend_field(&mut self, name: IStr) -> ObjMemberBuilder<ExtendBuilder> {
+ ObjMemberBuilder::new(ExtendBuilder(self), name, FieldIndex::default())
+ }
pub fn with_this(&self, this_obj: Self) -> Self {
Self(Cc::new(ObjValueInternals {
super_obj: self.0.super_obj.clone(),
@@ -131,6 +211,13 @@
}))
}
+ pub fn len(&self) -> usize {
+ self.fields_visibility()
+ .into_iter()
+ .filter(|(_, (visible, _))| *visible)
+ .count()
+ }
+
pub fn is_empty(&self) -> bool {
if !self.0.this_entries.is_empty() {
return false;
@@ -143,51 +230,93 @@
}
/// Run callback for every field found in object
- pub(crate) fn enum_fields(&self, handler: &mut impl FnMut(&IStr, &ObjMember) -> bool) -> bool {
+ pub(crate) fn enum_fields(
+ &self,
+ depth: SuperDepth,
+ handler: &mut impl FnMut(SuperDepth, &IStr, &ObjMember) -> bool,
+ ) -> bool {
if let Some(s) = &self.0.super_obj {
- if s.enum_fields(handler) {
+ if s.enum_fields(depth.deeper(), handler) {
return true;
}
}
for (name, member) in self.0.this_entries.iter() {
- if handler(name, member) {
+ if handler(depth, name, member) {
return true;
}
}
false
}
- pub fn fields_visibility(&self) -> FxHashMap<IStr, bool> {
+ pub fn fields_visibility(&self) -> FxHashMap<IStr, (bool, FieldSortKey)> {
let mut out = FxHashMap::default();
- self.enum_fields(&mut |name, member| {
+ self.enum_fields(SuperDepth::default(), &mut |depth, name, member| {
+ let new_sort_key = FieldSortKey::new(depth, member.original_index);
match member.visibility {
Visibility::Normal => {
let entry = out.entry(name.to_owned());
- entry.or_insert(true);
+ let v = entry.or_insert((true, new_sort_key));
+ v.1 = new_sort_key;
}
Visibility::Hidden => {
- out.insert(name.to_owned(), false);
+ out.insert(name.to_owned(), (false, new_sort_key));
}
Visibility::Unhide => {
- out.insert(name.to_owned(), true);
+ out.insert(name.to_owned(), (true, new_sort_key));
}
};
false
});
out
}
- pub fn fields_ex(&self, include_hidden: bool) -> Vec<IStr> {
+ pub fn fields_ex(
+ &self,
+ include_hidden: bool,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: bool,
+ ) -> Vec<IStr> {
+ #[cfg(feature = "exp-preserve-order")]
+ if preserve_order {
+ let (mut fields, mut keys): (Vec<_>, Vec<_>) = self
+ .fields_visibility()
+ .into_iter()
+ .filter(|(_, (visible, _))| include_hidden || *visible)
+ .enumerate()
+ .map(|(idx, (k, (_, sk)))| (k, (sk, idx)))
+ .unzip();
+ keys.sort_unstable_by_key(|v| v.0);
+ // Reorder in-place by resulting indexes
+ for i in 0..fields.len() {
+ let x = fields[i].clone();
+ let mut j = i;
+ loop {
+ let k = keys[j].1;
+ keys[j].1 = j;
+ if k == i {
+ break;
+ }
+ fields[j] = fields[k].clone();
+ j = k
+ }
+ fields[j] = x;
+ }
+ return fields;
+ }
+
let mut fields: Vec<_> = self
.fields_visibility()
.into_iter()
- .filter(|(_k, v)| include_hidden || *v)
+ .filter(|(_, (visible, _))| include_hidden || *visible)
.map(|(k, _)| k)
.collect();
fields.sort_unstable();
fields
}
- pub fn fields(&self) -> Vec<IStr> {
- self.fields_ex(false)
+ pub fn fields(&self, #[cfg(feature = "exp-preserve-order")] preserve_order: bool) -> Vec<IStr> {
+ self.fields_ex(
+ false,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ )
}
pub fn field_visibility(&self, name: IStr) -> Option<Visibility> {
@@ -236,11 +365,7 @@
self.get_raw(key, self.0.this_obj.as_ref())
}
- pub fn extend_with_field(self, key: IStr, value: ObjMember) -> Self {
- let mut new = GcHashMap::with_capacity(1);
- new.insert(key, value);
- Self::new(Some(self), Cc::new(new), Cc::new(Vec::new()))
- }
+ // pub fn extend_with(self, key: )
fn get_raw(&self, key: IStr, real_this: Option<&Self>) -> Result<Option<Val>> {
let real_this = real_this.unwrap_or(self);
@@ -339,6 +464,7 @@
super_obj: Option<ObjValue>,
map: GcHashMap<IStr, ObjMember>,
assertions: Vec<TraceBox<dyn ObjectAssertion>>,
+ next_field_index: FieldIndex,
}
impl ObjValueBuilder {
pub fn new() -> Self {
@@ -349,6 +475,7 @@
super_obj: None,
map: GcHashMap::with_capacity(capacity),
assertions: Vec::new(),
+ next_field_index: FieldIndex::default(),
}
}
pub fn reserve_asserts(&mut self, capacity: usize) -> &mut Self {
@@ -364,14 +491,10 @@
self.assertions.push(assertion);
self
}
- pub fn member(&mut self, name: IStr) -> ObjMemberBuilder {
- ObjMemberBuilder {
- value: self,
- name,
- add: false,
- visibility: Visibility::Normal,
- location: None,
- }
+ pub fn member(&mut self, name: IStr) -> ObjMemberBuilder<ValueBuilder> {
+ let field_index = self.next_field_index;
+ self.next_field_index = self.next_field_index.next();
+ ObjMemberBuilder::new(ValueBuilder(self), name, field_index)
}
pub fn build(self) -> ObjValue {
@@ -385,16 +508,28 @@
}
#[must_use = "value not added unless binding() was called"]
-pub struct ObjMemberBuilder<'v> {
- value: &'v mut ObjValueBuilder,
+pub struct ObjMemberBuilder<Kind> {
+ kind: Kind,
name: IStr,
add: bool,
visibility: Visibility,
+ original_index: FieldIndex,
location: Option<ExprLocation>,
}
#[allow(clippy::missing_const_for_fn)]
-impl<'v> ObjMemberBuilder<'v> {
+impl<Kind> ObjMemberBuilder<Kind> {
+ pub(crate) fn new(kind: Kind, name: IStr, original_index: FieldIndex) -> Self {
+ Self {
+ kind,
+ name,
+ original_index,
+ add: false,
+ visibility: Visibility::Normal,
+ location: None,
+ }
+ }
+
pub const fn with_add(mut self, add: bool) -> Self {
self.add = add;
self
@@ -413,6 +548,23 @@
self.location = Some(location);
self
}
+ fn build_member(self, binding: LazyBinding) -> (Kind, IStr, ObjMember) {
+ (
+ self.kind,
+ self.name,
+ ObjMember {
+ add: self.add,
+ visibility: self.visibility,
+ original_index: self.original_index,
+ invoke: binding,
+ location: self.location,
+ },
+ )
+ }
+}
+
+pub struct ValueBuilder<'v>(&'v mut ObjValueBuilder);
+impl<'v> ObjMemberBuilder<ValueBuilder<'v>> {
pub fn value(self, value: Val) -> Result<()> {
self.binding(LazyBinding::Bound(LazyVal::new_resolved(value)))
}
@@ -420,22 +572,31 @@
self.binding(LazyBinding::Bindable(Cc::new(bindable)))
}
pub fn binding(self, binding: LazyBinding) -> Result<()> {
- let old = self.value.map.insert(
- self.name.clone(),
- ObjMember {
- add: self.add,
- visibility: self.visibility,
- invoke: binding,
- location: self.location.clone(),
- },
- );
+ 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() {
push_frame(
- CallLocation(self.location.as_ref()),
- || format!("field <{}> initializtion", self.name.clone()),
- || throw!(DuplicateFieldName(self.name.clone())),
+ CallLocation(location.as_ref()),
+ || format!("field <{}> initializtion", name.clone()),
+ || throw!(DuplicateFieldName(name.clone())),
)?
}
Ok(())
}
}
+
+pub struct ExtendBuilder<'v>(&'v mut ObjValue);
+impl<'v> ObjMemberBuilder<ExtendBuilder<'v>> {
+ pub fn value(self, value: Val) {
+ self.binding(LazyBinding::Bound(LazyVal::new_resolved(value)))
+ }
+ pub fn bindable(self, bindable: TraceBox<dyn Bindable>) {
+ self.binding(LazyBinding::Bindable(Cc::new(bindable)))
+ }
+ pub fn binding(self, binding: LazyBinding) -> () {
+ let (receiver, name, member) = self.build_member(binding);
+ let new = receiver.0.clone();
+ *receiver.0 = new.extend_with_raw_member(name, member)
+ }
+}
crates/jrsonnet-evaluator/src/val.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/val.rs
+++ b/crates/jrsonnet-evaluator/src/val.rs
@@ -167,11 +167,31 @@
#[derive(Clone)]
pub enum ManifestFormat {
YamlStream(Box<ManifestFormat>),
- Yaml(usize),
- Json(usize),
+ Yaml {
+ padding: usize,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order: bool,
+ },
+ Json {
+ padding: usize,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order: bool,
+ },
ToString,
String,
}
+impl ManifestFormat {
+ #[cfg(feature = "exp-preserve-order")]
+ fn preserve_order(&self) -> bool {
+ match self {
+ ManifestFormat::YamlStream(s) => s.preserve_order(),
+ ManifestFormat::Yaml { preserve_order, .. } => *preserve_order,
+ ManifestFormat::Json { preserve_order, .. } => *preserve_order,
+ ManifestFormat::ToString => false,
+ ManifestFormat::String => false,
+ }
+ }
+}
#[derive(Debug, Clone, Trace)]
pub struct Slice {
@@ -559,6 +579,8 @@
mtype: ManifestType::ToString,
newline: "\n",
key_val_sep: ": ",
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order: false,
},
)?
.into(),
@@ -571,7 +593,10 @@
Self::Obj(obj) => obj,
_ => throw!(MultiManifestOutputIsNotAObject),
};
- let keys = obj.fields();
+ let keys = obj.fields(
+ #[cfg(feature = "exp-preserve-order")]
+ ty.preserve_order(),
+ );
let mut out = Vec::with_capacity(keys.len());
for key in keys {
let value = obj
@@ -622,8 +647,24 @@
out.into()
}
- ManifestFormat::Yaml(padding) => self.to_yaml(*padding)?,
- ManifestFormat::Json(padding) => self.to_json(*padding)?,
+ ManifestFormat::Yaml {
+ padding,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ } => self.to_yaml(
+ *padding,
+ #[cfg(feature = "exp-preserve-order")]
+ *preserve_order,
+ )?,
+ ManifestFormat::Json {
+ padding,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ } => self.to_json(
+ *padding,
+ #[cfg(feature = "exp-preserve-order")]
+ *preserve_order,
+ )?,
ManifestFormat::ToString => self.to_string()?,
ManifestFormat::String => match self {
Self::Str(s) => s.clone(),
@@ -633,7 +674,11 @@
}
/// For manifestification
- pub fn to_json(&self, padding: usize) -> Result<IStr> {
+ pub fn to_json(
+ &self,
+ padding: usize,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: bool,
+ ) -> Result<IStr> {
manifest_json_ex(
self,
&ManifestJsonOptions {
@@ -645,13 +690,19 @@
},
newline: "\n",
key_val_sep: ": ",
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
},
)
.map(|s| s.into())
}
/// Calls `std.manifestJson`
- pub fn to_std_json(&self, padding: usize) -> Result<Rc<str>> {
+ pub fn to_std_json(
+ &self,
+ padding: usize,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: bool,
+ ) -> Result<Rc<str>> {
manifest_json_ex(
self,
&ManifestJsonOptions {
@@ -659,12 +710,18 @@
mtype: ManifestType::Std,
newline: "\n",
key_val_sep: ": ",
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
},
)
.map(|s| s.into())
}
- pub fn to_yaml(&self, padding: usize) -> Result<IStr> {
+ pub fn to_yaml(
+ &self,
+ padding: usize,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: bool,
+ ) -> Result<IStr> {
let padding = &" ".repeat(padding);
manifest_yaml_ex(
self,
@@ -672,6 +729,8 @@
padding,
arr_element_padding: padding,
quote_keys: false,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
},
)
.map(|s| s.into())
@@ -733,8 +792,15 @@
if ObjValue::ptr_eq(a, b) {
return Ok(true);
}
- let fields = a.fields();
- if fields != b.fields() {
+ let fields = a.fields(
+ #[cfg(feature = "exp-preserve-order")]
+ false,
+ );
+ if fields
+ != b.fields(
+ #[cfg(feature = "exp-preserve-order")]
+ false,
+ ) {
return Ok(false);
}
for field in fields {
crates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-macros/src/lib.rs
+++ b/crates/jrsonnet-macros/src/lib.rs
@@ -122,6 +122,7 @@
ty: Box<Type>,
is_option: bool,
name: String,
+ cfg_attrs: Vec<Attribute>,
// ident: Ident,
},
Lazy {
@@ -134,20 +135,15 @@
impl ArgInfo {
fn parse(arg: &FnArg) -> Result<Self> {
- let typed = match arg {
+ let arg = match arg {
FnArg::Receiver(_) => unreachable!(),
FnArg::Typed(a) => a,
};
- let ident = match &typed.pat as &Pat {
+ let ident = match &arg.pat as &Pat {
Pat::Ident(i) => i.ident.clone(),
- _ => {
- return Err(Error::new(
- typed.pat.span(),
- "arg should be plain identifier",
- ))
- }
+ _ => return Err(Error::new(arg.pat.span(), "arg should be plain identifier")),
};
- let ty = &typed.ty;
+ let ty = &arg.ty;
if type_is_path(ty, "CallLocation").is_some() {
return Ok(Self::Location);
} else if type_is_path(ty, "Self").is_some() {
@@ -172,11 +168,18 @@
(false, ty.clone())
};
+ let cfg_attrs = arg
+ .attrs
+ .iter()
+ .filter(|a| a.path.is_ident("cfg"))
+ .cloned()
+ .collect();
+
Ok(Self::Normal {
ty,
is_option,
name: ident.to_string(),
- // ident,
+ cfg_attrs,
})
}
}
@@ -215,13 +218,22 @@
let params_desc = args.iter().flat_map(|a| match a {
ArgInfo::Normal {
- is_option, name, ..
- }
- | ArgInfo::Lazy { is_option, name } => Some(quote! {
+ is_option,
+ name,
+ cfg_attrs,
+ ..
+ } => Some(quote! {
+ #(#cfg_attrs)*
+ BuiltinParam {
+ name: std::borrow::Cow::Borrowed(#name),
+ has_default: #is_option,
+ },
+ }),
+ ArgInfo::Lazy { is_option, name } => Some(quote! {
BuiltinParam {
name: std::borrow::Cow::Borrowed(#name),
has_default: #is_option,
- }
+ },
}),
ArgInfo::Location => None,
ArgInfo::This => None,
@@ -232,23 +244,27 @@
ty,
is_option,
name,
- // ident,
+ cfg_attrs,
} => {
let eval = quote! {::jrsonnet_evaluator::push_description_frame(
|| format!("argument <{}> evaluation", #name),
|| <#ty>::try_from(value.evaluate()?),
)?};
- if *is_option {
+ let value = if *is_option {
quote! {if let Some(value) = parsed.get(#name) {
Some(#eval)
} else {
None
- }}
+ },}
} else {
quote! {{
let value = parsed.get(#name).expect("args shape is checked");
#eval
- }}
+ },}
+ };
+ quote! {
+ #(#cfg_attrs)*
+ #value
}
}
ArgInfo::Lazy { is_option, name } => {
@@ -260,12 +276,12 @@
}}
} else {
quote! {
- parsed.get(#name).expect("args shape is correct").clone()
+ parsed.get(#name).expect("args shape is correct").clone(),
}
}
}
- ArgInfo::Location => quote! {location},
- ArgInfo::This => quote! {self},
+ ArgInfo::Location => quote! {location,},
+ ArgInfo::This => quote! {self,},
});
let fields = attr.fields.iter().map(|field| {
@@ -309,7 +325,7 @@
parser::ExprLocation,
};
const PARAMS: &'static [BuiltinParam] = &[
- #(#params_desc),*
+ #(#params_desc)*
];
#static_ext
@@ -326,7 +342,7 @@
fn call(&self, context: Context, location: CallLocation, args: &dyn ArgsLike) -> Result<Val> {
let parsed = parse_builtin_call(context, &PARAMS, args, false)?;
- let result: #result = #name(#(#pass),*);
+ let result: #result = #name(#(#pass)*);
let result = result?;
result.try_into()
}