difftreelog
feat unify Arg and Typed handling for Thunk
in: master
8 files changed
crates/jrsonnet-evaluator/src/ctx.rsdiffbeforeafterboth1use std::fmt::Debug;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;56use crate::{7 error::ErrorKind::*, gc::GcHashMap, map::LayeredHashMap, ObjValue, Pending, Result, State,8 Thunk, Val,9};1011#[derive(Trace)]12struct ContextInternals {13 state: Option<State>,14 dollar: Option<ObjValue>,15 sup: Option<ObjValue>,16 this: Option<ObjValue>,17 bindings: LayeredHashMap,18}19impl Debug for ContextInternals {20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {21 f.debug_struct("Context").finish()22 }23}2425/// Context keeps information about current lexical code location26///27/// This information includes local variables, top-level object (`$`), current object (`this`), and super object (`super`)28#[derive(Debug, Clone, Trace)]29pub struct Context(Cc<ContextInternals>);30impl Context {31 pub fn new_future() -> Pending<Self> {32 Pending::new()33 }3435 pub fn state(&self) -> &State {36 self.037 .state38 .as_ref()39 .expect("used state from dummy context")40 }4142 pub fn dollar(&self) -> Option<&ObjValue> {43 self.0.dollar.as_ref()44 }4546 pub fn this(&self) -> Option<&ObjValue> {47 self.0.this.as_ref()48 }4950 pub fn super_obj(&self) -> Option<&ObjValue> {51 self.0.sup.as_ref()52 }5354 pub fn binding(&self, name: IStr) -> Result<Thunk<Val>> {55 use std::cmp::Ordering;5657 use crate::throw;5859 if let Some(val) = self.0.bindings.get(&name).cloned() {60 return Ok(val);61 }6263 let mut heap = Vec::new();64 self.0.bindings.clone().iter_keys(|k| {65 let conf = strsim::jaro_winkler(&k as &str, &name as &str);66 if conf < 0.8 {67 return;68 }69 heap.push((conf, k));70 });71 heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));7273 throw!(VariableIsNotDefined(74 name,75 heap.into_iter().map(|(_, k)| k).collect()76 ))77 }78 pub fn contains_binding(&self, name: IStr) -> bool {79 self.0.bindings.contains_key(&name)80 }81 #[must_use]82 pub fn into_future(self, ctx: Pending<Self>) -> Self {83 {84 ctx.0.borrow_mut().replace(self);85 }86 ctx.unwrap()87 }8889 #[must_use]90 pub fn with_var(self, name: IStr, value: Val) -> Self {91 let mut new_bindings = GcHashMap::with_capacity(1);92 new_bindings.insert(name, Thunk::evaluated(value));93 self.extend(new_bindings, None, None, None)94 }9596 #[must_use]97 pub fn extend(98 self,99 new_bindings: GcHashMap<IStr, Thunk<Val>>,100 new_dollar: Option<ObjValue>,101 new_sup: Option<ObjValue>,102 new_this: Option<ObjValue>,103 ) -> Self {104 let ctx = &self.0;105 let dollar = new_dollar.or_else(|| ctx.dollar.clone());106 let this = new_this.or_else(|| ctx.this.clone());107 let sup = new_sup.or_else(|| ctx.sup.clone());108 let bindings = if new_bindings.is_empty() {109 ctx.bindings.clone()110 } else {111 ctx.bindings.clone().extend(new_bindings)112 };113 Self(Cc::new(ContextInternals {114 state: ctx.state.clone(),115 dollar,116 sup,117 this,118 bindings,119 }))120 }121}122123impl PartialEq for Context {124 fn eq(&self, other: &Self) -> bool {125 Cc::ptr_eq(&self.0, &other.0)126 }127}128129pub struct ContextBuilder {130 state: Option<State>,131 bindings: GcHashMap<IStr, Thunk<Val>>,132 extend: Option<Context>,133}134135impl ContextBuilder {136 /// # Panics137 /// Panics aren't directly caused by this function, but if state from resulting context is used138 pub fn dangerous_empty_state() -> Self {139 Self {140 state: None,141 bindings: GcHashMap::new(),142 extend: None,143 }144 }145 pub fn new(state: State) -> Self {146 Self::with_capacity(state, 0)147 }148 pub fn with_capacity(state: State, capacity: usize) -> Self {149 Self {150 state: Some(state),151 bindings: GcHashMap::with_capacity(capacity),152 extend: None,153 }154 }155 pub fn extend(parent: Context) -> Self {156 Self {157 state: parent.0.state.clone(),158 bindings: GcHashMap::new(),159 extend: Some(parent),160 }161 }162 /// # Panics163 /// If `name` is already bound164 pub fn bind(&mut self, name: IStr, value: Thunk<Val>) -> &mut Self {165 let old = self.bindings.insert(name, value);166 assert!(old.is_none(), "variable bound twice in single context call");167 self168 }169 pub fn build(self) -> Context {170 if let Some(parent) = self.extend {171 // TODO: replace self.extend with Result<Context, State>, and remove `state` field172 parent.extend(self.bindings, None, None, None)173 } else {174 Context(Cc::new(ContextInternals {175 state: self.state,176 bindings: LayeredHashMap::new(self.bindings),177 dollar: None,178 sup: None,179 this: None,180 }))181 }182 }183}1use std::fmt::Debug;23use jrsonnet_gcmodule::{Cc, Trace};4use jrsonnet_interner::IStr;56use crate::{7 error::ErrorKind::*, gc::GcHashMap, map::LayeredHashMap, ObjValue, Pending, Result, State,8 Thunk, Val,9};1011#[derive(Trace)]12struct ContextInternals {13 state: Option<State>,14 dollar: Option<ObjValue>,15 sup: Option<ObjValue>,16 this: Option<ObjValue>,17 bindings: LayeredHashMap,18}19impl Debug for ContextInternals {20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {21 f.debug_struct("Context").finish()22 }23}2425/// Context keeps information about current lexical code location26///27/// This information includes local variables, top-level object (`$`), current object (`this`), and super object (`super`)28#[derive(Debug, Clone, Trace)]29pub struct Context(Cc<ContextInternals>);30impl Context {31 pub fn new_future() -> Pending<Self> {32 Pending::new()33 }3435 pub fn state(&self) -> &State {36 self.037 .state38 .as_ref()39 .expect("used state from dummy context")40 }4142 pub fn dollar(&self) -> Option<&ObjValue> {43 self.0.dollar.as_ref()44 }4546 pub fn this(&self) -> Option<&ObjValue> {47 self.0.this.as_ref()48 }4950 pub fn super_obj(&self) -> Option<&ObjValue> {51 self.0.sup.as_ref()52 }5354 pub fn binding(&self, name: IStr) -> Result<Thunk<Val>> {55 use std::cmp::Ordering;5657 use crate::throw;5859 if let Some(val) = self.0.bindings.get(&name).cloned() {60 return Ok(val);61 }6263 let mut heap = Vec::new();64 self.0.bindings.clone().iter_keys(|k| {65 let conf = strsim::jaro_winkler(&k as &str, &name as &str);66 if conf < 0.8 {67 return;68 }69 heap.push((conf, k));70 });71 heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));7273 throw!(VariableIsNotDefined(74 name,75 heap.into_iter().map(|(_, k)| k).collect()76 ))77 }78 pub fn contains_binding(&self, name: IStr) -> bool {79 self.0.bindings.contains_key(&name)80 }81 #[must_use]82 pub fn into_future(self, ctx: Pending<Self>) -> Self {83 {84 ctx.clone().fill(self);85 }86 ctx.unwrap()87 }8889 #[must_use]90 pub fn with_var(self, name: IStr, value: Val) -> Self {91 let mut new_bindings = GcHashMap::with_capacity(1);92 new_bindings.insert(name, Thunk::evaluated(value));93 self.extend(new_bindings, None, None, None)94 }9596 #[must_use]97 pub fn extend(98 self,99 new_bindings: GcHashMap<IStr, Thunk<Val>>,100 new_dollar: Option<ObjValue>,101 new_sup: Option<ObjValue>,102 new_this: Option<ObjValue>,103 ) -> Self {104 let ctx = &self.0;105 let dollar = new_dollar.or_else(|| ctx.dollar.clone());106 let this = new_this.or_else(|| ctx.this.clone());107 let sup = new_sup.or_else(|| ctx.sup.clone());108 let bindings = if new_bindings.is_empty() {109 ctx.bindings.clone()110 } else {111 ctx.bindings.clone().extend(new_bindings)112 };113 Self(Cc::new(ContextInternals {114 state: ctx.state.clone(),115 dollar,116 sup,117 this,118 bindings,119 }))120 }121}122123impl PartialEq for Context {124 fn eq(&self, other: &Self) -> bool {125 Cc::ptr_eq(&self.0, &other.0)126 }127}128129pub struct ContextBuilder {130 state: Option<State>,131 bindings: GcHashMap<IStr, Thunk<Val>>,132 extend: Option<Context>,133}134135impl ContextBuilder {136 /// # Panics137 /// Panics aren't directly caused by this function, but if state from resulting context is used138 pub fn dangerous_empty_state() -> Self {139 Self {140 state: None,141 bindings: GcHashMap::new(),142 extend: None,143 }144 }145 pub fn new(state: State) -> Self {146 Self::with_capacity(state, 0)147 }148 pub fn with_capacity(state: State, capacity: usize) -> Self {149 Self {150 state: Some(state),151 bindings: GcHashMap::with_capacity(capacity),152 extend: None,153 }154 }155 pub fn extend(parent: Context) -> Self {156 Self {157 state: parent.0.state.clone(),158 bindings: GcHashMap::new(),159 extend: Some(parent),160 }161 }162 /// # Panics163 /// If `name` is already bound164 pub fn bind(&mut self, name: IStr, value: Thunk<Val>) -> &mut Self {165 let old = self.bindings.insert(name, value);166 assert!(old.is_none(), "variable bound twice in single context call");167 self168 }169 pub fn build(self) -> Context {170 if let Some(parent) = self.extend {171 // TODO: replace self.extend with Result<Context, State>, and remove `state` field172 parent.extend(self.bindings, None, None, None)173 } else {174 Context(Cc::new(ContextInternals {175 state: self.state,176 bindings: LayeredHashMap::new(self.bindings),177 dollar: None,178 sup: None,179 this: None,180 }))181 }182 }183}crates/jrsonnet-evaluator/src/dynamic.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/dynamic.rs
+++ b/crates/jrsonnet-evaluator/src/dynamic.rs
@@ -1,29 +1,49 @@
-use std::cell::RefCell;
+use std::cell::OnceCell;
use jrsonnet_gcmodule::{Cc, Trace};
+use crate::{error::ErrorKind::InfiniteRecursionDetected, throw, val::ThunkValue, Result, Thunk};
+
// TODO: Replace with OnceCell once in std
#[derive(Clone, Trace)]
-pub struct Pending<V: Trace + 'static>(pub Cc<RefCell<Option<V>>>);
+pub struct Pending<V: Trace + 'static>(pub Cc<OnceCell<V>>);
impl<T: Trace + 'static> Pending<T> {
pub fn new() -> Self {
- Self(Cc::new(RefCell::new(None)))
+ Self(Cc::new(OnceCell::new()))
}
pub fn new_filled(v: T) -> Self {
- Self(Cc::new(RefCell::new(Some(v))))
+ let cell = OnceCell::new();
+ let _ = cell.set(v);
+ Self(Cc::new(cell))
}
/// # Panics
/// If wrapper is filled already
pub fn fill(self, value: T) {
- assert!(self.0.borrow().is_none(), "wrapper is filled already");
- self.0.borrow_mut().replace(value);
+ self.0
+ .set(value)
+ .map_err(|_| ())
+ .expect("wrapper is filled already")
}
}
impl<T: Clone + Trace + 'static> Pending<T> {
/// # Panics
/// If wrapper is not yet filled
pub fn unwrap(&self) -> T {
- self.0.borrow().as_ref().cloned().unwrap()
+ self.0.get().cloned().expect("pending was not filled")
+ }
+ pub fn try_get(&self) -> Option<T> {
+ self.0.get().cloned()
+ }
+}
+
+impl<T: Trace + Clone> ThunkValue for Pending<T> {
+ type Output = T;
+
+ fn get(self: Box<Self>) -> Result<Self::Output> {
+ let Some(value) = self.0.get() else {
+ throw!(InfiniteRecursionDetected);
+ };
+ Ok(value.clone())
}
}
@@ -32,3 +52,9 @@
Self::new()
}
}
+
+impl<T: Trace + Clone> Into<Thunk<T>> for Pending<T> {
+ fn into(self) -> Thunk<T> {
+ Thunk::new(self)
+ }
+}
crates/jrsonnet-evaluator/src/function/arglike.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/function/arglike.rs
+++ b/crates/jrsonnet-evaluator/src/function/arglike.rs
@@ -48,28 +48,22 @@
where
T: Typed + Clone,
{
- fn evaluate_arg(&self, _ctx: Context, _tailstrict: bool) -> Result<Thunk<Val>> {
+ fn evaluate_arg(&self, _ctx: Context, tailstrict: bool) -> Result<Thunk<Val>> {
+ if T::provides_lazy() && !tailstrict {
+ return Ok(T::into_lazy_untyped(self.clone()));
+ }
let val = T::into_untyped(self.clone())?;
Ok(Thunk::evaluated(val))
}
}
impl<T> OptionalContext for T where T: Typed + Clone {}
-impl ArgLike for Thunk<Val> {
- fn evaluate_arg(&self, _ctx: Context, tailstrict: bool) -> Result<Thunk<Val>> {
- if tailstrict {
- self.force()?;
- }
- Ok(self.clone())
- }
-}
-impl OptionalContext for Thunk<Val> {}
-
#[derive(Clone, Trace)]
pub enum TlaArg {
String(IStr),
Code(LocExpr),
Val(Val),
+ Lazy(Thunk<Val>),
}
impl ArgLike for TlaArg {
fn evaluate_arg(&self, ctx: Context, tailstrict: bool) -> Result<Thunk<Val>> {
@@ -84,6 +78,7 @@
})
}),
TlaArg::Val(val) => Ok(Thunk::evaluated(val.clone())),
+ TlaArg::Lazy(lazy) => Ok(lazy.clone()),
}
}
}
crates/jrsonnet-evaluator/src/obj.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/obj.rs
+++ b/crates/jrsonnet-evaluator/src/obj.rs
@@ -15,7 +15,9 @@
function::CallLocation,
gc::{GcHashMap, GcHashSet, TraceBox},
operator::evaluate_add_op,
- tb, throw, MaybeUnbound, Result, State, Thunk, Unbound, Val,
+ tb, throw,
+ val::ThunkValue,
+ MaybeUnbound, Result, State, Thunk, Unbound, Val,
};
#[cfg(not(feature = "exp-preserve-order"))]
crates/jrsonnet-evaluator/src/typed/conversions.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/typed/conversions.rs
+++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs
@@ -1,6 +1,6 @@
-use std::ops::Deref;
+use std::{collections::BTreeMap, marker::PhantomData, ops::Deref};
-use jrsonnet_gcmodule::Cc;
+use jrsonnet_gcmodule::{Cc, Trace};
use jrsonnet_interner::{IBytes, IStr};
pub use jrsonnet_macros::Typed;
use jrsonnet_types::{ComplexValType, ValType};
@@ -11,10 +11,28 @@
function::{native::NativeDesc, FuncDesc, FuncVal},
throw,
typed::CheckType,
- val::{IndexableVal, StrValue},
- ObjValue, ObjValueBuilder, Val,
+ val::{IndexableVal, StrValue, ThunkMapper},
+ ObjValue, ObjValueBuilder, Thunk, Val,
};
+#[derive(Trace)]
+struct FromUntyped<K: Trace>(PhantomData<fn() -> K>);
+impl<K> ThunkMapper<Val> for FromUntyped<K>
+where
+ K: Typed + Trace,
+{
+ type Output = K;
+
+ fn map(self, from: Val) -> Result<Self::Output> {
+ K::from_untyped(from)
+ }
+}
+impl<K: Trace> Default for FromUntyped<K> {
+ fn default() -> Self {
+ Self(PhantomData)
+ }
+}
+
pub trait TypedObj: Typed {
fn serialize(self, out: &mut ObjValueBuilder) -> Result<()>;
fn parse(obj: &ObjValue) -> Result<Self>;
@@ -28,8 +46,24 @@
pub trait Typed: Sized {
const TYPE: &'static ComplexValType;
fn into_untyped(typed: Self) -> Result<Val>;
+ fn into_lazy_untyped(typed: Self) -> Thunk<Val> {
+ Thunk::from(Self::into_untyped(typed))
+ }
fn from_untyped(untyped: Val) -> Result<Self>;
+ fn from_lazy_untyped(lazy: Thunk<Val>) -> Result<Self> {
+ Self::from_untyped(lazy.evaluate()?)
+ }
+
+ // Whatever caller should use `into_lazy_untyped` instead of `into_untyped`
+ fn provides_lazy() -> bool {
+ false
+ }
+ // Whatever caller should use `from_lazy_untyped` instead of `from_untyped` when possible
+ fn wants_lazy() -> bool {
+ false
+ }
+
/// Hack to make builtins be able to return non-result values, and make macros able to convert those values to result
/// This method returns identity in impl Typed for Result, and should not be overriden
#[doc(hidden)]
@@ -39,6 +73,54 @@
}
}
+impl<T> Typed for Thunk<T>
+where
+ T: Typed + Trace + Clone,
+{
+ const TYPE: &'static ComplexValType = &ComplexValType::Lazy(T::TYPE);
+
+ fn into_untyped(typed: Self) -> Result<Val> {
+ T::into_untyped(typed.evaluate()?)
+ }
+
+ fn from_untyped(untyped: Val) -> Result<Self> {
+ Self::from_lazy_untyped(Thunk::evaluated(untyped))
+ }
+
+ fn provides_lazy() -> bool {
+ true
+ }
+
+ fn into_lazy_untyped(inner: Self) -> Thunk<Val> {
+ #[derive(Trace)]
+ struct IntoUntyped<K: Trace>(PhantomData<fn() -> K>);
+ impl<K> ThunkMapper<K> for IntoUntyped<K>
+ where
+ K: Typed + Trace,
+ {
+ type Output = Val;
+
+ fn map(self, from: K) -> Result<Self::Output> {
+ K::into_untyped(from)
+ }
+ }
+ impl<K: Trace> Default for IntoUntyped<K> {
+ fn default() -> Self {
+ Self(PhantomData)
+ }
+ }
+ inner.map(<IntoUntyped<T>>::default())
+ }
+
+ fn wants_lazy() -> bool {
+ true
+ }
+
+ fn from_lazy_untyped(inner: Thunk<Val>) -> Result<Self> {
+ Ok(inner.map(<FromUntyped<T>>::default()))
+ }
+}
+
const MAX_SAFE_INTEGER: f64 = ((1u64 << (f64::MANTISSA_DIGITS + 1)) - 1) as f64;
macro_rules! impl_int {
crates/jrsonnet-evaluator/src/typed/mod.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/typed/mod.rs
+++ b/crates/jrsonnet-evaluator/src/typed/mod.rs
@@ -252,6 +252,7 @@
}
Ok(())
}
+ Self::Lazy(_lazy) => Ok(()),
}
}
}
crates/jrsonnet-evaluator/src/val.rsdiffbeforeafterboth--- a/crates/jrsonnet-evaluator/src/val.rs
+++ b/crates/jrsonnet-evaluator/src/val.rs
@@ -88,6 +88,54 @@
}
}
+pub trait ThunkMapper<Input>: Trace {
+ type Output;
+ fn map(self, from: Input) -> Result<Self::Output>;
+}
+impl<Input> Thunk<Input>
+where
+ Input: Trace + Clone,
+{
+ pub fn map<M>(self, mapper: M) -> Thunk<M::Output>
+ where
+ M: ThunkMapper<Input>,
+ M::Output: Trace,
+ {
+ #[derive(Trace)]
+ struct Mapped<Input: Trace, Mapper: Trace> {
+ inner: Thunk<Input>,
+ mapper: Mapper,
+ }
+ impl<Input, Mapper> ThunkValue for Mapped<Input, Mapper>
+ where
+ Input: Trace + Clone,
+ Mapper: ThunkMapper<Input>,
+ {
+ type Output = Mapper::Output;
+
+ fn get(self: Box<Self>) -> Result<Self::Output> {
+ let value = self.inner.evaluate()?;
+ let mapped = self.mapper.map(value)?;
+ Ok(mapped)
+ }
+ }
+
+ Thunk::new(Mapped::<Input, M> {
+ inner: self,
+ mapper,
+ })
+ }
+}
+
+impl<T: Trace> From<Result<T>> for Thunk<T> {
+ fn from(value: Result<T>) -> Self {
+ match value {
+ Ok(o) => Self::evaluated(o),
+ Err(e) => Self::errored(e),
+ }
+ }
+}
+
type CacheKey = (Option<WeakObjValue>, Option<WeakObjValue>);
#[derive(Trace, Clone)]
@@ -272,6 +320,11 @@
Self::Flat(value.into())
}
}
+impl From<IStr> for StrValue {
+ fn from(value: IStr) -> Self {
+ Self::Flat(value)
+ }
+}
impl Display for StrValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
crates/jrsonnet-types/src/lib.rsdiffbeforeafterboth--- a/crates/jrsonnet-types/src/lib.rs
+++ b/crates/jrsonnet-types/src/lib.rs
@@ -128,10 +128,12 @@
Array(Box<ComplexValType>),
ArrayRef(&'static ComplexValType),
ObjectRef(&'static [(&'static str, &'static ComplexValType)]),
+ AttrsOf(&'static ComplexValType),
Union(Vec<ComplexValType>),
UnionRef(&'static [&'static ComplexValType]),
Sum(Vec<ComplexValType>),
SumRef(&'static [&'static ComplexValType]),
+ Lazy(&'static ComplexValType),
}
impl From<ValType> for ComplexValType {
@@ -195,10 +197,18 @@
}
write!(f, "}}")?;
}
+ ComplexValType::AttrsOf(a) => {
+ if matches!(a, ComplexValType::Any) {
+ write!(f, "object")?;
+ } else {
+ write!(f, "AttrsOf<{a}>")?;
+ }
+ }
ComplexValType::Union(v) => write_union(f, true, v.iter())?,
ComplexValType::UnionRef(v) => write_union(f, true, v.iter().copied())?,
ComplexValType::Sum(v) => write_union(f, false, v.iter())?,
ComplexValType::SumRef(v) => write_union(f, false, v.iter().copied())?,
+ ComplexValType::Lazy(lazy) => write!(f, "Lazy<{lazy}>")?,
};
Ok(())
}