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 = item.clone();222 let item_fn: ItemFn = parse_macro_input!(item_fn);223224 match builtin_inner(attr, item_fn) {225 Ok(v) => v.into(),226 Err(e) => e.into_compile_error().into(),227 }228}229230#[allow(clippy::too_many_lines)]231fn builtin_inner(attr: BuiltinAttrs, mut fun: ItemFn) -> syn::Result<TokenStream> {232 let ReturnType::Type(_, result) = &fun.sig.output else {233 return Err(Error::new(234 fun.sig.span(),235 "builtin should return something",236 ));237 };238239 let name = fun.sig.ident.to_string();240 let args = fun241 .sig242 .inputs243 .iter_mut()244 .map(|arg| ArgInfo::parse(&name, arg))245 .collect::<Result<Vec<_>>>()?;246247 let params_desc = args.iter().filter_map(|a| match a {248 ArgInfo::Normal {249 optionality,250 name,251 cfg_attrs,252 ..253 } => {254 let name = name255 .as_ref()256 .map_or_else(|| quote! {None}, |n| quote! {ParamName::new_static(#n)});257 let is_optional = optionality.is_optional();258 Some(quote! {259 #(#cfg_attrs)*260 BuiltinParam::new(#name, #is_optional),261 })262 }263 ArgInfo::Lazy { is_option, name } => {264 let name = name265 .as_ref()266 .map_or_else(|| quote! {None}, |n| quote! {ParamName::new_static(#n)});267 Some(quote! {268 BuiltinParam::new(#name, #is_option),269 })270 }271 ArgInfo::Context | ArgInfo::Location | ArgInfo::This => None,272 });273274 let mut id = 0usize;275 let pass = args276 .iter()277 .map(|a| match a {278 ArgInfo::Normal { .. } | ArgInfo::Lazy { .. } => {279 let cid = id;280 id += 1;281 (quote! {#cid}, a)282 }283 ArgInfo::Context | ArgInfo::Location | ArgInfo::This => {284 (quote! {compile_error!("should not use id")}, a)285 }286 })287 .map(|(id, a)| match a {288 ArgInfo::Normal {289 ty,290 optionality,291 name,292 cfg_attrs,293 } => {294 let name = name.as_ref().map_or("<unnamed>", String::as_str);295 let eval = quote! {jrsonnet_evaluator::State::push_description(296 || format!("argument <{}> evaluation", #name),297 || <#ty>::from_untyped(value.evaluate()?),298 )?};299 let value = match optionality {300 Optionality::Required => quote! {{301 let value = parsed[#id].as_ref().expect("args shape is checked");302 #eval303 },},304 Optionality::Optional => quote! {if let Some(value) = &parsed[#id] {305 Some(#eval)306 } else {307 None308 },},309 Optionality::Default(expr) => quote! {if let Some(value) = &parsed[#id] {310 #eval311 } else {312 let v: #ty = #expr;313 v314 },},315 };316 quote! {317 #(#cfg_attrs)*318 #value319 }320 }321 ArgInfo::Lazy { is_option, .. } => {322 if *is_option {323 quote! {if let Some(value) = &parsed[#id] {324 Some(value.clone())325 } else {326 None327 },}328 } else {329 quote! {330 parsed[#id].as_ref().expect("args shape is correct").clone(),331 }332 }333 }334 ArgInfo::Context => quote! {ctx.clone(),},335 ArgInfo::Location => quote! {location,},336 ArgInfo::This => quote! {self,},337 });338339 let fields = attr.fields.iter().map(|field| {340 let attrs = &field.attrs;341 let name = &field.name;342 let ty = &field.ty;343 quote! {344 #(#attrs)*345 pub #name: #ty,346 }347 });348349 let name = &fun.sig.ident;350 let vis = &fun.vis;351 let static_ext = if attr.fields.is_empty() {352 quote! {353 impl #name {354 pub const INST: &'static dyn StaticBuiltin = &#name {};355 }356 impl StaticBuiltin for #name {}357 }358 } else {359 quote! {}360 };361 let static_derive_copy = if attr.fields.is_empty() {362 quote! {, Copy}363 } else {364 quote! {}365 };366367 Ok(quote! {368 #fun369370 #[doc(hidden)]371 #[allow(non_camel_case_types)]372 #[derive(Clone, jrsonnet_gcmodule::Trace #static_derive_copy)]373 #vis struct #name {374 #(#fields)*375 }376 const _: () = {377 use ::jrsonnet_evaluator::{378 State, Val,379 function::{builtin::{Builtin, StaticBuiltin, BuiltinParam, ParamName}, CallLocation, ArgsLike, parse::parse_builtin_call},380 Result, Context, typed::Typed,381 parser::ExprLocation,382 };383 const PARAMS: &'static [BuiltinParam] = &[384 #(#params_desc)*385 ];386387 #static_ext388 impl Builtin for #name389 where390 Self: 'static391 {392 fn name(&self) -> &str {393 stringify!(#name)394 }395 fn params(&self) -> &[BuiltinParam] {396 PARAMS397 }398 #[allow(unused_variables)]399 fn call(&self, ctx: Context, location: CallLocation, args: &dyn ArgsLike) -> Result<Val> {400 let parsed = parse_builtin_call(ctx.clone(), &PARAMS, args, false)?;401402 let result: #result = #name(#(#pass)*);403 <_ as Typed>::into_result(result)404 }405 fn as_any(&self) -> &dyn ::std::any::Any {406 self407 }408 }409 };410 })411}412413#[derive(Default)]414#[allow(clippy::struct_excessive_bools)]415struct TypedAttr {416 rename: Option<String>,417 flatten: bool,418 419 420 flatten_ok: bool,421 422 add: bool,423 424 hide: bool,425}426impl Parse for TypedAttr {427 fn parse(input: ParseStream) -> syn::Result<Self> {428 let mut out = Self::default();429 loop {430 let lookahead = input.lookahead1();431 if lookahead.peek(kw::rename) {432 input.parse::<kw::rename>()?;433 input.parse::<Token![=]>()?;434 let name = input.parse::<LitStr>()?;435 if out.rename.is_some() {436 return Err(Error::new(437 name.span(),438 "rename attribute may only be specified once",439 ));440 }441 out.rename = Some(name.value());442 } else if lookahead.peek(kw::flatten) {443 input.parse::<kw::flatten>()?;444 out.flatten = true;445 if input.peek(token::Paren) {446 let content;447 parenthesized!(content in input);448 let lookahead = content.lookahead1();449 if lookahead.peek(kw::ok) {450 content.parse::<kw::ok>()?;451 out.flatten_ok = true;452 } else {453 return Err(lookahead.error());454 }455 }456 } else if lookahead.peek(kw::add) {457 input.parse::<kw::add>()?;458 out.add = true;459 } else if lookahead.peek(kw::hide) {460 input.parse::<kw::hide>()?;461 out.hide = true;462 } else if input.is_empty() {463 break;464 } else {465 return Err(lookahead.error());466 }467 if input.peek(Token![,]) {468 input.parse::<Token![,]>()?;469 } else {470 break;471 }472 }473 Ok(out)474 }475}476477struct TypedField {478 attr: TypedAttr,479 ident: Ident,480 ty: Type,481 is_option: 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 Ok(Self {509 attr,510 ident,511 ty,512 is_option,513 })514 }515 516 fn name(&self) -> Option<String> {517 if self.attr.flatten {518 return None;519 }520 Some(521 self.attr522 .rename523 .clone()524 .unwrap_or_else(|| self.ident.to_string()),525 )526 }527528 fn expand_field(&self) -> Option<TokenStream> {529 if self.is_option {530 return None;531 }532 let name = self.name()?;533 let ty = &self.ty;534 Some(quote! {535 (#name, <#ty as Typed>::TYPE)536 })537 }538 fn expand_parse(&self) -> TokenStream {539 let ident = &self.ident;540 let ty = &self.ty;541 if self.attr.flatten {542 543 return if self.is_option {544 quote! {545 #ident: <#ty as TypedObj>::parse(&obj).ok(),546 }547 } else {548 quote! {549 #ident: <#ty as TypedObj>::parse(&obj)?,550 }551 };552 };553554 let name = self.name().unwrap();555 let value = if self.is_option {556 quote! {557 if let Some(value) = obj.get(#name.into())? {558 Some(<#ty as Typed>::from_untyped(value)?)559 } else {560 None561 }562 }563 } else {564 quote! {565 <#ty as Typed>::from_untyped(obj.get(#name.into())?.ok_or_else(|| ErrorKind::NoSuchField(#name.into(), vec![]))?)?566 }567 };568569 quote! {570 #ident: #value,571 }572 }573 fn expand_serialize(&self) -> TokenStream {574 let ident = &self.ident;575 let ty = &self.ty;576 self.name().map_or_else(577 || {578 if self.is_option {579 quote! {580 if let Some(value) = self.#ident {581 <#ty as TypedObj>::serialize(value, out)?;582 }583 }584 } else {585 quote! {586 <#ty as TypedObj>::serialize(self.#ident, out)?;587 }588 }589 },590 |name| {591 let hide = if self.attr.hide {592 quote! {.hide()}593 } else {594 quote! {}595 };596 let add = if self.attr.add {597 quote! {.add()}598 } else {599 quote! {}600 };601 if self.is_option {602 quote! {603 if let Some(value) = self.#ident {604 out.field(#name)605 #hide606 #add607 .try_value(<#ty as Typed>::into_untyped(value)?)?;608 }609 }610 } else {611 quote! {612 out.field(#name)613 #hide614 #add615 .try_value(<#ty as Typed>::into_untyped(self.#ident)?)?;616 }617 }618 },619 )620 }621}622623#[proc_macro_derive(Typed, attributes(typed))]624pub fn derive_typed(item: proc_macro::TokenStream) -> proc_macro::TokenStream {625 let input = parse_macro_input!(item as DeriveInput);626627 match derive_typed_inner(input) {628 Ok(v) => v.into(),629 Err(e) => e.to_compile_error().into(),630 }631}632633fn derive_typed_inner(input: DeriveInput) -> Result<TokenStream> {634 let syn::Data::Struct(data) = &input.data else {635 return Err(Error::new(input.span(), "only structs supported"));636 };637638 let ident = &input.ident;639 let fields = data640 .fields641 .iter()642 .map(TypedField::parse)643 .collect::<Result<Vec<_>>>()?;644645 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();646647 let typed = {648 let fields = fields649 .iter()650 .filter_map(TypedField::expand_field)651 .collect::<Vec<_>>();652 quote! {653 impl #impl_generics Typed for #ident #ty_generics #where_clause {654 const TYPE: &'static ComplexValType = &ComplexValType::ObjectRef(&[655 #(#fields,)*656 ]);657658 fn from_untyped(value: Val) -> JrResult<Self> {659 let obj = value.as_obj().expect("shape is correct");660 Self::parse(&obj)661 }662663 fn into_untyped(value: Self) -> JrResult<Val> {664 let mut out = ObjValueBuilder::new();665 value.serialize(&mut out)?;666 Ok(Val::Obj(out.build()))667 }668669 }670 }671 };672673 let fields_parse = fields.iter().map(TypedField::expand_parse);674 let fields_serialize = fields675 .iter()676 .map(TypedField::expand_serialize)677 .collect::<Vec<_>>();678679 Ok(quote! {680 const _: () = {681 use ::jrsonnet_evaluator::{682 typed::{ComplexValType, Typed, TypedObj, CheckType},683 Val, State,684 error::{ErrorKind, Result as JrResult},685 ObjValueBuilder, ObjValue,686 };687688 #typed689690 impl #impl_generics TypedObj for #ident #ty_generics #where_clause {691 fn serialize(self, out: &mut ObjValueBuilder) -> JrResult<()> {692 #(#fields_serialize)*693694 Ok(())695 }696 fn parse(obj: &ObjValue) -> JrResult<Self> {697 Ok(Self {698 #(#fields_parse)*699 })700 }701 }702 };703 })704}705706struct FormatInput {707 formatting: LitStr,708 arguments: Vec<Expr>,709}710impl Parse for FormatInput {711 fn parse(input: ParseStream) -> Result<Self> {712 let formatting = input.parse()?;713 let mut arguments = Vec::new();714715 while input.peek(Token![,]) {716 input.parse::<Token![,]>()?;717 if input.is_empty() {718 719 break;720 }721 let expr = input.parse()?;722 arguments.push(expr);723 }724725 if !input.is_empty() {726 return Err(syn::Error::new(input.span(), "unexpected trailing input"));727 }728729 Ok(Self {730 formatting,731 arguments,732 })733 }734}735fn is_format_str(i: &str) -> bool {736 let mut is_plain = true;737 738 739 let mut is_bracket = 0i8;740 for ele in i.chars() {741 match ele {742 '{' if is_bracket == -1 => {743 is_bracket = 0;744 }745 '}' if is_bracket == -1 => {746 is_plain = false;747 break;748 }749 '}' if is_bracket == 1 => {750 is_bracket = 0;751 }752 '{' if is_bracket == 1 => {753 is_plain = false;754 break;755 }756 '{' => {757 is_bracket = -1;758 }759 '}' => {760 is_bracket = 1;761 }762 _ if is_bracket != 0 => {763 is_plain = false;764 break;765 }766 _ => {}767 }768 }769 !is_plain || is_bracket != 0770}771impl FormatInput {772 fn expand(self) -> TokenStream {773 let format = self.formatting;774 if is_format_str(&format.value()) {775 let args = self.arguments;776 quote! {777 ::jrsonnet_evaluator::IStr::from(format!(#format #(, #args)*))778 }779 } else {780 if let Some(first) = self.arguments.first() {781 return syn::Error::new(782 first.span(),783 "string has no formatting codes, it should not have the arguments",784 )785 .into_compile_error();786 }787 quote! {788 ::jrsonnet_evaluator::IStr::from(#format)789 }790 }791 }792}793794795796797798799#[proc_macro]800pub fn format_istr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {801 let input = parse_macro_input!(input as FormatInput);802 input.expand().into()803}