|
| 1 | +# Procedural Macros |
| 2 | + |
| 3 | +> [!WARNING] |
| 4 | +> To use procedural macros, you need to have Rust toolchain (Cargo) installed on your machine. |
| 5 | +> Please see [Rust installation guide](https://www.rust-lang.org/tools/install) for more information. |
| 6 | +
|
| 7 | +## Summary |
| 8 | + |
| 9 | +Inspired by Rust's procedural macro system, Scarb procedural macros aim is to bring user-defined macros support to Cairo |
| 10 | +packages. |
| 11 | +In general, this allows writing expressions (`macro!()`), attributes (`#[macro]`), and derive |
| 12 | +macros (`#[derive(Macro)]`) that transform Cairo code in your package. |
| 13 | +This transformations can be loaded dynamically per compilation unit as dependencies. |
| 14 | + |
| 15 | +## Guide-level explanation |
| 16 | + |
| 17 | +### Procedural macro user perspective |
| 18 | + |
| 19 | +To use a procedural macro, a Cairo programmer will have to: |
| 20 | + |
| 21 | +- Declare a dependency on a package, that implements the procedural macro, by adding it to the `dependencies` section in |
| 22 | + the Scarb manifest file. |
| 23 | +- Use the procedural macro in Cairo code, by calling it, or adding an attribute or derive macro to a Cairo item. |
| 24 | + |
| 25 | +Since Scarb procedural macros are in fact Rust functions, that are distributed as source code and compiled into shared |
| 26 | +libraries, users are required to have Rust toolchain installed on their machine. |
| 27 | +Apart from this requirement, the user will not have to perform any additional steps to use a procedural macro. |
| 28 | +In particular, these two steps can be performed without any knowledge of Rust, or even the fact that the procedural |
| 29 | +macro is implemented in Rust. |
| 30 | + |
| 31 | +Specifically, following points are true: |
| 32 | + |
| 33 | +#### Procedural macro packages can be used as dependencies |
| 34 | + |
| 35 | +- Scarb packages can simply declare dependency relationships on other packages that implement Cairo procedural macros. |
| 36 | +- Because of semantics of Scarb package resolution, it will guarantee by itself, that only one instance of given |
| 37 | + procedural macro package exists in resolved package set. |
| 38 | + - In other words, Scarb will out of the box verify, that there is no simultaneous dependency on `proc-macro 1.0.0` |
| 39 | + and `proc-macro 2.0.0` or `proc-macro 1.0.1`. |
| 40 | +- Procedural macros will end up being actual Scarb compilation unit components, though, because they will have to be |
| 41 | + treated differently from regular components, they will not be listed under `components` fields, but rather in a new |
| 42 | + one: `plugins`. |
| 43 | + |
| 44 | +#### Procedural macro will be called from Cairo code |
| 45 | + |
| 46 | +The procedural macro have to be called from Cairo code in order to be executed during the compilation. |
| 47 | +In contrast to current behaviour of Cairo plugins, no longer will they be executed on each node of AST. |
| 48 | + |
| 49 | +The procedural macro will be triggered by one of three Cairo expressions |
| 50 | + |
| 51 | +- Macro call, e.g. `macro!` |
| 52 | +- Macro attribute, e.g. `#[macro]` |
| 53 | +- Macro derive, e.g. `#[derive(Macro)]` |
| 54 | + |
| 55 | +**Example:** |
| 56 | + |
| 57 | +Scarb manifest file: |
| 58 | + |
| 59 | +```toml |
| 60 | +[package] |
| 61 | +name = "hello-macros" |
| 62 | +version = "0.1.0" |
| 63 | + |
| 64 | +[dependencies] |
| 65 | +add-macro = "0.1.0" |
| 66 | +tracing-macro = "0.1.0" |
| 67 | +to-value-macro = "0.1.0" |
| 68 | +``` |
| 69 | + |
| 70 | +Cairo source file: |
| 71 | + |
| 72 | +```cairo |
| 73 | +use add_macro::add; |
| 74 | +use tracing_macro::instrument; |
| 75 | +use to_value_macro::ToValue; |
| 76 | +
|
| 77 | +#[derive(ToValue)] |
| 78 | +struct Input { |
| 79 | + value: felt252, |
| 80 | +} |
| 81 | +
|
| 82 | +#[instrument] |
| 83 | +fn main() -> felt252 { |
| 84 | + let a = Input { value: 1 }; |
| 85 | + let b = Input { value: 2 }; |
| 86 | + add!(a.to_value(), b.to_value()); |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +### Procedural macro author perspective |
| 91 | + |
| 92 | +Scarb procedural macros are in fact Rust functions, that take Cairo code as input and return modified Cairo code as an |
| 93 | +output. |
| 94 | + |
| 95 | +To implement a procedural macro, a programmer have to: |
| 96 | + |
| 97 | +- Create a new package, with a `Scarb.toml` manifest file, `Cargo.toml` manifest file and a `src/` directory besides. |
| 98 | +- The Scarb manifest file must define a `cairo-plugin` target type. |
| 99 | +- The Cargo manifest file must define a `crate-type = ["cdylib"]` on `[lib]` target. |
| 100 | +- Write a Rust library, inside the `src/` directory that implements the procedural macro API. |
| 101 | +- A Rust crate exposing an API for writing procedural macros is published for programmers under the |
| 102 | + name `cairo-lang-macro`. This crate must be added to the `Cargo.toml` file. |
| 103 | +- The Rust library contained in the package have to implement a functions responsible for code expansion. |
| 104 | +- This function accepts a `TokenStream` as an input and returns a `ProcMacroResult` as an output, both defined in the |
| 105 | + helper library. |
| 106 | +- The result struct contains the transformed `TokenStream`. Three kinds of results are possible: |
| 107 | + - If the `TokenStream` is the same as the input, the AST is not modified. |
| 108 | + - If the `TokenStream` is different from the input, the input is replaced with the generated code. |
| 109 | + - If the `TokenStream` is empty, the input is removed. |
| 110 | +- Alongside the new TokenStream, a procedural macro can emit compiler diagnostics, auxiliary data and full path |
| 111 | + identifiers, described in detail in advanced macro usage section. |
| 112 | + |
| 113 | +We define token stream as some encapsulation of code represented in plain Cairo. |
| 114 | +Token stream can be converted into a String with `to_string()` method. |
| 115 | + |
| 116 | +### Creating procedural macros with helpers |
| 117 | + |
| 118 | +To simplify the process of writing procedural macros, a set of helper macros is provided in the `cairo-lang-macro`. |
| 119 | +This helpers are implemented as Rust procedural macros that hide the details of FFI communication from Scarb procedural |
| 120 | +macro author. |
| 121 | +These three macro helpers are: |
| 122 | + |
| 123 | +1. #[`inline_macro`] - Implements an expression macro. Should be used on function that accepts single token stream. |
| 124 | +2. #[`attribute_macro`] - Implements an attribute macro. Should be used on function that accepts two token streams - |
| 125 | + first for the attribute arguments (`#[macro(arguments)]`) and second for the item the attribute is applied to. |
| 126 | +3. #[`derive_macro`] - Implements a derive macro. Should be used on function that accepts single token stream, the item |
| 127 | + the derive is applied to. Note that derives cannot replace the original item, but rather add new items to the module. |
| 128 | + |
| 129 | +### Parsing token streams |
| 130 | + |
| 131 | +To parse Cairo code, you can use the `cairo-lang-parser` crate, defined in the Cairo compiler repository and available |
| 132 | +on crates.io. |
| 133 | +The parser implemented there provides two helpful methods `parse_virtual` and `parse_virtual_with_diagnostics`, which |
| 134 | +accept token streams. |
| 135 | + |
| 136 | +Example: |
| 137 | + |
| 138 | +```rust |
| 139 | +use cairo_lang_macro::{ProcMacroResult, TokenStream, inline_macro}; |
| 140 | +use cairo_lang_parser::{SimpleParserDatabase}; |
| 141 | + |
| 142 | +#[inline_macro] |
| 143 | +pub fn some(token_stream: TokenStream) -> ProcMacroResult { |
| 144 | + let db = SimpleParserDatabase::default(); |
| 145 | + // To obtain parser diagnostics alongside parsed node. |
| 146 | + let (parsed_node, diagnostics) = db.parse_virtual_with_diagnostics(token_stream); |
| 147 | + // To obtain parsed node only, returning any diagnostics as an error. |
| 148 | + let parsed_node = db.parse_virtual(token_stream).unwrap(); |
| 149 | + (...) |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +### Procedural macro example: |
| 154 | + |
| 155 | +```toml |
| 156 | +# Scarb.toml |
| 157 | +[package] |
| 158 | +name = "some_macro" |
| 159 | +version = "0.1.0" |
| 160 | + |
| 161 | +[cairo-plugin] |
| 162 | +``` |
| 163 | + |
| 164 | +```toml |
| 165 | +# Cargo.toml |
| 166 | +[package] |
| 167 | +name = "some_macro" |
| 168 | +version = "0.1.0" |
| 169 | +edition = "2021" |
| 170 | +publish = false |
| 171 | + |
| 172 | +[lib] |
| 173 | +crate-type = ["cdylib"] |
| 174 | + |
| 175 | +[dependencies] |
| 176 | +cairo-lang-macro = "0.1.0" |
| 177 | +``` |
| 178 | + |
| 179 | +```rust |
| 180 | +// src/lib.rs |
| 181 | +use cairo_lang_macro::{ProcMacroResult, TokenStream, inline_macro}; |
| 182 | + |
| 183 | +/// The entry point of procedural macro implementation. |
| 184 | +#[inline_macro] |
| 185 | +pub fn some(token_stream: TokenStream) -> ProcMacroResult { |
| 186 | + // no-op |
| 187 | + ProcMacroResult::new(token_stream) |
| 188 | +} |
| 189 | +``` |
| 190 | + |
| 191 | +## Reference-level explanation |
| 192 | + |
| 193 | +### Procedural macros are special Scarb packages containing Rust code |
| 194 | + |
| 195 | +- Procedural macros are packaged as special Scarb packages, which use a native target type: `cairo-plugin`. |
| 196 | +- The procedural macro package will contain Rust source code, which will be shipped to users on Scarb project |
| 197 | + resolution through Scarb dependencies system (same as regular packages). |
| 198 | +- The procedural macro source code will be compiled on Scarb users system only. |
| 199 | +- Enabling this target means that the package does not contain any Cairo code. |
| 200 | +- This target is be _exclusive_: |
| 201 | + - It blocks defining other targets for the package, not even `lib`. |
| 202 | + - It will also not be possible to declare dependencies, or specify Cairo compiler settings, it won't make sense for |
| 203 | + these packages. |
| 204 | +- During Scarb workspace resolution, all procedural macro packages are resolved and their dependencies fetched. |
| 205 | +- Procedural macros are compiled inside the `plugins/proc_macro` subdirectory of Scarb cache. |
| 206 | +- The procedural macro compilation is shared between Scarb projects, to ensure no recompilation on each Scarb project |
| 207 | + build is required. |
| 208 | +- Procedural macros are compiled into shared libraries, with `.dylib` extension on OSX, `.so` extension on Linux |
| 209 | + and `.dll` on Windows. |
| 210 | + |
| 211 | +### Procedural macro packages can be used as regular dependencies |
| 212 | + |
| 213 | +See [the guide-level explanation](#Procedural-macro-packages-can-be-used-as-dependencies) for more details. |
| 214 | + |
| 215 | +### Scarb will build and load procedural macros on user machines |
| 216 | + |
| 217 | +- Source distribution takes burden of building procedural macros from their authors. |
| 218 | + - But it requires users to have Rust toolchain installed on their machines. |
| 219 | + - Scarb itself does not contain any Rust source code compilation capabilities. |
| 220 | + - Scarb requires users to have Cargo available, in case compiling a procedural macro is required. |
| 221 | + - Projects that do not rely on procedural macros can be built without Rust toolchain installed. |
| 222 | +- The procedural macros will be compiled with stable ABI layout of structs passing the FFI border. This should guarantee |
| 223 | + Rust ABI compatibility, regardless of Cargo toolchain version available on user machine. The `cdylib` crate type will |
| 224 | + be safe, and thus this should prevent runtime crashes. |
| 225 | +- Running Rust compiler, and storing `target` directory is completely private to Scarb. Users should not influence this |
| 226 | + process, which should be as hermetic as possible. |
| 227 | + |
| 228 | +### Procedural macro API in Cairo plugins |
| 229 | + |
| 230 | +- The procedural macro has to be called from Cairo code in order to be executed during the compilation. |
| 231 | +- The procedural macro can be triggered by one of three Cairo expressions |
| 232 | + - Macro call, e.g. `macro!` |
| 233 | + - Macro attribute, e.g. `#[macro]` |
| 234 | + - Macro derive, e.g. `#[derive(Macro)]` |
| 235 | +- The API for writing procedural macros for Cairo is published for programmers, versioned separately from Scarb. |
| 236 | +- In total, the implementation consists of three Rust crates. |
| 237 | + - First one, called `cairo-lang-macro`, contains the API definitions of the `TokenStream` and `ProcMacroResult` |
| 238 | + types used as input and output for macro implementation. |
| 239 | + - The second one, called `cairo-lang-macro-attributes`, contains implementation of Rust macros used for wrapping |
| 240 | + procedural macro entrypoint functions. These hide details of FFI communication from the procedural macro |
| 241 | + author. |
| 242 | + - The third one, called `cairo-lang-macro-stable`, contains the stable ABI versions of crates from |
| 243 | + the `cairo-lang-macro` crate, that can be used over the FFI communication boundary. The conversion between |
| 244 | + corresponding types from this two crates is implemented by the crate with API structs. |
| 245 | + - The first crate re-exports the contents of the second one. That's the only crate that macro authors should depend |
| 246 | + on. |
| 247 | +- The procedural macro implementation is a Rust function, accepting a `TokenStream` (described in detail in |
| 248 | + following sections) on input and returning the expansion result as an output. |
| 249 | +- The result struct contains the transformed `TokenStream`. Three kinds of results are possible: |
| 250 | + - If the `TokenStream` is the same as the input, the AST is not modified. |
| 251 | + - If the `TokenStream` is different from the input, the input is replaced with the generated code. |
| 252 | + - If the `TokenStream` is empty, the input is removed. |
| 253 | +- Alongside the new TokenStream, a procedural macro can emit auxiliary data, encoded as an arbitrary JSON. |
| 254 | +- The procedural macro can emit additional compiler diagnostics corresponding to the Cairo code it has been executed on. |
| 255 | +- The procedural macro can return optional full path markers. This markers can be used to obtain the full path to marked |
| 256 | + items in the auxiliary data after the compilation, even though the full paths are not known when the macro is |
| 257 | + executed. |
| 258 | +- The appropriate procedural macros will be executed based on the call in Cairo code by the new Cairo compiler |
| 259 | + internal `ProcMacroHost` plugin. This plugin will be called on each AST node and will decide if analyzed fragment |
| 260 | + requires code generation powered by an external plugin. |
| 261 | + |
| 262 | +### Advanced macro usage |
| 263 | + |
| 264 | +#### Token stream metadata |
| 265 | + |
| 266 | +As defined before, token stream is an encapsulation of Cairo code, that can be converted into a string. |
| 267 | +Additionally, token stream passed to the procedural macro contains metadata about the fragment of Code received. |
| 268 | +This metadata is represented by the `TokenStreamMetadata` struct, which contains the following fields: |
| 269 | + |
| 270 | +- `original_file_path` - The path to the file in users filesystem, from which the Cairo code was read. |
| 271 | +- `file_id` - An identifier assigned to the file by Scarb. This identifier is guaranteed to uniquely identify file |
| 272 | + across all files in the Scarb project. |
| 273 | + |
| 274 | +All fields in metadata struct are optional, but will be present in the token stream you receive from Scarb for |
| 275 | +expansion. |
| 276 | + |
| 277 | +This metadata can be obtained by calling `.metadata()` method on `TokenStream` struct. |
| 278 | + |
| 279 | +#### Diagnostics |
| 280 | + |
| 281 | +Procedural macros can emit compiler diagnostics, which will be displayed as warnings / errors to the user during the |
| 282 | +compilation. |
| 283 | +Diagnostics should be used to inform users about mistakes they made in their Cairo code, ideally suggesting a fix. |
| 284 | + |
| 285 | +Exemplary diagnostic reported to user: |
| 286 | + |
| 287 | +```shell |
| 288 | +error: Inline macro `some` failed. |
| 289 | +--> [..]lib.cairo:2:14 |
| 290 | + let _x = some!(); |
| 291 | + ^*****^ |
| 292 | +``` |
| 293 | + |
| 294 | +Diagnostics emitted within procedural macro will be displayed in the terminal, and the caret will be pointing to the |
| 295 | +place in users Cairo code, where the procedural macro is called. |
| 296 | + |
| 297 | +To emit diagnostics, the `with_diagnostics` method on `ProcMacroResult` struct can be used. |
| 298 | +Diagnostics can be emitted with two levels of severity: `error` and `warning`. |
| 299 | + |
| 300 | +```cairo |
| 301 | +use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro, Diagnostic}; |
| 302 | +
|
| 303 | +#[attribute_macro] |
| 304 | +pub fn some(_attr: TokenStream, token_stream: TokenStream) -> ProcMacroResult { |
| 305 | + let diag = Diagnostic::error("Some error from macro."); |
| 306 | + ProcMacroResult::new(token_stream) |
| 307 | + .with_diagnostics(diag.into()) |
| 308 | +} |
| 309 | +``` |
| 310 | + |
| 311 | +#### Auxiliary data |
| 312 | + |
| 313 | +Alongside the new TokenStream, a procedural macro can emit auxiliary data, encoded as an arbitrary JSON. |
| 314 | + |
| 315 | +```rust |
| 316 | +use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro, AuxData, PostProcessContext, post_process}; |
| 317 | +use serde::{Serialize, Deserialize}; |
| 318 | + |
| 319 | +#[derive(Debug, Serialize, Deserialize)] |
| 320 | +struct SomeMacroDataFormat { |
| 321 | + msg: String |
| 322 | +} |
| 323 | + |
| 324 | +#[attribute_macro] |
| 325 | +pub fn some(_attr: TokenStream, token_stream: TokenStream) -> ProcMacroResult { |
| 326 | + let value = SomeMacroDataFormat { msg: "Hello from some macro!".to_string() }; |
| 327 | + let value = serde_json::to_string(&value).unwrap(); |
| 328 | + let value: Vec<u8> = value.into_bytes(); |
| 329 | + let aux_data = AuxData::new(value); |
| 330 | + ProcMacroResult::new(token_stream).with_aux_data(aux_data) |
| 331 | +} |
| 332 | +``` |
| 333 | + |
| 334 | +This auxiliary data can be then consumed by a post-process callback defined within the procedural macro package, which |
| 335 | +will be executed as the last step of the project build, after the Cairo code is compiled. |
| 336 | +Your procedural macro can defined multiple post-process callbacks, in which case they all will be executed in an |
| 337 | +undefined order. |
| 338 | + |
| 339 | +```rust |
| 340 | +#[post_process] |
| 341 | +pub fn callback(context: PostProcessContext) { |
| 342 | + let aux_data = context.aux_data.into_iter() |
| 343 | + .map(|aux_data| { |
| 344 | + let value: Vec<u8> = aux_data.into(); |
| 345 | + let aux_data: SomeMacroDataFormat = serde_json::from_slice(&value).unwrap(); |
| 346 | + aux_data |
| 347 | + }) |
| 348 | + .collect::<Vec<_>>(); |
| 349 | + println!("{:?}", aux_data); |
| 350 | +} |
| 351 | +``` |
0 commit comments