use preserves::value::Map; use preserves_schema::compiler::*; use preserves_schema::compiler::types::Purpose; use preserves_schema::gen::schema::Schema; use proc_macro2::Span; use quote::ToTokens; use quote::quote; use std::fmt::Display; use syn::LitStr; use syn::Token; use syn::parenthesized; use syn::parse::Parser; use syn::punctuated::Punctuated; mod kw { use syn::custom_keyword; custom_keyword!(load); custom_keyword!(cross_reference); custom_keyword!(external_module); } #[derive(Debug)] enum Instruction { Namespace(String), Load(LitStr), CrossReference { namespace: String, bundle_path: LitStr, }, ExternalModule { module_path: ModulePath, namespace: String, } } fn syn_path_string(p: syn::Path) -> String { p.to_token_stream().to_string().replace(&[' ', '\t', '\n', '\r'], "") } fn syn_litstr_resolve(s: &syn::LitStr) -> String { let s: String = s.value(); match s.chars().nth(0) { Some('/') => s.into(), Some('<') => match &s[1..].split_once('>') { Some((envvar, remainder)) => match std::env::var(envvar) { Ok(p) => p + "/" + remainder, Err(_) => panic!("No such environment variable: {:?}", s), } None => panic!("Invalid relative path syntax: {:?}", s), }, _ => panic!("Invalid path syntax: {:?}", s) } } impl syn::parse::Parse for Instruction { fn parse(input: syn::parse::ParseStream) -> syn::Result { let lookahead = input.lookahead1(); if lookahead.peek(kw::load) { let _: kw::load = input.parse()?; let content; let _ = parenthesized!(content in input); let bundle_path: syn::LitStr = content.parse()?; Ok(Instruction::Load(bundle_path)) } else if lookahead.peek(kw::cross_reference) { let _: kw::cross_reference = input.parse()?; let content; let _ = parenthesized!(content in input); let namespace: syn::Path = content.parse()?; let _: Token![=] = content.parse()?; let bundle_path: syn::LitStr = content.parse()?; Ok(Instruction::CrossReference { namespace: syn_path_string(namespace), bundle_path, }) } else if lookahead.peek(kw::external_module) { let _: kw::external_module = input.parse()?; let content; let _ = parenthesized!(content in input); let module_path = Punctuated::::parse_separated_nonempty(&content)?; let _: Token![=] = content.parse()?; let namespace: syn::Path = content.parse()?; Ok(Instruction::ExternalModule { module_path: module_path.into_iter().map(|p| p.to_string()).collect(), namespace: syn_path_string(namespace), }) } else { let ns: syn::Path = input.parse()?; Ok(Instruction::Namespace(syn_path_string(ns))) } } } struct ModuleTree { own_body: String, children: Map, } impl Default for ModuleTree { fn default() -> Self { ModuleTree { own_body: String::new(), children: Map::default(), } } } impl ModuleTree { fn build(outputs: Map, String>) -> Self { let mut mt = ModuleTree::default(); for (p, c) in outputs.into_iter() { match p { None => mt.own_body = c, Some(k) => { let mut r = &mut mt; for e in k { r = mt.children.entry(names::render_modname(&e)).or_default(); } r.own_body = c; } } } mt } } impl Display for ModuleTree { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.own_body)?; for (label, mt) in self.children.iter() { write!(f, "\npub mod {} {{ ", label)?; mt.fmt(f)?; write!(f, "}}")?; } Ok(()) } } #[proc_macro] pub fn compile_preserves_schemas(src: proc_macro::TokenStream) -> proc_macro::TokenStream { let instructions = Punctuated::::parse_terminated .parse(src) .expect("valid sequence of compile_preserves_schemas instructions"); let mut namespace = None::; let mut bundles_to_load = Vec::::new(); let mut bundles_to_xref = Vec::::new(); let mut external_modules = Vec::::new(); for i in instructions.into_iter() { match i { Instruction::Namespace(n) => { if namespace.is_some() { panic!("Only one namespace is permitted") } namespace = Some(n) } Instruction::Load(p) => bundles_to_load.push(p), Instruction::ExternalModule { module_path, namespace } => { external_modules.push(ExternalModule::new(module_path, &namespace)); } Instruction::CrossReference { namespace, bundle_path } => { let mut bundle = Map::::new(); let is_schema = load_schema_or_bundle(&mut bundle, &syn_litstr_resolve(&bundle_path).into()) .expect("Invalid schema/bundle binary"); bundles_to_xref.push(bundle_path); for (k, _v) in bundle.into_iter() { external_modules.push(if is_schema { ExternalModule::new(k, &namespace) } else { let ns = namespace.clone(); let mut pieces = vec![ns.clone()]; pieces.extend( k.iter().map(|p| names::render_modname(&p)).collect::>()); ExternalModule::new(k, &pieces.join("::")) .set_fallback_language_types( move |v| vec![format!("{}::Language<{}>", ns, v)].into_iter().collect()) }); } } } } let namespace = namespace.expect("Missing namespace"); let mut dependency_paths = Vec::::new(); let mut c = CompilerConfig::new(namespace.clone()); for b in bundles_to_load.into_iter() { dependency_paths.push(syn::LitStr::new(&syn_litstr_resolve(&b), Span::call_site())); load_schema_or_bundle_with_purpose(&mut c.bundle, &syn_litstr_resolve(&b).into(), Purpose::Codegen) .expect(&b.value()); } for b in bundles_to_xref.into_iter() { dependency_paths.push(syn::LitStr::new(&syn_litstr_resolve(&b), Span::call_site())); load_schema_or_bundle_with_purpose(&mut c.bundle, &syn_litstr_resolve(&b).into(), Purpose::Xref) .expect(&b.value()); } for m in external_modules.into_iter() { c.add_external_module(m); } let mut outputs = Map::, String>::new(); let mut collector = CodeCollector { emit_mod_declarations: false, collect_module: CodeModuleCollector::Custom { collect_output: &mut |p, c| { outputs.insert(p.cloned(), c.to_owned()); Ok(()) }, }, }; compile(&c, &mut collector).expect("Compilation failed"); let top_module_source = format!( "pub mod {} {{ {} }}", names::render_modname(namespace.split("::").last().unwrap()), ModuleTree::build(outputs)); let top_module: syn::Item = syn::parse_str(&top_module_source) .expect("Invalid generated code"); quote!{ // TODO: this is ugly, but makes the code depend on the actual schema bundle: // See https://doc.rust-lang.org/nightly/proc_macro/tracked_path/fn.path.html // and https://github.com/rust-lang/rust/issues/99515 #( const _: &'static [u8] = include_bytes!(#dependency_paths); )* #top_module }.into() }