Skip to main content

noi_macro/
lib.rs

1use std::{env, fs, path::PathBuf};
2
3use heck::{ToSnakeCase, ToUpperCamelCase};
4use indexmap::IndexMap;
5use noi_core::{
6    export::{ExportFunction, Param, StructField, StructType, TypeRepr},
7    load_export_dir,
8};
9use proc_macro::TokenStream;
10use proc_macro_error::proc_macro_error;
11use quote::{format_ident, quote};
12use syn::{
13    parse::{Parse, ParseStream},
14    Ident, LitStr, Token,
15};
16
17/// Generates strongly typed Rust bindings for Noir `#[export]` functions.
18///
19/// The macro consumes `nargo export` artifacts (either via `export_dir = "..."` or the
20/// `NOI_EXPORT_DIR` environment variable) and emits per-function modules that expose
21/// typed `Args`, `Inputs`, and stubbed execution helpers.
22#[proc_macro]
23#[proc_macro_error]
24pub fn nrg(input: TokenStream) -> TokenStream {
25    let input = syn::parse_macro_input!(input as MacroInput);
26    match expand(input) {
27        Ok(tokens) => {
28            maybe_dump(&tokens);
29            tokens.into()
30        }
31        Err(err) => err.to_compile_error().into(),
32    }
33}
34
35fn expand(input: MacroInput) -> Result<proc_macro2::TokenStream, syn::Error> {
36    let export_dir = resolve_export_dir(input.export_dir)?;
37    let functions =
38        load_export_dir(&export_dir).map_err(|err| syn::Error::new(input.module.span(), err))?;
39
40    let mut registry = TypeRegistry::new(&input.module);
41    let mut function_modules = Vec::new();
42
43    for function in &functions {
44        function_modules.push(generate_function_module(function, &mut registry)?);
45    }
46
47    let struct_defs = registry.struct_defs();
48    let module_ident = &input.module;
49    let client = generate_client();
50
51    let tokens = quote! {
52        pub mod #module_ident {
53            use ::std::path::{Path, PathBuf};
54
55            #client
56
57            #(#struct_defs)*
58
59            #(#function_modules)*
60        }
61    };
62
63    Ok(tokens)
64}
65
66fn generate_client() -> proc_macro2::TokenStream {
67    quote! {
68        #[derive(Clone, Debug)]
69        pub struct Client {
70            program_dir: PathBuf,
71        }
72
73        impl Client {
74            pub fn new<P: Into<PathBuf>>(program_dir: P) -> Self {
75                Self { program_dir: program_dir.into() }
76            }
77
78            pub fn program_dir(&self) -> &Path {
79                &self.program_dir
80            }
81        }
82    }
83}
84
85fn generate_function_module(
86    function: &ExportFunction,
87    registry: &mut TypeRegistry,
88) -> Result<proc_macro2::TokenStream, syn::Error> {
89    let module_ident = format_ident!("{}", sanitize_snake(&function.name));
90    let doc = function.signature();
91
92    let params = build_param_specs(function, registry);
93
94    let args_struct = build_args_struct(&params, &doc);
95    let (public_struct, private_struct) = build_visibility_structs(&params);
96    let inputs_struct = build_inputs_struct();
97    let converters = build_converters(&params);
98    let output_ty = match &function.return_type {
99        Some(ty) => registry.ty_tokens(ty, TypeUsage::Reference),
100        None => quote!(()),
101    };
102
103    let artifact_path = canonical_path(&function.source_path);
104    let artifact_lit = LitStr::new(&artifact_path, proc_macro2::Span::call_site());
105
106    let simulate_fn = quote! {
107        pub fn simulate(_client: &super::Client, _args: Args) -> ::anyhow::Result<Output> {
108            Err(::anyhow::anyhow!("`noi` runner integration is not implemented yet"))
109        }
110    };
111
112    let module = quote! {
113        #[doc = #doc]
114        pub mod #module_ident {
115            use super::Client;
116
117            pub const ARTIFACT_JSON: &str = include_str!(#artifact_lit);
118
119            #args_struct
120            #public_struct
121            #private_struct
122            #inputs_struct
123            #converters
124
125            pub type Output = #output_ty;
126
127            #simulate_fn
128        }
129    };
130
131    Ok(module)
132}
133
134fn build_param_specs(function: &ExportFunction, registry: &mut TypeRegistry) -> Vec<ParamSpec> {
135    function
136        .parameters
137        .iter()
138        .map(|param| ParamSpec::new(param, registry))
139        .collect()
140}
141
142struct ParamSpec {
143    ident: Ident,
144    ty: proc_macro2::TokenStream,
145    visibility: VisibilityClass,
146}
147
148impl ParamSpec {
149    fn new(param: &Param, registry: &mut TypeRegistry) -> Self {
150        let ident = format_ident!("{}", sanitize_snake(&param.name));
151        let ty = registry.ty_tokens(&param.ty, TypeUsage::Reference);
152        let visibility = match param.visibility {
153            noi_core::export::Visibility::Public => VisibilityClass::Public,
154            noi_core::export::Visibility::Private => VisibilityClass::Private,
155        };
156        Self {
157            ident,
158            ty,
159            visibility,
160        }
161    }
162}
163
164enum VisibilityClass {
165    Public,
166    Private,
167}
168
169fn build_args_struct(params: &[ParamSpec], doc: &str) -> proc_macro2::TokenStream {
170    let fields = params.iter().map(|param| {
171        let ident = &param.ident;
172        let ty = &param.ty;
173        quote!(pub #ident: #ty,)
174    });
175    quote! {
176        #[doc = #doc]
177        #[derive(Clone, Debug, PartialEq)]
178        pub struct Args {
179            #(#fields)*
180        }
181    }
182}
183
184fn build_visibility_structs(
185    params: &[ParamSpec],
186) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
187    let mut public = Vec::new();
188    let mut private = Vec::new();
189    for param in params {
190        match param.visibility {
191            VisibilityClass::Public => public.push(param),
192            VisibilityClass::Private => private.push(param),
193        }
194    }
195
196    (
197        visibility_struct_tokens("PublicInputs", &public),
198        visibility_struct_tokens("PrivateInputs", &private),
199    )
200}
201
202fn visibility_struct_tokens(name: &str, fields: &[&ParamSpec]) -> proc_macro2::TokenStream {
203    let ident = format_ident!("{name}");
204    let field_tokens = fields.iter().map(|param| {
205        let ident = &param.ident;
206        let ty = &param.ty;
207        quote!(pub #ident: #ty,)
208    });
209
210    quote! {
211        #[derive(Clone, Debug, Default, PartialEq)]
212        pub struct #ident {
213            #(#field_tokens)*
214        }
215    }
216}
217
218fn build_inputs_struct() -> proc_macro2::TokenStream {
219    quote! {
220        #[derive(Clone, Debug, Default, PartialEq)]
221        pub struct Inputs {
222            pub public: PublicInputs,
223            pub private: PrivateInputs,
224        }
225    }
226}
227
228fn build_converters(params: &[ParamSpec]) -> proc_macro2::TokenStream {
229    let public_init: Vec<_> = params
230        .iter()
231        .filter(|param| matches!(param.visibility, VisibilityClass::Public))
232        .map(|param| {
233            let ident = &param.ident;
234            quote!(#ident: args.#ident,)
235        })
236        .collect();
237    let private_init: Vec<_> = params
238        .iter()
239        .filter(|param| matches!(param.visibility, VisibilityClass::Private))
240        .map(|param| {
241            let ident = &param.ident;
242            quote!(#ident: args.#ident,)
243        })
244        .collect();
245
246    let args_from_inputs_fields = params.iter().map(|param| {
247        let ident = &param.ident;
248        match param.visibility {
249            VisibilityClass::Public => quote!(#ident: inputs.public.#ident),
250            VisibilityClass::Private => quote!(#ident: inputs.private.#ident),
251        }
252    });
253
254    quote! {
255        impl From<Args> for Inputs {
256            fn from(args: Args) -> Self {
257                Self {
258                    public: PublicInputs {
259                        #(#public_init)*
260                    },
261                    private: PrivateInputs {
262                        #(#private_init)*
263                    },
264                }
265            }
266        }
267
268        impl From<Inputs> for Args {
269            fn from(inputs: Inputs) -> Self {
270                Self {
271                    #(#args_from_inputs_fields,)*
272                }
273            }
274        }
275    }
276}
277
278fn sanitize_snake(name: &str) -> String {
279    sanitize(name).to_snake_case()
280}
281
282fn sanitize_pascal(name: &str) -> String {
283    sanitize(name).to_upper_camel_case()
284}
285
286fn sanitize(name: &str) -> String {
287    let mut out = String::new();
288    for ch in name.chars() {
289        if ch.is_ascii_alphanumeric() {
290            out.push(ch);
291        } else {
292            out.push('_');
293        }
294    }
295    if out.is_empty() {
296        out.push('x');
297    }
298    if out.chars().next().unwrap().is_ascii_digit() {
299        out.insert(0, '_');
300    }
301    out
302}
303
304fn canonical_path(path: &PathBuf) -> String {
305    fs::canonicalize(path)
306        .unwrap_or_else(|_| path.clone())
307        .to_string_lossy()
308        .into_owned()
309}
310
311fn resolve_export_dir(explicit: Option<LitStr>) -> Result<PathBuf, syn::Error> {
312    if let Some(lit) = explicit {
313        return Ok(PathBuf::from(lit.value()));
314    }
315
316    match env::var("NOI_EXPORT_DIR") {
317        Ok(value) => Ok(PathBuf::from(value)),
318        Err(_) => Err(syn::Error::new(
319            proc_macro2::Span::call_site(),
320            "`NOI_EXPORT_DIR` is not set and `export_dir` was not provided",
321        )),
322    }
323}
324
325fn maybe_dump(tokens: &proc_macro2::TokenStream) {
326    if env::var("NOI_DEBUG").as_deref() != Ok("1") {
327        return;
328    }
329    let target_dir = env::var("CARGO_TARGET_DIR")
330        .map(PathBuf::from)
331        .unwrap_or_else(|_| PathBuf::from("target"));
332    let dump_path = target_dir.join("noi").join("expanded.rs");
333    if let Some(parent) = dump_path.parent() {
334        let _ = fs::create_dir_all(parent);
335    }
336    let _ = fs::write(dump_path, tokens.to_string());
337}
338
339struct MacroInput {
340    module: Ident,
341    export_dir: Option<LitStr>,
342}
343
344impl Parse for MacroInput {
345    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
346        let mut module = None;
347        let mut export_dir = None;
348
349        while !input.is_empty() {
350            let key: Ident = input.parse()?;
351            input.parse::<Token![=]>()?;
352            match key.to_string().as_str() {
353                "module" => {
354                    module = Some(input.parse()?);
355                }
356                "export_dir" => {
357                    export_dir = Some(input.parse()?);
358                }
359                other => {
360                    return Err(syn::Error::new(
361                        key.span(),
362                        format!("unknown argument `{other}`"),
363                    ))
364                }
365            }
366            if input.is_empty() {
367                break;
368            }
369            let _ = input.parse::<Token![,]>();
370        }
371
372        let module = module.ok_or_else(|| {
373            syn::Error::new(
374                proc_macro2::Span::call_site(),
375                "`module = <ident>` is required",
376            )
377        })?;
378
379        Ok(Self { module, export_dir })
380    }
381}
382
383struct TypeRegistry {
384    structs: IndexMap<StructType, Ident>,
385    defs: Vec<proc_macro2::TokenStream>,
386    module_name: String,
387    counter: usize,
388}
389
390#[derive(Clone, Copy)]
391enum TypeUsage {
392    Definition,
393    Reference,
394}
395
396impl TypeRegistry {
397    fn new(module: &Ident) -> Self {
398        Self {
399            structs: IndexMap::new(),
400            defs: Vec::new(),
401            module_name: module.to_string(),
402            counter: 0,
403        }
404    }
405
406    fn ty_tokens(&mut self, repr: &TypeRepr, usage: TypeUsage) -> proc_macro2::TokenStream {
407        match repr {
408            TypeRepr::Bool => quote!(bool),
409            TypeRepr::Field => quote!(::noi_core::types::FieldElement),
410            TypeRepr::Unsigned(bits) => {
411                let ident = format_ident!("u{}", bits);
412                quote!(#ident)
413            }
414            TypeRepr::Signed(bits) => {
415                let ident = format_ident!("i{}", bits);
416                quote!(#ident)
417            }
418            TypeRepr::Array(inner, len) => {
419                let inner_tokens = self.ty_tokens(inner, usage);
420                let len_lit = proc_macro2::Literal::usize_unsuffixed(*len);
421                quote!([#inner_tokens; #len_lit])
422            }
423            TypeRepr::Tuple(values) => {
424                let tokens = values
425                    .iter()
426                    .map(|value| self.ty_tokens(value, usage))
427                    .collect::<Vec<_>>();
428                match tokens.len() {
429                    0 => quote!(()),
430                    1 => {
431                        let ty = &tokens[0];
432                        quote!((#ty,))
433                    }
434                    _ => quote!((#(#tokens),*)),
435                }
436            }
437            TypeRepr::Struct(struct_ty) => {
438                let ident = self.ensure_struct(struct_ty);
439                match usage {
440                    TypeUsage::Definition => quote!(#ident),
441                    TypeUsage::Reference => quote!(super::#ident),
442                }
443            }
444        }
445    }
446
447    fn ensure_struct(&mut self, struct_ty: &StructType) -> Ident {
448        if let Some(existing) = self.structs.get(struct_ty) {
449            return existing.clone();
450        }
451
452        let ident = self.next_struct_ident(struct_ty);
453        self.structs.insert(struct_ty.clone(), ident.clone());
454
455        let fields = struct_ty
456            .fields
457            .iter()
458            .map(|field| self.struct_field_tokens(field))
459            .collect::<Vec<_>>();
460
461        let def = quote! {
462            #[derive(Clone, Debug, PartialEq, Default)]
463            pub struct #ident {
464                #(#fields)*
465            }
466        };
467        self.defs.push(def);
468
469        ident
470    }
471
472    fn struct_field_tokens(&mut self, field: &StructField) -> proc_macro2::TokenStream {
473        let ident = format_ident!("{}", sanitize_snake(&field.name));
474        let ty = self.ty_tokens(&field.ty, TypeUsage::Definition);
475        quote!(pub #ident: #ty,)
476    }
477
478    fn next_struct_ident(&mut self, struct_ty: &StructType) -> Ident {
479        let base = struct_ty
480            .name
481            .as_deref()
482            .map(sanitize_pascal)
483            .filter(|name| !name.is_empty())
484            .unwrap_or_else(|| {
485                format!(
486                    "{}Struct{}",
487                    self.module_name.to_upper_camel_case(),
488                    self.counter
489                )
490            });
491        self.counter += 1;
492        format_ident!("{base}")
493    }
494
495    fn struct_defs(&self) -> Vec<proc_macro2::TokenStream> {
496        self.defs.clone()
497    }
498}