//! `declare!`: a screen description, compiled to Rust at build time. //! //! The description IS the declaration. What a shape function used to build at //! request time, this builds once at compile time, so what is left in the //! binary is literals and holes. Design: wiki `quasi-declare-form`. //! //! # How this crate grows //! //! One production per conversion. A production exists here because a real //! screen demanded it, and the screen that demanded it is named in the commit //! that added it. Nothing is specified ahead of a screen that wants it, because //! that is what produced a form with twelve amendments and no implementation. //! //! When a conversion hits something the form cannot say, there are three //! outcomes and no fourth: use the remedy the deferred table names, add the //! production, or refuse it and rewrite the Rust. Adding the production is //! ordinary and expected. What is never acceptable is an escape hatch that //! admits an expression, a block in argument position, a closure or a struct //! literal, because those four are what keep `Node: Eq` intact. //! //! # One production reads a file //! //! `for in copy "" as ` is copy the macro reads at //! expansion time and writes out, one set of members per entry. It is not an //! escape hatch: what a content file may hold is text, and the loop is gone //! before `ast` sees it. See `copy.rs` for the split it implements and for //! why the reading has to be marked with an `include_bytes!`. mod ast; mod copy; mod emit; mod fill; mod pairs; mod parse; mod symbolic; use proc_macro::TokenStream; use syn::parse_macro_input; /// Declare one shape. /// /// ```ignore /// declare! { /// /// The strip over the table. /// shape header(view: &View) -> Node; /// /// let base = "/git/{view.owner}/{view.repo}"; /// region "git-file-header" as Group { /// across Wrap { /// beside Secondary text lines; /// beside Essential link "Source" to get "{base}/tree/{view.file_path}" navigating; /// } /// } /// } /// ``` #[proc_macro] pub fn declare(input: TokenStream) -> TokenStream { // Anything a refused expansion read, before this one reads its own. drop(copy::taken()); let declaration = parse_macro_input!(input as ast::Declaration); let copied = copy::taken(); match expand(&declaration) { Ok(expansion) => { let watched = watch(&copied); quote::quote! { #expansion #watched }.into() } Err(error) => error.into_compile_error().into(), } } /// What makes rustc rebuild when a content file changes. /// /// A macro that reads a file reads it behind the compiler's back. Naming each /// one in an `include_bytes!` puts it back in front: the path is absolute /// because the emitted code lands in whatever file the invocation sits in, and /// `include_bytes!` resolves a relative path against that file rather than /// against the manifest the macro resolved with. fn watch(copied: &[std::path::PathBuf]) -> proc_macro2::TokenStream { let markers = copied.iter().map(|path| { let path = path.to_string_lossy(); quote::quote! { const _: &[u8] = include_bytes!(#path); } }); quote::quote! { #(#markers)* } } /// The shape's function, and its staged twin if it asked for one. fn expand(declaration: &ast::Declaration) -> syn::Result { pairs::check(&declaration.items)?; let shape = emit::declaration(declaration)?; let promise = symbolic::promise(declaration); if !symbolic::wanted(declaration) { return Ok(quote::quote! { #shape #promise }); } let staged = symbolic::stage(declaration)?; let twin = emit::declaration(&staged.declaration)?; let filler = fill::filler(declaration, &staged.fill)?; let (guards, loops, holes, sites) = staged.counts; let counts = &staged.counts_ident; let vis = declaration .vis .clone() .unwrap_or(syn::Visibility::Inherited); let doc = format!( " What a plan has to answer for to drive [`{}`].", staged.declaration.name ); Ok(quote::quote! { #shape #promise #twin #filler #[doc = #doc] #vis const #counts: ::quasi_router::stage::Shape = ::quasi_router::stage::Shape { guards: #guards, loops: #loops, holes: #holes, sites: #sites, }; }) }