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
40struct 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 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
134fn 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
153fn 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
166fn 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 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 if seen.insert(PACKAGE_NAME.clone()) {
183 result.push(PACKAGE_NAME.clone());
184 }
185 result
186}
187
188fn 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 #[cfg(test)]
214 pub(crate) const TEST_REGISTER: &[#register_fn_ty] = &[
215 #(#register_fns),*
216 ];
217 }
218}
219
220fn 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}