1use std::string::String;23use proc_macro2::TokenStream;4use quote::quote;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, FnArg, GenericArgument, Ident, ItemFn, LitStr, Pat, Path,13 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!(flatten);100 syn::custom_keyword!(add);101 syn::custom_keyword!(hide);102 syn::custom_keyword!(ok);103}104105struct EmptyAttr;106impl Parse for EmptyAttr {107 fn parse(_input: ParseStream) -> Result<Self> {108 Ok(Self)109 }110}111112struct BuiltinAttrs {113 fields: Vec<Field>,114}115impl Parse for BuiltinAttrs {116 fn parse(input: ParseStream) -> syn::Result<Self> {117 if input.is_empty() {118 return Ok(Self { fields: Vec::new() });119 }120 input.parse::<kw::fields>()?;121 let fields;122 parenthesized!(fields in input);123 let p = Punctuated::<Field, Comma>::parse_terminated(&fields)?;124 Ok(Self {125 fields: p.into_iter().collect(),126 })127 }128}129130enum Optionality {131 Required,132 Optional,133 Default(Expr),134}135impl Optionality {136 fn is_optional(&self) -> bool {137 !matches!(self, Self::Required)138 }139}140141enum ArgInfo {142 Normal {143 ty: Box<Type>,144 optionality: Optionality,145 name: Option<String>,146 cfg_attrs: Vec<Attribute>,147 },148 Lazy {149 is_option: bool,150 name: Option<String>,151 },152 Context,153 Location,154 This,155}156157impl ArgInfo {158 fn parse(name: &str, arg: &mut FnArg) -> Result<Self> {159 let FnArg::Typed(arg) = arg else {160 unreachable!()161 };162 let ident = match &arg.pat as &Pat {163 Pat::Ident(i) => Some(i.ident.clone()),164 _ => None,165 };166 let ty = &arg.ty;167 if type_is_path(ty, "Context").is_some() {168 return Ok(Self::Context);169 } else if type_is_path(ty, "CallLocation").is_some() {170 return Ok(Self::Location);171 } else if type_is_path(ty, "Thunk").is_some() {172 return Ok(Self::Lazy {173 is_option: false,174 name: ident.map(|v| v.to_string()),175 });176 }177178 match ty as &Type {179 Type::Reference(r) if type_is_path(&r.elem, name).is_some() => return Ok(Self::This),180 _ => {}181 }182183 let (optionality, ty) = if let Some(default) = parse_attr::<_, _>(&arg.attrs, "default")? {184 remove_attr(&mut arg.attrs, "default");185 (Optionality::Default(default), ty.clone())186 } else if let Some(ty) = extract_type_from_option(ty)? {187 if type_is_path(ty, "Thunk").is_some() {188 return Ok(Self::Lazy {189 is_option: true,190 name: ident.map(|v| v.to_string()),191 });192 }193194 (Optionality::Optional, Box::new(ty.clone()))195 } else {196 (Optionality::Required, ty.clone())197 };198199 let cfg_attrs = arg200 .attrs201 .iter()202 .filter(|a| a.path().is_ident("cfg"))203 .cloned()204 .collect();205206 Ok(Self::Normal {207 ty,208 optionality,209 name: ident.map(|v| v.to_string()),210 cfg_attrs,211 })212 }213}214215#[proc_macro_attribute]216pub fn builtin(217 attr: proc_macro::TokenStream,218 item: proc_macro::TokenStream,219) -> proc_macro::TokenStream {220 let attr = parse_macro_input!(attr as BuiltinAttrs);221 let item_fn = parse_macro_input!(item as ItemFn);222223 match builtin_inner(attr, item_fn) {224 Ok(v) => v.into(),225 Err(e) => e.into_compile_error().into(),226 }227}228229#[allow(clippy::too_many_lines)]230fn builtin_inner(attr: BuiltinAttrs, mut fun: ItemFn) -> syn::Result<TokenStream> {231 let ReturnType::Type(_, result) = &fun.sig.output else {232 return Err(Error::new(233 fun.sig.span(),234 "builtin should return something",235 ));236 };237238 let name = fun.sig.ident.to_string();239 let args = fun240 .sig241 .inputs242 .iter_mut()243 .map(|arg| ArgInfo::parse(&name, arg))244 .collect::<Result<Vec<_>>>()?;245246 let params_desc = args.iter().filter_map(|a| match a {247 ArgInfo::Normal {248 optionality,249 name,250 cfg_attrs,251 ..252 } => {253 let name = name254 .as_ref()255 .map_or_else(|| quote! {None}, |n| quote! {ParamName::new_static(#n)});256 let is_optional = optionality.is_optional();257 Some(quote! {258 #(#cfg_attrs)*259 BuiltinParam::new(#name, #is_optional),260 })261 }262 ArgInfo::Lazy { is_option, name } => {263 let name = name264 .as_ref()265 .map_or_else(|| quote! {None}, |n| quote! {ParamName::new_static(#n)});266 Some(quote! {267 BuiltinParam::new(#name, #is_option),268 })269 }270 ArgInfo::Context | ArgInfo::Location | ArgInfo::This => None,271 });272273 let mut id = 0usize;274 let pass = args275 .iter()276 .map(|a| match a {277 ArgInfo::Normal { .. } | ArgInfo::Lazy { .. } => {278 let cid = id;279 id += 1;280 (quote! {#cid}, a)281 }282 ArgInfo::Context | ArgInfo::Location | ArgInfo::This => {283 (quote! {compile_error!("should not use id")}, a)284 }285 })286 .map(|(id, a)| match a {287 ArgInfo::Normal {288 ty,289 optionality,290 name,291 cfg_attrs,292 } => {293 let name = name.as_ref().map_or("<unnamed>", String::as_str);294 let eval = quote! {jrsonnet_evaluator::State::push_description(295 || format!("argument <{}> evaluation", #name),296 || <#ty>::from_untyped(value.evaluate()?),297 )?};298 let value = match optionality {299 Optionality::Required => quote! {{300 let value = parsed[#id].as_ref().expect("args shape is checked");301 #eval302 },},303 Optionality::Optional => quote! {if let Some(value) = &parsed[#id] {304 Some(#eval)305 } else {306 None307 },},308 Optionality::Default(expr) => quote! {if let Some(value) = &parsed[#id] {309 #eval310 } else {311 let v: #ty = #expr;312 v313 },},314 };315 quote! {316 #(#cfg_attrs)*317 #value318 }319 }320 ArgInfo::Lazy { is_option, .. } => {321 if *is_option {322 quote! {if let Some(value) = &parsed[#id] {323 Some(value.clone())324 } else {325 None326 },}327 } else {328 quote! {329 parsed[#id].as_ref().expect("args shape is correct").clone(),330 }331 }332 }333 ArgInfo::Context => quote! {ctx.clone(),},334 ArgInfo::Location => quote! {location,},335 ArgInfo::This => quote! {self,},336 });337338 let fields = attr.fields.iter().map(|field| {339 let attrs = &field.attrs;340 let name = &field.name;341 let ty = &field.ty;342 quote! {343 #(#attrs)*344 pub #name: #ty,345 }346 });347348 let name = &fun.sig.ident;349 let vis = &fun.vis;350 let static_ext = if attr.fields.is_empty() {351 quote! {352 impl #name {353 pub const INST: &'static dyn StaticBuiltin = &#name {};354 }355 impl StaticBuiltin for #name {}356 }357 } else {358 quote! {}359 };360 let static_derive_copy = if attr.fields.is_empty() {361 quote! {, Copy}362 } else {363 quote! {}364 };365366 Ok(quote! {367 #fun368369 #[doc(hidden)]370 #[allow(non_camel_case_types)]371 #[derive(Clone, jrsonnet_gcmodule::Trace #static_derive_copy)]372 #vis struct #name {373 #(#fields)*374 }375 const _: () = {376 use ::jrsonnet_evaluator::{377 State, Val,378 function::{builtin::{Builtin, StaticBuiltin, BuiltinParam, ParamName}, CallLocation, ArgsLike, parse::parse_builtin_call},379 Result, Context, typed::Typed,380 parser::ExprLocation,381 };382 const PARAMS: &'static [BuiltinParam] = &[383 #(#params_desc)*384 ];385386 #static_ext387 impl Builtin for #name388 where389 Self: 'static390 {391 fn name(&self) -> &str {392 stringify!(#name)393 }394 fn params(&self) -> &[BuiltinParam] {395 PARAMS396 }397 #[allow(unused_variables)]398 fn call(&self, ctx: Context, location: CallLocation, args: &dyn ArgsLike) -> Result<Val> {399 let parsed = parse_builtin_call(ctx.clone(), &PARAMS, args, false)?;400401 let result: #result = #name(#(#pass)*);402 <_ as Typed>::into_result(result)403 }404 fn as_any(&self) -> &dyn ::std::any::Any {405 self406 }407 }408 };409 })410}411412#[derive(Default)]413#[allow(clippy::struct_excessive_bools)]414struct TypedAttr {415 rename: Option<String>,416 flatten: bool,417 418 419 flatten_ok: bool,420 421 add: bool,422 423 hide: bool,424}425impl Parse for TypedAttr {426 fn parse(input: ParseStream) -> syn::Result<Self> {427 let mut out = Self::default();428 loop {429 let lookahead = input.lookahead1();430 if lookahead.peek(kw::rename) {431 input.parse::<kw::rename>()?;432 input.parse::<Token![=]>()?;433 let name = input.parse::<LitStr>()?;434 if out.rename.is_some() {435 return Err(Error::new(436 name.span(),437 "rename attribute may only be specified once",438 ));439 }440 out.rename = Some(name.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}482impl TypedField {483 fn parse(field: &syn::Field) -> Result<Self> {484 let attr = parse_attr::<TypedAttr, _>(&field.attrs, "typed")?.unwrap_or_default();485 let Some(ident) = field.ident.clone() else {486 return Err(Error::new(487 field.span(),488 "this field should appear in output object, but it has no visible name",489 ));490 };491 let (is_option, ty) = extract_type_from_option(&field.ty)?492 .map_or_else(|| (false, field.ty.clone()), |ty| (true, ty.clone()));493 if is_option && attr.flatten {494 if !attr.flatten_ok {495 return Err(Error::new(496 field.span(),497 "strategy should be set when flattening Option",498 ));499 }500 } else if attr.flatten_ok {501 return Err(Error::new(502 field.span(),503 "flatten(ok) is only useable on optional fields",504 ));505 }506507 Ok(Self {508 attr,509 ident,510 ty,511 is_option,512 })513 }514 515 fn name(&self) -> Option<String> {516 if self.attr.flatten {517 return None;518 }519 Some(520 self.attr521 .rename522 .clone()523 .unwrap_or_else(|| self.ident.to_string()),524 )525 }526527 fn expand_field(&self) -> Option<TokenStream> {528 if self.is_option {529 return None;530 }531 let name = self.name()?;532 let ty = &self.ty;533 Some(quote! {534 (#name, <#ty as Typed>::TYPE)535 })536 }537 fn expand_parse(&self) -> TokenStream {538 let ident = &self.ident;539 let ty = &self.ty;540 if self.attr.flatten {541 542 return if self.is_option {543 quote! {544 #ident: <#ty as TypedObj>::parse(&obj).ok(),545 }546 } else {547 quote! {548 #ident: <#ty as TypedObj>::parse(&obj)?,549 }550 };551 };552553 let name = self.name().unwrap();554 let value = if self.is_option {555 quote! {556 if let Some(value) = obj.get(#name.into())? {557 Some(<#ty as Typed>::from_untyped(value)?)558 } else {559 None560 }561 }562 } else {563 quote! {564 <#ty as Typed>::from_untyped(obj.get(#name.into())?.ok_or_else(|| ErrorKind::NoSuchField(#name.into(), vec![]))?)?565 }566 };567568 quote! {569 #ident: #value,570 }571 }572 fn expand_serialize(&self) -> TokenStream {573 let ident = &self.ident;574 let ty = &self.ty;575 self.name().map_or_else(576 || {577 if self.is_option {578 quote! {579 if let Some(value) = self.#ident {580 <#ty as TypedObj>::serialize(value, out)?;581 }582 }583 } else {584 quote! {585 <#ty as TypedObj>::serialize(self.#ident, out)?;586 }587 }588 },589 |name| {590 let hide = if self.attr.hide {591 quote! {.hide()}592 } else {593 quote! {}594 };595 let add = if self.attr.add {596 quote! {.add()}597 } else {598 quote! {}599 };600 if self.is_option {601 quote! {602 if let Some(value) = self.#ident {603 out.field(#name)604 #hide605 #add606 .try_value(<#ty as Typed>::into_untyped(value)?)?;607 }608 }609 } else {610 quote! {611 out.field(#name)612 #hide613 #add614 .try_value(<#ty as Typed>::into_untyped(self.#ident)?)?;615 }616 }617 },618 )619 }620}621622#[proc_macro_derive(Typed, attributes(typed))]623pub fn derive_typed(item: proc_macro::TokenStream) -> proc_macro::TokenStream {624 let input = parse_macro_input!(item as DeriveInput);625626 match derive_typed_inner(input) {627 Ok(v) => v.into(),628 Err(e) => e.to_compile_error().into(),629 }630}631632fn derive_typed_inner(input: DeriveInput) -> Result<TokenStream> {633 let syn::Data::Struct(data) = &input.data else {634 return Err(Error::new(input.span(), "only structs supported"));635 };636637 let ident = &input.ident;638 let fields = data639 .fields640 .iter()641 .map(TypedField::parse)642 .collect::<Result<Vec<_>>>()?;643644 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();645646 let typed = {647 let fields = fields648 .iter()649 .filter_map(TypedField::expand_field)650 .collect::<Vec<_>>();651 quote! {652 impl #impl_generics Typed for #ident #ty_generics #where_clause {653 const TYPE: &'static ComplexValType = &ComplexValType::ObjectRef(&[654 #(#fields,)*655 ]);656657 fn from_untyped(value: Val) -> JrResult<Self> {658 let obj = value.as_obj().expect("shape is correct");659 Self::parse(&obj)660 }661662 fn into_untyped(value: Self) -> JrResult<Val> {663 let mut out = ObjValueBuilder::new();664 value.serialize(&mut out)?;665 Ok(Val::Obj(out.build()))666 }667668 }669 }670 };671672 let fields_parse = fields.iter().map(TypedField::expand_parse);673 let fields_serialize = fields674 .iter()675 .map(TypedField::expand_serialize)676 .collect::<Vec<_>>();677678 Ok(quote! {679 const _: () = {680 use ::jrsonnet_evaluator::{681 typed::{ComplexValType, Typed, TypedObj, CheckType},682 Val, State,683 error::{ErrorKind, Result as JrResult},684 ObjValueBuilder, ObjValue,685 };686687 #typed688689 impl #impl_generics TypedObj for #ident #ty_generics #where_clause {690 fn serialize(self, out: &mut ObjValueBuilder) -> JrResult<()> {691 #(#fields_serialize)*692693 Ok(())694 }695 fn parse(obj: &ObjValue) -> JrResult<Self> {696 Ok(Self {697 #(#fields_parse)*698 })699 }700 }701 };702 })703}704705struct FormatInput {706 formatting: LitStr,707 arguments: Vec<Expr>,708}709impl Parse for FormatInput {710 fn parse(input: ParseStream) -> Result<Self> {711 let formatting = input.parse()?;712 let mut arguments = Vec::new();713714 while input.peek(Token![,]) {715 input.parse::<Token![,]>()?;716 if input.is_empty() {717 718 break;719 }720 let expr = input.parse()?;721 arguments.push(expr);722 }723724 if !input.is_empty() {725 return Err(syn::Error::new(input.span(), "unexpected trailing input"));726 }727728 Ok(Self {729 formatting,730 arguments,731 })732 }733}734fn is_format_str(i: &str) -> bool {735 let mut is_plain = true;736 737 738 let mut is_bracket = 0i8;739 for ele in i.chars() {740 match ele {741 '{' if is_bracket == -1 => {742 is_bracket = 0;743 }744 '}' if is_bracket == -1 => {745 is_plain = false;746 break;747 }748 '}' if is_bracket == 1 => {749 is_bracket = 0;750 }751 '{' if is_bracket == 1 => {752 is_plain = false;753 break;754 }755 '{' => {756 is_bracket = -1;757 }758 '}' => {759 is_bracket = 1;760 }761 _ if is_bracket != 0 => {762 is_plain = false;763 break;764 }765 _ => {}766 }767 }768 !is_plain || is_bracket != 0769}770impl FormatInput {771 fn expand(self) -> TokenStream {772 let format = self.formatting;773 if is_format_str(&format.value()) {774 let args = self.arguments;775 quote! {776 ::jrsonnet_evaluator::IStr::from(format!(#format #(, #args)*))777 }778 } else {779 if let Some(first) = self.arguments.first() {780 return syn::Error::new(781 first.span(),782 "string has no formatting codes, it should not have the arguments",783 )784 .into_compile_error();785 }786 quote! {787 ::jrsonnet_evaluator::IStr::from(#format)788 }789 }790 }791}792793794795796797798#[proc_macro]799pub fn format_istr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {800 let input = parse_macro_input!(input as FormatInput);801 input.expand().into()802}