Skip to main content

graphix_derive/
lib.rs

1use cargo_toml::Manifest;
2use proc_macro2::TokenStream;
3use quote::quote;
4use std::{
5    env,
6    path::{Component, Path, PathBuf},
7    sync::LazyLock,
8};
9use syn::{
10    parse_macro_input,
11    punctuated::{Pair, Punctuated},
12    token::{self, Comma},
13    Ident, Pat, Result, Token,
14};
15static PROJECT_ROOT: LazyLock<PathBuf> = LazyLock::new(|| {
16    env::var("CARGO_MANIFEST_DIR").expect("missing manifest dir").into()
17});
18
19static GRAPHIX_SRC: LazyLock<PathBuf> =
20    LazyLock::new(|| PROJECT_ROOT.join("src").join("graphix"));
21
22static CARGO_MANIFEST: LazyLock<Manifest> = LazyLock::new(|| {
23    Manifest::from_path(PROJECT_ROOT.join("Cargo.toml"))
24        .expect("failed to load cargo manifest")
25});
26
27static CRATE_NAME: LazyLock<String> =
28    LazyLock::new(|| env::var("CARGO_CRATE_NAME").expect("missing crate name"));
29
30static PACKAGE_NAME: LazyLock<String> =
31    LazyLock::new(|| match CRATE_NAME.strip_prefix("graphix_package_") {
32        Some(name) => name.into(),
33        None => CRATE_NAME.clone(),
34    });
35
36/* example
37defpackage! {
38    builtins => [
39        Foo,
40        submod::Bar,
41        Baz as Baz<R, E>,
42    ],
43    is_custom => |gx, env, e| {
44        todo!()
45    },
46    init_custom => |gx, env, stop, e| {
47        todo!()
48    }
49}
50*/
51
52/// A builtin entry: either a simple path (used for both NAME access and
53/// registration), or `Path as Type` where Path is used for `::NAME` access
54/// and Type is used for `register_builtin::<Type>()`.
55struct BuiltinEntry {
56    reg_type: syn::Type,
57}
58
59impl syn::parse::Parse for BuiltinEntry {
60    fn parse(input: syn::parse::ParseStream) -> Result<Self> {
61        let name_path: syn::Path = input.parse()?;
62        if input.peek(Token![as]) {
63            let _as: Token![as] = input.parse()?;
64            let reg_type: syn::Type = input.parse()?;
65            Ok(BuiltinEntry { reg_type })
66        } else {
67            let reg_type =
68                syn::Type::Path(syn::TypePath { qself: None, path: name_path.clone() });
69            Ok(BuiltinEntry { reg_type })
70        }
71    }
72}
73
74struct DefPackage {
75    builtins: Vec<BuiltinEntry>,
76    is_custom: Option<syn::ExprClosure>,
77    init_custom: Option<syn::ExprClosure>,
78}
79
80impl syn::parse::Parse for DefPackage {
81    fn parse(input: syn::parse::ParseStream) -> Result<Self> {
82        let mut builtins = Vec::new();
83        let mut is_custom = None;
84        let mut init_custom = None;
85        while !input.is_empty() {
86            let key: Ident = input.parse()?;
87            let _arrow: Token![=>] = input.parse()?;
88            if key == "builtins" {
89                let content;
90                let _bracket: token::Bracket = syn::bracketed!(content in input);
91                builtins = content
92                    .parse_terminated(BuiltinEntry::parse, Token![,])?
93                    .into_pairs()
94                    .map(|p| p.into_value())
95                    .collect();
96            } else if key == "is_custom" {
97                is_custom = Some(input.parse::<syn::ExprClosure>()?);
98            } else if key == "init_custom" {
99                init_custom = Some(input.parse::<syn::ExprClosure>()?);
100            } else {
101                return Err(input.error("unknown key"));
102            }
103            if !input.is_empty() {
104                let _comma: Option<Token![,]> = input.parse()?;
105            }
106        }
107        Ok(DefPackage { builtins, is_custom, init_custom })
108    }
109}
110
111fn check_invariants() {
112    if !CARGO_MANIFEST.bin.is_empty() {
113        panic!("graphix package crates may not have binary targets")
114    }
115    if !CARGO_MANIFEST.lib.is_some() {
116        panic!("graphix package crates must have a lib target")
117    }
118    let md = std::fs::metadata(&*GRAPHIX_SRC)
119        .expect("graphix projects must have a graphix-src directory");
120    if !md.is_dir() {
121        panic!("graphix projects must have a graphix-src directory")
122    }
123    // every package must depend on graphix-package-core (except core itself)
124    let is_core = *PACKAGE_NAME == "core";
125    if !is_core && !CARGO_MANIFEST.dependencies.contains_key("graphix-package-core") {
126        panic!("graphix packages must depend on graphix-package-core")
127    }
128}
129
130/// Collect graphix-package-* dependency names from a Cargo.toml section,
131/// preserving document order.
132fn collect_package_deps(
133    doc: &toml_edit::DocumentMut,
134    section: &str,
135    seen: &mut std::collections::HashSet<String>,
136    result: &mut Vec<String>,
137) {
138    if let Some(deps) = doc.get(section).and_then(|v| v.as_table()) {
139        for (key, _) in deps.iter() {
140            if let Some(name) = key.strip_prefix("graphix-package-") {
141                if seen.insert(name.to_string()) {
142                    result.push(name.to_string());
143                }
144            }
145        }
146    }
147}
148
149/// Collect graphix-package-* deps from [dependencies] only (used for
150/// register() calls that must compile without dev-dependencies).
151fn runtime_deps() -> Vec<String> {
152    let content = std::fs::read_to_string(PROJECT_ROOT.join("Cargo.toml"))
153        .expect("failed to read Cargo.toml");
154    let doc: toml_edit::DocumentMut =
155        content.parse().expect("failed to parse Cargo.toml");
156    let mut seen = std::collections::HashSet::new();
157    let mut result = Vec::new();
158    collect_package_deps(&doc, "dependencies", &mut seen, &mut result);
159    result
160}
161
162/// Collect graphix-package-* dependency names from both [dependencies] and
163/// [dev-dependencies], preserving the order written in Cargo.toml.
164/// Core always comes first. Used for TEST_REGISTER.
165fn package_deps() -> Vec<String> {
166    let content = std::fs::read_to_string(PROJECT_ROOT.join("Cargo.toml"))
167        .expect("failed to read Cargo.toml");
168    let doc: toml_edit::DocumentMut =
169        content.parse().expect("failed to parse Cargo.toml");
170    let mut seen = std::collections::HashSet::new();
171    let mut result = Vec::new();
172    // core always first
173    seen.insert("core".to_string());
174    result.push("core".to_string());
175    collect_package_deps(&doc, "dependencies", &mut seen, &mut result);
176    collect_package_deps(&doc, "dev-dependencies", &mut seen, &mut result);
177    // include ourselves if not already present
178    if seen.insert(PACKAGE_NAME.clone()) {
179        result.push(PACKAGE_NAME.clone());
180    }
181    result
182}
183
184/// Generate the TEST_REGISTER array from Cargo.toml deps.
185fn test_harness() -> TokenStream {
186    let deps = package_deps();
187    let register_fns: Vec<TokenStream> = deps.iter().map(|name| {
188        if *name == *PACKAGE_NAME {
189            quote! {
190                <crate::P as ::graphix_package::Package<::graphix_rt::NoExt>>::register
191            }
192        } else {
193            let crate_ident = syn::Ident::new(
194                &format!("graphix_package_{}", name.replace('-', "_")),
195                proc_macro2::Span::call_site(),
196            );
197            quote! {
198                <#crate_ident::P as ::graphix_package::Package<::graphix_rt::NoExt>>::register
199            }
200        }
201    }).collect();
202    let register_fn_ty = if *PACKAGE_NAME == "core" {
203        quote! { crate::testing::RegisterFn }
204    } else {
205        quote! { ::graphix_package_core::testing::RegisterFn }
206    };
207    quote! {
208        /// Register functions for all package dependencies (for testing).
209        #[cfg(test)]
210        pub(crate) const TEST_REGISTER: &[#register_fn_ty] = &[
211            #(#register_fns),*
212        ];
213    }
214}
215
216// walk the graphix files in src/graphix and build the vfs for this package
217fn graphix_files() -> Vec<TokenStream> {
218    let mut res = vec![];
219    for entry in walkdir::WalkDir::new(&*GRAPHIX_SRC) {
220        let entry = entry.expect("could not read");
221        if !entry.file_type().is_file() {
222            continue;
223        }
224        let ext = entry.path().extension().and_then(|e| e.to_str());
225        if ext != Some("gx") && ext != Some("gxi") {
226            continue;
227        }
228        let path = match entry.path().strip_prefix(&*GRAPHIX_SRC) {
229            Ok(p) if p == Path::new("main.gx") => continue,
230            Ok(p) => p,
231            Err(_) => continue,
232        };
233        let mut vfs_path = format!("/{}", PACKAGE_NAME.clone());
234        for c in path.components() {
235            match c {
236                Component::CurDir
237                | Component::ParentDir
238                | Component::RootDir
239                | Component::Prefix(_) => panic!("invalid path component {c:?}"),
240                Component::Normal(p) => match p.to_str() {
241                    None => panic!("invalid path component {c:?}"),
242                    Some(s) => {
243                        vfs_path.push('/');
244                        vfs_path.push_str(s)
245                    }
246                },
247            };
248        }
249        let mut compiler_path = PathBuf::new();
250        compiler_path.push("graphix");
251        compiler_path.push(path);
252        let compiler_path = compiler_path.to_string_lossy().into_owned();
253        res.push(quote! {
254            let path = ::netidx_core::path::Path::from(#vfs_path);
255            if modules.contains_key(&path) {
256                ::anyhow::bail!("duplicate graphix module {path}")
257            }
258            modules.insert(path, ::arcstr::literal!(include_str!(#compiler_path)))
259        })
260    }
261    res
262}
263
264fn main_program_impl() -> TokenStream {
265    let main_gx = GRAPHIX_SRC.join("main.gx");
266    if main_gx.exists() {
267        quote! {
268            fn main_program() -> Option<&'static str> {
269                if cfg!(feature = "standalone") {
270                    Some(include_str!("graphix/main.gx"))
271                } else {
272                    None
273                }
274            }
275        }
276    } else {
277        quote! {
278            fn main_program() -> Option<&'static str> { None }
279        }
280    }
281}
282
283fn register_builtins(builtins: &[BuiltinEntry]) -> Vec<TokenStream> {
284    let package_name = &*PACKAGE_NAME;
285    builtins.iter().map(|entry| {
286        let reg_type = &entry.reg_type;
287        quote! {
288            {
289                let name: &str = <#reg_type as ::graphix_compiler::BuiltIn<::graphix_rt::GXRt<X>, X::UserEvent>>::NAME;
290                if name.contains(|c: char| c != '_' && !c.is_ascii_alphanumeric()) {
291                    ::anyhow::bail!("invalid builtin name {}, must contain only ascii alphanumeric and _", name)
292                }
293                if !name.starts_with(#package_name) {
294                    ::anyhow::bail!("invalid builtin {} name must start with package name {}", name, #package_name)
295                }
296                ctx.register_builtin::<#reg_type>()?
297            }
298        }
299    }).collect()
300}
301
302fn check_args(name: &str, mut req: Vec<&'static str>, args: &Punctuated<Pat, Comma>) {
303    fn check_arg(name: &str, req: &mut Vec<&'static str>, pat: &Pat) {
304        if req.is_empty() {
305            panic!("{name} unexpected argument")
306        }
307        match pat {
308            Pat::Ident(i) => {
309                if &i.ident.to_string() == &req[0] {
310                    req.remove(0);
311                } else {
312                    panic!("{name} expected arguments {req:?}")
313                }
314            }
315            _ => panic!("{name} expected arguments {req:?}"),
316        }
317    }
318    for arg in args.pairs() {
319        match arg {
320            Pair::End(i) => {
321                check_arg(name, &mut req, i);
322            }
323            Pair::Punctuated(i, _) => {
324                check_arg(name, &mut req, i);
325            }
326        }
327    }
328    if !req.is_empty() {
329        panic!("{name} missing required arguments {req:?}")
330    }
331}
332
333fn is_custom(is_custom: &Option<syn::ExprClosure>) -> TokenStream {
334    match is_custom {
335        None => quote! { false },
336        Some(cl) => {
337            check_args("is_custom", vec!["gx", "env", "e"], &cl.inputs);
338            let body = &cl.body;
339            quote! { #body }
340        }
341    }
342}
343
344fn init_custom(is_custom: &Option<syn::ExprClosure>) -> TokenStream {
345    match is_custom {
346        None => quote! { unreachable!() },
347        Some(cl) => {
348            check_args("init_custom", vec!["gx", "env", "stop", "e"], &cl.inputs);
349            let body = &cl.body;
350            quote! { #body }
351        }
352    }
353}
354
355#[proc_macro]
356pub fn defpackage(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
357    check_invariants();
358    let input = parse_macro_input!(input as DefPackage);
359    let register_builtins = register_builtins(&input.builtins);
360    let is_custom = is_custom(&input.is_custom);
361    let init_custom = init_custom(&input.init_custom);
362    let graphix_files = graphix_files();
363    let main_program = main_program_impl();
364    let test_harness = test_harness();
365    let package_name = &*PACKAGE_NAME;
366
367    let dep_registers: Vec<TokenStream> = runtime_deps()
368        .iter()
369        .filter(|name| **name != *PACKAGE_NAME)
370        .map(|name| {
371            let crate_ident = syn::Ident::new(
372                &format!("graphix_package_{}", name.replace('-', "_")),
373                proc_macro2::Span::call_site(),
374            );
375            quote! {
376                <#crate_ident::P as ::graphix_package::Package<X>>::register(ctx, modules, root_mods)?;
377            }
378        })
379        .collect();
380
381    quote! {
382        pub struct P;
383
384        impl<X: ::graphix_rt::GXExt> ::graphix_package::Package<X> for P {
385            fn register(
386                ctx: &mut ::graphix_compiler::ExecCtx<::graphix_rt::GXRt<X>, X::UserEvent>,
387                modules: &mut ::fxhash::FxHashMap<::netidx_core::path::Path, ::arcstr::ArcStr>,
388                root_mods: &mut ::graphix_package::IndexSet<::arcstr::ArcStr>,
389            ) -> ::anyhow::Result<()> {
390                if root_mods.contains(#package_name) {
391                    return Ok(());
392                }
393                #(#dep_registers)*
394                #(#register_builtins;)*
395                #(#graphix_files;)*
396                root_mods.insert(::arcstr::literal!(#package_name));
397                Ok(())
398            }
399
400            #[allow(unused)]
401            fn is_custom(
402                gx: &::graphix_rt::GXHandle<X>,
403                env: &::graphix_compiler::env::Env,
404                e: &::graphix_rt::CompExp<X>,
405            ) -> bool {
406                #is_custom
407            }
408
409            #[allow(unused)]
410            fn init_custom(
411                gx: &::graphix_rt::GXHandle<X>,
412                env: &::graphix_compiler::env::Env,
413                stop: ::tokio::sync::oneshot::Sender<()>,
414                e: ::graphix_rt::CompExp<X>,
415            ) -> ::anyhow::Result<Box<dyn ::graphix_package::CustomDisplay<X>>> {
416                #init_custom
417            }
418
419            #main_program
420        }
421
422        #test_harness
423    }
424    .into()
425}
426