Skip to content

Commit ee28525

Browse files
authored
Add procedural macros page to docs (#1492)
commit-id:9204b5f2 --- **Stack**: - #1497 - #1496 - #1495 - #1494 - #1493 - #1492⚠️ *Part of a stack created by [spr](https://github.com/ejoffe/spr). Do not merge manually using the UI - doing so may have unexpected results.*
1 parent 520ada9 commit ee28525

File tree

2 files changed

+352
-0
lines changed

2 files changed

+352
-0
lines changed

website/.vitepress/config.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const sidebar = {
4949
items: [
5050
p("Compilation model", "/docs/reference/compilation-model"),
5151
p("Conditional compilation", "/docs/reference/conditional-compilation"),
52+
p("Procedural Macros", "/docs/reference/procedural-macro"),
5253
p("Global directories", "/docs/reference/global-directories"),
5354
p("Manifest", "/docs/reference/manifest"),
5455
p("Lockfile", "/docs/reference/lockfile"),
+351
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
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

Comments
 (0)