Skip to main content

ab_riscv_macros/
build.rs

1mod enum_definition;
2mod enum_impl;
3mod execution_impl;
4mod shared;
5mod state;
6#[cfg(test)]
7mod tests;
8
9use crate::build::enum_definition::{
10    collect_enum_definitions_from_dependencies, process_enum_definition,
11    process_pending_enum_definitions,
12};
13use crate::build::enum_impl::{
14    collect_original_enum_decoding_impls_from_dependencies, process_enum_impl,
15    process_pending_enum_impls,
16};
17use crate::build::execution_impl::{
18    collect_enum_csr_impls_from_dependencies,
19    collect_original_enum_execution_impls_from_dependencies, process_execution_impl,
20    process_pending_enum_execution_impls,
21};
22use crate::build::shared::{
23    INSTRUCTION_ATTRIBUTE, INSTRUCTION_EXECUTION_ATTRIBUTE, is_named_attribute,
24};
25use crate::build::state::State;
26use ab_riscv_macros_common::code_utils::pre_process_rust_code;
27use anyhow::Context;
28use quote::ToTokens;
29use std::path::{Path, PathBuf};
30use std::{env, fs, io, iter};
31use syn::visit::{Visit, visit_item_enum, visit_item_impl};
32use syn::{ItemEnum, ItemImpl};
33
34/// Processes all instruction macros in the crate when called from `build.rs`.
35///
36/// Items annotated with `#[instruction]` or `#[instruction_execution]` are found anywhere in the
37/// crate's Rust files, including inline modules and function bodies, and attributes may be
38/// path-qualified like `#[ab_riscv_macros::instruction]`. Attributes are recognized by the last
39/// segment of their path in the source code, there is no name resolution.
40///
41/// # Limitations
42///
43/// The following usages are skipped silently, and the corresponding macro then fails to include a
44/// file that was never generated:
45/// * renamed imports of the macros, like `#[foo]` after `use ab_riscv_macros::instruction as foo;`
46/// * items inside macro invocations (like `macro_rules!` or a function-like macro call), whose
47///   contents are not parsed
48/// * attributes produced by other macros (like `cfg_attr`)
49/// * files where no such attribute is written at the start of a line (indentation is fine)
50pub fn process_instruction_macros() -> anyhow::Result<()> {
51    let manifest_dir = env::var_os("CARGO_MANIFEST_DIR").context(
52        "Failed to retrieve `CARGO_MANIFEST_DIR` environment variable, make sure to call \
53        `process_instruction_macros` from `build.rs`",
54    )?;
55    let out_dir = env::var_os("OUT_DIR").context(
56        "Failed to retrieve `OUT_DIR` environment variable, make sure to call \
57        `process_instruction_macros` from `build.rs`",
58    )?;
59    let out_dir = Path::new(&out_dir);
60
61    let mut state = State::new();
62
63    for maybe_enum_definition in collect_enum_definitions_from_dependencies() {
64        let (
65            original_item_enum,
66            item_enum,
67            ignored_instructions,
68            direct_dependencies,
69            dependencies_for_enablement,
70            source,
71        ) = maybe_enum_definition?;
72
73        state.insert_known_enum_definition(
74            original_item_enum,
75            item_enum,
76            ignored_instructions,
77            direct_dependencies,
78            dependencies_for_enablement,
79            source,
80        )?;
81    }
82    for maybe_enum_impl in collect_original_enum_decoding_impls_from_dependencies() {
83        let (item_impl, source) = maybe_enum_impl?;
84        state.insert_known_original_enum_decoding_impl(item_impl, source)?;
85    }
86    for maybe_enum_csr_impl in collect_enum_csr_impls_from_dependencies() {
87        let (item_impl, source) = maybe_enum_csr_impl?;
88        state.insert_known_enum_csr_impl(item_impl, source)?;
89    }
90    for maybe_enum_execution_impl in collect_original_enum_execution_impls_from_dependencies() {
91        let (item_impl, source) = maybe_enum_execution_impl?;
92        state.insert_known_original_enum_execution_impl(item_impl, source)?;
93    }
94
95    for maybe_rust_file in rust_files_in(PathBuf::from(&manifest_dir)) {
96        let rust_file = maybe_rust_file.context("Failed to collect Rust files")?;
97        process_rust_file(&rust_file, out_dir, &mut state)
98            .with_context(|| format!("Failed to process Rust file `{}`", rust_file.display()))?;
99    }
100
101    process_pending_enum_definitions(out_dir, &mut state)?;
102    process_pending_enum_impls(out_dir, &mut state)?;
103    process_pending_enum_execution_impls(out_dir, &mut state)
104}
105
106fn rust_files_in(dir: PathBuf) -> Box<dyn Iterator<Item = io::Result<PathBuf>>> {
107    fn walk(dir: PathBuf) -> Box<dyn Iterator<Item = io::Result<PathBuf>>> {
108        let read_dir = match fs::read_dir(dir) {
109            Ok(iter) => iter,
110            Err(error) => {
111                return Box::new(iter::once(Err(error))) as Box<_>;
112            }
113        };
114
115        Box::new(read_dir.flat_map(move |entry_res| {
116            let entry = match entry_res {
117                Ok(entry) => entry,
118                Err(error) => {
119                    return Box::new(iter::once(Err(error))) as Box<_>;
120                }
121            };
122
123            let path = entry.path();
124
125            if path.is_dir() {
126                walk(path)
127            } else if path
128                .extension()
129                .and_then(|ext| ext.to_str())
130                .is_some_and(|ext| ext == "rs")
131            {
132                Box::new(iter::once(Ok(path))) as Box<_>
133            } else {
134                Box::new(iter::empty::<io::Result<PathBuf>>()) as Box<_>
135            }
136        }))
137    }
138
139    walk(dir)
140}
141
142/// Whether a Rust file may contain `#[instruction]` or `#[instruction_execution]` attributes.
143///
144/// This helps to quickly skip files that may use Rust nightly syntax not supported by `syn`, which
145/// is limited to stable Rust. Any line that starts with such an attribute (path-qualified or not,
146/// regardless of indentation) is a match.
147fn may_contain_instruction_macros(file_contents: &str) -> bool {
148    file_contents.lines().any(|line| {
149        let Some(attribute) = line.trim_start().strip_prefix("#[") else {
150            return false;
151        };
152        // Arguments may be delimited by any kind of brackets, and the path may contain whitespace
153        let path = attribute
154            .split_once(|c: char| {
155                !(c.is_alphanumeric() || c.is_whitespace() || matches!(c, '_' | ':'))
156            })
157            .map_or(attribute, |(path, _)| path);
158        let name = path.rsplit_once("::").map_or(path, |(_, name)| name);
159
160        matches!(
161            name.trim(),
162            INSTRUCTION_ATTRIBUTE | INSTRUCTION_EXECUTION_ATTRIBUTE
163        )
164    })
165}
166
167/// An item annotated with `#[instruction]` or `#[instruction_execution]`
168enum InstructionItem {
169    Enum(ItemEnum),
170    Impl(ItemImpl),
171}
172
173/// Collects enums and impls annotated with `#[instruction]` or `#[instruction_execution]`
174/// anywhere in a file, including inline modules and function bodies
175#[derive(Default)]
176struct InstructionItemsCollector {
177    items: Vec<InstructionItem>,
178}
179
180impl Visit<'_> for InstructionItemsCollector {
181    fn visit_item_enum(&mut self, i: &ItemEnum) {
182        if i.attrs
183            .iter()
184            .any(|attribute| is_named_attribute(attribute, INSTRUCTION_ATTRIBUTE))
185        {
186            self.items.push(InstructionItem::Enum(i.clone()));
187        }
188
189        visit_item_enum(self, i);
190    }
191
192    fn visit_item_impl(&mut self, i: &ItemImpl) {
193        if i.attrs.iter().any(|attribute| {
194            is_named_attribute(attribute, INSTRUCTION_ATTRIBUTE)
195                || is_named_attribute(attribute, INSTRUCTION_EXECUTION_ATTRIBUTE)
196        }) {
197            self.items.push(InstructionItem::Impl(i.clone()));
198        }
199
200        visit_item_impl(self, i);
201    }
202}
203
204fn process_rust_file(source: &Path, out_dir: &Path, state: &mut State) -> anyhow::Result<()> {
205    let mut file_contents = fs::read_to_string(source).context("Failed to read Rust file")?;
206    if !may_contain_instruction_macros(&file_contents) {
207        return Ok(());
208    }
209
210    pre_process_rust_code(&mut file_contents);
211
212    let file = syn::parse_file(&file_contents).context("Failed to parse Rust file")?;
213
214    let mut collector = InstructionItemsCollector::default();
215    collector.visit_file(&file);
216
217    for item in collector.items {
218        match item {
219            InstructionItem::Enum(item_enum) => {
220                let enum_name = item_enum.ident.clone();
221                process_enum_definition(item_enum, out_dir, state).with_context(|| {
222                    format!(
223                        "Failed to process enum `{enum_name}` in file `{}`",
224                        source.display()
225                    )
226                })?;
227            }
228            InstructionItem::Impl(item_impl) => {
229                let trait_name = item_impl.trait_.as_ref().map(|(path, _)| {
230                    path.segments
231                        .last()
232                        .expect("Path is never empty; qed")
233                        .ident
234                        .clone()
235                });
236                let type_name = item_impl.self_ty.clone();
237                if let Some(result) = process_enum_impl(item_impl.clone(), out_dir, state) {
238                    result.with_context(|| {
239                        format!(
240                            "Failed to process impl block (`{:?}` for `{}`) in file `{}`",
241                            trait_name.to_token_stream(),
242                            type_name.to_token_stream(),
243                            source.display()
244                        )
245                    })?;
246                } else if let Some(result) = process_execution_impl(item_impl, out_dir, state) {
247                    result.with_context(|| {
248                        format!(
249                            "Failed to process impl block (`{:?}` for `{}`) in file `{}`",
250                            trait_name.to_token_stream(),
251                            type_name.to_token_stream(),
252                            source.display()
253                        )
254                    })?;
255                }
256            }
257        }
258    }
259
260    Ok(())
261}