Skip to main content

graphix_derive/
lib.rs

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