1use std::string::String;23use proc_macro2::TokenStream;4use quote::{quote, quote_spanned};5use syn::{6 parenthesized,7 parse::{Parse, ParseStream},8 parse_macro_input,9 punctuated::Punctuated,10 spanned::Spanned,11 token::{self, Comma},12 Attribute, DeriveInput, Error, Expr, ExprClosure, FnArg, GenericArgument, Ident, ItemFn,13 LitStr, Pat, Path, PathArguments, Result, ReturnType, Token, Type,14};1516fn parse_attr<A: Parse, I>(attrs: &[Attribute], ident: I) -> Result<Option<A>>17where18 Ident: PartialEq<I>,19{20 let attrs = attrs21 .iter()22 .filter(|a| a.path().is_ident(&ident))23 .collect::<Vec<_>>();24 if attrs.len() > 1 {25 return Err(Error::new(26 attrs[1].span(),27 "this attribute may be specified only once",28 ));29 } else if attrs.is_empty() {30 return Ok(None);31 }32 let attr = attrs[0];33 let attr = attr.parse_args::<A>()?;3435 Ok(Some(attr))36}37fn remove_attr<I>(attrs: &mut Vec<Attribute>, ident: I)38where39 Ident: PartialEq<I>,40{41 attrs.retain(|a| !a.path().is_ident(&ident));42}4344fn path_is(path: &Path, needed: &str) -> bool {45 path.leading_colon.is_none()46 && !path.segments.is_empty()47 && path.segments.iter().last().unwrap().ident == needed48}4950fn type_is_path<'ty>(ty: &'ty Type, needed: &str) -> Option<&'ty PathArguments> {51 match ty {52 Type::Path(path) if path.qself.is_none() && path_is(&path.path, needed) => {53 let args = &path.path.segments.iter().last().unwrap().arguments;54 Some(args)55 }56 _ => None,57 }58}5960fn extract_type_from_option(ty: &Type) -> Result<Option<&Type>> {61 let Some(args) = type_is_path(ty, "Option") else {62 return Ok(None);63 };64 65 let PathArguments::AngleBracketed(params) = args else {66 return Err(Error::new(args.span(), "missing option generic"));67 };68 let generic_arg = params.args.iter().next().unwrap();69 70 let GenericArgument::Type(ty) = generic_arg else {71 return Err(Error::new(72 generic_arg.span(),73 "option generic should be a type",74 ));75 };76 Ok(Some(ty))77}7879struct Field {80 attrs: Vec<Attribute>,81 name: Ident,82 _colon: Token![:],83 ty: Type,84}85impl Parse for Field {86 fn parse(input: ParseStream) -> syn::Result<Self> {87 Ok(Self {88 attrs: input.call(Attribute::parse_outer)?,89 name: input.parse()?,90 _colon: input.parse()?,91 ty: input.parse()?,92 })93 }94}9596mod kw {97 syn::custom_keyword!(fields);98 syn::custom_keyword!(rename);99 syn::custom_keyword!(alias);100 syn::custom_keyword!(flatten);101 syn::custom_keyword!(add);102 syn::custom_keyword!(hide);103 syn::custom_keyword!(ok);104}105106struct BuiltinAttrs {107 fields: Vec<Field>,108}109impl Parse for BuiltinAttrs {110 fn parse(input: ParseStream) -> syn::Result<Self> {111 if input.is_empty() {112 return Ok(Self { fields: Vec::new() });113 }114 input.parse::<kw::fields>()?;115 let fields;116 parenthesized!(fields in input);117 let p = Punctuated::<Field, Comma>::parse_terminated(&fields)?;118 Ok(Self {119 fields: p.into_iter().collect(),120 })121 }122}123124enum Optionality {125 Required,126 Optional,127 Default(Expr),128}129130#[allow(clippy::large_enum_variant, reason = "this macro is not that hot for it to matter")]131enum ArgInfo {132 Normal {133 ty: Box<Type>,134 optionality: Optionality,135 name: Option<String>,136 cfg_attrs: Vec<Attribute>,137 },138 Lazy {139 is_option: bool,140 name: Option<String>,141 },142 Context,143 Location,144 This,145}146147impl ArgInfo {148 fn parse(name: &str, arg: &mut FnArg) -> Result<Self> {149 let FnArg::Typed(arg) = arg else {150 unreachable!()151 };152 let ident = match &arg.pat as &Pat {153 Pat::Ident(i) => Some(i.ident.clone()),154 _ => None,155 };156 let ty = &arg.ty;157 if type_is_path(ty, "Context").is_some() {158 return Ok(Self::Context);159 } else if type_is_path(ty, "CallLocation").is_some() {160 return Ok(Self::Location);161 } else if type_is_path(ty, "Thunk").is_some() {162 return Ok(Self::Lazy {163 is_option: false,164 name: ident.map(|v| v.to_string()),165 });166 }167168 match ty as &Type {169 Type::Reference(r) if type_is_path(&r.elem, name).is_some() => return Ok(Self::This),170 _ => {}171 }172173 let (optionality, ty) = if let Some(default) = parse_attr::<_, _>(&arg.attrs, "default")? {174 remove_attr(&mut arg.attrs, "default");175 (Optionality::Default(default), ty.clone())176 } else if let Some(ty) = extract_type_from_option(ty)? {177 if type_is_path(ty, "Thunk").is_some() {178 return Ok(Self::Lazy {179 is_option: true,180 name: ident.map(|v| v.to_string()),181 });182 }183184 (Optionality::Optional, Box::new(ty.clone()))185 } else {186 (Optionality::Required, ty.clone())187 };188189 let cfg_attrs = arg190 .attrs191 .iter()192 .filter(|a| a.path().is_ident("cfg"))193 .cloned()194 .collect();195196 Ok(Self::Normal {197 ty,198 optionality,199 name: ident.map(|v| v.to_string()),200 cfg_attrs,201 })202 }203}204205#[proc_macro_attribute]206pub fn builtin(207 attr: proc_macro::TokenStream,208 item: proc_macro::TokenStream,209) -> proc_macro::TokenStream {210 let attr = parse_macro_input!(attr as BuiltinAttrs);211 let item_fn = parse_macro_input!(item as ItemFn);212213 match builtin_inner(attr, item_fn) {214 Ok(v) => v.into(),215 Err(e) => e.into_compile_error().into(),216 }217}218219#[allow(clippy::too_many_lines)]220fn builtin_inner(attr: BuiltinAttrs, mut fun: ItemFn) -> syn::Result<TokenStream> {221 let ReturnType::Type(_, result) = &fun.sig.output else {222 return Err(Error::new(223 fun.sig.span(),224 "builtin should return something",225 ));226 };227228 let name = fun.sig.ident.to_string();229 let args = fun230 .sig231 .inputs232 .iter_mut()233 .map(|arg| ArgInfo::parse(&name, arg))234 .collect::<Result<Vec<_>>>()?;235236 let params_desc = args.iter().filter_map(|a| match a {237 ArgInfo::Normal {238 optionality,239 name,240 cfg_attrs,241 ..242 } => {243 let name = name244 .as_ref()245 .map_or_else(|| quote! {unnamed}, |n| quote! {named(#n)});246 let default = match optionality {247 Optionality::Required => quote!(ParamDefault::None),248 Optionality::Optional => quote!(ParamDefault::Exists),249 Optionality::Default(e) => quote!(ParamDefault::Literal(stringify!(#e))),250 };251 Some(quote! {252 #(#cfg_attrs)*253 [#name => #default],254 })255 }256 ArgInfo::Lazy { is_option, name } => {257 let name = name258 .as_ref()259 .map_or_else(|| quote! {unnamed}, |n| quote! {named(#n)});260 Some(quote! {261 [#name => ParamDefault::exists(#is_option)],262 })263 }264 ArgInfo::Context | ArgInfo::Location | ArgInfo::This => None,265 });266267 let mut id = 0usize;268 let pass = args269 .iter()270 .map(|a| match a {271 ArgInfo::Normal { .. } | ArgInfo::Lazy { .. } => {272 let cid = id;273 id += 1;274 (quote! {#cid}, a)275 }276 ArgInfo::Context | ArgInfo::Location | ArgInfo::This => {277 (quote! {compile_error!("should not use id")}, a)278 }279 })280 .map(|(id, a)| match a {281 ArgInfo::Normal {282 ty,283 optionality,284 name,285 cfg_attrs,286 } => {287 let name = name.as_ref().map_or("<unnamed>", String::as_str);288 let eval = quote! {jrsonnet_evaluator::in_description_frame(289 || format!("argument <{}> evaluation", #name),290 || <#ty>::from_untyped(value.evaluate()?),291 )?};292 let value = match optionality {293 Optionality::Required => quote! {{294 let value = parsed[#id].as_ref().expect("args shape is checked");295 #eval296 },},297 Optionality::Optional => quote! {if let Some(value) = &parsed[#id] {298 Some(#eval)299 } else {300 None301 },},302 Optionality::Default(expr) => quote! {if let Some(value) = &parsed[#id] {303 #eval304 } else {305 let v: #ty = #expr;306 v307 },},308 };309 quote! {310 #(#cfg_attrs)*311 #value312 }313 }314 ArgInfo::Lazy { is_option, .. } => {315 if *is_option {316 quote! {if let Some(value) = &parsed[#id] {317 Some(value.clone())318 } else {319 None320 },}321 } else {322 quote! {323 parsed[#id].as_ref().expect("args shape is correct").clone(),324 }325 }326 }327 ArgInfo::Context => quote! {ctx.clone(),},328 ArgInfo::Location => quote! {location,},329 ArgInfo::This => quote! {self,},330 });331332 let fields = attr.fields.iter().map(|field| {333 let attrs = &field.attrs;334 let name = &field.name;335 let ty = &field.ty;336 quote! {337 #(#attrs)*338 pub #name: #ty,339 }340 });341342 let name = &fun.sig.ident;343 let vis = &fun.vis;344 let static_ext = if attr.fields.is_empty() {345 quote! {346 impl #name {347 pub const INST: &'static dyn StaticBuiltin = &#name {};348 }349 impl StaticBuiltin for #name {}350 }351 } else {352 quote! {}353 };354 let static_derive_copy = if attr.fields.is_empty() {355 quote! {, Copy}356 } else {357 quote! {}358 };359360 Ok(quote! {361 #fun362363 #[doc(hidden)]364 #[allow(non_camel_case_types)]365 #[derive(Clone, jrsonnet_gcmodule::Trace #static_derive_copy)]366 #vis struct #name {367 #(#fields)*368 }369 const _: () = {370 use ::jrsonnet_evaluator::{371 State, Val,372 function::{builtin::{Builtin, StaticBuiltin}, FunctionSignature, ParamParse, ParamName, ParamDefault, CallLocation, ArgsLike, parse::parse_builtin_call},373 Result, Context, typed::Typed,374 parser::Span, params,375 };376 params!(377 #(#params_desc)*378 );379380 #static_ext381 impl Builtin for #name382 where383 Self: 'static384 {385 fn name(&self) -> &str {386 stringify!(#name)387 }388 fn params(&self) -> FunctionSignature {389 PARAMS.with(|p| p.clone())390 }391 #[allow(unused_variables)]392 fn call(&self, ctx: Context, location: CallLocation, args: &dyn ArgsLike) -> Result<Val> {393 let parsed = parse_builtin_call(ctx.clone(), self.params(), args, false)?;394395 let result: #result = #name(#(#pass)*);396 <_ as Typed>::into_result(result)397 }398 fn as_any(&self) -> &dyn ::std::any::Any {399 self400 }401 }402 };403 })404}405406#[derive(Default)]407#[allow(clippy::struct_excessive_bools)]408struct TypedAttr {409 rename: Option<String>,410 aliases: Vec<String>,411 flatten: bool,412 413 414 flatten_ok: bool,415 416 add: bool,417 418 hide: bool,419}420impl Parse for TypedAttr {421 fn parse(input: ParseStream) -> syn::Result<Self> {422 let mut out = Self::default();423 loop {424 let lookahead = input.lookahead1();425 if lookahead.peek(kw::rename) {426 input.parse::<kw::rename>()?;427 input.parse::<Token![=]>()?;428 let name = input.parse::<LitStr>()?;429 if out.rename.is_some() {430 return Err(Error::new(431 name.span(),432 "rename attribute may only be specified once",433 ));434 }435 out.rename = Some(name.value());436 } else if lookahead.peek(kw::alias) {437 input.parse::<kw::alias>()?;438 input.parse::<Token![=]>()?;439 let alias = input.parse::<LitStr>()?;440 out.aliases.push(alias.value());441 } else if lookahead.peek(kw::flatten) {442 input.parse::<kw::flatten>()?;443 out.flatten = true;444 if input.peek(token::Paren) {445 let content;446 parenthesized!(content in input);447 let lookahead = content.lookahead1();448 if lookahead.peek(kw::ok) {449 content.parse::<kw::ok>()?;450 out.flatten_ok = true;451 } else {452 return Err(lookahead.error());453 }454 }455 } else if lookahead.peek(kw::add) {456 input.parse::<kw::add>()?;457 out.add = true;458 } else if lookahead.peek(kw::hide) {459 input.parse::<kw::hide>()?;460 out.hide = true;461 } else if input.is_empty() {462 break;463 } else {464 return Err(lookahead.error());465 }466 if input.peek(Token![,]) {467 input.parse::<Token![,]>()?;468 } else {469 break;470 }471 }472 Ok(out)473 }474}475476struct TypedField {477 attr: TypedAttr,478 ident: Ident,479 ty: Type,480 is_option: bool,481 is_lazy: bool,482}483impl TypedField {484 fn parse(field: &syn::Field) -> Result<Self> {485 let attr = parse_attr::<TypedAttr, _>(&field.attrs, "typed")?.unwrap_or_default();486 let Some(ident) = field.ident.clone() else {487 return Err(Error::new(488 field.span(),489 "this field should appear in output object, but it has no visible name",490 ));491 };492 let (is_option, ty) = extract_type_from_option(&field.ty)?493 .map_or_else(|| (false, field.ty.clone()), |ty| (true, ty.clone()));494 if is_option && attr.flatten {495 if !attr.flatten_ok {496 return Err(Error::new(497 field.span(),498 "strategy should be set when flattening Option",499 ));500 }501 } else if attr.flatten_ok {502 return Err(Error::new(503 field.span(),504 "flatten(ok) is only useable on optional fields",505 ));506 }507508 let is_lazy = type_is_path(&ty, "Thunk").is_some();509510 Ok(Self {511 attr,512 ident,513 ty,514 is_option,515 is_lazy,516 })517 }518 519 fn name(&self) -> Option<String> {520 if self.attr.flatten {521 return None;522 }523 Some(524 self.attr525 .rename526 .clone()527 .unwrap_or_else(|| self.ident.to_string()),528 )529 }530531 fn expand_field(&self) -> Option<TokenStream> {532 if self.is_option {533 return None;534 }535 let name = self.name()?;536 let ty = &self.ty;537 Some(quote! {538 (#name, <#ty as Typed>::TYPE)539 })540 }541542 fn expand_parse(&self) -> TokenStream {543 if self.is_option {544 self.expand_parse_optional()545 } else {546 self.expand_parse_mandatory()547 }548 }549550 fn expand_parse_optional(&self) -> TokenStream {551 let ident = &self.ident;552 let ty = &self.ty;553554 555 if self.attr.flatten {556 return quote! {557 #ident: <#ty as TypedObj>::parse(&obj).ok(),558 };559 }560561 let name = self.name().unwrap();562 let aliases = &self.attr.aliases;563564 quote! {565 #ident: {566 let __value = if let Some(__v) = obj.get(#name.into())? {567 Some(__v)568 } #(else if let Some(__v) = obj.get(#aliases.into())? {569 Some(__v)570 })* else {571 None572 };573574 __value.map(<#ty as Typed>::from_untyped).transpose()?575 },576 }577 }578579 fn expand_parse_mandatory(&self) -> TokenStream {580 let ident = &self.ident;581 let ty = &self.ty;582583 584 if self.attr.flatten {585 return quote! {586 #ident: <#ty as TypedObj>::parse(&obj)?,587 };588 }589590 let name = self.name().unwrap();591 let aliases = &self.attr.aliases;592593 let error_text = if aliases.is_empty() {594 595 #[allow(clippy::redundant_clone)]596 name.clone()597 } else {598 format!("{name} (alias {})", aliases.join(", "))599 };600601 quote! {602 #ident: {603 let __value = if let Some(__v) = obj.get(#name.into())? {604 __v605 } #(else if let Some(__v) = obj.get(#aliases.into())? {606 __v607 })* else {608 return Err(ErrorKind::NoSuchField(#error_text.into(), vec![]).into());609 };610611 <#ty as Typed>::from_untyped(__value)?612 },613 }614 }615616 fn expand_serialize(&self) -> TokenStream {617 let ident = &self.ident;618 let ty = &self.ty;619 self.name().map_or_else(620 || {621 if self.is_option {622 quote! {623 if let Some(value) = self.#ident {624 <#ty as TypedObj>::serialize(value, out)?;625 }626 }627 } else {628 quote! {629 <#ty as TypedObj>::serialize(self.#ident, out)?;630 }631 }632 },633 |name| {634 let hide = if self.attr.hide {635 quote! {.hide()}636 } else {637 quote! {}638 };639 let add = if self.attr.add {640 quote! {.add()}641 } else {642 quote! {}643 };644 let value = if self.is_lazy {645 quote! {646 out.field(#name)647 #hide648 #add649 .try_thunk(<#ty as Typed>::into_lazy_untyped(value))?;650 }651 } else {652 quote! {653 out.field(#name)654 #hide655 #add656 .try_value(<#ty as Typed>::into_untyped(value)?)?;657 }658 };659 if self.is_option {660 quote! {661 if let Some(value) = self.#ident {662 #value663 }664 }665 } else {666 quote! {667 {668 let value = self.#ident;669 #value670 }671 }672 }673 },674 )675 }676}677678#[proc_macro_derive(Typed, attributes(typed))]679pub fn derive_typed(item: proc_macro::TokenStream) -> proc_macro::TokenStream {680 let input = parse_macro_input!(item as DeriveInput);681682 match derive_typed_inner(input) {683 Ok(v) => v.into(),684 Err(e) => e.to_compile_error().into(),685 }686}687688fn derive_typed_inner(input: DeriveInput) -> Result<TokenStream> {689 let syn::Data::Struct(data) = &input.data else {690 return Err(Error::new(input.span(), "only structs supported"));691 };692693 let ident = &input.ident;694 let fields = data695 .fields696 .iter()697 .map(TypedField::parse)698 .collect::<Result<Vec<_>>>()?;699700 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();701702 let typed = {703 let fields = fields704 .iter()705 .filter_map(TypedField::expand_field)706 .collect::<Vec<_>>();707 quote! {708 impl #impl_generics Typed for #ident #ty_generics #where_clause {709 const TYPE: &'static ComplexValType = &ComplexValType::ObjectRef(&[710 #(#fields,)*711 ]);712713 fn from_untyped(value: Val) -> JrResult<Self> {714 let obj = value.as_obj().expect("shape is correct");715 Self::parse(&obj)716 }717718 fn into_untyped(value: Self) -> JrResult<Val> {719 let mut out = ObjValueBuilder::new();720 value.serialize(&mut out)?;721 Ok(Val::Obj(out.build()))722 }723724 }725 }726 };727728 let fields_parse = fields.iter().map(TypedField::expand_parse);729 let fields_serialize = fields730 .iter()731 .map(TypedField::expand_serialize)732 .collect::<Vec<_>>();733734 Ok(quote! {735 const _: () = {736 use ::jrsonnet_evaluator::{737 typed::{ComplexValType, Typed, TypedObj, CheckType},738 Val, State,739 error::{ErrorKind, Result as JrResult},740 ObjValueBuilder, ObjValue,741 };742743 #typed744745 impl #impl_generics TypedObj for #ident #ty_generics #where_clause {746 fn serialize(self, out: &mut ObjValueBuilder) -> JrResult<()> {747 #(#fields_serialize)*748749 Ok(())750 }751 fn parse(obj: &ObjValue) -> JrResult<Self> {752 Ok(Self {753 #(#fields_parse)*754 })755 }756 }757 };758 })759}760761struct FormatInput {762 formatting: LitStr,763 arguments: Vec<Expr>,764}765impl Parse for FormatInput {766 fn parse(input: ParseStream) -> Result<Self> {767 let formatting = input.parse()?;768 let mut arguments = Vec::new();769770 while input.peek(Token![,]) {771 input.parse::<Token![,]>()?;772 if input.is_empty() {773 774 break;775 }776 let expr = input.parse()?;777 arguments.push(expr);778 }779780 if !input.is_empty() {781 return Err(syn::Error::new(input.span(), "unexpected trailing input"));782 }783784 Ok(Self {785 formatting,786 arguments,787 })788 }789}790fn is_format_str(i: &str) -> bool {791 let mut is_plain = true;792 793 794 let mut is_bracket = 0i8;795 for ele in i.chars() {796 match ele {797 '{' if is_bracket == -1 => {798 is_bracket = 0;799 }800 '}' if is_bracket == -1 => {801 is_plain = false;802 break;803 }804 '}' if is_bracket == 1 => {805 is_bracket = 0;806 }807 '{' if is_bracket == 1 => {808 is_plain = false;809 break;810 }811 '{' => {812 is_bracket = -1;813 }814 '}' => {815 is_bracket = 1;816 }817 _ if is_bracket != 0 => {818 is_plain = false;819 break;820 }821 _ => {}822 }823 }824 !is_plain || is_bracket != 0825}826impl FormatInput {827 fn expand(self) -> TokenStream {828 let format = self.formatting;829 if is_format_str(&format.value()) {830 let args = self.arguments;831 quote! {832 ::jrsonnet_evaluator::IStr::from(format!(#format #(, #args)*))833 }834 } else {835 if let Some(first) = self.arguments.first() {836 return syn::Error::new(837 first.span(),838 "string has no formatting codes, it should not have the arguments",839 )840 .into_compile_error();841 }842 quote! {843 ::jrsonnet_evaluator::IStr::from(#format)844 }845 }846 }847}848849850851852853854#[proc_macro]855pub fn format_istr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {856 let input = parse_macro_input!(input as FormatInput);857 input.expand().into()858}859860861#[proc_macro]862#[allow(non_snake_case)]863pub fn Thunk(input: proc_macro::TokenStream) -> proc_macro::TokenStream {864 let input = parse_macro_input!(input as ExprClosure);865866 let span = input.inputs.span();867 let move_check = input.capture.is_none().then(|| {868 quote_spanned! {span => {869 compile_error!("Thunk! needs to be called with move closure");870 }}871 });872873 let (env, closure, args) = syn_dissect_closure::split_env(input);874875 let trace_check = args.iter().map(|el| {876 let span = el.span();877 quote_spanned! {span => ::jrsonnet_evaluator::gc::assert_trace(&#el);}878 });879880 quote! {{881 #move_check882 #(#trace_check)*883 ::jrsonnet_evaluator::Thunk::new(::jrsonnet_evaluator::val::MemoizedClosureThunk::new(#env, #closure))884 }}.into()885}