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

difftreelog

refactor split TypedObj derives

oqrkzkryYaroslav Bolyukin2026-03-22parent: #bd50dd1.patch.diff
in: master

5 files changed

modifiedcrates/jrsonnet-evaluator/src/arr/spec.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/arr/spec.rs
+++ b/crates/jrsonnet-evaluator/src/arr/spec.rs
@@ -609,7 +609,7 @@
 	}
 }
 
-#[derive(Typed)]
+#[derive(Typed, IntoUntyped)]
 pub struct KeyValue {
 	key: IStr,
 	value: Thunk<Val>,
modifiedcrates/jrsonnet-evaluator/src/typed/conversions.rsdiffbeforeafterboth
after · crates/jrsonnet-evaluator/src/typed/conversions.rs
1use std::{collections::BTreeMap, marker::PhantomData, ops::Deref};23use jrsonnet_gcmodule::Trace;4use jrsonnet_interner::{IBytes, IStr};5use jrsonnet_types::{ComplexValType, ValType};67use crate::{8	arr::{ArrValue, BytesArray},9	bail,10	function::FuncVal,11	typed::CheckType,12	val::{IndexableVal, NumValue, StrValue, ThunkMapper},13	ObjValue, ObjValueBuilder, Result, ResultExt, Thunk, Val,14};1516#[doc(hidden)]17pub mod __typed_macro_prelude {18	pub use ::jrsonnet_evaluator::{19		error::{ErrorKind, Result as JrResult},20		typed::{21			CheckType, ComplexValType, FromUntyped, IntoUntyped, ParseTypedObj, SerializeTypedObj,22			Typed,23		},24		IStr, ObjValue, ObjValueBuilder, State, Val,25	};26}27pub use jrsonnet_macros::{FromUntyped, IntoUntyped, Typed};2829#[derive(Trace)]30struct ThunkFromUntyped<K: Trace>(PhantomData<fn() -> K>);31impl<K> ThunkMapper<Val> for ThunkFromUntyped<K>32where33	K: Typed + FromUntyped + Trace,34{35	type Output = K;3637	fn map(self, from: Val) -> Result<Self::Output> {38		K::from_untyped(from)39	}40}41impl<K: Trace> Default for ThunkFromUntyped<K> {42	fn default() -> Self {43		Self(PhantomData)44	}45}46#[derive(Trace)]47struct ThunkIntoUntyped<K: Trace>(PhantomData<fn() -> K>);48impl<K> ThunkMapper<K> for ThunkIntoUntyped<K>49where50	K: Typed + Trace + IntoUntyped,51{52	type Output = Val;5354	fn map(self, from: K) -> Result<Self::Output> {55		K::into_untyped(from)56	}57}58impl<K: Trace> Default for ThunkIntoUntyped<K> {59	fn default() -> Self {60		Self(PhantomData)61	}62}6364pub trait ParseTypedObj: Typed {65	fn parse(obj: &ObjValue) -> Result<Self>;66}67pub trait SerializeTypedObj: Typed {68	fn serialize(self, out: &mut ObjValueBuilder) -> Result<()>;69	fn into_object(self) -> Result<ObjValue> {70		let mut builder = ObjValueBuilder::new();71		self.serialize(&mut builder)?;72		Ok(builder.build())73	}74}7576pub trait Typed: Sized {77	const TYPE: &'static ComplexValType;78}79pub trait IntoUntyped: Typed {80	// Whatever caller should use `into_lazy_untyped` instead of `into_untyped`81	fn provides_lazy() -> bool {82		false83	}84	fn into_untyped(typed: Self) -> Result<Val>;85	fn into_lazy_untyped(typed: Self) -> Thunk<Val> {86		Thunk::from(Self::into_untyped(typed))87	}88}89pub trait IntoUntypedResult: Typed {90	/// Hack to make builtins be able to return non-result values, and make macros able to convert those values to result91	/// This method returns identity in impl Typed for Result, and should not be overriden92	#[doc(hidden)]93	fn into_untyped_result(typed: Self) -> Result<Val>;94}95impl<T> IntoUntypedResult for T96where97	T: IntoUntyped,98{99	fn into_untyped_result(typed: Self) -> Result<Val> {100		T::into_untyped(typed)101	}102}103104pub trait FromUntyped: Typed {105	fn from_untyped(untyped: Val) -> Result<Self>;106	fn from_lazy_untyped(lazy: Thunk<Val>) -> Result<Self> {107		Self::from_untyped(lazy.evaluate()?)108	}109110	// Whatever caller should use `from_lazy_untyped` instead of `from_untyped` when possible111	fn wants_lazy() -> bool {112		false113	}114}115116impl<T> Typed for Thunk<T>117where118	T: Typed + Trace + Clone,119{120	const TYPE: &'static ComplexValType = &ComplexValType::Lazy(T::TYPE);121}122123impl<T> IntoUntyped for Thunk<T>124where125	T: Typed + IntoUntyped + Trace + Clone,126{127	fn into_untyped(typed: Self) -> Result<Val> {128		T::into_untyped(typed.evaluate()?)129	}130	fn provides_lazy() -> bool {131		true132	}133134	fn into_lazy_untyped(inner: Self) -> Thunk<Val> {135		inner.map(<ThunkIntoUntyped<T>>::default())136	}137}138139impl<T> FromUntyped for Thunk<T>140where141	T: Typed + FromUntyped + Trace + Clone,142{143	fn from_untyped(untyped: Val) -> Result<Self> {144		Self::from_lazy_untyped(Thunk::evaluated(untyped))145	}146147	fn wants_lazy() -> bool {148		true149	}150151	fn from_lazy_untyped(inner: Thunk<Val>) -> Result<Self> {152		Ok(inner.map(<ThunkFromUntyped<T>>::default()))153	}154}155156pub const MAX_SAFE_INTEGER: f64 = ((1u64 << (f64::MANTISSA_DIGITS)) - 1) as f64;157pub const MIN_SAFE_INTEGER: f64 = (-((1i64 << (f64::MANTISSA_DIGITS)) - 1)) as f64;158159macro_rules! impl_int {160	($($ty:ty)*) => {$(161		impl Typed for $ty {162			const TYPE: &'static ComplexValType =163				&ComplexValType::BoundedNumber(Some(Self::MIN as f64), Some(Self::MAX as f64));164		}165		impl FromUntyped for $ty {166			fn from_untyped(value: Val) -> Result<Self> {167				<Self as Typed>::TYPE.check(&value)?;168				match value {169					Val::Num(n) => {170						let n = n.get();171						#[allow(clippy::float_cmp)]172						if n.trunc() != n {173							bail!(174								"cannot convert number with fractional part to {}",175								stringify!($ty)176							)177						}178						Ok(n as Self)179					}180					_ => unreachable!(),181				}182			}183		}184		impl IntoUntyped for $ty {185			fn into_untyped(value: Self) -> Result<Val> {186				Ok(Val::Num(value.into()))187			}188		}189	)*};190}191192impl_int!(i8 u8 i16 u16 i32 u32);193194macro_rules! impl_bounded_int {195	($($name:ident = $ty:ty)*) => {$(196		#[derive(Clone, Copy)]197		pub struct $name<const MIN: $ty, const MAX: $ty>($ty);198		impl<const MIN: $ty, const MAX: $ty> $name<MIN, MAX> {199			pub const fn new(value: $ty) -> Option<$name<MIN, MAX>> {200				if value >= MIN && value <= MAX {201					Some(Self(value))202				} else {203					None204				}205			}206			pub const fn value(self) -> $ty {207				self.0208			}209		}210		impl<const MIN: $ty, const MAX: $ty> Deref for $name<MIN, MAX> {211			type Target = $ty;212			fn deref(&self) -> &Self::Target {213				&self.0214			}215		}216217		impl<const MIN: $ty, const MAX: $ty> Typed for $name<MIN, MAX> {218			const TYPE: &'static ComplexValType =219				&ComplexValType::BoundedNumber(220					Some(MIN as f64),221					Some(MAX as f64),222				);223		}224225		impl<const MIN: $ty, const MAX: $ty> FromUntyped for $name<MIN, MAX> {226			fn from_untyped(value: Val) -> Result<Self> {227				<Self as Typed>::TYPE.check(&value)?;228				match value {229					Val::Num(n) => {230						let n = n.get();231						#[allow(clippy::float_cmp)]232						if n.trunc() != n {233							bail!(234								"cannot convert number with fractional part to {}",235								stringify!($ty)236							)237						}238						Ok(Self(n as $ty))239					}240					_ => unreachable!(),241				}242			}243		}244245		impl<const MIN: $ty, const MAX: $ty> IntoUntyped for $name<MIN, MAX> {246			#[allow(clippy::cast_lossless)]247			fn into_untyped(value: Self) -> Result<Val> {248				Ok(Val::try_num(value.0)?)249			}250		}251	)*};252}253254impl_bounded_int!(255	BoundedI8 = i8256	BoundedI16 = i16257	BoundedI32 = i32258	BoundedI64 = i64259	BoundedUsize = usize260);261262impl Typed for f64 {263	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Num);264}265impl IntoUntyped for f64 {266	fn into_untyped(value: Self) -> Result<Val> {267		Ok(Val::try_num(value)?)268	}269}270impl FromUntyped for f64 {271	fn from_untyped(value: Val) -> Result<Self> {272		<Self as Typed>::TYPE.check(&value)?;273		match value {274			Val::Num(n) => Ok(n.get()),275			_ => unreachable!(),276		}277	}278}279280pub struct PositiveF64(pub f64);281impl Typed for PositiveF64 {282	const TYPE: &'static ComplexValType = &ComplexValType::BoundedNumber(Some(0.0), None);283}284impl IntoUntyped for PositiveF64 {285	fn into_untyped(value: Self) -> Result<Val> {286		Ok(Val::try_num(value.0)?)287	}288}289impl FromUntyped for PositiveF64 {290	fn from_untyped(value: Val) -> Result<Self> {291		<Self as Typed>::TYPE.check(&value)?;292		match value {293			Val::Num(n) => Ok(Self(n.get())),294			_ => unreachable!(),295		}296	}297}298impl Typed for usize {299	const TYPE: &'static ComplexValType =300		&ComplexValType::BoundedNumber(Some(0.0), Some(MAX_SAFE_INTEGER));301}302impl IntoUntyped for usize {303	fn into_untyped(value: Self) -> Result<Val> {304		Ok(Val::try_num(value)?)305	}306}307impl FromUntyped for usize {308	fn from_untyped(value: Val) -> Result<Self> {309		<Self as Typed>::TYPE.check(&value)?;310		match value {311			Val::Num(n) => {312				let n = n.get();313				#[allow(clippy::float_cmp)]314				if n.trunc() != n {315					bail!("cannot convert number with fractional part to usize")316				}317				Ok(n as Self)318			}319			_ => unreachable!(),320		}321	}322}323324impl Typed for IStr {325	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Str);326}327impl IntoUntyped for IStr {328	fn into_untyped(value: Self) -> Result<Val> {329		Ok(Val::string(value))330	}331}332impl FromUntyped for IStr {333	fn from_untyped(value: Val) -> Result<Self> {334		<Self as Typed>::TYPE.check(&value)?;335		match value {336			Val::Str(s) => Ok(s.into_flat()),337			_ => unreachable!(),338		}339	}340}341342impl Typed for String {343	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Str);344}345impl IntoUntyped for String {346	fn into_untyped(value: Self) -> Result<Val> {347		Ok(Val::string(value))348	}349}350impl FromUntyped for String {351	fn from_untyped(value: Val) -> Result<Self> {352		<Self as Typed>::TYPE.check(&value)?;353		match value {354			Val::Str(s) => Ok(s.to_string()),355			_ => unreachable!(),356		}357	}358}359360impl Typed for StrValue {361	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Str);362}363impl IntoUntyped for StrValue {364	fn into_untyped(value: Self) -> Result<Val> {365		Ok(Val::Str(value))366	}367}368impl FromUntyped for StrValue {369	fn from_untyped(value: Val) -> Result<Self> {370		<Self as Typed>::TYPE.check(&value)?;371		match value {372			Val::Str(s) => Ok(s),373			_ => unreachable!(),374		}375	}376}377378impl Typed for char {379	const TYPE: &'static ComplexValType = &ComplexValType::Char;380}381impl IntoUntyped for char {382	fn into_untyped(value: Self) -> Result<Val> {383		Ok(Val::string(value))384	}385}386impl FromUntyped for char {387	fn from_untyped(value: Val) -> Result<Self> {388		<Self as Typed>::TYPE.check(&value)?;389		match value {390			Val::Str(s) => Ok(s.into_flat().chars().next().unwrap()),391			_ => unreachable!(),392		}393	}394}395396// TODO: View into vec using ArrayLike?397impl<T> Typed for Vec<T>398where399	T: Typed,400{401	const TYPE: &'static ComplexValType = &ComplexValType::ArrayRef(T::TYPE);402}403impl<T: Typed + IntoUntyped> IntoUntyped for Vec<T> {404	fn into_untyped(value: Self) -> Result<Val> {405		Ok(Val::Arr(406			value407				.into_iter()408				.map(T::into_untyped)409				.collect::<Result<ArrValue>>()?,410		))411	}412}413impl<T: Typed + FromUntyped> FromUntyped for Vec<T> {414	fn from_untyped(value: Val) -> Result<Self> {415		let Val::Arr(a) = value else {416			<Self as Typed>::TYPE.check(&value)?;417			unreachable!("typecheck should fail")418		};419		a.iter()420			.enumerate()421			.map(|(i, r)| {422				r.and_then(|t| {423					T::from_untyped(t).with_description(|| format!("parsing elem <{i}>"))424				})425			})426			.collect::<Result<Self>>()427	}428}429430// TODO: View into BTreeMap using ObjectCore?431impl<K, V> Typed for BTreeMap<K, V>432where433	K: Typed + Ord,434	V: Typed,435{436	const TYPE: &'static ComplexValType = &ComplexValType::AttrsOf(V::TYPE);437}438impl<K, V> IntoUntyped for BTreeMap<K, V>439where440	K: Typed + Ord + IntoUntyped,441	V: Typed + IntoUntyped,442{443	fn into_untyped(typed: Self) -> Result<Val> {444		let mut out = ObjValueBuilder::with_capacity(typed.len());445		for (k, v) in typed {446			let Some(key) = K::into_untyped(k)?.as_str() else {447				bail!("map key should serialize to string");448			};449			let value = V::into_untyped(v)?;450			out.field(key).value(value);451		}452		Ok(Val::Obj(out.build()))453	}454}455impl<K, V> FromUntyped for BTreeMap<K, V>456where457	K: FromUntyped + Ord,458	V: FromUntyped,459{460	fn from_untyped(value: Val) -> Result<Self> {461		Self::TYPE.check(&value)?;462		let obj = value.as_obj().expect("typecheck should fail");463464		let mut out = Self::new();465		if V::wants_lazy() {466			for key in obj.fields_ex(467				false,468				#[cfg(feature = "exp-preserve-order")]469				false,470			) {471				let value = obj.get_lazy(key.clone()).expect("field exists");472				let value = V::from_lazy_untyped(value)?;473				let key = K::from_untyped(Val::Str(key.into()))?;474				let _ = out.insert(key, value);475			}476		} else {477			for (key, value) in obj.iter(478				#[cfg(feature = "exp-preserve-order")]479				false,480			) {481				let key = K::from_untyped(Val::Str(key.into()))?;482				let value = V::from_untyped(value?)?;483				let _ = out.insert(key, value);484			}485		}486		Ok(out)487	}488}489490impl Typed for Val {491	const TYPE: &'static ComplexValType = &ComplexValType::Any;492}493impl IntoUntyped for Val {494	fn into_untyped(typed: Self) -> Result<Val> {495		Ok(typed)496	}497}498impl FromUntyped for Val {499	fn from_untyped(untyped: Val) -> Result<Self> {500		Ok(untyped)501	}502}503504#[doc(hidden)]505impl<T> Typed for Result<T>506where507	T: Typed,508{509	const TYPE: &'static ComplexValType = &ComplexValType::Any;510}511impl<T: IntoUntyped> IntoUntypedResult for Result<T> {512	fn into_untyped_result(typed: Self) -> Result<Val> {513		typed.map(T::into_untyped)?514	}515}516517/// Specialization518impl Typed for IBytes {519	const TYPE: &'static ComplexValType =520		&ComplexValType::ArrayRef(&ComplexValType::BoundedNumber(Some(0.0), Some(255.0)));521}522impl IntoUntyped for IBytes {523	fn into_untyped(value: Self) -> Result<Val> {524		Ok(Val::Arr(ArrValue::bytes(value)))525	}526}527impl FromUntyped for IBytes {528	fn from_untyped(value: Val) -> Result<Self> {529		let Val::Arr(a) = &value else {530			<Self as Typed>::TYPE.check(&value)?;531			unreachable!()532		};533		if let Some(bytes) = a.as_any().downcast_ref::<BytesArray>() {534			return Ok(bytes.0.as_slice().into());535		}536		<Self as Typed>::TYPE.check(&value)?;537		// Any::downcast_ref::<ByteArray>(&a);538		let mut out = Vec::with_capacity(a.len());539		for e in a.iter() {540			let r = e?;541			out.push(u8::from_untyped(r)?);542		}543		Ok(out.as_slice().into())544	}545}546547pub struct M1;548impl Typed for M1 {549	const TYPE: &'static ComplexValType = &ComplexValType::BoundedNumber(Some(-1.0), Some(-1.0));550}551impl IntoUntyped for M1 {552	fn into_untyped(_: Self) -> Result<Val> {553		Ok(Val::Num(NumValue::new(-1.0).expect("finite")))554	}555}556impl FromUntyped for M1 {557	fn from_untyped(value: Val) -> Result<Self> {558		<Self as Typed>::TYPE.check(&value)?;559		Ok(Self)560	}561}562563macro_rules! decl_either {564	($($name: ident, $($id: ident)*);*) => {$(565		#[derive(Clone)]566		pub enum $name<$($id),*> {567			$($id($id)),*568		}569		impl<$($id),*> Typed for $name<$($id),*>570		where571			$($id: Typed,)*572		{573			const TYPE: &'static ComplexValType = &ComplexValType::UnionRef(&[$($id::TYPE),*]);574		}575		impl<$($id),*> IntoUntyped for $name<$($id),*>576		where577			$($id: Typed + IntoUntyped,)*578		{579			fn into_untyped(value: Self) -> Result<Val> {580				match value {$(581					$name::$id(v) => $id::into_untyped(v)582				),*}583			}584		}585586		impl<$($id),*> FromUntyped for $name<$($id),*>587		where588			$($id: Typed + FromUntyped,)*589		{590			fn from_untyped(value: Val) -> Result<Self> {591				$(592					if $id::TYPE.check(&value).is_ok() {593						$id::from_untyped(value).map(Self::$id)594					} else595				)* {596					<Self as Typed>::TYPE.check(&value)?;597					unreachable!()598				}599			}600		}601	)*}602}603decl_either!(604	Either1, A;605	Either2, A B;606	Either3, A B C;607	Either4, A B C D;608	Either5, A B C D E;609	Either6, A B C D E F;610	Either7, A B C D E F G611);612#[macro_export]613macro_rules! Either {614	($a:ty) => {$crate::typed::Either1<$a>};615	($a:ty, $b:ty) => {$crate::typed::Either2<$a, $b>};616	($a:ty, $b:ty, $c:ty) => {$crate::typed::Either3<$a, $b, $c>};617	($a:ty, $b:ty, $c:ty, $d:ty) => {$crate::typed::Either4<$a, $b, $c, $d>};618	($a:ty, $b:ty, $c:ty, $d:ty, $e:ty) => {$crate::typed::Either5<$a, $b, $c, $d, $e>};619	($a:ty, $b:ty, $c:ty, $d:ty, $e:ty, $f:ty) => {$crate::typed::Either6<$a, $b, $c, $d, $e, $f>};620	($a:ty, $b:ty, $c:ty, $d:ty, $e:ty, $f:ty, $g:ty) => {$crate::typed::Either7<$a, $b, $c, $d, $e, $f, $g>};621}622pub use Either;623624pub type MyType = Either![u32, f64, String];625626impl Typed for ArrValue {627	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Arr);628}629impl IntoUntyped for ArrValue {630	fn into_untyped(value: Self) -> Result<Val> {631		Ok(Val::Arr(value))632	}633}634impl FromUntyped for ArrValue {635	fn from_untyped(value: Val) -> Result<Self> {636		<Self as Typed>::TYPE.check(&value)?;637		match value {638			Val::Arr(a) => Ok(a),639			_ => unreachable!(),640		}641	}642}643644impl Typed for FuncVal {645	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Func);646}647impl IntoUntyped for FuncVal {648	fn into_untyped(value: Self) -> Result<Val> {649		Ok(Val::Func(value))650	}651}652impl FromUntyped for FuncVal {653	fn from_untyped(value: Val) -> Result<Self> {654		<Self as Typed>::TYPE.check(&value)?;655		match value {656			Val::Func(a) => Ok(a),657			_ => unreachable!(),658		}659	}660}661662impl Typed for ObjValue {663	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Obj);664}665impl IntoUntyped for ObjValue {666	fn into_untyped(value: Self) -> Result<Val> {667		Ok(Val::Obj(value))668	}669}670impl FromUntyped for ObjValue {671	fn from_untyped(value: Val) -> Result<Self> {672		<Self as Typed>::TYPE.check(&value)?;673		match value {674			Val::Obj(a) => Ok(a),675			_ => unreachable!(),676		}677	}678}679680impl Typed for bool {681	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Bool);682}683impl IntoUntyped for bool {684	fn into_untyped(value: Self) -> Result<Val> {685		Ok(Val::Bool(value))686	}687}688impl FromUntyped for bool {689	fn from_untyped(value: Val) -> Result<Self> {690		<Self as Typed>::TYPE.check(&value)?;691		match value {692			Val::Bool(a) => Ok(a),693			_ => unreachable!(),694		}695	}696}697698impl Typed for IndexableVal {699	const TYPE: &'static ComplexValType = &ComplexValType::UnionRef(&[700		&ComplexValType::Simple(ValType::Arr),701		&ComplexValType::Simple(ValType::Str),702	]);703}704impl IntoUntyped for IndexableVal {705	fn into_untyped(value: Self) -> Result<Val> {706		match value {707			Self::Str(s) => Ok(Val::string(s)),708			Self::Arr(a) => Ok(Val::Arr(a)),709		}710	}711}712impl FromUntyped for IndexableVal {713	fn from_untyped(value: Val) -> Result<Self> {714		<Self as Typed>::TYPE.check(&value)?;715		value.into_indexable()716	}717}718719pub struct Null;720impl Typed for Null {721	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Null);722}723impl IntoUntyped for Null {724	fn into_untyped(_: Self) -> Result<Val> {725		Ok(Val::Null)726	}727}728impl FromUntyped for Null {729	fn from_untyped(value: Val) -> Result<Self> {730		<Self as Typed>::TYPE.check(&value)?;731		Ok(Self)732	}733}734735impl<T> Typed for Option<T>736where737	T: Typed,738{739	const TYPE: &'static ComplexValType =740		&ComplexValType::UnionRef(&[&ComplexValType::Simple(ValType::Null), T::TYPE]);741}742impl<T> IntoUntyped for Option<T>743where744	T: Typed + IntoUntyped,745{746	fn into_untyped(typed: Self) -> Result<Val> {747		typed.map_or_else(|| Ok(Val::Null), |v| T::into_untyped(v))748	}749}750impl<T> FromUntyped for Option<T>751where752	T: Typed + FromUntyped,753{754	fn from_untyped(untyped: Val) -> Result<Self> {755		if matches!(untyped, Val::Null) {756			Ok(None)757		} else {758			T::from_untyped(untyped).map(Some)759		}760	}761}762763impl Typed for NumValue {764	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Num);765}766impl IntoUntyped for NumValue {767	fn into_untyped(typed: Self) -> Result<Val> {768		Ok(Val::Num(typed))769	}770}771impl FromUntyped for NumValue {772	fn from_untyped(untyped: Val) -> Result<Self> {773		Self::TYPE.check(&untyped)?;774		match untyped {775			Val::Num(v) => Ok(v),776			_ => unreachable!(),777		}778	}779}
modifiedcrates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth
--- a/crates/jrsonnet-macros/src/lib.rs
+++ b/crates/jrsonnet-macros/src/lib.rs
@@ -13,7 +13,7 @@
 	LitStr, Meta, Pat, Path, PathArguments, Result, ReturnType, Token, Type,
 };
 
-use self::typed::derive_typed_inner;
+use self::typed::{derive_from_untyped_inner, derive_into_untyped_inner, derive_typed_inner};
 
 mod names;
 mod typed;
@@ -451,6 +451,24 @@
 		Err(e) => e.to_compile_error().into(),
 	}
 }
+#[proc_macro_derive(IntoUntyped, attributes(typed))]
+pub fn derive_into_untyped(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
+	let input = parse_macro_input!(item as DeriveInput);
+
+	match derive_into_untyped_inner(input) {
+		Ok(v) => v.into(),
+		Err(e) => e.to_compile_error().into(),
+	}
+}
+#[proc_macro_derive(FromUntyped, attributes(typed))]
+pub fn derive_from_untyped(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
+	let input = parse_macro_input!(item as DeriveInput);
+
+	match derive_from_untyped_inner(input) {
+		Ok(v) => v.into(),
+		Err(e) => e.to_compile_error().into(),
+	}
+}
 
 struct FormatInput {
 	formatting: LitStr,
modifiedcrates/jrsonnet-macros/src/typed.rsdiffbeforeafterboth
--- a/crates/jrsonnet-macros/src/typed.rs
+++ b/crates/jrsonnet-macros/src/typed.rs
@@ -301,26 +301,49 @@
 
 	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
 
-	let capacity = fields.len();
+	let fields = fields
+		.iter()
+		.filter_map(TypedField::expand_field)
+		.collect::<Vec<_>>();
+	Ok(quote! {
+		const _: () = {
+			use ::jrsonnet_evaluator::typed::__typed_macro_prelude::*;
 
-	let typed = {
-		let fields = fields
-			.iter()
-			.filter_map(TypedField::expand_field)
-			.collect::<Vec<_>>();
-		quote! {
 			impl #impl_generics Typed for #ident #ty_generics #where_clause {
 				const TYPE: &'static ComplexValType = &ComplexValType::ObjectRef(&[
 					#(#fields,)*
 				]);
 			}
+		};
+	})
+}
+pub fn derive_into_untyped_inner(input: DeriveInput) -> Result<TokenStream> {
+	let syn::Data::Struct(data) = &input.data else {
+		return Err(Error::new(input.span(), "only structs supported"));
+	};
+
+	let ident = &input.ident;
+	let fields = data
+		.fields
+		.iter()
+		.map(TypedField::parse)
+		.collect::<Result<Vec<_>>>()?;
+
+	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
+	let capacity = fields.len();
+
+	let mut names = Names::default();
+
+	let fields_serialize = fields
+		.iter()
+		.map(|f| f.expand_serialize(&mut names))
+		.collect::<Vec<_>>();
 
-			impl #impl_generics FromUntyped for #ident #ty_generics #where_clause {
-				fn from_untyped(value: Val) -> JrResult<Self> {
-					let obj = value.as_obj().expect("shape is correct");
-					Self::parse(&obj)
-				}
-			}
+	let names_expanded = names.expand();
+	Ok(quote! {
+		const _: () = {
+			use ::jrsonnet_evaluator::typed::__typed_macro_prelude::*;
 
 			impl #impl_generics IntoUntyped for #ident #ty_generics #where_clause {
 				fn into_untyped(value: Self) -> JrResult<Val> {
@@ -329,42 +352,57 @@
 					Ok(Val::Obj(out.build()))
 				}
 			}
-		}
+
+			#names_expanded
+
+			impl #impl_generics SerializeTypedObj for #ident #ty_generics #where_clause {
+				fn serialize(self, out: &mut ObjValueBuilder) -> JrResult<()> {
+					NAMES.with(|__names| {
+						#(#fields_serialize)*
+
+						Ok(())
+					})
+				}
+			}
+		};
+	})
+}
+pub fn derive_from_untyped_inner(input: DeriveInput) -> Result<TokenStream> {
+	let syn::Data::Struct(data) = &input.data else {
+		return Err(Error::new(input.span(), "only structs supported"));
 	};
 
+	let ident = &input.ident;
+	let fields = data
+		.fields
+		.iter()
+		.map(TypedField::parse)
+		.collect::<Result<Vec<_>>>()?;
+
+	let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+
 	let mut names = Names::default();
 
 	let fields_parse = fields
 		.iter()
 		.map(|f| f.expand_parse(&mut names))
-		.collect::<Vec<_>>();
-	let fields_serialize = fields
-		.iter()
-		.map(|f| f.expand_serialize(&mut names))
 		.collect::<Vec<_>>();
 
 	let names_expanded = names.expand();
 	Ok(quote! {
 		const _: () = {
-			use ::jrsonnet_evaluator::{
-				typed::{ComplexValType, Typed, IntoUntyped, FromUntyped, TypedObj, CheckType},
-				Val, State,
-				error::{ErrorKind, Result as JrResult},
-				ObjValueBuilder, ObjValue, IStr,
-			};
+			use ::jrsonnet_evaluator::typed::__typed_macro_prelude::*;
 
-			#typed
+			impl #impl_generics FromUntyped for #ident #ty_generics #where_clause {
+				fn from_untyped(value: Val) -> JrResult<Self> {
+					let obj = value.as_obj().expect("shape is correct");
+					Self::parse(&obj)
+				}
+			}
 
 			#names_expanded
 
-			impl #impl_generics TypedObj for #ident #ty_generics #where_clause {
-				fn serialize(self, out: &mut ObjValueBuilder) -> JrResult<()> {
-					NAMES.with(|__names| {
-						#(#fields_serialize)*
-
-						Ok(())
-					})
-				}
+			impl #impl_generics ParseTypedObj for #ident #ty_generics #where_clause {
 				fn parse(obj: &ObjValue) -> JrResult<Self> {
 					NAMES.with(|__names| Ok(Self {
 						#(#fields_parse)*
modifiedcrates/jrsonnet-stdlib/src/manifest/ini.rsdiffbeforeafterboth
--- a/crates/jrsonnet-stdlib/src/manifest/ini.rs
+++ b/crates/jrsonnet-stdlib/src/manifest/ini.rs
@@ -82,7 +82,7 @@
 	Ok(())
 }
 
-#[derive(Typed)]
+#[derive(Typed, FromUntyped)]
 struct IniObj {
 	main: Option<ObjValue>,
 	// TODO: Preserve section order?