Skip to main content

yog_interop/
lib.rs

1//! Proc-macros for mods' exports (`docs/design/bus.md`, stage 4).
2//!
3//! `#[yog_export]` on a fn makes it a bus service at `mod_id:name` (its
4//! `ctor` registers it when the mod's library loads; `export_mod!`
5//! provides it); on a struct or enum it derives the Yog Data schema the
6//! calls carry it with. `import!` writes the calling side.
7
8use proc_macro::TokenStream;
9use quote::{format_ident, quote};
10
11mod mirror;
12
13// ── #[yog_export] ─────────────────────────────────────────────────────────
14
15/// The derives an exported type carries: its Yog Data schema, through
16/// `yog_api`'s re-export (a mod depends on `yog_api` only).
17fn schema_derive() -> proc_macro2::TokenStream {
18    quote! {
19        #[derive(::yog_api::yog_data::Schema)]
20        #[data(crate = "::yog_api::yog_data")]
21    }
22}
23
24/// An exported enum with data is tagged (`#[data(tag = "type")]`) unless
25/// it names its own tag: the mod writes the enum, not its format.
26fn enum_tag(e: &syn::ItemEnum) -> proc_macro2::TokenStream {
27    use yog_ids::data_attr::{DATA, TAG};
28    let has_data = e
29        .variants
30        .iter()
31        .any(|v| !matches!(v.fields, syn::Fields::Unit));
32    let tagged = e.attrs.iter().filter(|a| a.path().is_ident(DATA)).any(|a| {
33        let mut tag = false;
34        // Every `key = value` read past; the tag noted.
35        let _ = a.parse_nested_meta(|m| {
36            tag |= m.path.is_ident(TAG);
37            if m.input.peek(syn::Token![=]) {
38                m.value()?.parse::<syn::Expr>()?;
39            }
40            Ok(())
41        });
42        tag
43    });
44    if !has_data || tagged {
45        return quote! {};
46    }
47    let tag = yog_ids::word::TYPE;
48    let (data, tag_key) = (format_ident!("{DATA}"), format_ident!("{TAG}"));
49    quote! { #[#data(#tag_key = #tag)] }
50}
51
52/// Whether `ty` is a `Registry` (a leading `&mut Registry` is the
53/// exporting mod's own, never part of a call).
54fn is_registry_ty(ty: &syn::Type) -> bool {
55    let ty = match ty {
56        syn::Type::Reference(r) => &*r.elem,
57        other => other,
58    };
59    quote! { #ty }
60        .to_string()
61        .replace(' ', "")
62        .ends_with("Registry")
63}
64
65/// A function's call parameters (a leading `Registry` left out), each a
66/// plain name — `__yog_argN` for a pattern that isn't one.
67fn call_params(sig: &syn::Signature) -> (bool, Vec<(syn::Ident, syn::Type)>) {
68    let mut typed: Vec<&syn::PatType> = sig
69        .inputs
70        .iter()
71        .filter_map(|a| match a {
72            syn::FnArg::Typed(pt) => Some(pt),
73            syn::FnArg::Receiver(_) => None,
74        })
75        .collect();
76    let registry = typed.first().is_some_and(|pt| is_registry_ty(&pt.ty));
77    if registry {
78        typed.remove(0);
79    }
80    let params = typed
81        .iter()
82        .enumerate()
83        .map(|(i, pt)| {
84            let name = match &*pt.pat {
85                syn::Pat::Ident(p) => p.ident.clone(),
86                _ => format_ident!("__yog_arg{}", i),
87            };
88            (name, (*pt.ty).clone())
89        })
90        .collect();
91    (registry, params)
92}
93
94/// How a call's arguments cross the bus: a lone parameter as itself (so
95/// a caller needs only its type — handlers, callbacks), several as a struct
96/// of them by name (`__YogArgs_name`).
97struct Args {
98    /// The type carried.
99    ty: proc_macro2::TokenStream,
100    /// That struct's definition (nothing for a lone parameter).
101    def: proc_macro2::TokenStream,
102    /// The value carried, made of the parameters (in scope by name).
103    pack: proc_macro2::TokenStream,
104    /// The parameters again, out of the value carried (`a`).
105    unpack: proc_macro2::TokenStream,
106}
107
108fn args_of(name: &syn::Ident, params: &[(syn::Ident, syn::Type)]) -> Args {
109    if let [(param, ty)] = params {
110        return Args {
111            ty: quote! { #ty },
112            def: quote! {},
113            pack: quote! { &#param },
114            unpack: quote! { a },
115        };
116    }
117    let args = format_ident!("__YogArgs_{}", name);
118    let fields = params.iter().map(|(n, t)| quote! { #n: #t });
119    let names: Vec<&syn::Ident> = params.iter().map(|(n, _)| n).collect();
120    let derive = schema_derive();
121    Args {
122        ty: quote! { #args },
123        def: quote! {
124            #[doc(hidden)]
125            // Named after the function (`__YogArgs_give_item`), which is snake case.
126            #[allow(non_camel_case_types)]
127            #derive
128            struct #args { #(#fields),* }
129        },
130        pack: quote! { &#args { #(#names),* } },
131        unpack: quote! { #(a.#names),* },
132    }
133}
134
135fn output_type(sig: &syn::Signature) -> proc_macro2::TokenStream {
136    match &sig.output {
137        syn::ReturnType::Default => quote! { () },
138        syn::ReturnType::Type(_, ty) => quote! { #ty },
139    }
140}
141
142/// Marks a function, struct or enum for other mods: a function becomes the
143/// service `mod_id:name`, a type gets the schema its values cross with.
144/// A function's doc comment, its lines joined.
145fn doc_of(attrs: &[syn::Attribute]) -> String {
146    let lines: Vec<String> = attrs
147        .iter()
148        .filter(|a| a.path().is_ident("doc"))
149        .filter_map(|a| match &a.meta {
150            syn::Meta::NameValue(nv) => match &nv.value {
151                syn::Expr::Lit(syn::ExprLit {
152                    lit: syn::Lit::Str(s),
153                    ..
154                }) => Some(s.value().trim().to_owned()),
155                _ => None,
156            },
157            _ => None,
158        })
159        .collect();
160    lines.join(" ")
161}
162
163/// The interfaces an export says it implements: `implements = "a", implements = "b"`.
164fn implements_of(attr: TokenStream) -> syn::Result<Vec<String>> {
165    let mut out = Vec::new();
166    let parser = syn::meta::parser(|meta| {
167        if meta.path.is_ident("implements") {
168            out.push(meta.value()?.parse::<syn::LitStr>()?.value());
169            Ok(())
170        } else {
171            Err(meta.error("unknown `yog_export` argument (implements)"))
172        }
173    });
174    syn::parse::Parser::parse(parser, attr)?;
175    Ok(out)
176}
177
178#[proc_macro_attribute]
179pub fn yog_export(attr: TokenStream, item: TokenStream) -> TokenStream {
180    let implements = match implements_of(attr) {
181        Ok(i) => i,
182        Err(e) => return e.to_compile_error().into(),
183    };
184    let tokens = proc_macro2::TokenStream::from(item.clone());
185    if syn::parse2::<syn::ItemStruct>(tokens.clone()).is_ok() {
186        let derive = schema_derive();
187        return TokenStream::from(quote! { #derive #tokens });
188    }
189    if let Ok(e) = syn::parse2::<syn::ItemEnum>(tokens.clone()) {
190        let derive = schema_derive();
191        let tag = enum_tag(&e);
192        return TokenStream::from(quote! { #derive #tag #tokens });
193    }
194    let func = syn::parse_macro_input!(item as syn::ItemFn);
195    let name = &func.sig.ident;
196    let name_str = name.to_string();
197    let serve_name = format_ident!("__yog_serve_{}", name);
198    let ctor_name = format_ident!("__yog_ctor_register_{}", name);
199    let (registry, params) = call_params(&func.sig);
200    let Args {
201        ty: args,
202        def: args_def,
203        unpack,
204        ..
205    } = args_of(name, &params);
206    let output = output_type(&func.sig);
207    let doc = doc_of(&func.attrs);
208    // The exporting mod's own `Registry`, from the API it captured at load.
209    let (setup, registry_arg) = if registry {
210        (
211            quote! { let mut __yog_registry = unsafe { ::yog_api::Registry::from_raw(::yog_api::__current_api_ptr() as *const ::yog_api::YogApi) }; },
212            quote! { &mut __yog_registry, },
213        )
214    } else {
215        (quote! {}, quote! {})
216    };
217    TokenStream::from(quote! {
218        #func
219
220        #args_def
221
222        #[doc(hidden)]
223        unsafe extern "C" fn #serve_name(
224            _ud: *mut ::std::os::raw::c_void,
225            payload: ::yog_api::YogBytes,
226            reply: *const ::yog_api::YogReply,
227        ) -> ::yog_api::YogStatus {
228            unsafe {
229                ::yog_api::__yog_serve::<#args, #output>(payload, reply, |a| {
230                    #setup
231                    #name(#registry_arg #unpack)
232                })
233            }
234        }
235
236        // Runs when this mod's library loads, before `yog_mod_register`.
237        #[::yog_api::ctor::ctor(unsafe, crate_path = ::yog_api::ctor)]
238        #[doc(hidden)]
239        fn #ctor_name() {
240            ::yog_api::__yog_export_registry().lock().push(
241                ::yog_api::YogExportEntry {
242                    name: #name_str,
243                    handler: #serve_name,
244                    doc: #doc,
245                    shapes: || (<#args as ::yog_api::yog_data::Schema>::shape(), <#output as ::yog_api::yog_data::Schema>::shape()),
246                    implements: &[#(#implements),*],
247                }
248            );
249        }
250    })
251}
252
253// ── #[yog_mirror] ─────────────────────────────────────────────────────────
254
255/// Makes a struct a mirror's row (see `mirror.rs`): `#[yog_mirror(space =
256/// …, owner = "mod")]`, a field's fact named by `#[fact(…)]`.
257#[proc_macro_attribute]
258pub fn yog_mirror(attr: TokenStream, item: TokenStream) -> TokenStream {
259    mirror::expand(attr.into(), item.into())
260        .unwrap_or_else(syn::Error::into_compile_error)
261        .into()
262}
263
264// ── #[bench] ─────────────────────────────────────────────────────────────
265
266/// Marks a function in a mod's `benches/*.rs` as a benchmark `yog bench`
267/// runs: `fn name(b: &mut criterion::Bencher)` for one benchmark, or
268/// `fn name(c: &mut criterion::Criterion)` to set up groups. yog generates
269/// the file's `main`; this only makes the function reachable from it.
270#[proc_macro_attribute]
271pub fn bench(_attr: TokenStream, item: TokenStream) -> TokenStream {
272    let mut f = syn::parse_macro_input!(item as syn::ItemFn);
273    if matches!(f.vis, syn::Visibility::Inherited) {
274        f.vis = syn::parse_quote!(pub(crate));
275    }
276    TokenStream::from(quote! { #f })
277}
278
279// ── import! macro ────────────────────────────────────────────────────────
280
281/// Another mod's exports, callable here: `import! { from "mod_id" { pub fn
282/// name(a: A) -> R; pub struct T { .. } } }` — each fn calls `mod_id:name`
283/// on the bus and answers `Option<R>` (`None`: the mod isn't loaded, or it
284/// failed); each type gets the schema its values cross with.
285#[proc_macro]
286pub fn import(input: TokenStream) -> TokenStream {
287    let import_block = syn::parse_macro_input!(input as ImportBlock);
288    let mod_name = &import_block.mod_name;
289    let mod_ident = format_ident!("{}", mod_name.replace('-', "_"));
290    let mut items = proc_macro2::TokenStream::new();
291    for item in &import_block.items {
292        match item {
293            ImportItem::Func(func) => items.extend(generate_import_fn(func, mod_name)),
294            ImportItem::Type(t) => {
295                let derive = schema_derive();
296                items.extend(quote! { #derive #t });
297            }
298        }
299    }
300    TokenStream::from(quote! { pub mod #mod_ident { use super::*; #items } })
301}
302
303/// An imported function: `pub fn name(args) -> Out;` (a body, if written, is ignored).
304struct ImportFn {
305    vis: syn::Visibility,
306    sig: syn::Signature,
307}
308
309fn generate_import_fn(func: &ImportFn, mod_name: &str) -> proc_macro2::TokenStream {
310    let name = &func.sig.ident;
311    let name_str = name.to_string();
312    let vis = &func.vis;
313    let (_, params) = call_params(&func.sig);
314    let Args {
315        ty: args,
316        def: args_def,
317        pack,
318        ..
319    } = args_of(name, &params);
320    let output = output_type(&func.sig);
321    let names: Vec<&syn::Ident> = params.iter().map(|(n, _)| n).collect();
322    let types: Vec<&syn::Type> = params.iter().map(|(_, t)| t).collect();
323    quote! {
324        #args_def
325
326        /// The other mod's answer; `None` when it isn't loaded or failed (see its log).
327        #vis fn #name(#(#names: #types),*) -> ::std::option::Option<#output> {
328            ::yog_api::call_export::<#args, #output>(#mod_name, #name_str, #pack)
329        }
330    }
331}
332
333// ── Parse helpers ────────────────────────────────────────────────────────
334
335struct ImportBlock {
336    mod_name: String,
337    items: Vec<ImportItem>,
338}
339enum ImportItem {
340    Func(Box<ImportFn>),
341    /// A struct or enum, as written.
342    Type(proc_macro2::TokenStream),
343}
344
345impl syn::parse::Parse for ImportBlock {
346    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
347        let from_kw: syn::Ident = input.parse()?;
348        if from_kw != "from" {
349            return Err(syn::Error::new(from_kw.span(), "expected `from`"));
350        }
351        let lit: syn::LitStr = input.parse()?;
352        let mod_name = lit.value();
353        let content;
354        syn::braced!(content in input);
355        let mut items = Vec::new();
356        while !content.is_empty() {
357            let attrs = content.call(syn::Attribute::parse_outer)?;
358            let vis: syn::Visibility = content.parse()?;
359            if content.peek(syn::Token![fn]) {
360                let sig: syn::Signature = content.parse()?;
361                if content.peek(syn::token::Brace) {
362                    let _body: syn::Block = content.parse()?;
363                } else {
364                    content.parse::<syn::Token![;]>()?;
365                }
366                items.push(ImportItem::Func(Box::new(ImportFn { vis, sig })));
367            } else if content.peek(syn::Token![enum]) {
368                let mut e: syn::ItemEnum = content.parse()?;
369                e.attrs = attrs;
370                e.vis = vis;
371                items.push(ImportItem::Type(quote! { #e }));
372            } else {
373                let mut s: syn::ItemStruct = content.parse()?;
374                s.attrs = attrs;
375                s.vis = vis;
376                items.push(ImportItem::Type(quote! { #s }));
377                let _ = content.parse::<syn::Token![;]>();
378            }
379        }
380        Ok(ImportBlock { mod_name, items })
381    }
382}