difftreelog
perf move std.object[Keys]Values[All] to native
in: master
6 files changed
crates/jrsonnet-evaluator/src/arr/spec.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/arr/spec.rs
+++ b/crates/jrsonnet-evaluator/src/arr/spec.rs
@@ -9,8 +9,9 @@
error::ErrorKind::InfiniteRecursionDetected,
evaluate,
function::FuncVal,
+ typed::Typed,
val::{StrValue, ThunkValue},
- Context, Error, Result, Thunk, Val,
+ Context, Error, ObjValue, Result, Thunk, Val,
};
pub trait ArrayLike: Any + Trace + Debug {
@@ -576,3 +577,103 @@
self.data.is_cheap()
}
}
+
+#[derive(Trace, Debug)]
+pub struct PickObjectValues {
+ obj: ObjValue,
+ keys: Vec<IStr>,
+}
+
+impl PickObjectValues {
+ pub fn new(obj: ObjValue, keys: Vec<IStr>) -> Self {
+ Self { obj, keys }
+ }
+}
+
+impl ArrayLike for PickObjectValues {
+ fn len(&self) -> usize {
+ self.keys.len()
+ }
+
+ fn get(&self, index: usize) -> Result<Option<Val>> {
+ let Some(key) = self.keys.get(index) else {
+ return Ok(None);
+ };
+ Ok(Some(self.obj.get_or_bail(key.clone())?))
+ }
+
+ fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
+ let Some(key) = self.keys.get(index) else {
+ return None;
+ };
+ Some(self.obj.get_lazy_or_bail(key.clone()))
+ }
+
+ fn get_cheap(&self, _index: usize) -> Option<Val> {
+ None
+ }
+
+ fn is_cheap(&self) -> bool {
+ false
+ }
+}
+
+#[derive(Trace, Debug)]
+pub struct PickObjectKeyValues {
+ obj: ObjValue,
+ keys: Vec<IStr>,
+}
+
+impl PickObjectKeyValues {
+ pub fn new(obj: ObjValue, keys: Vec<IStr>) -> Self {
+ Self { obj, keys }
+ }
+}
+
+#[derive(Typed)]
+pub struct KeyValue {
+ key: IStr,
+ value: Thunk<Val>,
+}
+
+impl ArrayLike for PickObjectKeyValues {
+ fn len(&self) -> usize {
+ self.keys.len()
+ }
+
+ fn get(&self, index: usize) -> Result<Option<Val>> {
+ let Some(key) = self.keys.get(index) else {
+ return Ok(None);
+ };
+ Ok(Some(
+ KeyValue::into_untyped(KeyValue {
+ key: key.clone(),
+ value: Thunk::evaluated(self.obj.get_or_bail(key.clone())?),
+ })
+ .expect("convertible"),
+ ))
+ }
+
+ fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {
+ let Some(key) = self.keys.get(index) else {
+ return None;
+ };
+ // Nothing can fail in the key part, yet value is still
+ // lazy-evaluated
+ Some(Thunk::evaluated(
+ KeyValue::into_untyped(KeyValue {
+ key: key.clone(),
+ value: self.obj.get_lazy_or_bail(key.clone()),
+ })
+ .expect("convertible"),
+ ))
+ }
+
+ fn get_cheap(&self, _index: usize) -> Option<Val> {
+ None
+ }
+
+ fn is_cheap(&self) -> bool {
+ false
+ }
+}
crates/jrsonnet-evaluator/src/obj.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/obj.rs
+++ b/crates/jrsonnet-evaluator/src/obj.rs
@@ -12,12 +12,13 @@
use rustc_hash::FxHashMap;
use crate::{
- error::{Error, ErrorKind::*},
+ arr::{PickObjectKeyValues, PickObjectValues},
+ error::{suggest_object_fields, Error, ErrorKind::*},
function::CallLocation,
gc::{GcHashMap, GcHashSet, TraceBox},
operator::evaluate_add_op,
tb, throw,
- val::ThunkValue,
+ val::{ArrValue, ThunkValue},
MaybeUnbound, Result, State, Thunk, Unbound, Val,
};
@@ -398,6 +399,14 @@
self.0.get_for(key, this)
}
+ pub fn get_or_bail(&self, key: IStr) -> Result<Val> {
+ let Some(value) = self.get(key.clone())? else {
+ let suggestions = suggest_object_fields(self, key.clone());
+ throw!(NoSuchField(key, suggestions))
+ };
+ Ok(value)
+ }
+
fn get_raw(&self, key: IStr, this: ObjValue) -> Result<Option<Val>> {
self.0.get_for_uncached(key, this)
}
@@ -452,6 +461,25 @@
key,
}))
}
+ pub fn get_lazy_or_bail(&self, key: IStr) -> Thunk<Val> {
+ #[derive(Trace)]
+ struct ThunkGet {
+ obj: ObjValue,
+ key: IStr,
+ }
+ impl ThunkValue for ThunkGet {
+ type Output = Val;
+
+ fn get(self: Box<Self>) -> Result<Self::Output> {
+ Ok(self.obj.get_or_bail(self.key)?)
+ }
+ }
+
+ Thunk::new(ThunkGet {
+ obj: self.clone(),
+ key,
+ })
+ }
pub fn ptr_eq(a: &Self, b: &Self) -> bool {
Cc::ptr_eq(&a.0, &b.0)
}
@@ -529,6 +557,51 @@
preserve_order,
)
}
+ pub fn values_ex(
+ &self,
+ include_hidden: bool,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: bool,
+ ) -> ArrValue {
+ ArrValue::new(PickObjectValues::new(
+ self.clone(),
+ self.fields_ex(
+ include_hidden,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ ),
+ ))
+ }
+ pub fn values(&self, #[cfg(feature = "exp-preserve-order")] preserve_order: bool) -> ArrValue {
+ self.values_ex(
+ false,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ )
+ }
+ pub fn key_values_ex(
+ &self,
+ include_hidden: bool,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: bool,
+ ) -> ArrValue {
+ ArrValue::new(PickObjectKeyValues::new(
+ self.clone(),
+ self.fields_ex(
+ include_hidden,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ ),
+ ))
+ }
+ pub fn key_values(
+ &self,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: bool,
+ ) -> ArrValue {
+ self.key_values_ex(
+ false,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ )
+ }
}
impl OopObject {
crates/jrsonnet-evaluator/src/val.rsdiffbeforeafterboth1use std::{2 cell::RefCell,3 fmt::{self, Debug, Display},4 hash::Hasher,5 mem::replace,6 rc::Rc,7};89use jrsonnet_gcmodule::{Cc, Trace};10use jrsonnet_interner::IStr;11use jrsonnet_types::ValType;12use rustc_hash::FxHasher;1314pub use crate::arr::{ArrValue, ArrayLike};15use crate::{16 error::{Error, ErrorKind::*},17 function::FuncVal,18 gc::{GcHashMap, TraceBox},19 manifest::{ManifestFormat, ToStringFormat},20 tb, throw,21 typed::BoundedUsize,22 ObjValue, Result, Unbound, WeakObjValue,23};2425pub trait ThunkValue: Trace {26 type Output;27 fn get(self: Box<Self>) -> Result<Self::Output>;28}2930#[derive(Trace)]31enum ThunkInner<T: Trace> {32 Computed(T),33 Errored(Error),34 Waiting(TraceBox<dyn ThunkValue<Output = T>>),35 Pending,36}3738/// Lazily evaluated value39#[allow(clippy::module_name_repetitions)]40#[derive(Clone, Trace)]41pub struct Thunk<T: Trace>(Cc<RefCell<ThunkInner<T>>>);4243impl<T: Trace> Thunk<T> {44 pub fn evaluated(val: T) -> Self {45 Self(Cc::new(RefCell::new(ThunkInner::Computed(val))))46 }47 pub fn new(f: impl ThunkValue<Output = T> + 'static) -> Self {48 Self(Cc::new(RefCell::new(ThunkInner::Waiting(tb!(f)))))49 }50 pub fn errored(e: Error) -> Self {51 Self(Cc::new(RefCell::new(ThunkInner::Errored(e))))52 }53}5455impl<T> Thunk<T>56where57 T: Clone + Trace,58{59 pub fn force(&self) -> Result<()> {60 self.evaluate()?;61 Ok(())62 }6364 /// Evaluate thunk, or return cached value65 ///66 /// # Errors67 ///68 /// - Lazy value evaluation returned error69 /// - This method was called during inner value evaluation70 pub fn evaluate(&self) -> Result<T> {71 match &*self.0.borrow() {72 ThunkInner::Computed(v) => return Ok(v.clone()),73 ThunkInner::Errored(e) => return Err(e.clone()),74 ThunkInner::Pending => return Err(InfiniteRecursionDetected.into()),75 ThunkInner::Waiting(..) => (),76 };77 let ThunkInner::Waiting(value) = replace(&mut *self.0.borrow_mut(), ThunkInner::Pending)78 else {79 unreachable!();80 };81 let new_value = match value.0.get() {82 Ok(v) => v,83 Err(e) => {84 *self.0.borrow_mut() = ThunkInner::Errored(e.clone());85 return Err(e);86 }87 };88 *self.0.borrow_mut() = ThunkInner::Computed(new_value.clone());89 Ok(new_value)90 }91}9293pub trait ThunkMapper<Input>: Trace {94 type Output;95 fn map(self, from: Input) -> Result<Self::Output>;96}97impl<Input> Thunk<Input>98where99 Input: Trace + Clone,100{101 pub fn map<M>(self, mapper: M) -> Thunk<M::Output>102 where103 M: ThunkMapper<Input>,104 M::Output: Trace,105 {106 #[derive(Trace)]107 struct Mapped<Input: Trace, Mapper: Trace> {108 inner: Thunk<Input>,109 mapper: Mapper,110 }111 impl<Input, Mapper> ThunkValue for Mapped<Input, Mapper>112 where113 Input: Trace + Clone,114 Mapper: ThunkMapper<Input>,115 {116 type Output = Mapper::Output;117118 fn get(self: Box<Self>) -> Result<Self::Output> {119 let value = self.inner.evaluate()?;120 let mapped = self.mapper.map(value)?;121 Ok(mapped)122 }123 }124125 Thunk::new(Mapped::<Input, M> {126 inner: self,127 mapper,128 })129 }130}131132impl<T: Trace> From<Result<T>> for Thunk<T> {133 fn from(value: Result<T>) -> Self {134 match value {135 Ok(o) => Self::evaluated(o),136 Err(e) => Self::errored(e),137 }138 }139}140141impl<T: Trace + Default> Default for Thunk<T> {142 fn default() -> Self {143 Self::evaluated(T::default())144 }145}146147type CacheKey = (Option<WeakObjValue>, Option<WeakObjValue>);148149#[derive(Trace, Clone)]150pub struct CachedUnbound<I, T>151where152 I: Unbound<Bound = T>,153 T: Trace,154{155 cache: Cc<RefCell<GcHashMap<CacheKey, T>>>,156 value: I,157}158impl<I: Unbound<Bound = T>, T: Trace> CachedUnbound<I, T> {159 pub fn new(value: I) -> Self {160 Self {161 cache: Cc::new(RefCell::new(GcHashMap::new())),162 value,163 }164 }165}166impl<I: Unbound<Bound = T>, T: Clone + Trace> Unbound for CachedUnbound<I, T> {167 type Bound = T;168 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<T> {169 let cache_key = (170 sup.as_ref().map(|s| s.clone().downgrade()),171 this.as_ref().map(|t| t.clone().downgrade()),172 );173 {174 if let Some(t) = self.cache.borrow().get(&cache_key) {175 return Ok(t.clone());176 }177 }178 let bound = self.value.bind(sup, this)?;179180 {181 let mut cache = self.cache.borrow_mut();182 cache.insert(cache_key, bound.clone());183 }184185 Ok(bound)186 }187}188189impl<T: Debug + Trace> Debug for Thunk<T> {190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {191 write!(f, "Lazy")192 }193}194impl<T: Trace> PartialEq for Thunk<T> {195 fn eq(&self, other: &Self) -> bool {196 Cc::ptr_eq(&self.0, &other.0)197 }198}199200/// Represents a Jsonnet value, which can be sliced or indexed (string or array).201#[allow(clippy::module_name_repetitions)]202pub enum IndexableVal {203 /// String.204 Str(IStr),205 /// Array.206 Arr(ArrValue),207}208impl IndexableVal {209 pub fn to_array(self) -> ArrValue {210 match self {211 IndexableVal::Str(s) => ArrValue::chars(s.chars()),212 IndexableVal::Arr(arr) => arr,213 }214 }215 /// Slice the value.216 ///217 /// # Implementation218 ///219 /// For strings, will create a copy of specified interval.220 ///221 /// For arrays, nothing will be copied on this call, instead [`ArrValue::Slice`] view will be returned.222 pub fn slice(223 self,224 index: Option<BoundedUsize<0, { i32::MAX as usize }>>,225 end: Option<BoundedUsize<0, { i32::MAX as usize }>>,226 step: Option<BoundedUsize<1, { i32::MAX as usize }>>,227 ) -> Result<Self> {228 match &self {229 IndexableVal::Str(s) => {230 let index = index.as_deref().copied().unwrap_or(0);231 let end = end.as_deref().copied().unwrap_or(usize::MAX);232 let step = step.as_deref().copied().unwrap_or(1);233234 if index >= end {235 return Ok(Self::Str("".into()));236 }237238 Ok(Self::Str(239 (s.chars()240 .skip(index)241 .take(end - index)242 .step_by(step)243 .collect::<String>())244 .into(),245 ))246 }247 IndexableVal::Arr(arr) => {248 let index = index.as_deref().copied().unwrap_or(0);249 let end = end.as_deref().copied().unwrap_or(usize::MAX).min(arr.len());250 let step = step.as_deref().copied().unwrap_or(1);251252 if index >= end {253 return Ok(Self::Arr(ArrValue::empty()));254 }255256 Ok(Self::Arr(257 arr.clone()258 .slice(Some(index), Some(end), Some(step))259 .expect("arguments checked"),260 ))261 }262 }263 }264}265266#[derive(Debug, Clone, Trace)]267pub enum StrValue {268 Flat(IStr),269 Tree(Rc<(StrValue, StrValue, usize)>),270}271impl StrValue {272 pub fn concat(a: StrValue, b: StrValue) -> Self {273 // TODO: benchmark for an optimal value, currently just a arbitrary choice274 const STRING_EXTEND_THRESHOLD: usize = 100;275276 if a.is_empty() {277 b278 } else if b.is_empty() {279 a280 } else if a.len() + b.len() < STRING_EXTEND_THRESHOLD {281 Self::Flat(format!("{a}{b}").into())282 } else {283 let len = a.len() + b.len();284 Self::Tree(Rc::new((a, b, len)))285 }286 }287 pub fn into_flat(self) -> IStr {288 #[cold]289 fn write_buf(s: &StrValue, out: &mut String) {290 match s {291 StrValue::Flat(f) => out.push_str(f),292 StrValue::Tree(t) => {293 write_buf(&t.0, out);294 write_buf(&t.1, out);295 }296 }297 }298 match self {299 StrValue::Flat(f) => f,300 StrValue::Tree(_) => {301 let mut buf = String::with_capacity(self.len());302 write_buf(&self, &mut buf);303 buf.into()304 }305 }306 }307 pub fn len(&self) -> usize {308 match self {309 StrValue::Flat(v) => v.len(),310 StrValue::Tree(t) => t.2,311 }312 }313 pub fn is_empty(&self) -> bool {314 match self {315 Self::Flat(v) => v.is_empty(),316 // Can't create non-flat empty string317 Self::Tree(_) => false,318 }319 }320}321impl From<&str> for StrValue {322 fn from(value: &str) -> Self {323 Self::Flat(value.into())324 }325}326impl From<String> for StrValue {327 fn from(value: String) -> Self {328 Self::Flat(value.into())329 }330}331impl From<IStr> for StrValue {332 fn from(value: IStr) -> Self {333 Self::Flat(value)334 }335}336impl Display for StrValue {337 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {338 match self {339 StrValue::Flat(v) => write!(f, "{v}"),340 StrValue::Tree(t) => {341 write!(f, "{}", t.0)?;342 write!(f, "{}", t.1)343 }344 }345 }346}347impl PartialEq for StrValue {348 fn eq(&self, other: &Self) -> bool {349 let a = self.clone().into_flat();350 let b = other.clone().into_flat();351 a == b352 }353}354impl Eq for StrValue {}355impl PartialOrd for StrValue {356 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {357 Some(self.cmp(other))358 }359}360impl Ord for StrValue {361 fn cmp(&self, other: &Self) -> std::cmp::Ordering {362 let a = self.clone().into_flat();363 let b = other.clone().into_flat();364 a.cmp(&b)365 }366}367368/// Represents any valid Jsonnet value.369#[derive(Debug, Clone, Trace, Default)]370pub enum Val {371 /// Represents a Jsonnet boolean.372 Bool(bool),373 /// Represents a Jsonnet null value.374 #[default]375 Null,376 /// Represents a Jsonnet string.377 Str(StrValue),378 /// Represents a Jsonnet number.379 /// Should be finite, and not NaN380 /// This restriction isn't enforced by enum, as enum field can't be marked as private381 Num(f64),382 /// Experimental bigint383 #[cfg(feature = "exp-bigint")]384 BigInt(#[trace(skip)] Box<num_bigint::BigInt>),385 /// Represents a Jsonnet array.386 Arr(ArrValue),387 /// Represents a Jsonnet object.388 Obj(ObjValue),389 /// Represents a Jsonnet function.390 Func(FuncVal),391}392393#[cfg(target_pointer_width = "64")]394static_assertions::assert_eq_size!(Val, [u8; 24]);395396impl From<IndexableVal> for Val {397 fn from(v: IndexableVal) -> Self {398 match v {399 IndexableVal::Str(s) => Self::Str(StrValue::Flat(s)),400 IndexableVal::Arr(a) => Self::Arr(a),401 }402 }403}404405impl Val {406 pub const fn as_bool(&self) -> Option<bool> {407 match self {408 Self::Bool(v) => Some(*v),409 _ => None,410 }411 }412 pub const fn as_null(&self) -> Option<()> {413 match self {414 Self::Null => Some(()),415 _ => None,416 }417 }418 pub fn as_str(&self) -> Option<IStr> {419 match self {420 Self::Str(s) => Some(s.clone().into_flat()),421 _ => None,422 }423 }424 pub const fn as_num(&self) -> Option<f64> {425 match self {426 Self::Num(n) => Some(*n),427 _ => None,428 }429 }430 pub fn as_arr(&self) -> Option<ArrValue> {431 match self {432 Self::Arr(a) => Some(a.clone()),433 _ => None,434 }435 }436 pub fn as_obj(&self) -> Option<ObjValue> {437 match self {438 Self::Obj(o) => Some(o.clone()),439 _ => None,440 }441 }442 pub fn as_func(&self) -> Option<FuncVal> {443 match self {444 Self::Func(f) => Some(f.clone()),445 _ => None,446 }447 }448449 /// Creates `Val::Num` after checking for numeric overflow.450 /// As numbers are `f64`, we can just check for their finity.451 pub fn new_checked_num(num: f64) -> Result<Self> {452 if num.is_finite() {453 Ok(Self::Num(num))454 } else {455 throw!("overflow")456 }457 }458459 pub const fn value_type(&self) -> ValType {460 match self {461 Self::Str(..) => ValType::Str,462 Self::Num(..) => ValType::Num,463 #[cfg(feature = "exp-bigint")]464 Self::BigInt(..) => ValType::BigInt,465 Self::Arr(..) => ValType::Arr,466 Self::Obj(..) => ValType::Obj,467 Self::Bool(_) => ValType::Bool,468 Self::Null => ValType::Null,469 Self::Func(..) => ValType::Func,470 }471 }472473 pub fn manifest(&self, format: impl ManifestFormat) -> Result<String> {474 fn manifest_dyn(val: &Val, manifest: &dyn ManifestFormat) -> Result<String> {475 manifest.manifest(val.clone())476 }477 manifest_dyn(self, &format)478 }479480 pub fn to_string(&self) -> Result<IStr> {481 Ok(match self {482 Self::Bool(true) => "true".into(),483 Self::Bool(false) => "false".into(),484 Self::Null => "null".into(),485 Self::Str(s) => s.clone().into_flat(),486 _ => self.manifest(ToStringFormat).map(IStr::from)?,487 })488 }489490 pub fn into_indexable(self) -> Result<IndexableVal> {491 Ok(match self {492 Val::Str(s) => IndexableVal::Str(s.into_flat()),493 Val::Arr(arr) => IndexableVal::Arr(arr),494 _ => throw!(ValueIsNotIndexable(self.value_type())),495 })496 }497}498499const fn is_function_like(val: &Val) -> bool {500 matches!(val, Val::Func(_))501}502503/// Native implementation of `std.primitiveEquals`504pub fn primitive_equals(val_a: &Val, val_b: &Val) -> Result<bool> {505 Ok(match (val_a, val_b) {506 (Val::Bool(a), Val::Bool(b)) => a == b,507 (Val::Null, Val::Null) => true,508 (Val::Str(a), Val::Str(b)) => a == b,509 (Val::Num(a), Val::Num(b)) => (a - b).abs() <= f64::EPSILON,510 #[cfg(feature = "exp-bigint")]511 (Val::BigInt(a), Val::BigInt(b)) => a == b,512 (Val::Arr(_), Val::Arr(_)) => {513 throw!("primitiveEquals operates on primitive types, got array")514 }515 (Val::Obj(_), Val::Obj(_)) => {516 throw!("primitiveEquals operates on primitive types, got object")517 }518 (a, b) if is_function_like(a) && is_function_like(b) => {519 throw!("cannot test equality of functions")520 }521 (_, _) => false,522 })523}524525/// Native implementation of `std.equals`526pub fn equals(val_a: &Val, val_b: &Val) -> Result<bool> {527 if val_a.value_type() != val_b.value_type() {528 return Ok(false);529 }530 match (val_a, val_b) {531 (Val::Arr(a), Val::Arr(b)) => {532 if ArrValue::ptr_eq(a, b) {533 return Ok(true);534 }535 if a.len() != b.len() {536 return Ok(false);537 }538 for (a, b) in a.iter().zip(b.iter()) {539 if !equals(&a?, &b?)? {540 return Ok(false);541 }542 }543 Ok(true)544 }545 (Val::Obj(a), Val::Obj(b)) => {546 if ObjValue::ptr_eq(a, b) {547 return Ok(true);548 }549 let fields = a.fields(550 #[cfg(feature = "exp-preserve-order")]551 false,552 );553 if fields554 != b.fields(555 #[cfg(feature = "exp-preserve-order")]556 false,557 ) {558 return Ok(false);559 }560 for field in fields {561 if !equals(562 &a.get(field.clone())?.expect("field exists"),563 &b.get(field)?.expect("field exists"),564 )? {565 return Ok(false);566 }567 }568 Ok(true)569 }570 (a, b) => Ok(primitive_equals(a, b)?),571 }572}1use std::{2 cell::RefCell,3 fmt::{self, Debug, Display},4 mem::replace,5 rc::Rc,6};78use jrsonnet_gcmodule::{Cc, Trace};9use jrsonnet_interner::IStr;10use jrsonnet_types::ValType;1112pub use crate::arr::{ArrValue, ArrayLike};13use crate::{14 error::{Error, ErrorKind::*},15 function::FuncVal,16 gc::{GcHashMap, TraceBox},17 manifest::{ManifestFormat, ToStringFormat},18 tb, throw,19 typed::BoundedUsize,20 ObjValue, Result, Unbound, WeakObjValue,21};2223pub trait ThunkValue: Trace {24 type Output;25 fn get(self: Box<Self>) -> Result<Self::Output>;26}2728#[derive(Trace)]29enum ThunkInner<T: Trace> {30 Computed(T),31 Errored(Error),32 Waiting(TraceBox<dyn ThunkValue<Output = T>>),33 Pending,34}3536/// Lazily evaluated value37#[allow(clippy::module_name_repetitions)]38#[derive(Clone, Trace)]39pub struct Thunk<T: Trace>(Cc<RefCell<ThunkInner<T>>>);4041impl<T: Trace> Thunk<T> {42 pub fn evaluated(val: T) -> Self {43 Self(Cc::new(RefCell::new(ThunkInner::Computed(val))))44 }45 pub fn new(f: impl ThunkValue<Output = T> + 'static) -> Self {46 Self(Cc::new(RefCell::new(ThunkInner::Waiting(tb!(f)))))47 }48 pub fn errored(e: Error) -> Self {49 Self(Cc::new(RefCell::new(ThunkInner::Errored(e))))50 }51 pub fn result(res: Result<T, Error>) -> Self {52 match res {53 Ok(o) => Self::evaluated(o),54 Err(e) => Self::errored(e),55 }56 }57}5859impl<T> Thunk<T>60where61 T: Clone + Trace,62{63 pub fn force(&self) -> Result<()> {64 self.evaluate()?;65 Ok(())66 }6768 /// Evaluate thunk, or return cached value69 ///70 /// # Errors71 ///72 /// - Lazy value evaluation returned error73 /// - This method was called during inner value evaluation74 pub fn evaluate(&self) -> Result<T> {75 match &*self.0.borrow() {76 ThunkInner::Computed(v) => return Ok(v.clone()),77 ThunkInner::Errored(e) => return Err(e.clone()),78 ThunkInner::Pending => return Err(InfiniteRecursionDetected.into()),79 ThunkInner::Waiting(..) => (),80 };81 let ThunkInner::Waiting(value) = replace(&mut *self.0.borrow_mut(), ThunkInner::Pending)82 else {83 unreachable!();84 };85 let new_value = match value.0.get() {86 Ok(v) => v,87 Err(e) => {88 *self.0.borrow_mut() = ThunkInner::Errored(e.clone());89 return Err(e);90 }91 };92 *self.0.borrow_mut() = ThunkInner::Computed(new_value.clone());93 Ok(new_value)94 }95}9697pub trait ThunkMapper<Input>: Trace {98 type Output;99 fn map(self, from: Input) -> Result<Self::Output>;100}101impl<Input> Thunk<Input>102where103 Input: Trace + Clone,104{105 pub fn map<M>(self, mapper: M) -> Thunk<M::Output>106 where107 M: ThunkMapper<Input>,108 M::Output: Trace,109 {110 #[derive(Trace)]111 struct Mapped<Input: Trace, Mapper: Trace> {112 inner: Thunk<Input>,113 mapper: Mapper,114 }115 impl<Input, Mapper> ThunkValue for Mapped<Input, Mapper>116 where117 Input: Trace + Clone,118 Mapper: ThunkMapper<Input>,119 {120 type Output = Mapper::Output;121122 fn get(self: Box<Self>) -> Result<Self::Output> {123 let value = self.inner.evaluate()?;124 let mapped = self.mapper.map(value)?;125 Ok(mapped)126 }127 }128129 Thunk::new(Mapped::<Input, M> {130 inner: self,131 mapper,132 })133 }134}135136impl<T: Trace> From<Result<T>> for Thunk<T> {137 fn from(value: Result<T>) -> Self {138 match value {139 Ok(o) => Self::evaluated(o),140 Err(e) => Self::errored(e),141 }142 }143}144145impl<T: Trace + Default> Default for Thunk<T> {146 fn default() -> Self {147 Self::evaluated(T::default())148 }149}150151type CacheKey = (Option<WeakObjValue>, Option<WeakObjValue>);152153#[derive(Trace, Clone)]154pub struct CachedUnbound<I, T>155where156 I: Unbound<Bound = T>,157 T: Trace,158{159 cache: Cc<RefCell<GcHashMap<CacheKey, T>>>,160 value: I,161}162impl<I: Unbound<Bound = T>, T: Trace> CachedUnbound<I, T> {163 pub fn new(value: I) -> Self {164 Self {165 cache: Cc::new(RefCell::new(GcHashMap::new())),166 value,167 }168 }169}170impl<I: Unbound<Bound = T>, T: Clone + Trace> Unbound for CachedUnbound<I, T> {171 type Bound = T;172 fn bind(&self, sup: Option<ObjValue>, this: Option<ObjValue>) -> Result<T> {173 let cache_key = (174 sup.as_ref().map(|s| s.clone().downgrade()),175 this.as_ref().map(|t| t.clone().downgrade()),176 );177 {178 if let Some(t) = self.cache.borrow().get(&cache_key) {179 return Ok(t.clone());180 }181 }182 let bound = self.value.bind(sup, this)?;183184 {185 let mut cache = self.cache.borrow_mut();186 cache.insert(cache_key, bound.clone());187 }188189 Ok(bound)190 }191}192193impl<T: Debug + Trace> Debug for Thunk<T> {194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {195 write!(f, "Lazy")196 }197}198impl<T: Trace> PartialEq for Thunk<T> {199 fn eq(&self, other: &Self) -> bool {200 Cc::ptr_eq(&self.0, &other.0)201 }202}203204/// Represents a Jsonnet value, which can be sliced or indexed (string or array).205#[allow(clippy::module_name_repetitions)]206pub enum IndexableVal {207 /// String.208 Str(IStr),209 /// Array.210 Arr(ArrValue),211}212impl IndexableVal {213 pub fn to_array(self) -> ArrValue {214 match self {215 IndexableVal::Str(s) => ArrValue::chars(s.chars()),216 IndexableVal::Arr(arr) => arr,217 }218 }219 /// Slice the value.220 ///221 /// # Implementation222 ///223 /// For strings, will create a copy of specified interval.224 ///225 /// For arrays, nothing will be copied on this call, instead [`ArrValue::Slice`] view will be returned.226 pub fn slice(227 self,228 index: Option<BoundedUsize<0, { i32::MAX as usize }>>,229 end: Option<BoundedUsize<0, { i32::MAX as usize }>>,230 step: Option<BoundedUsize<1, { i32::MAX as usize }>>,231 ) -> Result<Self> {232 match &self {233 IndexableVal::Str(s) => {234 let index = index.as_deref().copied().unwrap_or(0);235 let end = end.as_deref().copied().unwrap_or(usize::MAX);236 let step = step.as_deref().copied().unwrap_or(1);237238 if index >= end {239 return Ok(Self::Str("".into()));240 }241242 Ok(Self::Str(243 (s.chars()244 .skip(index)245 .take(end - index)246 .step_by(step)247 .collect::<String>())248 .into(),249 ))250 }251 IndexableVal::Arr(arr) => {252 let index = index.as_deref().copied().unwrap_or(0);253 let end = end.as_deref().copied().unwrap_or(usize::MAX).min(arr.len());254 let step = step.as_deref().copied().unwrap_or(1);255256 if index >= end {257 return Ok(Self::Arr(ArrValue::empty()));258 }259260 Ok(Self::Arr(261 arr.clone()262 .slice(Some(index), Some(end), Some(step))263 .expect("arguments checked"),264 ))265 }266 }267 }268}269270#[derive(Debug, Clone, Trace)]271pub enum StrValue {272 Flat(IStr),273 Tree(Rc<(StrValue, StrValue, usize)>),274}275impl StrValue {276 pub fn concat(a: StrValue, b: StrValue) -> Self {277 // TODO: benchmark for an optimal value, currently just a arbitrary choice278 const STRING_EXTEND_THRESHOLD: usize = 100;279280 if a.is_empty() {281 b282 } else if b.is_empty() {283 a284 } else if a.len() + b.len() < STRING_EXTEND_THRESHOLD {285 Self::Flat(format!("{a}{b}").into())286 } else {287 let len = a.len() + b.len();288 Self::Tree(Rc::new((a, b, len)))289 }290 }291 pub fn into_flat(self) -> IStr {292 #[cold]293 fn write_buf(s: &StrValue, out: &mut String) {294 match s {295 StrValue::Flat(f) => out.push_str(f),296 StrValue::Tree(t) => {297 write_buf(&t.0, out);298 write_buf(&t.1, out);299 }300 }301 }302 match self {303 StrValue::Flat(f) => f,304 StrValue::Tree(_) => {305 let mut buf = String::with_capacity(self.len());306 write_buf(&self, &mut buf);307 buf.into()308 }309 }310 }311 pub fn len(&self) -> usize {312 match self {313 StrValue::Flat(v) => v.len(),314 StrValue::Tree(t) => t.2,315 }316 }317 pub fn is_empty(&self) -> bool {318 match self {319 Self::Flat(v) => v.is_empty(),320 // Can't create non-flat empty string321 Self::Tree(_) => false,322 }323 }324}325impl From<&str> for StrValue {326 fn from(value: &str) -> Self {327 Self::Flat(value.into())328 }329}330impl From<String> for StrValue {331 fn from(value: String) -> Self {332 Self::Flat(value.into())333 }334}335impl From<IStr> for StrValue {336 fn from(value: IStr) -> Self {337 Self::Flat(value)338 }339}340impl Display for StrValue {341 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {342 match self {343 StrValue::Flat(v) => write!(f, "{v}"),344 StrValue::Tree(t) => {345 write!(f, "{}", t.0)?;346 write!(f, "{}", t.1)347 }348 }349 }350}351impl PartialEq for StrValue {352 fn eq(&self, other: &Self) -> bool {353 let a = self.clone().into_flat();354 let b = other.clone().into_flat();355 a == b356 }357}358impl Eq for StrValue {}359impl PartialOrd for StrValue {360 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {361 Some(self.cmp(other))362 }363}364impl Ord for StrValue {365 fn cmp(&self, other: &Self) -> std::cmp::Ordering {366 let a = self.clone().into_flat();367 let b = other.clone().into_flat();368 a.cmp(&b)369 }370}371372/// Represents any valid Jsonnet value.373#[derive(Debug, Clone, Trace, Default)]374pub enum Val {375 /// Represents a Jsonnet boolean.376 Bool(bool),377 /// Represents a Jsonnet null value.378 #[default]379 Null,380 /// Represents a Jsonnet string.381 Str(StrValue),382 /// Represents a Jsonnet number.383 /// Should be finite, and not NaN384 /// This restriction isn't enforced by enum, as enum field can't be marked as private385 Num(f64),386 /// Experimental bigint387 #[cfg(feature = "exp-bigint")]388 BigInt(#[trace(skip)] Box<num_bigint::BigInt>),389 /// Represents a Jsonnet array.390 Arr(ArrValue),391 /// Represents a Jsonnet object.392 Obj(ObjValue),393 /// Represents a Jsonnet function.394 Func(FuncVal),395}396397#[cfg(target_pointer_width = "64")]398static_assertions::assert_eq_size!(Val, [u8; 24]);399400impl From<IndexableVal> for Val {401 fn from(v: IndexableVal) -> Self {402 match v {403 IndexableVal::Str(s) => Self::Str(StrValue::Flat(s)),404 IndexableVal::Arr(a) => Self::Arr(a),405 }406 }407}408409impl Val {410 pub const fn as_bool(&self) -> Option<bool> {411 match self {412 Self::Bool(v) => Some(*v),413 _ => None,414 }415 }416 pub const fn as_null(&self) -> Option<()> {417 match self {418 Self::Null => Some(()),419 _ => None,420 }421 }422 pub fn as_str(&self) -> Option<IStr> {423 match self {424 Self::Str(s) => Some(s.clone().into_flat()),425 _ => None,426 }427 }428 pub const fn as_num(&self) -> Option<f64> {429 match self {430 Self::Num(n) => Some(*n),431 _ => None,432 }433 }434 pub fn as_arr(&self) -> Option<ArrValue> {435 match self {436 Self::Arr(a) => Some(a.clone()),437 _ => None,438 }439 }440 pub fn as_obj(&self) -> Option<ObjValue> {441 match self {442 Self::Obj(o) => Some(o.clone()),443 _ => None,444 }445 }446 pub fn as_func(&self) -> Option<FuncVal> {447 match self {448 Self::Func(f) => Some(f.clone()),449 _ => None,450 }451 }452453 /// Creates `Val::Num` after checking for numeric overflow.454 /// As numbers are `f64`, we can just check for their finity.455 pub fn new_checked_num(num: f64) -> Result<Self> {456 if num.is_finite() {457 Ok(Self::Num(num))458 } else {459 throw!("overflow")460 }461 }462463 pub const fn value_type(&self) -> ValType {464 match self {465 Self::Str(..) => ValType::Str,466 Self::Num(..) => ValType::Num,467 #[cfg(feature = "exp-bigint")]468 Self::BigInt(..) => ValType::BigInt,469 Self::Arr(..) => ValType::Arr,470 Self::Obj(..) => ValType::Obj,471 Self::Bool(_) => ValType::Bool,472 Self::Null => ValType::Null,473 Self::Func(..) => ValType::Func,474 }475 }476477 pub fn manifest(&self, format: impl ManifestFormat) -> Result<String> {478 fn manifest_dyn(val: &Val, manifest: &dyn ManifestFormat) -> Result<String> {479 manifest.manifest(val.clone())480 }481 manifest_dyn(self, &format)482 }483484 pub fn to_string(&self) -> Result<IStr> {485 Ok(match self {486 Self::Bool(true) => "true".into(),487 Self::Bool(false) => "false".into(),488 Self::Null => "null".into(),489 Self::Str(s) => s.clone().into_flat(),490 _ => self.manifest(ToStringFormat).map(IStr::from)?,491 })492 }493494 pub fn into_indexable(self) -> Result<IndexableVal> {495 Ok(match self {496 Val::Str(s) => IndexableVal::Str(s.into_flat()),497 Val::Arr(arr) => IndexableVal::Arr(arr),498 _ => throw!(ValueIsNotIndexable(self.value_type())),499 })500 }501}502503const fn is_function_like(val: &Val) -> bool {504 matches!(val, Val::Func(_))505}506507/// Native implementation of `std.primitiveEquals`508pub fn primitive_equals(val_a: &Val, val_b: &Val) -> Result<bool> {509 Ok(match (val_a, val_b) {510 (Val::Bool(a), Val::Bool(b)) => a == b,511 (Val::Null, Val::Null) => true,512 (Val::Str(a), Val::Str(b)) => a == b,513 (Val::Num(a), Val::Num(b)) => (a - b).abs() <= f64::EPSILON,514 #[cfg(feature = "exp-bigint")]515 (Val::BigInt(a), Val::BigInt(b)) => a == b,516 (Val::Arr(_), Val::Arr(_)) => {517 throw!("primitiveEquals operates on primitive types, got array")518 }519 (Val::Obj(_), Val::Obj(_)) => {520 throw!("primitiveEquals operates on primitive types, got object")521 }522 (a, b) if is_function_like(a) && is_function_like(b) => {523 throw!("cannot test equality of functions")524 }525 (_, _) => false,526 })527}528529/// Native implementation of `std.equals`530pub fn equals(val_a: &Val, val_b: &Val) -> Result<bool> {531 if val_a.value_type() != val_b.value_type() {532 return Ok(false);533 }534 match (val_a, val_b) {535 (Val::Arr(a), Val::Arr(b)) => {536 if ArrValue::ptr_eq(a, b) {537 return Ok(true);538 }539 if a.len() != b.len() {540 return Ok(false);541 }542 for (a, b) in a.iter().zip(b.iter()) {543 if !equals(&a?, &b?)? {544 return Ok(false);545 }546 }547 Ok(true)548 }549 (Val::Obj(a), Val::Obj(b)) => {550 if ObjValue::ptr_eq(a, b) {551 return Ok(true);552 }553 let fields = a.fields(554 #[cfg(feature = "exp-preserve-order")]555 false,556 );557 if fields558 != b.fields(559 #[cfg(feature = "exp-preserve-order")]560 false,561 ) {562 return Ok(false);563 }564 for field in fields {565 if !equals(566 &a.get(field.clone())?.expect("field exists"),567 &b.get(field)?.expect("field exists"),568 )? {569 return Ok(false);570 }571 }572 Ok(true)573 }574 (a, b) => Ok(primitive_equals(a, b)?),575 }576}crates/jrsonnet-stdlib/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/lib.rs
+++ b/crates/jrsonnet-stdlib/src/lib.rs
@@ -138,6 +138,10 @@
("base64DecodeBytes", builtin_base64_decode_bytes::INST),
// Objects
("objectFieldsEx", builtin_object_fields_ex::INST),
+ ("objectValues", builtin_object_values::INST),
+ ("objectValuesAll", builtin_object_values_all::INST),
+ ("objectKeysValues", builtin_object_keys_values::INST),
+ ("objectKeysValuesAll", builtin_object_keys_values_all::INST),
("objectHasEx", builtin_object_has_ex::INST),
("objectRemoveKey", builtin_object_remove_key::INST),
// Manifest
crates/jrsonnet-stdlib/src/objects.rsdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/objects.rs
+++ b/crates/jrsonnet-stdlib/src/objects.rs
@@ -1,6 +1,6 @@
use jrsonnet_evaluator::{
function::builtin,
- val::{StrValue, Val},
+ val::{ArrValue, StrValue, Val},
IStr, ObjValue, ObjValueBuilder,
};
@@ -23,6 +23,82 @@
.collect::<Vec<_>>()
}
+pub fn builtin_object_values_ex(
+ o: ObjValue,
+ include_hidden: bool,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: Option<bool>,
+) -> ArrValue {
+ #[cfg(feature = "exp-preserve-order")]
+ let preserve_order = preserve_order.unwrap_or(false);
+ o.values_ex(
+ include_hidden,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ )
+}
+#[builtin]
+pub fn builtin_object_values(
+ o: ObjValue,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: Option<bool>,
+) -> ArrValue {
+ builtin_object_values_ex(
+ o,
+ false,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ )
+}
+#[builtin]
+pub fn builtin_object_values_all(
+ o: ObjValue,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: Option<bool>,
+) -> ArrValue {
+ builtin_object_values_ex(
+ o,
+ true,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ )
+}
+
+pub fn builtin_object_keys_values_ex(
+ o: ObjValue,
+ include_hidden: bool,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: Option<bool>,
+) -> ArrValue {
+ #[cfg(feature = "exp-preserve-order")]
+ let preserve_order = preserve_order.unwrap_or(false);
+ o.key_values_ex(
+ include_hidden,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ )
+}
+#[builtin]
+pub fn builtin_object_keys_values(
+ o: ObjValue,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: Option<bool>,
+) -> ArrValue {
+ builtin_object_keys_values_ex(
+ o,
+ false,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ )
+}
+#[builtin]
+pub fn builtin_object_keys_values_all(
+ o: ObjValue,
+ #[cfg(feature = "exp-preserve-order")] preserve_order: Option<bool>,
+) -> ArrValue {
+ builtin_object_keys_values_ex(
+ o,
+ true,
+ #[cfg(feature = "exp-preserve-order")]
+ preserve_order,
+ )
+}
+
#[builtin]
pub fn builtin_object_has_ex(obj: ObjValue, fname: IStr, hidden: bool) -> bool {
obj.has_field_ex(fname, hidden)
crates/jrsonnet-stdlib/src/std.jsonnetdiffbeforeafterboth--- a/crates/jrsonnet-stdlib/src/std.jsonnet
+++ b/crates/jrsonnet-stdlib/src/std.jsonnet
@@ -268,18 +268,6 @@
objectHasAll(o, f)::
std.objectHasEx(o, f, true),
- objectValues(o)::
- [o[k] for k in std.objectFields(o)],
-
- objectValuesAll(o)::
- [o[k] for k in std.objectFieldsAll(o)],
-
- objectKeysValues(o)::
- [{ key: k, value: o[k] } for k in std.objectFields(o)],
-
- objectKeysValuesAll(o)::
- [{ key: k, value: o[k] } for k in std.objectFieldsAll(o)],
-
resolvePath(f, r)::
local arr = std.split(f, '/');
std.join('/', std.makeArray(std.length(arr) - 1, function(i) arr[i]) + [r]),