Examples
WARNING
Procedural macros, by design, introduce a lot of overhead during the compilation. They may also be harder to maintain. Prefer the declarative inline macros written directly in Cairo, unless you have a specific reason to use procedural macros. There are several reasons for this:
- Compilation overhead: procedural macros are Rust crates compiled into shared libraries, adding a full Rust compilation step (via Cargo) on top of the Cairo build and significantly increasing build times.
- Rust toolchain dependency: anyone using your macro must have the Rust toolchain (Cargo) installed on their machine (unless the macro is distributed as a precompiled shared library, which is not always the case).
- Harder to debug: errors in macro expansion surface as confusing Cairo compiler diagnostics, making them difficult to diagnose.
- Higher maintenance burden: they require knowledge of both Rust and Cairo, and the FFI boundary between them adds complexity.
Please see the declarative macros chapter in Cairo Book for more information.
INFO
To use procedural macros, you need to have Rust toolchain (Cargo) installed on your machine. Please see Rust installation guide for more information.
All of the examples listed below can be found within Scarb repository here.
Example 1: returning a value
Note, we omit the toml files here, as their content is the same as in the previous example.
Usually you want to define a procedural macro that injects some code into your Cairo project. In this example, we will create an inline procedural macro that returns a single numerical value as a token.
use cairo_lang_macro::{inline_macro, Diagnostics, ProcMacroResult, TextSpan, Token, TokenStream, TokenTree};
#[inline_macro]
pub fn fib(args: TokenStream) -> ProcMacroResult {
let argument = match parse_arguments(args) {
Ok(arg) => arg,
Err(diagnostics) => {
return ProcMacroResult::new(TokenStream::new(vec![])).with_diagnostics(diagnostics)
}
};
let result = fib(argument);
ProcMacroResult::new(TokenStream::new(vec![TokenTree::Ident(Token::new(
result.to_string(),
TextSpan::call_site(),
))]))
}
/// Parse argument into a numerical value.
///
/// Always expects a single, numerical value in parentheses.
/// Panics otherwise.
fn parse_arguments(args: TokenStream) -> Result<u32, Diagnostics> {
let args = args.to_string();
let (_prefix, rest) = args
.split_once("(")
.ok_or_else(|| Diagnostics::new(Vec::new()).error("Invalid format: expected '('"))?;
let (argument, _suffix) = rest
.rsplit_once(")")
.ok_or_else(|| Diagnostics::new(Vec::new()).error("Invalid format: expected ')'"))?;
let argument = argument
.parse::<u32>()
.map_err(|_| Diagnostics::new(Vec::new()).error("Invalid argument: expected a number"))?;
Ok(argument)
}
/// Calculate n-th Fibonacci number.
fn fib(mut n: u32) -> u32 {
let mut a: u32 = 0;
let mut b: u32 = 1;
while n != 0 {
n = n - 1;
let temp = b;
b = a + b;
a = temp;
}
a
}This example is a bit more complex than the previous one. The macro works in three steps:
- Parse inline macro arguments.
- Perform some computation in Rust.
- Construct and return a new
TokenStreamas a result.
The first step is done by the parse_arguments function, in a very primitive way. We convert the whole input TokenStream into a single string and then look for left and right parentheses. We always assume the argument to be a single numerical value.
WARNING
This function is only useful for demonstration. In reality, you should make your parser more robust and never should assume that the input is valid. Properly handling parsing errors is a must if you want your users to understand why their code is not compiling. Please see parsing token stream for more information.
We then call the fib function, which calculates a number in Fibonacci sequence. Note that this calculation happens during the compilation, when the procedural macro expansion happens, not during the Cairo program execution.
The result is a single numerical value, that we convert to a TokenStream, by wrapping it in three subsequent abstractions: Token, TokenTree and TokenStream. Token represents a single Cairo token, and consists of two parts: a string representing the token content and a span. Span is a location in the source code of a project that uses this macro. It is used to persist information about the origin of tokens that are moved or copied from user code. For new tokens, that you create in your macro like we do here, it should be set to TextSpan::call_site(), which is a span that points to the location of the macro call. TokenTree is an additional enum that describes the type of the token, currently only TokenTree::Ident is used (but may be more in the future). Finally, TokenStream is a stream of TokenTrees, that can be iterated over or converted into a string.
Then you can use this macro in your Cairo code: Note that fib!(16) actually calls the fib inline macro we defined before.
fn main() -> u32 {
fib!(16)
}
#[cfg(test)]
mod tests {
use super::main;
#[test]
fn it_works() {
assert(main() == 987, 'invalid value returned!');
}
}If you test your program with scarb test, it works:
Collected 1 test(s) from hello_world package
Running 1 test(s) from src/
[PASS] hello_world::tests::it_works (l1_gas: ~0, l1_data_gas: ~0, l2_gas: ~40000)
Tests: 1 passed, 0 failed, 0 ignored, 0 filtered outNotice how no computations actually happen during Cairo program execution. This Cairo project compiles into the following CASM code:
[ap + 0] = 987, ap++;
ret;INFO
To see a real life example of a procedural macro that offloads some work into compile time, you can take a look at the alexandria project.
Example 2: building token stream with quote! macro
In our macro, we manually construct the token stream we return. This approach is fine for basic and very short results, like the single numerical value we return, but it does not scale very well for longer results. Constructing longer token streams this way, say a whole new function you want to return, would not be very convenient.
The cairo-lang-macro crate defines a quote! macro, which can be used to build TokenStreams from Rust variables. This acts as a convenient wrapper around creating and pushing tokens into a TokenStream manually.
For instance, if we decide we no longer want to return a single value from our macro, but rather create a const variable declaration with it, we can use the quote! macro to make our implementation more concise.
We first change how we use our macro. The main function now returns FIB16 constant, that will be later created by the macro expansion. We move the macro call to the top level of the module.
fib!(16);
fn main() -> u32 {
FIB16
}We also change the fib function to use the quote! macro. Inside the macro call, we declare the constant value as if it was a normal Cairo source file. When we want to substitute some Rust variable with its value, we can use its name prefixed with a hash sign #.
We can do this with any variable that implements ToPrimitiveTokenStream trait from cairo-lang-primitive-token crate. This trait is implemented for TokenStream itself, so we can use quote! for composition of multiple token streams.
#[inline_macro]
pub fn fib(args: TokenStream) -> ProcMacroResult {
let argument = parse_arguments(args);
let result = fib(argument);
let result = TokenTree::Ident(Token::new(result.to_string(), TextSpan::call_site()));
ProcMacroResult::new(quote! {
const FIB16: u32 = #result;
})
}In a similar manner, you can use syntax nodes from the cairo-lang-syntax AST as variables in the macro. This is especially useful when you need to copy some Cairo code from the input token stream, say, some function annotated with your attribute procedural macro.
use cairo_lang_macro::{attribute_macro, quote, ProcMacroResult};
#[attribute_macro]
fn attr_name() {
// Parse incoming token stream.
let db = SimpleParserDatabase::default();
let (node, _diagnostics) = db.parse_token_stream(&body);
// Create `SyntaxNodeWithDb`, from a single syntax node.
// This struct implements `ToPrimitiveTokenStream` trait, thus can be used as argument to `quote!`.
let node = SyntaxNodeWithDb::new(&node, &db);
// Use the node in `quote!` macro.
ProcMacroResult::new(quote! {
#node
})
}Example 3: creating a new function
Working example of this approach can be an attribute macro that creates a new function wrapper. This new function will call the original function with some argument. The name of the wrapper function and argument value will be controlled by attribute macro arguments.
// src/lib.rs
use cairo_lang_macro::{
attribute_macro, quote, Diagnostics, ProcMacroResult, TextSpan, Token, TokenStream, TokenTree,
};
use cairo_lang_parser::utils::SimpleParserDatabase;
use cairo_lang_syntax::node::{
ast::{self, ModuleItem},
helpers::HasName,
kind::SyntaxKind,
with_db::SyntaxNodeWithDb,
SyntaxNode, Terminal, TypedSyntaxNode,
};
#[attribute_macro]
fn create_wrapper(args: TokenStream, body: TokenStream) -> ProcMacroResult {
let db = SimpleParserDatabase::default();
let new_token = |content| TokenTree::Ident(Token::new(content, TextSpan::call_site()));
let (wrapper_name, argument_value) = match parse_arguments(&db, args) {
Ok((name, value)) => (name, value),
Err(diag) => return ProcMacroResult::new(body).with_diagnostics(diag),
};
let wrapper_name = new_token(wrapper_name);
let argument_value = new_token(argument_value);
let (node, _diagnostics) = db.parse_token_stream(&body);
let function_name = match parse_function_name(&db, node.clone()) {
Ok(name) => name,
Err(diag) => return ProcMacroResult::new(body).with_diagnostics(diag),
};
let function_name = new_token(function_name);
let node = SyntaxNodeWithDb::new(&node, &db);
ProcMacroResult::new(quote! {
#node
fn #wrapper_name() -> u32 {
#function_name(#argument_value)
}
})
}
fn parse_function_name<'db>(
db: &'db SimpleParserDatabase,
node: SyntaxNode<'db>,
) -> Result<String, Diagnostics> {
if node.kind(db) != SyntaxKind::SyntaxFile {
return Err(Diagnostics::new(Vec::new()).error("Expected SyntaxFile"));
}
let file = ast::SyntaxFile::from_syntax_node(db, node);
let items = file.items(db).elements_vec(db);
if items.len() != 1 {
return Err(Diagnostics::new(Vec::new()).error("Expected exactly one item"));
}
match items.into_iter().next() {
Some(ModuleItem::FreeFunction(f)) => Ok(f.name(db).text(db).to_string(db)),
Some(_) => Err(Diagnostics::new(Vec::new()).error("Expected a function")),
None => Err(Diagnostics::new(Vec::new()).error("Expected exactly one item")),
}
}
fn parse_arguments(
db: &SimpleParserDatabase,
args: TokenStream,
) -> Result<(String, String), Diagnostics> {
let (node, _diagnostics) = db.parse_token_stream_expr(&args);
if node.kind(db) != SyntaxKind::ExprListParenthesized {
return Err(Diagnostics::new(Vec::new()).error("Expected parenthesized expression list"));
}
let expr = ast::ExprListParenthesized::from_syntax_node(db, node);
let mut expressions = expr.expressions(db).elements_vec(db).into_iter();
let wrapper_name_expr = match expressions.next() {
Some(e) => e,
None => return Err(Diagnostics::new(Vec::new()).error("Expected wrapper name argument")),
};
let wrapper_name = wrapper_name_expr.as_syntax_node().get_text(db).to_string();
let value_expr = match expressions.next() {
Some(e) => e,
None => return Err(Diagnostics::new(Vec::new()).error("Expected value argument")),
};
let value = value_expr.as_syntax_node().get_text(db).to_string();
Ok((wrapper_name, value))
}We can use the new attribute to generate a wrapper for our fib function.
// hello_world/src/lib.cairo
fn main() -> u32 {
named_wrapper()
}
#[create_wrapper(named_wrapper,16)]
fn fib(mut n: u32) -> u32 {
let mut a: u32 = 0;
let mut b: u32 = 1;
while n != 0 {
n = n - 1;
let temp = b;
b = a + b;
a = temp;
}
a
}
#[cfg(test)]
mod tests {
use super::main;
#[test]
fn it_works() {
assert(main() == 987, 'invalid value returned!');
}
}Our test will ensure that the wrapper function can be called and returns the correct value.
Collected 1 test(s) from hello_world package
Running 1 test(s) from src/
[PASS] hello_world::tests::it_works (l1_gas: ~0, l1_data_gas: ~0, l2_gas: ~80000)
Tests: 1 passed, 0 failed, 0 ignored, 0 filtered outExample 4: modifying an already existing function
Using the quote macro, we can also modify the body of the function that the attribute macro is being applied to. In this example, with the use of an attribute macro, we will define a completely new variable inside the function, which later will be used in a user code. We also make all the diagnostics from the user code correctly mapped to the original code.
// src/lib.rs
use cairo_lang_macro::{
attribute_macro, quote, Diagnostics, ProcMacroResult, TokenStream,
};
use cairo_lang_parser::utils::SimpleParserDatabase;
use cairo_lang_syntax::node::{ast, with_db::SyntaxNodeWithDb, TypedSyntaxNode};
#[attribute_macro]
fn my_macro(_args: TokenStream, body: TokenStream) -> ProcMacroResult {
let diagnostics = Diagnostics::new(Vec::new());
// Initialize parser and parse the incoming token stream.
let db = SimpleParserDatabase::default();
// Parse incoming token stream.
let (node, _diagnostics) = db.parse_token_stream(&body);
// Locate the function item this attribute macro is applied to.
let module_item_list = match node.get_children(&db).get(0) {
Some(item) => item,
None => {
let diagnostics =
diagnostics.error("This attribute macro should be only used for a function");
return ProcMacroResult::new(body).with_diagnostics(diagnostics);
}
};
let function = match module_item_list.get_children(&db).get(0) {
Some(item) => item,
None => {
let diagnostics =
diagnostics.error("This attribute macro should be only used for a function");
return ProcMacroResult::new(body).with_diagnostics(diagnostics);
}
};
// Extract the function's syntax components.
let expr = ast::FunctionWithBody::from_syntax_node(&db, *function);
let attributes = expr.attributes(&db);
let visibility = expr.visibility(&db);
let declaration = expr.declaration(&db);
let body_expr = expr.body(&db);
// Pull out braces and the first two statements from the body.
let l_brace = body_expr.lbrace(&db);
let r_brace = body_expr.rbrace(&db);
let mut statements = body_expr.statements(&db).elements(&db);
let first_statement = match statements.next() {
Some(stmt) => stmt,
None => {
let diagnostics = diagnostics
.error("function needs at least 2 statements to be valid candidate for attr macro");
return ProcMacroResult::new(body).with_diagnostics(diagnostics);
}
};
let second_statement = match statements.next() {
Some(stmt) => stmt,
None => {
let diagnostics = diagnostics
.error("function needs at least 2 statements to be valid candidate for attr macro");
return ProcMacroResult::new(body).with_diagnostics(diagnostics);
}
};
// Convert syntax nodes into `SyntaxNodeWithDb` for quoting.
let attributes_node = attributes.as_syntax_node();
let visibility_node = visibility.as_syntax_node();
let declaration_node = declaration.as_syntax_node();
let l_brace_node = l_brace.as_syntax_node();
let r_brace_node = r_brace.as_syntax_node();
let first_statement_node = first_statement.as_syntax_node();
let second_statement_node = second_statement.as_syntax_node();
let attributes_result = SyntaxNodeWithDb::new(&attributes_node, &db);
let visibility_result = SyntaxNodeWithDb::new(&visibility_node, &db);
let declaration_result = SyntaxNodeWithDb::new(&declaration_node, &db);
let l_brace_result = SyntaxNodeWithDb::new(&l_brace_node, &db);
let r_brace_result = SyntaxNodeWithDb::new(&r_brace_node, &db);
let first_statement_result = SyntaxNodeWithDb::new(&first_statement_node, &db);
let second_statement_result = SyntaxNodeWithDb::new(&second_statement_node, &db);
// Rebuild the function, injecting a statement between the first two.
ProcMacroResult::new(quote! {
#attributes_result
#visibility_result #declaration_result #l_brace_result
#first_statement_result
let macro_variable: felt252 = 2;
#second_statement_result
#r_brace_result
})
}We can use the new attribute with a function, that will have an access to the new variable. The variable will be inserted right after the first original statement of the function.
// hello_world/src/lib.cairo
#[my_macro]
fn example_function() {
let _variable1: felt252 = 1;
let _variable2: felt252 = macro_variable;
}This way, we ensure that this code is fully valid and the scarb check will end with a success:
Finished checking `dev` profile target(s) in 1 secondNote that all the original user code used in the quote! macro will be correctly mapped to the origin code. If something would be wrong with the user code. For example, if user makes a mistake in its own code, like that:
// hello_world/src/lib.cairo
#[my_macro]
fn example_function() {
let _variable1: felt252 = non_existing_variable;
let _variable2: felt252 = macro_variable;
}we will get this error:
error[E0006]: Identifier not found.
--> .../src/lib.cairo:3:31
let _variable1: felt252 = non_existing_variable;
^^^^^^^^^^^^^^^^^^^^^
note: this error originates in the attribute macro: `my_macro`which is pointing to the original user code, as he's the one that's the author of this piece of code.
If we make a mistake while generating code in the macro like this:
// src/lib.rs
#[attribute_macro]
fn my_macro(_attr: TokenStream, code: TokenStream) -> ProcMacroResult {
...
// Rebuild the function, injecting a statement between the first two.
ProcMacroResult::new(quote! {
#attributes_result
#visibility_result #declaration_result #l_brace_result
#first_statement_result
let macro_variable: felt252 = total_nonsense;
#second_statement_result
#r_brace_result
})
}we are left with error, that maps directly to the attribute macro (which is correct, because it's the macro author's fault here):
error[E0006]: Identifier not found.
--> .../src/lib.cairo:1:1
#[my_macro]
^^^^^^^^^^^
note: this error originates in the attribute macro: `my_macro`