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
before · crates/jrsonnet-evaluator/src/typed/conversions.rs
1use std::{collections::BTreeMap, marker::PhantomData, ops::Deref};23use jrsonnet_gcmodule::Trace;4use jrsonnet_interner::{IBytes, IStr};5pub use jrsonnet_macros::Typed;6use jrsonnet_types::{ComplexValType, ValType};78use crate::{9	arr::{ArrValue, BytesArray},10	bail,11	function::FuncVal,12	typed::CheckType,13	val::{IndexableVal, NumValue, StrValue, ThunkMapper},14	ObjValue, ObjValueBuilder, Result, ResultExt, Thunk, Val,15};1617#[derive(Trace)]18struct ThunkFromUntyped<K: Trace>(PhantomData<fn() -> K>);19impl<K> ThunkMapper<Val> for ThunkFromUntyped<K>20where21	K: Typed + FromUntyped + Trace,22{23	type Output = K;2425	fn map(self, from: Val) -> Result<Self::Output> {26		K::from_untyped(from)27	}28}29impl<K: Trace> Default for ThunkFromUntyped<K> {30	fn default() -> Self {31		Self(PhantomData)32	}33}34#[derive(Trace)]35struct ThunkIntoUntyped<K: Trace>(PhantomData<fn() -> K>);36impl<K> ThunkMapper<K> for ThunkIntoUntyped<K>37where38	K: Typed + Trace + IntoUntyped,39{40	type Output = Val;4142	fn map(self, from: K) -> Result<Self::Output> {43		K::into_untyped(from)44	}45}46impl<K: Trace> Default for ThunkIntoUntyped<K> {47	fn default() -> Self {48		Self(PhantomData)49	}50}5152pub trait TypedObj: Typed {53	fn serialize(self, out: &mut ObjValueBuilder) -> Result<()>;54	fn parse(obj: &ObjValue) -> Result<Self>;55	fn into_object(self) -> Result<ObjValue> {56		let mut builder = ObjValueBuilder::new();57		self.serialize(&mut builder)?;58		Ok(builder.build())59	}60}6162pub trait Typed: Sized {63	const TYPE: &'static ComplexValType;64}65pub trait IntoUntyped: Typed {66	// Whatever caller should use `into_lazy_untyped` instead of `into_untyped`67	fn provides_lazy() -> bool {68		false69	}70	fn into_untyped(typed: Self) -> Result<Val>;71	fn into_lazy_untyped(typed: Self) -> Thunk<Val> {72		Thunk::from(Self::into_untyped(typed))73	}74}75pub trait IntoUntypedResult: Typed {76	/// Hack to make builtins be able to return non-result values, and make macros able to convert those values to result77	/// This method returns identity in impl Typed for Result, and should not be overriden78	#[doc(hidden)]79	fn into_untyped_result(typed: Self) -> Result<Val>;80}81impl<T> IntoUntypedResult for T82where83	T: IntoUntyped,84{85	fn into_untyped_result(typed: Self) -> Result<Val> {86		T::into_untyped(typed)87	}88}8990pub trait FromUntyped: Typed {91	fn from_untyped(untyped: Val) -> Result<Self>;92	fn from_lazy_untyped(lazy: Thunk<Val>) -> Result<Self> {93		Self::from_untyped(lazy.evaluate()?)94	}9596	// Whatever caller should use `from_lazy_untyped` instead of `from_untyped` when possible97	fn wants_lazy() -> bool {98		false99	}100}101102impl<T> Typed for Thunk<T>103where104	T: Typed + Trace + Clone,105{106	const TYPE: &'static ComplexValType = &ComplexValType::Lazy(T::TYPE);107}108109impl<T> IntoUntyped for Thunk<T>110where111	T: Typed + IntoUntyped + Trace + Clone,112{113	fn into_untyped(typed: Self) -> Result<Val> {114		T::into_untyped(typed.evaluate()?)115	}116	fn provides_lazy() -> bool {117		true118	}119120	fn into_lazy_untyped(inner: Self) -> Thunk<Val> {121		inner.map(<ThunkIntoUntyped<T>>::default())122	}123}124125impl<T> FromUntyped for Thunk<T>126where127	T: Typed + FromUntyped + Trace + Clone,128{129	fn from_untyped(untyped: Val) -> Result<Self> {130		Self::from_lazy_untyped(Thunk::evaluated(untyped))131	}132133	fn wants_lazy() -> bool {134		true135	}136137	fn from_lazy_untyped(inner: Thunk<Val>) -> Result<Self> {138		Ok(inner.map(<ThunkFromUntyped<T>>::default()))139	}140}141142pub const MAX_SAFE_INTEGER: f64 = ((1u64 << (f64::MANTISSA_DIGITS)) - 1) as f64;143pub const MIN_SAFE_INTEGER: f64 = (-((1i64 << (f64::MANTISSA_DIGITS)) - 1)) as f64;144145macro_rules! impl_int {146	($($ty:ty)*) => {$(147		impl Typed for $ty {148			const TYPE: &'static ComplexValType =149				&ComplexValType::BoundedNumber(Some(Self::MIN as f64), Some(Self::MAX as f64));150		}151		impl FromUntyped for $ty {152			fn from_untyped(value: Val) -> Result<Self> {153				<Self as Typed>::TYPE.check(&value)?;154				match value {155					Val::Num(n) => {156						let n = n.get();157						#[allow(clippy::float_cmp)]158						if n.trunc() != n {159							bail!(160								"cannot convert number with fractional part to {}",161								stringify!($ty)162							)163						}164						Ok(n as Self)165					}166					_ => unreachable!(),167				}168			}169		}170		impl IntoUntyped for $ty {171			fn into_untyped(value: Self) -> Result<Val> {172				Ok(Val::Num(value.into()))173			}174		}175	)*};176}177178impl_int!(i8 u8 i16 u16 i32 u32);179180macro_rules! impl_bounded_int {181	($($name:ident = $ty:ty)*) => {$(182		#[derive(Clone, Copy)]183		pub struct $name<const MIN: $ty, const MAX: $ty>($ty);184		impl<const MIN: $ty, const MAX: $ty> $name<MIN, MAX> {185			pub const fn new(value: $ty) -> Option<$name<MIN, MAX>> {186				if value >= MIN && value <= MAX {187					Some(Self(value))188				} else {189					None190				}191			}192			pub const fn value(self) -> $ty {193				self.0194			}195		}196		impl<const MIN: $ty, const MAX: $ty> Deref for $name<MIN, MAX> {197			type Target = $ty;198			fn deref(&self) -> &Self::Target {199				&self.0200			}201		}202203		impl<const MIN: $ty, const MAX: $ty> Typed for $name<MIN, MAX> {204			const TYPE: &'static ComplexValType =205				&ComplexValType::BoundedNumber(206					Some(MIN as f64),207					Some(MAX as f64),208				);209		}210211		impl<const MIN: $ty, const MAX: $ty> FromUntyped for $name<MIN, MAX> {212			fn from_untyped(value: Val) -> Result<Self> {213				<Self as Typed>::TYPE.check(&value)?;214				match value {215					Val::Num(n) => {216						let n = n.get();217						#[allow(clippy::float_cmp)]218						if n.trunc() != n {219							bail!(220								"cannot convert number with fractional part to {}",221								stringify!($ty)222							)223						}224						Ok(Self(n as $ty))225					}226					_ => unreachable!(),227				}228			}229		}230231		impl<const MIN: $ty, const MAX: $ty> IntoUntyped for $name<MIN, MAX> {232			#[allow(clippy::cast_lossless)]233			fn into_untyped(value: Self) -> Result<Val> {234				Ok(Val::try_num(value.0)?)235			}236		}237	)*};238}239240impl_bounded_int!(241	BoundedI8 = i8242	BoundedI16 = i16243	BoundedI32 = i32244	BoundedI64 = i64245	BoundedUsize = usize246);247248impl Typed for f64 {249	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Num);250}251impl IntoUntyped for f64 {252	fn into_untyped(value: Self) -> Result<Val> {253		Ok(Val::try_num(value)?)254	}255}256impl FromUntyped for f64 {257	fn from_untyped(value: Val) -> Result<Self> {258		<Self as Typed>::TYPE.check(&value)?;259		match value {260			Val::Num(n) => Ok(n.get()),261			_ => unreachable!(),262		}263	}264}265266pub struct PositiveF64(pub f64);267impl Typed for PositiveF64 {268	const TYPE: &'static ComplexValType = &ComplexValType::BoundedNumber(Some(0.0), None);269}270impl IntoUntyped for PositiveF64 {271	fn into_untyped(value: Self) -> Result<Val> {272		Ok(Val::try_num(value.0)?)273	}274}275impl FromUntyped for PositiveF64 {276	fn from_untyped(value: Val) -> Result<Self> {277		<Self as Typed>::TYPE.check(&value)?;278		match value {279			Val::Num(n) => Ok(Self(n.get())),280			_ => unreachable!(),281		}282	}283}284impl Typed for usize {285	const TYPE: &'static ComplexValType =286		&ComplexValType::BoundedNumber(Some(0.0), Some(MAX_SAFE_INTEGER));287}288impl IntoUntyped for usize {289	fn into_untyped(value: Self) -> Result<Val> {290		Ok(Val::try_num(value)?)291	}292}293impl FromUntyped for usize {294	fn from_untyped(value: Val) -> Result<Self> {295		<Self as Typed>::TYPE.check(&value)?;296		match value {297			Val::Num(n) => {298				let n = n.get();299				#[allow(clippy::float_cmp)]300				if n.trunc() != n {301					bail!("cannot convert number with fractional part to usize")302				}303				Ok(n as Self)304			}305			_ => unreachable!(),306		}307	}308}309310impl Typed for IStr {311	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Str);312}313impl IntoUntyped for IStr {314	fn into_untyped(value: Self) -> Result<Val> {315		Ok(Val::string(value))316	}317}318impl FromUntyped for IStr {319	fn from_untyped(value: Val) -> Result<Self> {320		<Self as Typed>::TYPE.check(&value)?;321		match value {322			Val::Str(s) => Ok(s.into_flat()),323			_ => unreachable!(),324		}325	}326}327328impl Typed for String {329	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Str);330}331impl IntoUntyped for String {332	fn into_untyped(value: Self) -> Result<Val> {333		Ok(Val::string(value))334	}335}336impl FromUntyped for String {337	fn from_untyped(value: Val) -> Result<Self> {338		<Self as Typed>::TYPE.check(&value)?;339		match value {340			Val::Str(s) => Ok(s.to_string()),341			_ => unreachable!(),342		}343	}344}345346impl Typed for StrValue {347	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Str);348}349impl IntoUntyped for StrValue {350	fn into_untyped(value: Self) -> Result<Val> {351		Ok(Val::Str(value))352	}353}354impl FromUntyped for StrValue {355	fn from_untyped(value: Val) -> Result<Self> {356		<Self as Typed>::TYPE.check(&value)?;357		match value {358			Val::Str(s) => Ok(s),359			_ => unreachable!(),360		}361	}362}363364impl Typed for char {365	const TYPE: &'static ComplexValType = &ComplexValType::Char;366}367impl IntoUntyped for char {368	fn into_untyped(value: Self) -> Result<Val> {369		Ok(Val::string(value))370	}371}372impl FromUntyped for char {373	fn from_untyped(value: Val) -> Result<Self> {374		<Self as Typed>::TYPE.check(&value)?;375		match value {376			Val::Str(s) => Ok(s.into_flat().chars().next().unwrap()),377			_ => unreachable!(),378		}379	}380}381382// TODO: View into vec using ArrayLike?383impl<T> Typed for Vec<T>384where385	T: Typed,386{387	const TYPE: &'static ComplexValType = &ComplexValType::ArrayRef(T::TYPE);388}389impl<T: Typed + IntoUntyped> IntoUntyped for Vec<T> {390	fn into_untyped(value: Self) -> Result<Val> {391		Ok(Val::Arr(392			value393				.into_iter()394				.map(T::into_untyped)395				.collect::<Result<ArrValue>>()?,396		))397	}398}399impl<T: Typed + FromUntyped> FromUntyped for Vec<T> {400	fn from_untyped(value: Val) -> Result<Self> {401		let Val::Arr(a) = value else {402			<Self as Typed>::TYPE.check(&value)?;403			unreachable!("typecheck should fail")404		};405		a.iter()406			.enumerate()407			.map(|(i, r)| {408				r.and_then(|t| {409					T::from_untyped(t).with_description(|| format!("parsing elem <{i}>"))410				})411			})412			.collect::<Result<Self>>()413	}414}415416// TODO: View into BTreeMap using ObjectCore?417impl<K, V> Typed for BTreeMap<K, V>418where419	K: Typed + Ord,420	V: Typed,421{422	const TYPE: &'static ComplexValType = &ComplexValType::AttrsOf(V::TYPE);423}424impl<K, V> IntoUntyped for BTreeMap<K, V>425where426	K: Typed + Ord + IntoUntyped,427	V: Typed + IntoUntyped,428{429	fn into_untyped(typed: Self) -> Result<Val> {430		let mut out = ObjValueBuilder::with_capacity(typed.len());431		for (k, v) in typed {432			let Some(key) = K::into_untyped(k)?.as_str() else {433				bail!("map key should serialize to string");434			};435			let value = V::into_untyped(v)?;436			out.field(key).value(value);437		}438		Ok(Val::Obj(out.build()))439	}440}441impl<K, V> FromUntyped for BTreeMap<K, V>442where443	K: FromUntyped + Ord,444	V: FromUntyped,445{446	fn from_untyped(value: Val) -> Result<Self> {447		Self::TYPE.check(&value)?;448		let obj = value.as_obj().expect("typecheck should fail");449450		let mut out = Self::new();451		if V::wants_lazy() {452			for key in obj.fields_ex(453				false,454				#[cfg(feature = "exp-preserve-order")]455				false,456			) {457				let value = obj.get_lazy(key.clone()).expect("field exists");458				let value = V::from_lazy_untyped(value)?;459				let key = K::from_untyped(Val::Str(key.into()))?;460				let _ = out.insert(key, value);461			}462		} else {463			for (key, value) in obj.iter(464				#[cfg(feature = "exp-preserve-order")]465				false,466			) {467				let key = K::from_untyped(Val::Str(key.into()))?;468				let value = V::from_untyped(value?)?;469				let _ = out.insert(key, value);470			}471		}472		Ok(out)473	}474}475476impl Typed for Val {477	const TYPE: &'static ComplexValType = &ComplexValType::Any;478}479impl IntoUntyped for Val {480	fn into_untyped(typed: Self) -> Result<Val> {481		Ok(typed)482	}483}484impl FromUntyped for Val {485	fn from_untyped(untyped: Val) -> Result<Self> {486		Ok(untyped)487	}488}489490#[doc(hidden)]491impl<T> Typed for Result<T>492where493	T: Typed,494{495	const TYPE: &'static ComplexValType = &ComplexValType::Any;496}497impl<T: IntoUntyped> IntoUntypedResult for Result<T> {498	fn into_untyped_result(typed: Self) -> Result<Val> {499		typed.map(T::into_untyped)?500	}501}502503/// Specialization504impl Typed for IBytes {505	const TYPE: &'static ComplexValType =506		&ComplexValType::ArrayRef(&ComplexValType::BoundedNumber(Some(0.0), Some(255.0)));507}508impl IntoUntyped for IBytes {509	fn into_untyped(value: Self) -> Result<Val> {510		Ok(Val::Arr(ArrValue::bytes(value)))511	}512}513impl FromUntyped for IBytes {514	fn from_untyped(value: Val) -> Result<Self> {515		let Val::Arr(a) = &value else {516			<Self as Typed>::TYPE.check(&value)?;517			unreachable!()518		};519		if let Some(bytes) = a.as_any().downcast_ref::<BytesArray>() {520			return Ok(bytes.0.as_slice().into());521		}522		<Self as Typed>::TYPE.check(&value)?;523		// Any::downcast_ref::<ByteArray>(&a);524		let mut out = Vec::with_capacity(a.len());525		for e in a.iter() {526			let r = e?;527			out.push(u8::from_untyped(r)?);528		}529		Ok(out.as_slice().into())530	}531}532533pub struct M1;534impl Typed for M1 {535	const TYPE: &'static ComplexValType = &ComplexValType::BoundedNumber(Some(-1.0), Some(-1.0));536}537impl IntoUntyped for M1 {538	fn into_untyped(_: Self) -> Result<Val> {539		Ok(Val::Num(NumValue::new(-1.0).expect("finite")))540	}541}542impl FromUntyped for M1 {543	fn from_untyped(value: Val) -> Result<Self> {544		<Self as Typed>::TYPE.check(&value)?;545		Ok(Self)546	}547}548549macro_rules! decl_either {550	($($name: ident, $($id: ident)*);*) => {$(551		#[derive(Clone)]552		pub enum $name<$($id),*> {553			$($id($id)),*554		}555		impl<$($id),*> Typed for $name<$($id),*>556		where557			$($id: Typed,)*558		{559			const TYPE: &'static ComplexValType = &ComplexValType::UnionRef(&[$($id::TYPE),*]);560		}561		impl<$($id),*> IntoUntyped for $name<$($id),*>562		where563			$($id: Typed + IntoUntyped,)*564		{565			fn into_untyped(value: Self) -> Result<Val> {566				match value {$(567					$name::$id(v) => $id::into_untyped(v)568				),*}569			}570		}571572		impl<$($id),*> FromUntyped for $name<$($id),*>573		where574			$($id: Typed + FromUntyped,)*575		{576			fn from_untyped(value: Val) -> Result<Self> {577				$(578					if $id::TYPE.check(&value).is_ok() {579						$id::from_untyped(value).map(Self::$id)580					} else581				)* {582					<Self as Typed>::TYPE.check(&value)?;583					unreachable!()584				}585			}586		}587	)*}588}589decl_either!(590	Either1, A;591	Either2, A B;592	Either3, A B C;593	Either4, A B C D;594	Either5, A B C D E;595	Either6, A B C D E F;596	Either7, A B C D E F G597);598#[macro_export]599macro_rules! Either {600	($a:ty) => {$crate::typed::Either1<$a>};601	($a:ty, $b:ty) => {$crate::typed::Either2<$a, $b>};602	($a:ty, $b:ty, $c:ty) => {$crate::typed::Either3<$a, $b, $c>};603	($a:ty, $b:ty, $c:ty, $d:ty) => {$crate::typed::Either4<$a, $b, $c, $d>};604	($a:ty, $b:ty, $c:ty, $d:ty, $e:ty) => {$crate::typed::Either5<$a, $b, $c, $d, $e>};605	($a:ty, $b:ty, $c:ty, $d:ty, $e:ty, $f:ty) => {$crate::typed::Either6<$a, $b, $c, $d, $e, $f>};606	($a:ty, $b:ty, $c:ty, $d:ty, $e:ty, $f:ty, $g:ty) => {$crate::typed::Either7<$a, $b, $c, $d, $e, $f, $g>};607}608pub use Either;609610pub type MyType = Either![u32, f64, String];611612impl Typed for ArrValue {613	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Arr);614}615impl IntoUntyped for ArrValue {616	fn into_untyped(value: Self) -> Result<Val> {617		Ok(Val::Arr(value))618	}619}620impl FromUntyped for ArrValue {621	fn from_untyped(value: Val) -> Result<Self> {622		<Self as Typed>::TYPE.check(&value)?;623		match value {624			Val::Arr(a) => Ok(a),625			_ => unreachable!(),626		}627	}628}629630impl Typed for FuncVal {631	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Func);632}633impl IntoUntyped for FuncVal {634	fn into_untyped(value: Self) -> Result<Val> {635		Ok(Val::Func(value))636	}637}638impl FromUntyped for FuncVal {639	fn from_untyped(value: Val) -> Result<Self> {640		<Self as Typed>::TYPE.check(&value)?;641		match value {642			Val::Func(a) => Ok(a),643			_ => unreachable!(),644		}645	}646}647648impl Typed for ObjValue {649	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Obj);650}651impl IntoUntyped for ObjValue {652	fn into_untyped(value: Self) -> Result<Val> {653		Ok(Val::Obj(value))654	}655}656impl FromUntyped for ObjValue {657	fn from_untyped(value: Val) -> Result<Self> {658		<Self as Typed>::TYPE.check(&value)?;659		match value {660			Val::Obj(a) => Ok(a),661			_ => unreachable!(),662		}663	}664}665666impl Typed for bool {667	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Bool);668}669impl IntoUntyped for bool {670	fn into_untyped(value: Self) -> Result<Val> {671		Ok(Val::Bool(value))672	}673}674impl FromUntyped for bool {675	fn from_untyped(value: Val) -> Result<Self> {676		<Self as Typed>::TYPE.check(&value)?;677		match value {678			Val::Bool(a) => Ok(a),679			_ => unreachable!(),680		}681	}682}683684impl Typed for IndexableVal {685	const TYPE: &'static ComplexValType = &ComplexValType::UnionRef(&[686		&ComplexValType::Simple(ValType::Arr),687		&ComplexValType::Simple(ValType::Str),688	]);689}690impl IntoUntyped for IndexableVal {691	fn into_untyped(value: Self) -> Result<Val> {692		match value {693			Self::Str(s) => Ok(Val::string(s)),694			Self::Arr(a) => Ok(Val::Arr(a)),695		}696	}697}698impl FromUntyped for IndexableVal {699	fn from_untyped(value: Val) -> Result<Self> {700		<Self as Typed>::TYPE.check(&value)?;701		value.into_indexable()702	}703}704705pub struct Null;706impl Typed for Null {707	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Null);708}709impl IntoUntyped for Null {710	fn into_untyped(_: Self) -> Result<Val> {711		Ok(Val::Null)712	}713}714impl FromUntyped for Null {715	fn from_untyped(value: Val) -> Result<Self> {716		<Self as Typed>::TYPE.check(&value)?;717		Ok(Self)718	}719}720721impl<T> Typed for Option<T>722where723	T: Typed,724{725	const TYPE: &'static ComplexValType =726		&ComplexValType::UnionRef(&[&ComplexValType::Simple(ValType::Null), T::TYPE]);727}728impl<T> IntoUntyped for Option<T>729where730	T: Typed + IntoUntyped,731{732	fn into_untyped(typed: Self) -> Result<Val> {733		typed.map_or_else(|| Ok(Val::Null), |v| T::into_untyped(v))734	}735}736impl<T> FromUntyped for Option<T>737where738	T: Typed + FromUntyped,739{740	fn from_untyped(untyped: Val) -> Result<Self> {741		if matches!(untyped, Val::Null) {742			Ok(None)743		} else {744			T::from_untyped(untyped).map(Some)745		}746	}747}748749impl Typed for NumValue {750	const TYPE: &'static ComplexValType = &ComplexValType::Simple(ValType::Num);751}752impl IntoUntyped for NumValue {753	fn into_untyped(typed: Self) -> Result<Val> {754		Ok(Val::Num(typed))755	}756}757impl FromUntyped for NumValue {758	fn from_untyped(untyped: Val) -> Result<Self> {759		Self::TYPE.check(&untyped)?;760		match untyped {761			Val::Num(v) => Ok(v),762			_ => unreachable!(),763		}764	}765}
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?