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#[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(¶ms, &doc);
95 let (public_struct, private_struct) = build_visibility_structs(¶ms);
96 let inputs_struct = build_inputs_struct();
97 let converters = build_converters(¶ms);
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(¶m.name));
151 let ty = registry.ty_tokens(¶m.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 = ¶m.ident;
172 let ty = ¶m.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 = ¶m.ident;
206 let ty = ¶m.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 = ¶m.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 = ¶m.ident;
242 quote!(#ident: args.#ident,)
243 })
244 .collect();
245
246 let args_from_inputs_fields = params.iter().map(|param| {
247 let ident = ¶m.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}