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 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 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!(method);131 syn::custom_keyword!(ok);132}133134struct BuiltinAttrs {135 fields: Vec<Field>,136}137impl Parse for BuiltinAttrs {138 fn parse(input: ParseStream) -> syn::Result<Self> {139 if input.is_empty() {140 return Ok(Self { fields: Vec::new() });141 }142 input.parse::<kw::fields>()?;143 let fields;144 parenthesized!(fields in input);145 let p = Punctuated::<Field, Comma>::parse_terminated(&fields)?;146 Ok(Self {147 fields: p.into_iter().collect(),148 })149 }150}151152enum Optionality {153 Required,154 Optional,155 Default(Expr),156 TypeDefault,157}158159#[allow(160 clippy::large_enum_variant,161 reason = "this macro is not that hot for it to matter"162)]163enum ArgInfo {164 Normal {165 ty: Box<Type>,166 optionality: Optionality,167 name: Option<String>,168 cfg_attrs: Vec<Attribute>,169 },170 Lazy {171 is_option: bool,172 name: Option<String>,173 },174 Context,175 Location,176 This,177}178179impl ArgInfo {180 fn parse(name: &str, arg: &mut FnArg) -> Result<Self> {181 let FnArg::Typed(arg) = arg else {182 unreachable!()183 };184 let ident = match &arg.pat as &Pat {185 Pat::Ident(i) => Some(i.ident.clone()),186 _ => None,187 };188 let ty = &arg.ty;189 if type_is_path(ty, "Context").is_some() {190 return Ok(Self::Context);191 } else if type_is_path(ty, "CallLocation").is_some() {192 return Ok(Self::Location);193 } else if type_is_path(ty, "Thunk").is_some() {194 return Ok(Self::Lazy {195 is_option: false,196 name: ident.map(|v| v.to_string()),197 });198 }199200 match ty as &Type {201 Type::Reference(r) if type_is_path(&r.elem, name).is_some() => return Ok(Self::This),202 _ => {}203 }204205 let (optionality, ty) = if try_parse_attr_noargs(&arg.attrs, "default")? {206 remove_attr(&mut arg.attrs, "default");207 (Optionality::TypeDefault, ty.clone())208 } else if let Some(default) = parse_attr::<_, _>(&arg.attrs, "default")? {209 remove_attr(&mut arg.attrs, "default");210 (Optionality::Default(default), ty.clone())211 } else if let Some(ty) = extract_type_from_option(ty)? {212 if type_is_path(ty, "Thunk").is_some() {213 return Ok(Self::Lazy {214 is_option: true,215 name: ident.map(|v| v.to_string()),216 });217 }218219 (Optionality::Optional, Box::new(ty.clone()))220 } else {221 (Optionality::Required, ty.clone())222 };223224 let cfg_attrs = arg225 .attrs226 .iter()227 .filter(|a| a.path().is_ident("cfg"))228 .cloned()229 .collect();230231 Ok(Self::Normal {232 ty,233 optionality,234 name: ident.map(|v| v.to_string()),235 cfg_attrs,236 })237 }238}239240#[proc_macro_attribute]241pub fn builtin(242 attr: proc_macro::TokenStream,243 item: proc_macro::TokenStream,244) -> proc_macro::TokenStream {245 let attr = parse_macro_input!(attr as BuiltinAttrs);246 let item_fn = parse_macro_input!(item as ItemFn);247248 match builtin_inner(attr, item_fn) {249 Ok(v) => v.into(),250 Err(e) => e.into_compile_error().into(),251 }252}253254#[allow(clippy::too_many_lines)]255fn builtin_inner(attr: BuiltinAttrs, mut fun: ItemFn) -> syn::Result<TokenStream> {256 let ReturnType::Type(_, result) = &fun.sig.output else {257 return Err(Error::new(258 fun.sig.span(),259 "builtin should return something",260 ));261 };262263 let name = fun.sig.ident.to_string();264 let args = fun265 .sig266 .inputs267 .iter_mut()268 .map(|arg| ArgInfo::parse(&name, arg))269 .collect::<Result<Vec<_>>>()?;270271 let params_desc = args.iter().filter_map(|a| match a {272 ArgInfo::Normal {273 optionality,274 name,275 cfg_attrs,276 ..277 } => {278 let name = name279 .as_ref()280 .map_or_else(|| quote! {unnamed}, |n| quote! {named(#n)});281 let default = match optionality {282 Optionality::Required => quote!(ParamDefault::None),283 Optionality::Optional | Optionality::TypeDefault => quote!(ParamDefault::Exists),284 Optionality::Default(e) => quote!(ParamDefault::Literal(stringify!(#e))),285 };286 Some(quote! {287 #(#cfg_attrs)*288 [#name => #default],289 })290 }291 ArgInfo::Lazy { is_option, name } => {292 let name = name293 .as_ref()294 .map_or_else(|| quote! {unnamed}, |n| quote! {named(#n)});295 Some(quote! {296 [#name => ParamDefault::exists(#is_option)],297 })298 }299 ArgInfo::Context | ArgInfo::Location | ArgInfo::This => None,300 });301302 let mut id = 0usize;303 let pass = args304 .iter()305 .map(|a| match a {306 ArgInfo::Normal { .. } | ArgInfo::Lazy { .. } => {307 let cid = id;308 id += 1;309 (quote! {#cid}, a)310 }311 ArgInfo::Context | ArgInfo::Location | ArgInfo::This => {312 (quote! {compile_error!("should not use id")}, a)313 }314 })315 .map(|(id, a)| match a {316 ArgInfo::Normal {317 ty,318 optionality,319 name,320 cfg_attrs,321 } => {322 let name = name.as_ref().map_or("<unnamed>", String::as_str);323 let eval = quote! {jrsonnet_evaluator::in_description_frame(324 || format!("argument <{}> evaluation", #name),325 || <#ty as FromUntyped>::from_untyped(value.evaluate()?),326 )?};327 let value = match optionality {328 Optionality::Required => quote! {{329 let value = parsed[#id].as_ref().expect("args shape is checked");330 #eval331 },},332 Optionality::Optional => quote! {if let Some(value) = &parsed[#id] {333 Some(#eval)334 } else {335 None336 },},337 Optionality::Default(expr) => quote! {if let Some(value) = &parsed[#id] {338 #eval339 } else {340 let v: #ty = #expr;341 v342 },},343 Optionality::TypeDefault => quote! {if let Some(value) = &parsed[#id] {344 #eval345 } else {346 let v: #ty = Default::default();347 v348 },},349 };350 quote! {351 #(#cfg_attrs)*352 #value353 }354 }355 ArgInfo::Lazy { is_option, .. } => {356 if *is_option {357 quote! {if let Some(value) = &parsed[#id] {358 Some(value.clone())359 } else {360 None361 },}362 } else {363 quote! {364 parsed[#id].as_ref().expect("args shape is correct").clone(),365 }366 }367 }368 ArgInfo::Context => quote! {ctx.clone(),},369 ArgInfo::Location => quote! {location,},370 ArgInfo::This => quote! {self,},371 });372373 let fields = attr.fields.iter().map(|field| {374 let attrs = &field.attrs;375 let name = &field.name;376 let ty = &field.ty;377 quote! {378 #(#attrs)*379 pub #name: #ty,380 }381 });382383 let name = &fun.sig.ident;384 let vis = &fun.vis;385 let static_derive_copy = if attr.fields.is_empty() {386 quote! {, Copy, Default}387 } else {388 quote! {}389 };390391 Ok(quote! {392 #fun393394 #[doc(hidden)]395 #[allow(non_camel_case_types)]396 #[derive(Clone, jrsonnet_gcmodule::Trace #static_derive_copy)]397 #vis struct #name {398 #(#fields)*399 }400 const _: () = {401 use ::jrsonnet_evaluator::{402 State, Val,403 function::{builtin::Builtin, FunctionSignature, ParamParse, ParamName, ParamDefault, CallLocation},404 Result, Context, typed::{Typed, FromUntyped, IntoUntypedResult},405 parser::Span, params, Thunk,406 };407 params!(408 #(#params_desc)*409 );410411 impl Builtin for #name412 where413 Self: 'static414 {415 fn name(&self) -> &str {416 stringify!(#name)417 }418 fn params(&self) -> FunctionSignature {419 PARAMS.with(|p| p.clone())420 }421 #[allow(unused_variables)]422 fn call(&self, location: CallLocation<'_>, parsed: &[Option<Thunk<Val>>]) -> Result<Val> {423 let result: #result = #name(#(#pass)*);424 <_ as IntoUntypedResult>::into_untyped_result(result)425 }426 fn as_any(&self) -> &dyn ::std::any::Any {427 self428 }429 }430 };431 })432}433434#[proc_macro_derive(Typed, attributes(typed))]435pub fn derive_typed(item: proc_macro::TokenStream) -> proc_macro::TokenStream {436 let input = parse_macro_input!(item as DeriveInput);437438 match derive_typed_inner(input) {439 Ok(v) => v.into(),440 Err(e) => e.to_compile_error().into(),441 }442}443#[proc_macro_derive(IntoUntyped, attributes(typed))]444pub fn derive_into_untyped(item: proc_macro::TokenStream) -> proc_macro::TokenStream {445 let input = parse_macro_input!(item as DeriveInput);446447 match derive_into_untyped_inner(input) {448 Ok(v) => v.into(),449 Err(e) => e.to_compile_error().into(),450 }451}452#[proc_macro_derive(FromUntyped, attributes(typed))]453pub fn derive_from_untyped(item: proc_macro::TokenStream) -> proc_macro::TokenStream {454 let input = parse_macro_input!(item as DeriveInput);455456 match derive_from_untyped_inner(input) {457 Ok(v) => v.into(),458 Err(e) => e.to_compile_error().into(),459 }460}461462struct FormatInput {463 formatting: LitStr,464 arguments: Vec<Expr>,465}466impl Parse for FormatInput {467 fn parse(input: ParseStream) -> Result<Self> {468 let formatting = input.parse()?;469 let mut arguments = Vec::new();470471 while input.peek(Token![,]) {472 input.parse::<Token![,]>()?;473 if input.is_empty() {474 475 break;476 }477 let expr = input.parse()?;478 arguments.push(expr);479 }480481 if !input.is_empty() {482 return Err(syn::Error::new(input.span(), "unexpected trailing input"));483 }484485 Ok(Self {486 formatting,487 arguments,488 })489 }490}491fn is_format_str(i: &str) -> bool {492 let mut is_plain = true;493 494 495 let mut is_bracket = 0i8;496 for ele in i.chars() {497 match ele {498 '{' if is_bracket == -1 => {499 is_bracket = 0;500 }501 '}' if is_bracket == -1 => {502 is_plain = false;503 break;504 }505 '}' if is_bracket == 1 => {506 is_bracket = 0;507 }508 '{' if is_bracket == 1 => {509 is_plain = false;510 break;511 }512 '{' => {513 is_bracket = -1;514 }515 '}' => {516 is_bracket = 1;517 }518 _ if is_bracket != 0 => {519 is_plain = false;520 break;521 }522 _ => {}523 }524 }525 !is_plain || is_bracket != 0526}527impl FormatInput {528 fn expand(self) -> TokenStream {529 let format = self.formatting;530 if is_format_str(&format.value()) {531 let args = self.arguments;532 quote! {533 ::jrsonnet_evaluator::IStr::from(format!(#format #(, #args)*))534 }535 } else {536 if let Some(first) = self.arguments.first() {537 return syn::Error::new(538 first.span(),539 "string has no formatting codes, it should not have the arguments",540 )541 .into_compile_error();542 }543 quote! {544 ::jrsonnet_evaluator::IStr::from(#format)545 }546 }547 }548}549550551552553554555#[proc_macro]556pub fn format_istr(input: proc_macro::TokenStream) -> proc_macro::TokenStream {557 let input = parse_macro_input!(input as FormatInput);558 input.expand().into()559}560561562#[proc_macro]563#[allow(non_snake_case)]564pub fn Thunk(input: proc_macro::TokenStream) -> proc_macro::TokenStream {565 let input = parse_macro_input!(input as ExprClosure);566567 let span = input.inputs.span();568 let move_check = input.capture.is_none().then(|| {569 quote_spanned! {span => {570 compile_error!("Thunk! needs to be called with move closure");571 }}572 });573574 let (env, closure, args) = syn_dissect_closure::split_env(input);575576 let trace_check = args.iter().map(|el| {577 let span = el.span();578 quote_spanned! {span => ::jrsonnet_evaluator::gc::assert_trace(&#el);}579 });580581 quote! {{582 #move_check583 #(#trace_check)*584 ::jrsonnet_evaluator::Thunk::new(::jrsonnet_evaluator::val::MemoizedClosureThunk::new(#env, #closure))585 }}.into()586}