difftreelog
feat(macros) #[typed(method)]
in: master
2 files changed
crates/jrsonnet-macros/src/lib.rsdiffbeforeafterboth1use 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}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!(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 // Trailing comma475 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 // -1 = {494 // +1 = }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}549550/// `IStr` formatting helper551///552/// Using `format!("literal with no codes").into()` is slower than just `"literal with no codes".into()`553/// This macro looks for formatting codes in the input string, and uses554/// `format!()` only when necessary555#[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}560561/// Create Thunk using closure syntax562#[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}crates/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