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::CALLBACKS)]
211        #[linkme(crate = ::cairo_lang_macro::linkme)]
212        static #callback_link: ::cairo_lang_macro::Callback = ::cairo_lang_macro::Callback::PostProcess(#item_name);
213    };
214
215    TokenStream::from(expanded)
216}
217
218/// Constructs the fingerprint callback.
219///
220/// A fingerprint is an `u64` value used to determine if Cairo code depending on this
221/// procedural macro should be recompiled or can use the incremental cache artifacts from
222/// the previous build.
223///
224/// This callback enables macro authors to force recompilation of macro depending on code.
225/// This is necessary when the macro output depends on anything but the code passed as expansion
226/// arguments (e.g. environmental variables, etc.).
227///
228/// Multiple callbacks can be defined within the macro, in which case values of fingerprints will be
229/// combined, using a formula akin to `boost::hash_combine` implementation.
230#[proc_macro_attribute]
231pub fn fingerprint(_args: TokenStream, input: TokenStream) -> TokenStream {
232    let item: ItemFn = parse_macro_input!(input as ItemFn);
233    let item = hide_name(item);
234    let item_name = &item.sig.ident;
235
236    let callback_link = format!(
237        "FINGERPRINT_DESERIALIZE_{}",
238        item_name.to_string().to_uppercase()
239    );
240    let callback_link = Ident::new(callback_link.as_str(), item.span());
241
242    let expanded = quote! {
243        #item
244
245        #[::cairo_lang_macro::linkme::distributed_slice(::cairo_lang_macro::CALLBACKS)]
246        #[linkme(crate = ::cairo_lang_macro::linkme)]
247        static #callback_link: ::cairo_lang_macro::Callback = ::cairo_lang_macro::Callback::Fingerprint(#item_name);
248    };
249
250    TokenStream::from(expanded)
251}
252
253/// Rename item to hide it from the macro source code.
254fn hide_name(mut item: ItemFn) -> ItemFn {
255    let id = short_hash(item.sig.ident.to_string());
256    let item_name = format!("{}_{}", item.sig.ident, id);
257    item.sig.ident = Ident::new(item_name.as_str(), item.sig.ident.span());
258    item
259}
260
261const EXEC_ATTR_PREFIX: &str = "__exec_attr_";
262
263#[proc_macro]
264pub fn executable_attribute(input: TokenStream) -> TokenStream {
265    let input: LitStr = parse_macro_input!(input as LitStr);
266    let callback_link = format!("EXEC_ATTR_DESERIALIZE{}", input.value().to_uppercase());
267    let callback_link = Ident::new(callback_link.as_str(), input.span());
268    let item_name = format!("{EXEC_ATTR_PREFIX}{}", input.value());
269    let org_name = Ident::new(item_name.as_str(), input.span());
270    let expanded = quote! {
271        fn #org_name() {
272            // No op to ensure no function with the same name is created.
273        }
274
275        #[::cairo_lang_macro::linkme::distributed_slice(::cairo_lang_macro::MACRO_DEFINITIONS_SLICE)]
276        #[linkme(crate = ::cairo_lang_macro::linkme)]
277        static #callback_link: ::cairo_lang_macro::ExpansionDefinition =
278            ::cairo_lang_macro::ExpansionDefinition{
279                name: #item_name,
280                doc: "",
281                kind: ::cairo_lang_macro::ExpansionKind::Attr,
282                fun: ::cairo_lang_macro::ExpansionFunc::Attr(::cairo_lang_macro::no_op_attr),
283            };
284    };
285    TokenStream::from(expanded)
286}