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

difftreelog

feat(macros) #[typed(method)]

tzummrkzYaroslav Bolyukin2026-04-25parent: #9fe9fbb.patch.diff
in: master

2 files changed

modifiedcrates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth
before · crates/jrsonnet-macros/src/lib.rs
1use std::string::String;23use proc_macro2::TokenStream;4use quote::{quote, quote_spanned};5use syn::{6	Attribute, DeriveInput, Error, Expr, ExprClosure, FnArg, GenericArgument, Ident, ItemFn,7	LitStr, Meta, Pat, Path, PathArguments, Result, ReturnType, Token, Type, parenthesized,8	parse::{Parse, ParseStream},9	parse_macro_input,10	punctuated::Punctuated,11	spanned::Spanned,12	token::Comma,13};1415use self::typed::{derive_from_untyped_inner, derive_into_untyped_inner, derive_typed_inner};1617mod names;18mod typed;1920fn try_parse_attr_noargs<I>(attrs: &[Attribute], ident: I) -> Result<bool>21where22	Ident: PartialEq<I>,23{24	let attrs = attrs25		.iter()26		.filter(|a| a.path().is_ident(&ident))27		.collect::<Vec<_>>();28	if attrs.len() > 1 {29		return Err(Error::new(30			attrs[1].span(),31			"this attribute may be specified only once",32		));33	} else if attrs.is_empty() {34		return Ok(false);35	}36	let attr = attrs[0];3738	match attr.meta {39		Meta::Path(_) => Ok(true),40		_ => Ok(false),41	}42}43fn parse_attr<A: Parse, I>(attrs: &[Attribute], ident: I) -> Result<Option<A>>44where45	Ident: PartialEq<I>,46{47	let attrs = attrs48		.iter()49		.filter(|a| a.path().is_ident(&ident))50		.collect::<Vec<_>>();51	if attrs.len() > 1 {52		return Err(Error::new(53			attrs[1].span(),54			"this attribute may be specified only once",55		));56	} else if attrs.is_empty() {57		return Ok(None);58	}59	let attr = attrs[0];60	let attr = attr.parse_args::<A>()?;6162	Ok(Some(attr))63}64fn remove_attr<I>(attrs: &mut Vec<Attribute>, ident: I)65where66	Ident: PartialEq<I>,67{68	attrs.retain(|a| !a.path().is_ident(&ident));69}7071fn path_is(path: &Path, needed: &str) -> bool {72	path.leading_colon.is_none()73		&& !path.segments.is_empty()74		&& path.segments.iter().last().unwrap().ident == needed75}7677fn type_is_path<'ty>(ty: &'ty Type, needed: &str) -> Option<&'ty PathArguments> {78	match ty {79		Type::Path(path) if path.qself.is_none() && path_is(&path.path, needed) => {80			let args = &path.path.segments.iter().last().unwrap().arguments;81			Some(args)82		}83		_ => None,84	}85}8687fn extract_type_from_option(ty: &Type) -> Result<Option<&Type>> {88	let Some(args) = type_is_path(ty, "Option") else {89		return Ok(None);90	};91	// It should have only on angle-bracketed param ("<String>"):92	let PathArguments::AngleBracketed(params) = args else {93		return Err(Error::new(args.span(), "missing option generic"));94	};95	let generic_arg = params.args.iter().next().unwrap();96	// This argument must be a type:97	let GenericArgument::Type(ty) = generic_arg else {98		return Err(Error::new(99			generic_arg.span(),100			"option generic should be a type",101		));102	};103	Ok(Some(ty))104}105106struct Field {107	attrs: Vec<Attribute>,108	name: Ident,109	_colon: Token![:],110	ty: Type,111}112impl Parse for Field {113	fn parse(input: ParseStream) -> syn::Result<Self> {114		Ok(Self {115			attrs: input.call(Attribute::parse_outer)?,116			name: input.parse()?,117			_colon: input.parse()?,118			ty: input.parse()?,119		})120	}121}122123mod kw {124	syn::custom_keyword!(fields);125	syn::custom_keyword!(rename);126	syn::custom_keyword!(alias);127	syn::custom_keyword!(flatten);128	syn::custom_keyword!(add);129	syn::custom_keyword!(hide);130	syn::custom_keyword!(ok);131}132133struct BuiltinAttrs {134	fields: Vec<Field>,135}136impl Parse for BuiltinAttrs {137	fn parse(input: ParseStream) -> syn::Result<Self> {138		if input.is_empty() {139			return Ok(Self { fields: Vec::new() });140		}141		input.parse::<kw::fields>()?;142		let fields;143		parenthesized!(fields in input);144		let p = Punctuated::<Field, Comma>::parse_terminated(&fields)?;145		Ok(Self {146			fields: p.into_iter().collect(),147		})148	}149}150151enum Optionality {152	Required,153	Optional,154	Default(Expr),155	TypeDefault,156}157158#[allow(159	clippy::large_enum_variant,160	reason = "this macro is not that hot for it to matter"161)]162enum ArgInfo {163	Normal {164		ty: Box<Type>,165		optionality: Optionality,166		name: Option<String>,167		cfg_attrs: Vec<Attribute>,168	},169	Lazy {170		is_option: bool,171		name: Option<String>,172	},173	Context,174	Location,175	This,176}177178impl ArgInfo {179	fn parse(name: &str, arg: &mut FnArg) -> Result<Self> {180		let FnArg::Typed(arg) = arg else {181			unreachable!()182		};183		let ident = match &arg.pat as &Pat {184			Pat::Ident(i) => Some(i.ident.clone()),185			_ => None,186		};187		let ty = &arg.ty;188		if type_is_path(ty, "Context").is_some() {189			return Ok(Self::Context);190		} else if type_is_path(ty, "CallLocation").is_some() {191			return Ok(Self::Location);192		} else if type_is_path(ty, "Thunk").is_some() {193			return Ok(Self::Lazy {194				is_option: false,195				name: ident.map(|v| v.to_string()),196			});197		}198199		match ty as &Type {200			Type::Reference(r) if type_is_path(&r.elem, name).is_some() => return Ok(Self::This),201			_ => {}202		}203204		let (optionality, ty) = if try_parse_attr_noargs(&arg.attrs, "default")? {205			remove_attr(&mut arg.attrs, "default");206			(Optionality::TypeDefault, ty.clone())207		} else if let Some(default) = parse_attr::<_, _>(&arg.attrs, "default")? {208			remove_attr(&mut arg.attrs, "default");209			(Optionality::Default(default), ty.clone())210		} else if let Some(ty) = extract_type_from_option(ty)? {211			if type_is_path(ty, "Thunk").is_some() {212				return Ok(Self::Lazy {213					is_option: true,214					name: ident.map(|v| v.to_string()),215				});216			}217218			(Optionality::Optional, Box::new(ty.clone()))219		} else {220			(Optionality::Required, ty.clone())221		};222223		let cfg_attrs = arg224			.attrs225			.iter()226			.filter(|a| a.path().is_ident("cfg"))227			.cloned()228			.collect();229230		Ok(Self::Normal {231			ty,232			optionality,233			name: ident.map(|v| v.to_string()),234			cfg_attrs,235		})236	}237}238239#[proc_macro_attribute]240pub fn builtin(241	attr: proc_macro::TokenStream,242	item: proc_macro::TokenStream,243) -> proc_macro::TokenStream {244	let attr = parse_macro_input!(attr as BuiltinAttrs);245	let item_fn = parse_macro_input!(item as ItemFn);246247	match builtin_inner(attr, item_fn) {248		Ok(v) => v.into(),249		Err(e) => e.into_compile_error().into(),250	}251}252253#[allow(clippy::too_many_lines)]254fn builtin_inner(attr: BuiltinAttrs, mut fun: ItemFn) -> syn::Result<TokenStream> {255	let ReturnType::Type(_, result) = &fun.sig.output else {256		return Err(Error::new(257			fun.sig.span(),258			"builtin should return something",259		));260	};261262	let name = fun.sig.ident.to_string();263	let args = fun264		.sig265		.inputs266		.iter_mut()267		.map(|arg| ArgInfo::parse(&name, arg))268		.collect::<Result<Vec<_>>>()?;269270	let params_desc = args.iter().filter_map(|a| match a {271		ArgInfo::Normal {272			optionality,273			name,274			cfg_attrs,275			..276		} => {277			let name = name278				.as_ref()279				.map_or_else(|| quote! {unnamed}, |n| quote! {named(#n)});280			let default = match optionality {281				Optionality::Required => quote!(ParamDefault::None),282				Optionality::Optional | Optionality::TypeDefault => quote!(ParamDefault::Exists),283				Optionality::Default(e) => quote!(ParamDefault::Literal(stringify!(#e))),284			};285			Some(quote! {286				#(#cfg_attrs)*287				[#name => #default],288			})289		}290		ArgInfo::Lazy { is_option, name } => {291			let name = name292				.as_ref()293				.map_or_else(|| quote! {unnamed}, |n| quote! {named(#n)});294			Some(quote! {295				[#name => ParamDefault::exists(#is_option)],296			})297		}298		ArgInfo::Context | ArgInfo::Location | ArgInfo::This => None,299	});300301	let mut id = 0usize;302	let pass = args303		.iter()304		.map(|a| match a {305			ArgInfo::Normal { .. } | ArgInfo::Lazy { .. } => {306				let cid = id;307				id += 1;308				(quote! {#cid}, a)309			}310			ArgInfo::Context | ArgInfo::Location | ArgInfo::This => {311				(quote! {compile_error!("should not use id")}, a)312			}313		})314		.map(|(id, a)| match a {315			ArgInfo::Normal {316				ty,317				optionality,318				name,319				cfg_attrs,320			} => {321				let name = name.as_ref().map_or("<unnamed>", String::as_str);322				let eval = quote! {jrsonnet_evaluator::in_description_frame(323					|| format!("argument <{}> evaluation", #name),324					|| <#ty as FromUntyped>::from_untyped(value.evaluate()?),325				)?};326				let value = match optionality {327					Optionality::Required => quote! {{328						let value = parsed[#id].as_ref().expect("args shape is checked");329						#eval330					},},331					Optionality::Optional => quote! {if let Some(value) = &parsed[#id] {332						Some(#eval)333					} else {334						None335					},},336					Optionality::Default(expr) => quote! {if let Some(value) = &parsed[#id] {337						#eval338					} else {339						let v: #ty = #expr;340						v341					},},342					Optionality::TypeDefault => quote! {if let Some(value) = &parsed[#id] {343						#eval344					} else {345						let v: #ty = Default::default();346						v347					},},348				};349				quote! {350					#(#cfg_attrs)*351					#value352				}353			}354			ArgInfo::Lazy { is_option, .. } => {355				if *is_option {356					quote! {if let Some(value) = &parsed[#id] {357						Some(value.clone())358					} else {359						None360					},}361				} else {362					quote! {363						parsed[#id].as_ref().expect("args shape is correct").clone(),364					}365				}366			}367			ArgInfo::Context => quote! {ctx.clone(),},368			ArgInfo::Location => quote! {location,},369			ArgInfo::This => quote! {self,},370		});371372	let fields = attr.fields.iter().map(|field| {373		let attrs = &field.attrs;374		let name = &field.name;375		let ty = &field.ty;376		quote! {377			#(#attrs)*378			pub #name: #ty,379		}380	});381382	let name = &fun.sig.ident;383	let vis = &fun.vis;384	let static_ext = if attr.fields.is_empty() {385		quote! {386			impl #name {387				pub const INST: &'static dyn StaticBuiltin = &#name {};388			}389			impl StaticBuiltin for #name {}390		}391	} else {392		quote! {}393	};394	let static_derive_copy = if attr.fields.is_empty() {395		quote! {, Copy}396	} else {397		quote! {}398	};399400	Ok(quote! {401		#fun402403		#[doc(hidden)]404		#[allow(non_camel_case_types)]405		#[derive(Clone, jrsonnet_gcmodule::Trace #static_derive_copy)]406		#vis struct #name {407			#(#fields)*408		}409		const _: () = {410			use ::jrsonnet_evaluator::{411				State, Val,412				function::{builtin::{Builtin, StaticBuiltin}, FunctionSignature, ParamParse, ParamName, ParamDefault, CallLocation},413				Result, Context, typed::{Typed, FromUntyped, IntoUntypedResult},414				parser::Span, params, Thunk,415			};416			params!(417				#(#params_desc)*418			);419420			#static_ext421			impl Builtin for #name422			where423				Self: 'static424			{425				fn name(&self) -> &str {426					stringify!(#name)427				}428				fn params(&self) -> FunctionSignature {429					PARAMS.with(|p| p.clone())430				}431				#[allow(unused_variables)]432				fn call(&self, location: CallLocation<'_>, parsed: &[Option<Thunk<Val>>]) -> Result<Val> {433					let result: #result = #name(#(#pass)*);434					<_ as IntoUntypedResult>::into_untyped_result(result)435				}436				fn as_any(&self) -> &dyn ::std::any::Any {437					self438				}439			}440		};441	})442}443444#[proc_macro_derive(Typed, attributes(typed))]445pub fn derive_typed(item: proc_macro::TokenStream) -> proc_macro::TokenStream {446	let input = parse_macro_input!(item as DeriveInput);447448	match derive_typed_inner(input) {449		Ok(v) => v.into(),450		Err(e) => e.to_compile_error().into(),451	}452}453#[proc_macro_derive(IntoUntyped, attributes(typed))]454pub fn derive_into_untyped(item: proc_macro::TokenStream) -> proc_macro::TokenStream {455	let input = parse_macro_input!(item as DeriveInput);456457	match derive_into_untyped_inner(input) {458		Ok(v) => v.into(),459		Err(e) => e.to_compile_error().into(),460	}461}462#[proc_macro_derive(FromUntyped, attributes(typed))]463pub fn derive_from_untyped(item: proc_macro::TokenStream) -> proc_macro::TokenStream {464	let input = parse_macro_input!(item as DeriveInput);465466	match derive_from_untyped_inner(input) {467		Ok(v) => v.into(),468		Err(e) => e.to_compile_error().into(),469	}470}471472struct FormatInput {473	formatting: LitStr,474	arguments: Vec<Expr>,475}476impl Parse for FormatInput {477	fn parse(input: ParseStream) -> Result<Self> {478		let formatting = input.parse()?;479		let mut arguments = Vec::new();480481		while input.peek(Token![,]) {482			input.parse::<Token![,]>()?;483			if input.is_empty() {484				// Trailing comma485				break;486			}487			let expr = input.parse()?;488			arguments.push(expr);489		}490491		if !input.is_empty() {492			return Err(syn::Error::new(input.span(), "unexpected trailing input"));493		}494495		Ok(Self {496			formatting,497			arguments,498		})499	}500}501fn is_format_str(i: &str) -> bool {502	let mut is_plain = true;503	// -1 = {504	// +1 = }505	let mut is_bracket = 0i8;506	for ele in i.chars() {507		match ele {508			'{' if is_bracket == -1 => {509				is_bracket = 0;510			}511			'}' if is_bracket == -1 => {512				is_plain = false;513				break;514			}515			'}' if is_bracket == 1 => {516				is_bracket = 0;517			}518			'{' if is_bracket == 1 => {519				is_plain = false;520				break;521			}522			'{' => {523				is_bracket = -1;524			}525			'}' => {526				is_bracket = 1;527			}528			_ if is_bracket != 0 => {529				is_plain = false;530				break;531			}532			_ => {}533		}534	}535	!is_plain || is_bracket != 0536}537impl FormatInput {538	fn expand(self) -> TokenStream {539		let format = self.formatting;540		if is_format_str(&format.value()) {541			let args = self.arguments;542			quote! {543				::jrsonnet_evaluator::IStr::from(format!(#format #(, #args)*))544			}545		} else {546			if let Some(first) = self.arguments.first() {547				return syn::Error::new(548					first.span(),549					"string has no formatting codes, it should not have the arguments",550				)551				.into_compile_error();552			}553			quote! {554				::jrsonnet_evaluator::IStr::from(#format)555			}556		}557	}558}559560/// `IStr` formatting helper561///562/// Using `format!("literal with no codes").into()` is slower than just `"literal with no codes".into()`563/// This macro looks for formatting codes in the input string, and uses564/// `format!()` only when necessary565#[proc_macro]566pub fn format_istr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {567	let input = parse_macro_input!(input as FormatInput);568	input.expand().into()569}570571/// Create Thunk using closure syntax572#[proc_macro]573#[allow(non_snake_case)]574pub fn Thunk(input: proc_macro::TokenStream) -> proc_macro::TokenStream {575	let input = parse_macro_input!(input as ExprClosure);576577	let span = input.inputs.span();578	let move_check = input.capture.is_none().then(|| {579		quote_spanned! {span => {580			compile_error!("Thunk! needs to be called with move closure");581		}}582	});583584	let (env, closure, args) = syn_dissect_closure::split_env(input);585586	let trace_check = args.iter().map(|el| {587		let span = el.span();588		quote_spanned! {span => ::jrsonnet_evaluator::gc::assert_trace(&#el);}589	});590591	quote! {{592		#move_check593		#(#trace_check)*594		::jrsonnet_evaluator::Thunk::new(::jrsonnet_evaluator::val::MemoizedClosureThunk::new(#env, #closure))595	}}.into()596}
modifiedcrates/jrsonnet-macros/src/typed.rsdiffbeforeafterboth
--- a/crates/jrsonnet-macros/src/typed.rs
+++ b/crates/jrsonnet-macros/src/typed.rs
@@ -1,10 +1,10 @@
 use proc_macro2::TokenStream;
 use quote::quote;
 use syn::{
-	DeriveInput, Error, Ident, LitStr, Result, Token, Type, parenthesized,
+	parenthesized,
 	parse::{Parse, ParseStream},
 	spanned::Spanned as _,
-	token,
+	token, DeriveInput, Error, Ident, LitStr, Result, Token, Type,
 };
 
 use crate::{extract_type_from_option, kw, names::Names, parse_attr, type_is_path};
@@ -22,6 +22,8 @@
 	add: bool,
 	// Should it be `field::` instead of `field:`
 	hide: bool,
+	// Builtin value
+	method: bool,
 }
 impl Parse for TypedAttr {
 	fn parse(input: ParseStream) -> syn::Result<Self> {
@@ -64,6 +66,9 @@
 			} else if lookahead.peek(kw::hide) {
 				input.parse::<kw::hide>()?;
 				out.hide = true;
+			} else if lookahead.peek(kw::method) {
+				input.parse::<kw::method>()?;
+				out.method = true;
 			} else if input.is_empty() {
 				break;
 			} else {
@@ -134,7 +139,7 @@
 	}
 
 	fn expand_field(&self) -> Option<TokenStream> {
-		if self.is_option {
+		if self.is_option || self.attr.method {
 			return None;
 		}
 		let name = self.name()?;
@@ -256,7 +261,11 @@
 				} else {
 					quote! {}
 				};
-				let value = if self.is_lazy {
+				let value = if self.attr.method {
+					quote! {
+						out.method(__names[#name].clone(), value);
+					}
+				} else if self.is_lazy {
 					quote! {
 						out.field(__names[#name].clone())
 							#hide