Skip to main content

wazabin_qcode_macro/
lib.rs

1//! The `qcode!` macro.
2//!
3//! At compile time, this crate parses QCode source with
4//! [`wazabin_qcode_parser`] and validates its syntax. At run time, the emitted
5//! code lowers that source into the caller's `qcode::Context` through
6//! `qcode::lower::lower_str_with_externals`. It also binds each declared name
7//! (functions, blocks, SSA values, varnodes, and block parameters) to a Rust
8//! `let`, so callers can reference it after the invocation. `{capture}` atoms
9//! are resolved from the surrounding Rust scope.
10//!
11//! # Usage
12//!
13//! Depend on [`qcode`], which re-exports this macro; there is no reason to
14//! depend on this crate directly.
15//!
16//! ```ignore
17//! use qcode::{context::Context, qcode};
18//!
19//! let mut ctx = Context::new();
20//! let addend: u64 = 3;
21//!
22//! qcode!(
23//!     ctx,
24//!     "
25//!     fn f:
26//!     <entry>
27//!         %sum = i64 0x2 + {addend};
28//!         goto <exit @r=%sum>;
29//!     <exit @r:i64>
30//!         goto <0x1001>;
31//!     "
32//! );
33//!
34//! // `f`, `entry`, `exit`, `sum` and `r` are now Rust bindings.
35//! assert_eq!(ctx.get_value(sum.into()).is_some(), true);
36//! ```
37//!
38//! The example is not compiled here: `qcode` depends on this crate, so this
39//! crate cannot depend back on it even for a test. The macro's behaviour is
40//! covered by `qcode`'s own test suite.
41//!
42//! [`qcode`]: https://docs.rs/qcode
43
44use proc_macro::TokenStream;
45use proc_macro_crate::{FoundCrate, crate_name};
46use quote::{format_ident, quote};
47use syn::{
48    Expr, LitStr, Token,
49    parse::{Parse, ParseStream},
50    parse_macro_input,
51};
52
53use wazabin_qcode_parser::ast::{
54    Atom, ExprNode, FnDecl, Label, Program, ProgramKind, Statement, TupleField, TypedAtom,
55};
56
57struct QCodeInput {
58    expr: Expr,
59    program: LitStr,
60}
61
62impl Parse for QCodeInput {
63    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
64        let expr: Expr = input.parse()?;
65        let _comma: Token![,] = input.parse()?;
66        let program: LitStr = input.parse()?;
67        Ok(Self { expr, program })
68    }
69}
70
71#[proc_macro]
72pub fn qcode(input: TokenStream) -> TokenStream {
73    let input = parse_macro_input!(input as QCodeInput);
74    match compile(&input.expr, &input.program.value()) {
75        Ok(tokens) => tokens.into(),
76        Err(err) => err.to_compile_error().into(),
77    }
78}
79
80/// The set of names a program binds, partitioned by kind, in first-seen order.
81#[derive(Default)]
82struct Names {
83    functions: Vec<String>,
84    blocks: Vec<String>,
85    ssa: Vec<String>,
86    varnodes: Vec<String>,
87    temps: Vec<String>,
88    block_params: Vec<String>,
89    externals: Vec<String>,
90}
91
92fn push_unique(set: &mut Vec<String>, name: &str) {
93    if !set.iter().any(|n| n == name) {
94        set.push(name.to_owned());
95    }
96}
97
98fn compile(expr: &Expr, source: &str) -> syn::Result<proc_macro2::TokenStream> {
99    let err = |e: String| syn::Error::new(proc_macro2::Span::call_site(), e);
100    let program = wazabin_qcode_parser::qcode_from_str(source).map_err(|e| err(e.to_string()))?;
101
102    let mut names = Names::default();
103    collect_program(&program, &mut names);
104
105    // `{capture}` atoms that aren't shadowed by a program-defined name reference
106    // Rust values in the caller's scope; everything else resolves internally.
107    let defined: std::collections::HashSet<&str> = names
108        .functions
109        .iter()
110        .chain(&names.blocks)
111        .chain(&names.ssa)
112        .chain(&names.varnodes)
113        .chain(&names.temps)
114        .chain(&names.block_params)
115        .map(String::as_str)
116        .collect();
117    let captures: Vec<&String> = names
118        .externals
119        .iter()
120        .filter(|n| !defined.contains(n.as_str()))
121        .collect();
122
123    let qcode = resolve_crate("qcode")?;
124
125    let capture_inserts = captures.iter().map(|name| {
126        let ident = format_ident!("{}", name);
127        quote! {
128            __qcode_ext.insert(
129                ::std::string::String::from(#name),
130                #qcode::value::ValueId::from(#ident),
131            );
132        }
133    });
134
135    let bind = |ty: proc_macro2::TokenStream,
136                getter: proc_macro2::TokenStream,
137                items: &[String]|
138     -> Vec<proc_macro2::TokenStream> {
139        items
140            .iter()
141            .map(|name| {
142                let ident = format_ident!("{}", name);
143                quote! {
144                    #[allow(unused_variables)]
145                    let #ident: #qcode::value::#ty = __qcode_syms.#getter(#name);
146                }
147            })
148            .collect()
149    };
150
151    let fn_binds = bind(quote!(FunctionId), quote!(function), &names.functions);
152    let block_binds = bind(quote!(BlockId), quote!(block), &names.blocks);
153    let ssa_binds = bind(quote!(InstructionId), quote!(ssa), &names.ssa);
154    let varnode_binds = bind(quote!(VarnodeId), quote!(varnode), &names.varnodes);
155    let temp_binds = bind(quote!(TempId), quote!(temp), &names.temps);
156    let param_binds = bind(
157        quote!(BlockParamId),
158        quote!(block_param),
159        &names.block_params,
160    );
161
162    // NB: emit bare `let` statements (no wrapping block) — callers rely on the
163    // bound names leaking into their scope, exactly as the old macro did.
164    Ok(quote! {
165        let mut __qcode_ext: ::std::collections::HashMap<
166            ::std::string::String,
167            #qcode::value::ValueId,
168        > = ::std::collections::HashMap::new();
169        #(#capture_inserts)*
170        let __qcode_syms = #qcode::lower::lower_str_with_externals(&mut (#expr), #source, __qcode_ext)
171            .expect("qcode! lowering failed");
172        #(#fn_binds)*
173        #(#block_binds)*
174        #(#ssa_binds)*
175        #(#varnode_binds)*
176        #(#temp_binds)*
177        #(#param_binds)*
178        let _ = &__qcode_syms;
179    })
180}
181
182fn resolve_crate(name: &str) -> syn::Result<proc_macro2::TokenStream> {
183    match crate_name(name) {
184        Ok(FoundCrate::Itself) => Ok(quote!(crate)),
185        Ok(FoundCrate::Name(found)) => {
186            let ident = syn::Ident::new(&found, proc_macro2::Span::call_site());
187            Ok(quote!(::#ident))
188        }
189        Err(e) => Err(syn::Error::new(
190            proc_macro2::Span::call_site(),
191            format!("could not resolve `{name}` crate for qcode! macro: {e}"),
192        )),
193    }
194}
195
196fn collect_program(program: &Program, names: &mut Names) {
197    match &program.kind {
198        ProgramKind::Statements(stmts) => {
199            let split = stmts
200                .iter()
201                .position(|stmt| !matches!(stmt.inner(), Statement::LocalDecl { .. }))
202                .unwrap_or(stmts.len());
203            collect_statements(&stmts[..split], names, false);
204            collect_statements(&stmts[split..], names, true);
205        }
206        ProgramKind::Functions { varnodes, fns } => {
207            collect_statements(varnodes, names, false);
208            for fn_decl in fns {
209                collect_fn(fn_decl, names);
210            }
211        }
212    }
213}
214
215fn collect_fn(fn_decl: &FnDecl, names: &mut Names) {
216    push_unique(&mut names.functions, &fn_decl.name);
217    collect_statements(&fn_decl.statements, names, true);
218}
219
220fn collect_statements(stmts: &[Statement], names: &mut Names, local_temps: bool) {
221    for stmt in stmts {
222        collect_statement(stmt.inner(), names, local_temps);
223    }
224}
225
226fn collect_statement(stmt: &Statement, names: &mut Names, local_temps: bool) {
227    match stmt {
228        Statement::LocalDecl { name, .. } => {
229            if local_temps {
230                push_unique(&mut names.temps, name);
231            } else {
232                push_unique(&mut names.varnodes, name);
233            }
234        }
235        Statement::Assign { name, expr, .. } => {
236            push_unique(&mut names.ssa, name);
237            collect_expr(expr, names);
238        }
239        Statement::Expr(expr) => collect_expr(expr, names),
240        Statement::LabelDecl {
241            label: Label::Named { name, params, .. },
242            ..
243        } => {
244            push_unique(&mut names.blocks, name);
245            for p in params {
246                push_unique(&mut names.block_params, &p.name);
247            }
248        }
249        Statement::LabelDecl { .. } => {}
250        Statement::Branch { args, .. } => {
251            for (_, atom) in args {
252                collect_atom(atom, names);
253            }
254        }
255        Statement::BranchInd { ptr, .. } => collect_atom(ptr, names),
256        Statement::Switch {
257            scrutinee,
258            cases,
259            default,
260            ..
261        } => {
262            collect_atom(scrutinee, names);
263            let case_args = cases.iter().flat_map(|(_, _, args)| args);
264            let default_args = default.iter().flat_map(|(_, args)| args);
265            for (_, atom) in case_args.chain(default_args) {
266                collect_atom(atom, names);
267            }
268        }
269        Statement::CBranch {
270            condition,
271            target_args,
272            fallthrough_args,
273            ..
274        } => {
275            collect_atom(condition, names);
276            for (_, atom) in target_args.iter().chain(fallthrough_args) {
277                collect_atom(atom, names);
278            }
279        }
280        Statement::Call { args, .. } => {
281            for (_, atom) in args {
282                collect_atom(atom, names);
283            }
284        }
285        Statement::CallInd { ptr, args, .. } => {
286            collect_atom(ptr, names);
287            for atom in args {
288                collect_atom(atom, names);
289            }
290        }
291        Statement::Return { ptr, value, .. } => {
292            collect_atom(ptr, names);
293            if let Some(v) = value {
294                collect_atom(v, names);
295            }
296        }
297        Statement::ReturnValue { value, .. } => collect_atom(value, names),
298        // Operand-less marker: no names to collect.
299        Statement::BadInsn { .. } => {}
300        Statement::Assert { condition, .. } => collect_atom(condition, names),
301        Statement::Commented { inner, .. } => collect_statement(inner, names, local_temps),
302    }
303}
304
305fn collect_expr(expr: &ExprNode, names: &mut Names) {
306    match expr {
307        ExprNode::Atom(a) => collect_atom(a, names),
308        ExprNode::Unop { src, .. } => collect_atom(src, names),
309        ExprNode::Binary { lhs, rhs, .. } => {
310            collect_atom(lhs, names);
311            collect_atom(rhs, names);
312        }
313        ExprNode::Cast { src, .. } => collect_atom(src, names),
314        ExprNode::Load { ptr, .. } => collect_atom(ptr, names),
315        ExprNode::Store { ptr, src, .. } => {
316            collect_atom(ptr, names);
317            collect_atom(src, names);
318        }
319        ExprNode::FuncCall { args, .. }
320        | ExprNode::Intrinsic { args, .. }
321        | ExprNode::Apply { args, .. } => {
322            for a in args {
323                collect_atom(a, names);
324            }
325        }
326        ExprNode::Map { src, captures, .. } => {
327            collect_atom(src, names);
328            for a in captures {
329                collect_atom(a, names);
330            }
331        }
332        ExprNode::Scan {
333            init,
334            src,
335            captures,
336            ..
337        } => {
338            collect_atom(init, names);
339            collect_atom(src, names);
340            for a in captures {
341                collect_atom(a, names);
342            }
343        }
344        ExprNode::Tuple { fields } => {
345            for TupleField { value, .. } in fields {
346                collect_atom(value, names);
347            }
348        }
349        ExprNode::Extract { agg, .. } => collect_atom(agg, names),
350        ExprNode::Gep { base, .. } => collect_atom(base, names),
351        ExprNode::Range { src, .. } => collect_atom(src, names),
352    }
353}
354
355fn collect_atom(atom: &TypedAtom, names: &mut Names) {
356    if let Atom::External(name) = &atom.atom {
357        push_unique(&mut names.externals, name);
358    }
359}