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
36struct 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 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
130fn 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
149fn 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
162fn 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 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 if seen.insert(PACKAGE_NAME.clone()) {
179 result.push(PACKAGE_NAME.clone());
180 }
181 result
182}
183
184fn 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 #[cfg(test)]
210 pub(crate) const TEST_REGISTER: &[#register_fn_ty] = &[
211 #(#register_fns),*
212 ];
213 }
214}
215
216fn 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