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
before · crates/jrsonnet-evaluator/src/arr/spec.rs
1use std::rc::Rc;2use std::{any::Any, cell::RefCell, fmt::Debug, mem::replace};34use jrsonnet_gcmodule::{Cc, Trace};5use jrsonnet_interner::{IBytes, IStr};6use jrsonnet_parser::{Expr, Spanned};78use super::ArrValue;9use crate::function::NativeFn;10use crate::{11	error::ErrorKind::InfiniteRecursionDetected,12	evaluate,13	typed::{IntoUntyped, Typed},14	val::ThunkValue,15	Context, Error, ObjValue, Result, Thunk, Val,16};1718pub trait ArrayLike: Any + Trace + Debug {19	fn len(&self) -> usize;20	fn is_empty(&self) -> bool {21		self.len() == 022	}23	fn get(&self, index: usize) -> Result<Option<Val>>;24	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>>;25	fn get_cheap(&self, index: usize) -> Option<Val>;2627	fn is_cheap(&self) -> bool;28}2930#[derive(Debug, Trace)]31pub struct SliceArray {32	pub(crate) inner: ArrValue,33	pub(crate) from: u32,34	pub(crate) to: u32,35	pub(crate) step: u32,36}3738impl SliceArray {39	fn map_idx(&self, index: usize) -> usize {40		self.from as usize + self.step as usize * index41	}42}43impl ArrayLike for SliceArray {44	fn len(&self) -> usize {45		(self.to - self.from).div_ceil(self.step) as usize46	}4748	fn get(&self, index: usize) -> Result<Option<Val>> {49		self.inner.get(self.map_idx(index))50	}5152	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {53		self.inner.get_lazy(self.map_idx(index))54	}5556	fn get_cheap(&self, index: usize) -> Option<Val> {57		self.inner.get_cheap(self.map_idx(index))58	}59	fn is_cheap(&self) -> bool {60		self.inner.is_cheap()61	}62}6364#[derive(Trace, Debug)]65pub struct CharArray(pub Vec<char>);66impl ArrayLike for CharArray {67	fn len(&self) -> usize {68		self.0.len()69	}7071	fn get(&self, index: usize) -> Result<Option<Val>> {72		Ok(self.get_cheap(index))73	}7475	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {76		self.get_cheap(index).map(Thunk::evaluated)77	}7879	fn get_cheap(&self, index: usize) -> Option<Val> {80		self.0.get(index).map(|v| Val::string(*v))81	}82	fn is_cheap(&self) -> bool {83		true84	}85}8687#[derive(Trace, Debug)]88pub struct BytesArray(pub IBytes);89impl ArrayLike for BytesArray {90	fn len(&self) -> usize {91		self.0.len()92	}9394	fn get(&self, index: usize) -> Result<Option<Val>> {95		Ok(self.get_cheap(index))96	}9798	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {99		self.get_cheap(index).map(Thunk::evaluated)100	}101102	fn get_cheap(&self, index: usize) -> Option<Val> {103		self.0.get(index).map(|v| Val::Num((*v).into()))104	}105	fn is_cheap(&self) -> bool {106		true107	}108}109110#[derive(Debug, Trace, Clone)]111enum ArrayThunk {112	Computed(Val),113	Errored(Error),114	Waiting,115	Pending,116}117118#[derive(Debug, Trace, Clone)]119pub struct ExprArray {120	ctx: Context,121	src: Rc<Vec<Spanned<Expr>>>,122	cached: Cc<RefCell<Vec<ArrayThunk>>>,123}124impl ExprArray {125	pub fn new(ctx: Context, src: Rc<Vec<Spanned<Expr>>>) -> Self {126		Self {127			ctx,128			cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; src.len()])),129			src,130		}131	}132}133impl ArrayLike for ExprArray {134	fn len(&self) -> usize {135		self.cached.borrow().len()136	}137	fn get(&self, index: usize) -> Result<Option<Val>> {138		if index >= self.len() {139			return Ok(None);140		}141		match &self.cached.borrow()[index] {142			ArrayThunk::Computed(c) => return Ok(Some(c.clone())),143			ArrayThunk::Errored(e) => return Err(e.clone()),144			ArrayThunk::Pending => return Err(InfiniteRecursionDetected.into()),145			ArrayThunk::Waiting => {}146		}147148		let ArrayThunk::Waiting =149			replace(&mut self.cached.borrow_mut()[index], ArrayThunk::Pending)150		else {151			unreachable!()152		};153154		let new_value = match evaluate(self.ctx.clone(), &self.src[index]) {155			Ok(v) => v,156			Err(e) => {157				self.cached.borrow_mut()[index] = ArrayThunk::Errored(e.clone());158				return Err(e);159			}160		};161		self.cached.borrow_mut()[index] = ArrayThunk::Computed(new_value.clone());162		Ok(Some(new_value))163	}164	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {165		#[derive(Trace)]166		struct ExprArrThunk {167			expr: ExprArray,168			index: usize,169		}170		impl ThunkValue for ExprArrThunk {171			type Output = Val;172173			fn get(&self) -> Result<Self::Output> {174				self.expr175					.get(self.index)176					.transpose()177					.expect("index checked")178			}179		}180181		if index >= self.len() {182			return None;183		}184		match &self.cached.borrow()[index] {185			ArrayThunk::Computed(c) => return Some(Thunk::evaluated(c.clone())),186			ArrayThunk::Errored(e) => return Some(Thunk::errored(e.clone())),187			ArrayThunk::Waiting | ArrayThunk::Pending => {}188		}189190		Some(Thunk::new(ExprArrThunk {191			expr: self.clone(),192			index,193		}))194	}195	fn get_cheap(&self, _index: usize) -> Option<Val> {196		None197	}198	fn is_cheap(&self) -> bool {199		false200	}201}202203#[derive(Trace, Debug)]204pub struct ExtendedArray {205	pub a: ArrValue,206	pub b: ArrValue,207	split: usize,208	len: usize,209}210impl ExtendedArray {211	pub fn new(a: ArrValue, b: ArrValue) -> Self {212		let a_len = a.len();213		let b_len = b.len();214		Self {215			a,216			b,217			split: a_len,218			len: a_len.checked_add(b_len).expect("too large array value"),219		}220	}221}222223struct WithExactSize<I>(I, usize);224impl<I, T> Iterator for WithExactSize<I>225where226	I: Iterator<Item = T>,227{228	type Item = T;229230	fn next(&mut self) -> Option<Self::Item> {231		self.0.next()232	}233	fn nth(&mut self, n: usize) -> Option<Self::Item> {234		self.0.nth(n)235	}236	fn size_hint(&self) -> (usize, Option<usize>) {237		(self.1, Some(self.1))238	}239}240impl<I> DoubleEndedIterator for WithExactSize<I>241where242	I: DoubleEndedIterator,243{244	fn next_back(&mut self) -> Option<Self::Item> {245		self.0.next_back()246	}247	fn nth_back(&mut self, n: usize) -> Option<Self::Item> {248		self.0.nth_back(n)249	}250}251impl<I> ExactSizeIterator for WithExactSize<I>252where253	I: Iterator,254{255	fn len(&self) -> usize {256		self.1257	}258}259impl ArrayLike for ExtendedArray {260	fn get(&self, index: usize) -> Result<Option<Val>> {261		if self.split > index {262			self.a.get(index)263		} else {264			self.b.get(index - self.split)265		}266	}267	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {268		if self.split > index {269			self.a.get_lazy(index)270		} else {271			self.b.get_lazy(index - self.split)272		}273	}274275	fn len(&self) -> usize {276		self.len277	}278279	fn get_cheap(&self, index: usize) -> Option<Val> {280		if self.split > index {281			self.a.get_cheap(index)282		} else {283			self.b.get_cheap(index - self.split)284		}285	}286	fn is_cheap(&self) -> bool {287		self.a.is_cheap() && self.b.is_cheap()288	}289}290291#[derive(Trace, Debug)]292pub struct LazyArray(pub Vec<Thunk<Val>>);293impl ArrayLike for LazyArray {294	fn len(&self) -> usize {295		self.0.len()296	}297	fn get(&self, index: usize) -> Result<Option<Val>> {298		let Some(v) = self.0.get(index) else {299			return Ok(None);300		};301		v.evaluate().map(Some)302	}303	fn get_cheap(&self, _index: usize) -> Option<Val> {304		None305	}306	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {307		self.0.get(index).cloned()308	}309	fn is_cheap(&self) -> bool {310		false311	}312}313314#[derive(Trace, Debug)]315pub struct EagerArray(pub Vec<Val>);316impl ArrayLike for EagerArray {317	fn len(&self) -> usize {318		self.0.len()319	}320321	fn get(&self, index: usize) -> Result<Option<Val>> {322		Ok(self.0.get(index).cloned())323	}324325	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {326		self.0.get(index).cloned().map(Thunk::evaluated)327	}328329	fn get_cheap(&self, index: usize) -> Option<Val> {330		self.0.get(index).cloned()331	}332	fn is_cheap(&self) -> bool {333		true334	}335}336337/// Inclusive range type338#[derive(Debug, Trace, PartialEq, Eq)]339pub struct RangeArray {340	start: i32,341	end: i32,342}343impl RangeArray {344	pub fn empty() -> Self {345		Self::new_exclusive(0, 0)346	}347	pub fn new_exclusive(start: i32, end: i32) -> Self {348		end.checked_sub(1)349			.map_or_else(Self::empty, |end| Self { start, end })350	}351	pub fn new_inclusive(start: i32, end: i32) -> Self {352		Self { start, end }353	}354	fn range(&self) -> impl ExactSizeIterator<Item = i32> + DoubleEndedIterator {355		WithExactSize(356			self.start..=self.end,357			(self.end as usize)358				.wrapping_sub(self.start as usize)359				.wrapping_add(1),360		)361	}362}363364impl ArrayLike for RangeArray {365	fn len(&self) -> usize {366		self.range().len()367	}368	fn is_empty(&self) -> bool {369		self.range().len() == 0370	}371372	fn get(&self, index: usize) -> Result<Option<Val>> {373		Ok(self.get_cheap(index))374	}375376	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {377		self.get_cheap(index).map(Thunk::evaluated)378	}379380	fn get_cheap(&self, index: usize) -> Option<Val> {381		self.range().nth(index).map(|i| Val::Num(i.into()))382	}383	fn is_cheap(&self) -> bool {384		true385	}386}387388#[derive(Debug, Trace)]389pub struct ReverseArray(pub ArrValue);390impl ArrayLike for ReverseArray {391	fn len(&self) -> usize {392		self.0.len()393	}394395	fn get(&self, index: usize) -> Result<Option<Val>> {396		self.0.get(self.0.len() - index - 1)397	}398399	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {400		self.0.get_lazy(self.0.len() - index - 1)401	}402403	fn get_cheap(&self, index: usize) -> Option<Val> {404		self.0.get_cheap(self.0.len() - index - 1)405	}406	fn is_cheap(&self) -> bool {407		self.0.is_cheap()408	}409}410411#[derive(Trace, Clone, Debug)]412pub enum ArrayMapper {413	Plain(NativeFn!((Val) -> Val)),414	WithIndex(NativeFn!((u32, Val) -> Val)),415}416417#[derive(Trace, Debug, Clone)]418pub struct MappedArray {419	inner: ArrValue,420	cached: Cc<RefCell<Vec<ArrayThunk>>>,421	mapper: ArrayMapper,422}423impl MappedArray {424	pub fn new(inner: ArrValue, mapper: ArrayMapper) -> Self {425		let len = inner.len();426		Self {427			inner,428			cached: Cc::new(RefCell::new(vec![ArrayThunk::Waiting; len])),429			mapper,430		}431	}432	fn evaluate(&self, index: usize, value: Val) -> Result<Val> {433		match &self.mapper {434			ArrayMapper::Plain(f) => f.call(value),435			ArrayMapper::WithIndex(f) => f.call(index as u32, value),436		}437	}438}439impl ArrayLike for MappedArray {440	fn len(&self) -> usize {441		self.cached.borrow().len()442	}443444	fn get(&self, index: usize) -> Result<Option<Val>> {445		if index >= self.len() {446			return Ok(None);447		}448		match &self.cached.borrow()[index] {449			ArrayThunk::Computed(c) => return Ok(Some(c.clone())),450			ArrayThunk::Errored(e) => return Err(e.clone()),451			ArrayThunk::Pending => return Err(InfiniteRecursionDetected.into()),452			ArrayThunk::Waiting => {}453		}454455		let ArrayThunk::Waiting =456			replace(&mut self.cached.borrow_mut()[index], ArrayThunk::Pending)457		else {458			unreachable!()459		};460461		let val = self462			.inner463			.get(index)464			.transpose()465			.expect("index checked")466			.and_then(|r| self.evaluate(index, r));467468		let new_value = match val {469			Ok(v) => v,470			Err(e) => {471				self.cached.borrow_mut()[index] = ArrayThunk::Errored(e.clone());472				return Err(e);473			}474		};475		self.cached.borrow_mut()[index] = ArrayThunk::Computed(new_value.clone());476		Ok(Some(new_value))477	}478	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {479		#[derive(Trace)]480		struct MappedArrayThunk {481			arr: MappedArray,482			index: usize,483		}484		impl ThunkValue for MappedArrayThunk {485			type Output = Val;486487			fn get(&self) -> Result<Self::Output> {488				self.arr.get(self.index).transpose().expect("index checked")489			}490		}491492		if index >= self.len() {493			return None;494		}495		match &self.cached.borrow()[index] {496			ArrayThunk::Computed(c) => return Some(Thunk::evaluated(c.clone())),497			ArrayThunk::Errored(e) => return Some(Thunk::errored(e.clone())),498			ArrayThunk::Waiting | ArrayThunk::Pending => {}499		}500501		Some(Thunk::new(MappedArrayThunk {502			arr: self.clone(),503			index,504		}))505	}506507	fn get_cheap(&self, _index: usize) -> Option<Val> {508		None509	}510	fn is_cheap(&self) -> bool {511		false512	}513}514515#[derive(Trace, Debug)]516pub struct RepeatedArray {517	data: ArrValue,518	repeats: usize,519	total_len: usize,520}521impl RepeatedArray {522	pub fn new(data: ArrValue, repeats: usize) -> Option<Self> {523		let total_len = data.len().checked_mul(repeats)?;524		Some(Self {525			data,526			repeats,527			total_len,528		})529	}530}531532impl ArrayLike for RepeatedArray {533	fn len(&self) -> usize {534		self.total_len535	}536537	fn get(&self, index: usize) -> Result<Option<Val>> {538		if index > self.total_len {539			return Ok(None);540		}541		self.data.get(index % self.data.len())542	}543544	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {545		if index > self.total_len {546			return None;547		}548		self.data.get_lazy(index % self.data.len())549	}550551	fn get_cheap(&self, index: usize) -> Option<Val> {552		if index > self.total_len {553			return None;554		}555		self.data.get_cheap(index % self.data.len())556	}557	fn is_cheap(&self) -> bool {558		self.data.is_cheap()559	}560}561562#[derive(Trace, Debug)]563pub struct PickObjectValues {564	obj: ObjValue,565	keys: Vec<IStr>,566}567568impl PickObjectValues {569	pub fn new(obj: ObjValue, keys: Vec<IStr>) -> Self {570		Self { obj, keys }571	}572}573574impl ArrayLike for PickObjectValues {575	fn len(&self) -> usize {576		self.keys.len()577	}578579	fn get(&self, index: usize) -> Result<Option<Val>> {580		let Some(key) = self.keys.get(index) else {581			return Ok(None);582		};583		Ok(Some(self.obj.get_or_bail(key.clone())?))584	}585586	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {587		let key = self.keys.get(index)?;588		Some(self.obj.get_lazy_or_bail(key.clone()))589	}590591	fn get_cheap(&self, _index: usize) -> Option<Val> {592		None593	}594595	fn is_cheap(&self) -> bool {596		false597	}598}599600#[derive(Trace, Debug)]601pub struct PickObjectKeyValues {602	obj: ObjValue,603	keys: Vec<IStr>,604}605606impl PickObjectKeyValues {607	pub fn new(obj: ObjValue, keys: Vec<IStr>) -> Self {608		Self { obj, keys }609	}610}611612#[derive(Typed)]613pub struct KeyValue {614	key: IStr,615	value: Thunk<Val>,616}617618impl ArrayLike for PickObjectKeyValues {619	fn len(&self) -> usize {620		self.keys.len()621	}622623	fn get(&self, index: usize) -> Result<Option<Val>> {624		let Some(key) = self.keys.get(index) else {625			return Ok(None);626		};627		Ok(Some(628			KeyValue::into_untyped(KeyValue {629				key: key.clone(),630				value: Thunk::evaluated(self.obj.get_or_bail(key.clone())?),631			})632			.expect("convertible"),633		))634	}635636	fn get_lazy(&self, index: usize) -> Option<Thunk<Val>> {637		let key = self.keys.get(index)?;638		// Nothing can fail in the key part, yet value is still639		// lazy-evaluated640		Some(Thunk::evaluated(641			KeyValue::into_untyped(KeyValue {642				key: key.clone(),643				value: self.obj.get_lazy_or_bail(key.clone()),644			})645			.expect("convertible"),646		))647	}648649	fn get_cheap(&self, _index: usize) -> Option<Val> {650		None651	}652653	fn is_cheap(&self) -> bool {654		false655	}656}
modifiedcrates/jrsonnet-evaluator/src/typed/conversions.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/typed/conversions.rs
+++ b/crates/jrsonnet-evaluator/src/typed/conversions.rs
@@ -2,7 +2,6 @@
 
 use jrsonnet_gcmodule::Trace;
 use jrsonnet_interner::{IBytes, IStr};
-pub use jrsonnet_macros::Typed;
 use jrsonnet_types::{ComplexValType, ValType};
 
 use crate::{
@@ -14,6 +13,19 @@
 	ObjValue, ObjValueBuilder, Result, ResultExt, Thunk, Val,
 };
 
+#[doc(hidden)]
+pub mod __typed_macro_prelude {
+	pub use ::jrsonnet_evaluator::{
+		error::{ErrorKind, Result as JrResult},
+		typed::{
+			CheckType, ComplexValType, FromUntyped, IntoUntyped, ParseTypedObj, SerializeTypedObj,
+			Typed,
+		},
+		IStr, ObjValue, ObjValueBuilder, State, Val,
+	};
+}
+pub use jrsonnet_macros::{FromUntyped, IntoUntyped, Typed};
+
 #[derive(Trace)]
 struct ThunkFromUntyped<K: Trace>(PhantomData<fn() -> K>);
 impl<K> ThunkMapper<Val> for ThunkFromUntyped<K>
@@ -49,9 +61,11 @@
 	}
 }
 
-pub trait TypedObj: Typed {
-	fn serialize(self, out: &mut ObjValueBuilder) -> Result<()>;
+pub trait ParseTypedObj: Typed {
 	fn parse(obj: &ObjValue) -> Result<Self>;
+}
+pub trait SerializeTypedObj: Typed {
+	fn serialize(self, out: &mut ObjValueBuilder) -> Result<()>;
 	fn into_object(self) -> Result<ObjValue> {
 		let mut builder = ObjValueBuilder::new();
 		self.serialize(&mut builder)?;
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?