git.delta.rocks / jrsonnet / refs/commits / f66194917e5d

difftreelog

feat unify Arg and Typed handling for Thunk

Yaroslav Bolyukin2023-07-27parent: #a85a211.patch.diff
in: master

8 files changed

modifiedcrates/jrsonnet-evaluator/src/ctx.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/ctx.rs
+++ b/crates/jrsonnet-evaluator/src/ctx.rs
@@ -81,7 +81,7 @@
 	#[must_use]
 	pub fn into_future(self, ctx: Pending<Self>) -> Self {
 		{
-			ctx.0.borrow_mut().replace(self);
+			ctx.clone().fill(self);
 		}
 		ctx.unwrap()
 	}
modifiedcrates/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)
+	}
+}
modifiedcrates/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()),
 		}
 	}
 }
modifiedcrates/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"))]
modifiedcrates/jrsonnet-evaluator/src/typed/conversions.rsdiffbeforeafterboth
before · crates/jrsonnet-evaluator/src/typed/conversions.rs
1use std::ops::Deref;23use jrsonnet_gcmodule::Cc;4use jrsonnet_interner::{IBytes, IStr};5pub use jrsonnet_macros::Typed;6use jrsonnet_types::{ComplexValType, ValType};78use crate::{9	arr::ArrValue,10	error::Result,11	function::{native::NativeDesc, FuncDesc, FuncVal},12	throw,13	typed::CheckType,14	val::{IndexableVal, StrValue},15	ObjValue, ObjValueBuilder, Val,16};1718pub trait TypedObj: Typed {19	fn serialize(self, out: &mut ObjValueBuilder) -> Result<()>;20	fn parse(obj: &ObjValue) -> Result<Self>;21	fn into_object(self) -> Result<ObjValue> {22		let mut builder = ObjValueBuilder::new();23		self.serialize(&mut builder)?;24		Ok(builder.build())25	}26}2728pub trait Typed: Sized {29	const TYPE: &'static ComplexValType;30	fn into_untyped(typed: Self) -> Result<Val>;31	fn from_untyped(untyped: Val) -> Result<Self>;3233	/// Hack to make builtins be able to return non-result values, and make macros able to convert those values to result34	/// This method returns identity in impl Typed for Result, and should not be overriden35	#[doc(hidden)]36	fn into_result(typed: Self) -> Result<Val> {37		let value = Self::into_untyped(typed)?;38		Ok(value)39	}40}4142const MAX_SAFE_INTEGER: f64 = ((1u64 << (f64::MANTISSA_DIGITS + 1)) - 1) as f64;4344macro_rules! impl_int {45	($($ty:ty)*) => {$(46		impl Typed for $ty {47			const TYPE: &'static ComplexValType =48				&ComplexValType::BoundedNumber(Some(Self::MIN as f64), Some(Self::MAX as f64));49			fn from_untyped(value: Val) -> Result<Self> {50				<Self as Typed>::TYPE.check(&value)?;51				match value {52					Val::Num(n) => {53						#[allow(clippy::float_cmp)]54						if n.trunc() != n {55							throw!(56								"cannot convert number with fractional part to {}",57								stringify!($ty)58							)59						}60						Ok(n as Self)61					}62					_ => unreachable!(),63				}64			}65			fn into_untyped(value: Self) -> Result<Val> {66				Ok(Val::Num(value as f64))67			}68		}69	)*};70}7172impl_int!(i8 u8 i16 u16 i32 u32);7374macro_rules! impl_bounded_int {75	($($name:ident = $ty:ty)*) => {$(76		#[derive(Clone, Copy)]77		pub struct $name<const MIN: $ty, const MAX: $ty>($ty);78		impl<const MIN: $ty, const MAX: $ty> $name<MIN, MAX> {79			pub const fn new(value: $ty) -> Option<$name<MIN, MAX>> {80				if value >= MIN && value <= MAX {81					Some(Self(value))82				} else {83					None84				}85			}86			pub const fn value(self) -> $ty {87				self.088			}89		}90		impl<const MIN: $ty, const MAX: $ty> Deref for $name<MIN, MAX> {91			type Target = $ty;92			fn deref(&self) -> &Self::Target {93				&self.094			}95		}9697		impl<const MIN: $ty, const MAX: $ty> Typed for $name<MIN, MAX> {98			const TYPE: &'static ComplexValType =99				&ComplexValType::BoundedNumber(100					Some(MIN as f64),101					Some(MAX as f64),102				);103104			fn from_untyped(value: Val) -> Result<Self> {105				<Self as Typed>::TYPE.check(&value)?;106				match value {107					Val::Num(n) => {108						#[allow(clippy::float_cmp)]109						if n.trunc() != n {110							throw!(111								"cannot convert number with fractional part to {}",112								stringify!($ty)113							)114						}115						Ok(Self(n as $ty))116					}117					_ => unreachable!(),118				}119			}120121			fn into_untyped(value: Self) -> Result<Val> {122				Ok(Val::Num(value.0 as f64))123			}124		}125	)*};126}127128impl_bounded_int!(129	BoundedI8 = i8130	BoundedI16 = i16131	BoundedI32 = i32132	BoundedI64 = i64133	BoundedUsize = usize134);135136impl Typed for f64 {137	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Num);138139	fn into_untyped(value: Self) -> Result<Val> {140		Ok(Val::Num(value))141	}142143	fn from_untyped(value: Val) -> Result<Self> {144		<Self as Typed>::TYPE.check(&value)?;145		match value {146			Val::Num(n) => Ok(n),147			_ => unreachable!(),148		}149	}150}151152pub struct PositiveF64(pub f64);153impl Typed for PositiveF64 {154	const TYPE: &'static ComplexValType = &ComplexValType::BoundedNumber(Some(0.0), None);155156	fn into_untyped(value: Self) -> Result<Val> {157		Ok(Val::Num(value.0))158	}159160	fn from_untyped(value: Val) -> Result<Self> {161		<Self as Typed>::TYPE.check(&value)?;162		match value {163			Val::Num(n) => Ok(Self(n)),164			_ => unreachable!(),165		}166	}167}168impl Typed for usize {169	const TYPE: &'static ComplexValType =170		&ComplexValType::BoundedNumber(Some(0.0), Some(MAX_SAFE_INTEGER));171172	fn into_untyped(value: Self) -> Result<Val> {173		if value > MAX_SAFE_INTEGER as Self {174			throw!("number is too large")175		}176		Ok(Val::Num(value as f64))177	}178179	fn from_untyped(value: Val) -> Result<Self> {180		<Self as Typed>::TYPE.check(&value)?;181		match value {182			Val::Num(n) => {183				#[allow(clippy::float_cmp)]184				if n.trunc() != n {185					throw!("cannot convert number with fractional part to usize")186				}187				Ok(n as Self)188			}189			_ => unreachable!(),190		}191	}192}193194impl Typed for IStr {195	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Str);196197	fn into_untyped(value: Self) -> Result<Val> {198		Ok(Val::Str(StrValue::Flat(value)))199	}200201	fn from_untyped(value: Val) -> Result<Self> {202		<Self as Typed>::TYPE.check(&value)?;203		match value {204			Val::Str(s) => Ok(s.into_flat()),205			_ => unreachable!(),206		}207	}208}209210impl Typed for String {211	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Str);212213	fn into_untyped(value: Self) -> Result<Val> {214		Ok(Val::Str(StrValue::Flat(value.into())))215	}216217	fn from_untyped(value: Val) -> Result<Self> {218		<Self as Typed>::TYPE.check(&value)?;219		match value {220			Val::Str(s) => Ok(s.to_string()),221			_ => unreachable!(),222		}223	}224}225226impl Typed for char {227	const TYPE: &'static ComplexValType = &ComplexValType::Char;228229	fn into_untyped(value: Self) -> Result<Val> {230		Ok(Val::Str(StrValue::Flat(value.to_string().into())))231	}232233	fn from_untyped(value: Val) -> Result<Self> {234		<Self as Typed>::TYPE.check(&value)?;235		match value {236			Val::Str(s) => Ok(s.into_flat().chars().next().unwrap()),237			_ => unreachable!(),238		}239	}240}241242impl<T> Typed for Vec<T>243where244	T: Typed,245{246	const TYPE: &'static ComplexValType = &ComplexValType::ArrayRef(T::TYPE);247248	fn into_untyped(value: Self) -> Result<Val> {249		Ok(Val::Arr(250			value251				.into_iter()252				.map(T::into_untyped)253				.collect::<Result<ArrValue>>()?,254		))255	}256257	fn from_untyped(value: Val) -> Result<Self> {258		let Val::Arr(a) = value else {259			<Self as Typed>::TYPE.check(&value)?;260			unreachable!("typecheck should fail")261		};262		a.iter()263			.map(|r| r.and_then(T::from_untyped))264			.collect::<Result<Vec<T>>>()265	}266}267268impl Typed for Val {269	const TYPE: &'static ComplexValType = &ComplexValType::Any;270271	fn into_untyped(typed: Self) -> Result<Val> {272		Ok(typed)273	}274	fn from_untyped(untyped: Val) -> Result<Self> {275		Ok(untyped)276	}277}278279// Hack280#[doc(hidden)]281impl<T> Typed for Result<T>282where283	T: Typed,284{285	const TYPE: &'static ComplexValType = &ComplexValType::Any;286287	fn into_untyped(_typed: Self) -> Result<Val> {288		panic!("do not use this conversion")289	}290291	fn from_untyped(_untyped: Val) -> Result<Self> {292		panic!("do not use this conversion")293	}294295	fn into_result(typed: Self) -> Result<Val> {296		typed.map(T::into_untyped)?297	}298}299300/// Specialization301impl Typed for IBytes {302	const TYPE: &'static ComplexValType =303		&ComplexValType::ArrayRef(&ComplexValType::BoundedNumber(Some(0.0), Some(255.0)));304305	fn into_untyped(value: Self) -> Result<Val> {306		Ok(Val::Arr(ArrValue::bytes(value)))307	}308309	fn from_untyped(value: Val) -> Result<Self> {310		if let Val::Arr(ArrValue::Bytes(bytes)) = value {311			return Ok(bytes.0);312		}313		<Self as Typed>::TYPE.check(&value)?;314		match value {315			Val::Arr(a) => {316				let mut out = Vec::with_capacity(a.len());317				for e in a.iter() {318					let r = e?;319					out.push(u8::from_untyped(r)?);320				}321				Ok(out.as_slice().into())322			}323			_ => unreachable!(),324		}325	}326}327328pub struct M1;329impl Typed for M1 {330	const TYPE: &'static ComplexValType = &ComplexValType::BoundedNumber(Some(-1.0), Some(-1.0));331332	fn into_untyped(_: Self) -> Result<Val> {333		Ok(Val::Num(-1.0))334	}335336	fn from_untyped(value: Val) -> Result<Self> {337		<Self as Typed>::TYPE.check(&value)?;338		Ok(Self)339	}340}341342macro_rules! decl_either {343	($($name: ident, $($id: ident)*);*) => {$(344		#[derive(Clone)]345		pub enum $name<$($id),*> {346			$($id($id)),*347		}348		impl<$($id),*> Typed for $name<$($id),*>349		where350			$($id: Typed,)*351		{352			const TYPE: &'static ComplexValType = &ComplexValType::UnionRef(&[$($id::TYPE),*]);353354			fn into_untyped(value: Self) -> Result<Val> {355				match value {$(356					$name::$id(v) => $id::into_untyped(v)357				),*}358			}359360			fn from_untyped(value: Val) -> Result<Self> {361				$(362					if $id::TYPE.check(&value).is_ok() {363						$id::from_untyped(value).map(Self::$id)364					} else365				)* {366					<Self as Typed>::TYPE.check(&value)?;367					unreachable!()368				}369			}370		}371	)*}372}373decl_either!(374	Either1, A;375	Either2, A B;376	Either3, A B C;377	Either4, A B C D;378	Either5, A B C D E;379	Either6, A B C D E F;380	Either7, A B C D E F G381);382#[macro_export]383macro_rules! Either {384	($a:ty) => {Either1<$a>};385	($a:ty, $b:ty) => {Either2<$a, $b>};386	($a:ty, $b:ty, $c:ty) => {Either3<$a, $b, $c>};387	($a:ty, $b:ty, $c:ty, $d:ty) => {Either4<$a, $b, $c, $d>};388	($a:ty, $b:ty, $c:ty, $d:ty, $e:ty) => {Either5<$a, $b, $c, $d, $e>};389	($a:ty, $b:ty, $c:ty, $d:ty, $e:ty, $f:ty) => {Either6<$a, $b, $c, $d, $e, $f>};390	($a:ty, $b:ty, $c:ty, $d:ty, $e:ty, $f:ty, $g:ty) => {Either7<$a, $b, $c, $d, $e, $f, $g>};391}392pub use Either;393394pub type MyType = Either![u32, f64, String];395396impl Typed for ArrValue {397	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Arr);398399	fn into_untyped(value: Self) -> Result<Val> {400		Ok(Val::Arr(value))401	}402403	fn from_untyped(value: Val) -> Result<Self> {404		<Self as Typed>::TYPE.check(&value)?;405		match value {406			Val::Arr(a) => Ok(a),407			_ => unreachable!(),408		}409	}410}411412impl Typed for FuncVal {413	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Func);414415	fn into_untyped(value: Self) -> Result<Val> {416		Ok(Val::Func(value))417	}418419	fn from_untyped(value: Val) -> Result<Self> {420		<Self as Typed>::TYPE.check(&value)?;421		match value {422			Val::Func(a) => Ok(a),423			_ => unreachable!(),424		}425	}426}427428impl Typed for Cc<FuncDesc> {429	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Func);430431	fn into_untyped(value: Self) -> Result<Val> {432		Ok(Val::Func(FuncVal::Normal(value)))433	}434435	fn from_untyped(value: Val) -> Result<Self> {436		<Self as Typed>::TYPE.check(&value)?;437		match value {438			Val::Func(FuncVal::Normal(desc)) => Ok(desc),439			Val::Func(_) => throw!("expected normal function, not builtin"),440			_ => unreachable!(),441		}442	}443}444445impl Typed for ObjValue {446	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Obj);447448	fn into_untyped(value: Self) -> Result<Val> {449		Ok(Val::Obj(value))450	}451452	fn from_untyped(value: Val) -> Result<Self> {453		<Self as Typed>::TYPE.check(&value)?;454		match value {455			Val::Obj(a) => Ok(a),456			_ => unreachable!(),457		}458	}459}460461impl Typed for bool {462	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Bool);463464	fn into_untyped(value: Self) -> Result<Val> {465		Ok(Val::Bool(value))466	}467468	fn from_untyped(value: Val) -> Result<Self> {469		<Self as Typed>::TYPE.check(&value)?;470		match value {471			Val::Bool(a) => Ok(a),472			_ => unreachable!(),473		}474	}475}476impl Typed for IndexableVal {477	const TYPE: &'static ComplexValType = &ComplexValType::UnionRef(&[478		&ComplexValType::Simple(ValType::Arr),479		&ComplexValType::Simple(ValType::Str),480	]);481482	fn into_untyped(value: Self) -> Result<Val> {483		match value {484			IndexableVal::Str(s) => Ok(Val::Str(StrValue::Flat(s))),485			IndexableVal::Arr(a) => Ok(Val::Arr(a)),486		}487	}488489	fn from_untyped(value: Val) -> Result<Self> {490		<Self as Typed>::TYPE.check(&value)?;491		value.into_indexable()492	}493}494495pub struct Null;496impl Typed for Null {497	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Null);498499	fn into_untyped(_: Self) -> Result<Val> {500		Ok(Val::Null)501	}502503	fn from_untyped(value: Val) -> Result<Self> {504		<Self as Typed>::TYPE.check(&value)?;505		Ok(Self)506	}507}508509pub struct NativeFn<D: NativeDesc>(D::Value);510impl<D: NativeDesc> Deref for NativeFn<D> {511	type Target = D::Value;512513	fn deref(&self) -> &Self::Target {514		&self.0515	}516}517impl<D: NativeDesc> Typed for NativeFn<D> {518	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Func);519520	fn into_untyped(_typed: Self) -> Result<Val> {521		throw!("can only convert functions from jsonnet to native")522	}523524	fn from_untyped(untyped: Val) -> Result<Self> {525		Ok(Self(526			untyped527				.as_func()528				.expect("shape is checked")529				.into_native::<D>(),530		))531	}532}
modifiedcrates/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(()),
 		}
 	}
 }
modifiedcrates/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 {
modifiedcrates/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(())
 	}