cairo_lang_macro_attributes/
lib.rs

1use proc_macro::TokenStream;
2use quote::{ToTokens, quote};
3use scarb_stable_hash::short_hash;
4use syn::spanned::Spanned;
5use syn::{
6    Expr, Ident, ItemFn, LitStr, Meta, Result, Token, parse::Parse, parse::ParseStream,
7    parse_macro_input,
8};
9
10/// Constructs the attribute macro implementation.
11///
12/// This macro hides the conversion to stable ABI structs from the user.
13///
14/// Note, that this macro can be used multiple times, to define multiple independent attribute macros.
15#[proc_macro_attribute]
16pub fn attribute_macro(args: TokenStream, input: TokenStream) -> TokenStream {
17    macro_helper(
18        input,
19        parse_macro_input!(args as AttributeArgs),
20        quote!(::cairo_lang_macro::ExpansionKind::Attr),
21        quote!(::cairo_lang_macro::ExpansionFunc::Attr),
22    )
23}
24
25/// Constructs the inline macro implementation.
26///
27/// This macro hides the conversion to stable ABI structs from the user.
28///
29/// Note, that this macro can be used multiple times, to define multiple independent attribute macros.
30#[proc_macro_attribute]
31pub fn inline_macro(args: TokenStream, input: TokenStream) -> TokenStream {
32    // Emit compilation error if `parent` argument is used.
33    let attribute_args = parse_macro_input!(args as AttributeArgs);
34    if let Some(path) = attribute_args.parent_module_path {
35        return syn::Error::new(path.span(), "inline macro cannot use `parent` argument")
36            .to_compile_error()
37            .into();
38    }
39    // Otherwise, proceed with the macro expansion.
40    macro_helper(
41        input,
42        Default::default(),
43        quote!(::cairo_lang_macro::ExpansionKind::Inline),
44        quote!(::cairo_lang_macro::ExpansionFunc::Other),
45    )
46}
47
48/// Constructs the derive macro implementation.
49///
50/// This macro hides the conversion to stable ABI structs from the user.
51///
52/// Note, that this macro can be used multiple times, to define multiple independent attribute macros.
53#[proc_macro_attribute]
54pub fn derive_macro(args: TokenStream, input: TokenStream) -> TokenStream {
55    macro_helper(
56        input,
57        parse_macro_input!(args as AttributeArgs),
58        quote!(::cairo_lang_macro::ExpansionKind::Derive),
59        quote!(::cairo_lang_macro::ExpansionFunc::Other),
60    )
61}
62
63fn macro_helper(
64    input: TokenStream,
65    args: AttributeArgs,
66    kind: impl ToTokens,
67    func: impl ToTokens,
68) -> TokenStream {
69    let item: ItemFn = parse_macro_input!(input as ItemFn);
70
71    let original_item_name = item.sig.ident.to_string();
72    let expansion_name = if let Some(path) = args.parent_module_path {
73        let value = path.value();
74        if !is_valid_path(&value) {
75            return syn::Error::new(path.span(), "`parent` argument is not a valid path")
76                .to_compile_error()
77                .into();
78        }
79        format!("{value}::{original_item_name}")
80    } else {
81        original_item_name
82    };
83    let doc = item
84        .attrs
85        .iter()
86        .filter_map(|attr| match &attr.meta {
87            Meta::NameValue(meta) => meta.path.is_ident("doc").then(|| match &meta.value {
88                Expr::Lit(lit) => match &lit.lit {
89                    syn::Lit::Str(lit) => Some(lit.value().trim().to_string()),
90                    _ => None,
91                },
92                _ => None,
93            }),
94            _ => None,
95        })
96        .flatten()
97        .collect::<Vec<_>>()
98        .join("\n");
99    let item = hide_name(item);
100    let item_name = &item.sig.ident;
101
102    let callback_link = format!(
103        "EXPANSIONS_DESERIALIZE_{}",
104        item_name.to_string().to_uppercase()
105    );
106
107    let callback_link = Ident::new(callback_link.as_str(), item.span());
108
109    let expanded = quote! {
110        #item
111
112        #[::cairo_lang_macro::linkme::distributed_slice(::cairo_lang_macro::MACRO_DEFINITIONS_SLICE)]
113        #[linkme(crate = ::cairo_lang_macro::linkme)]
114        static #callback_link: ::cairo_lang_macro::ExpansionDefinition =
115            ::cairo_lang_macro::ExpansionDefinition{
116                name: #expansion_name,
117                doc: #doc,
118                kind: #kind,
119                fun: #func(#item_name),
120            };
121    };
122    TokenStream::from(expanded)
123}
124
125#[derive(Default)]
126struct AttributeArgs {
127    parent_module_path: Option<LitStr>,
128}
129
130impl Parse for AttributeArgs {
131    fn parse(input: ParseStream) -> Result<Self> {
132        if input.is_empty() {
133            return Ok(Self {
134                parent_module_path: None,
135            });
136        }
137        let parent_identifier: Ident = input.parse()?;
138        if parent_identifier != "parent" {
139            return Err(input.error("only `parent` argument is supported"));
140        }
141        let _eq_token: Token![=] = input.parse()?;
142        let parent_module_path: LitStr = input.parse()?;
143        Ok(Self {
144            parent_module_path: Some(parent_module_path),
145        })
146    }
147}
148
149fn is_valid_path(path: &str) -> bool {
150    let mut chars = path.chars().peekable();
151    let mut last_was_colon = false;
152    while let Some(c) = chars.next() {
153        if c.is_alphanumeric() || c == '_' {
154            last_was_colon = false;
155        } else if c == ':' {
156            if last_was_colon {
157                // If the last character was also a colon, continue
158                last_was_colon = false;
159            } else {
160                // If the next character is not a colon, it's an error
161                if chars.peek() != Some(&':') {
162                    return false;
163                }
164                last_was_colon = true;
165            }
166        } else {
167            return false;
168        }
169    }
170    // If the loop ends with a colon flag still true, it means the string ended with a single colon.
171    !last_was_colon
172}
173
174/// Constructs the post-processing callback.
175///
176/// This callback will be called after the source code compilation (and thus after all the procedural
177/// macro expansion calls).
178/// The post-processing callback is the only function defined by the procedural macro that is
179/// allowed to have side effects.
180///
181/// This macro will be called with a list of all auxiliary data emitted by the macro during code expansion.
182///
183/// This data can be used to collect additional information from the source code of a project
184/// that is being compiled during the macro execution.
185/// For instance, you can create a procedural macro that collects some information stored by
186/// the Cairo programmer as attributes in the project source code.
187/// This callback will be called after the source code compilation (and thus after all the procedural
188/// macro executions). All auxiliary data emitted by the procedural macro during source code compilation
189/// will be passed to the callback as an argument.
190///
191/// This macro hides the conversion to stable ABI structs from the user.
192///
193/// If multiple callbacks are defined within the macro, all the implementations will be executed.
194/// No guarantees can be made regarding the order of execution.
195#[proc_macro_attribute]
196pub fn post_process(_args: TokenStream, input: TokenStream) -> TokenStream {
197    let item: ItemFn = parse_macro_input!(input as ItemFn);
198    let item = hide_name(item);
199    let item_name = &item.sig.ident;
200
201    let callback_link = format!(
202        "POST_PROCESS_DESERIALIZE_{}",
203        item_name.to_string().to_uppercase()
204    );
205    let callback_link = Ident::new(callback_link.as_str(), item.span());
206
207    let expanded = quote! {
208        #item
209
210        #[::cairo_lang_macro::linkme::distributed_slice(::cairo_lang_macro::AUX_DATA_CALLBACKS)]
211        #[linkme(crate = ::cairo_lang_macro::linkme)]
212        static #callback_link: fn(::cairo_lang_macro::PostProcessContext) = #item_name;
213    };
214
215    TokenStream::from(expanded)
216}
217
218/// Rename item to hide it from the macro source code.
219fn hide_name(mut item: ItemFn) -> ItemFn {
220    let id = short_hash(item.sig.ident.to_string());
221    let item_name = format!("{}_{}", item.sig.ident, id);
222    item.sig.ident = Ident::new(item_name.as_str(), item.sig.ident.span());
223    item
224}
225
226const EXEC_ATTR_PREFIX: &str = "__exec_attr_";
227
228#[proc_macro]
229pub fn executable_attribute(input: TokenStream) -> TokenStream {
230    let input: LitStr = parse_macro_input!(input as LitStr);
231    let callback_link = format!("EXEC_ATTR_DESERIALIZE{}", input.value().to_uppercase());
232    let callback_link = Ident::new(callback_link.as_str(), input.span());
233    let item_name = format!("{EXEC_ATTR_PREFIX}{}", input.value());
234    let org_name = Ident::new(item_name.as_str(), input.span());
235    let expanded = quote! {
236        fn #org_name() {
237            // No op to ensure no function with the same name is created.
238        }
239
240        #[::cairo_lang_macro::linkme::distributed_slice(::cairo_lang_macro::MACRO_DEFINITIONS_SLICE)]
241        #[linkme(crate = ::cairo_lang_macro::linkme)]
242        static #callback_link: ::cairo_lang_macro::ExpansionDefinition =
243            ::cairo_lang_macro::ExpansionDefinition{
244                name: #item_name,
245                doc: "",
246                kind: ::cairo_lang_macro::ExpansionKind::Attr,
247                fun: ::cairo_lang_macro::ExpansionFunc::Attr(::cairo_lang_macro::no_op_attr),
248            };
249    };
250    TokenStream::from(expanded)
251}