//! Turning a parsed declaration into the Rust a shape function used to be. //! //! Every construct emitted here is a constructor `quasi_router` already has, so //! nothing in the generated code is reachable only through this macro. The //! expansion is the same tree the hand-written shape built, made at the same //! moment for the same request; what changes is who wrote it. //! //! Two rules decide most of this file. Rule R2 says what the shape's return //! type makes: a `Node` shape's single emission IS the result, so there is no //! fabricated container to fill. And rule R8 says nothing borrows implicitly, //! so an `&` appears in the output only where the declaration wrote one. use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote}; use syn::spanned::Spanned as _; use syn::{LitStr, Result}; use crate::ast::{ Action, Arg, Declaration, Emission, Guard, Hole, HoleRoot, Interpolated, Item, Pattern, Predicate, RegionKind, Source, Step, StrPart, }; use crate::parse::{COMPARISONS, VOCABULARY}; /// What the items of a body accrete onto. #[derive(Clone, Copy, PartialEq, Eq)] enum Container { /// A region: emissions accrete onto a `Slot`. Slot, /// A row: every emission is `beside` and carries a priority. Run, /// One control: its body sets, and emits nothing. Act, /// One field: its body sets, and emits nothing. Field, /// A document: its members are regions, held as slots rather than nodes. Screen, /// One row of a list: its members are the controls it offers. Row, /// A table: its members are its columns and its rows. Table, /// One column: its body says how it narrows, and it emits nothing. Column, /// One cell: its body says what opening it does. Cell, /// One node member: its body says what it is told afterwards. Node, /// One picture: its body sets on the `Image`, and it emits nothing. Image, /// One canvas: its body sets on the `Canvas` and draws inside its scope. Canvas, /// One tag: its body says what it is like, and it emits nothing. Tag, /// An axis: its members are `at`, each a placement and the row on it. Timeline, /// One meter: its body says what it is like, and it emits nothing. Meter, /// One figure: its body says what it is like, and it emits nothing. Figure, /// One repeating question: its body says how few may be left, and it emits /// nothing. Repeats, } pub fn declaration(declaration: &Declaration) -> Result { let Declaration { docs, flags, vis, name, params, returns, items, } = declaration; let shaped = shaped_name(returns)?; let docs = docs.iter().map(|line| { let line = LitStr::new(line, Span::call_site()); quote!(#[doc = #line]) }); // `staged` asks for the twin in `symbolic` and `constant` asks for the // shim beside it. Neither is an attribute rustc has ever heard of. let flags = flags .iter() .filter(|flag| *flag != crate::symbolic::FLAG && *flag != crate::symbolic::CONSTANT) .map(|flag| quote!(#[#flag])); let params = params.iter().map(|param| { let name = ¶m.name; let ty = ¶m.ty; quote!(#name: #ty) }); let body = value_body(items, &shaped, name.span())?; let returns = returns_type(returns)?; Ok(quote! { #(#docs)* #(#flags)* #vis fn #name(#(#params),*) -> #returns #body }) } /// The vocabulary type a shape returns, written out. /// /// The vocabulary type, not the path the file happened to import it under: /// team.rs takes `Screen as Described` because it has a function called /// `screen`, and a declaration names what it returns rather than what the module /// around it calls that. /// /// Public to the crate because `symbolic`'s `#[constant]` shim has the same /// signature as the shape it stands for, and a second spelling of this mapping /// would drift from the first the moment a shaped type was added. pub fn returns_type(returns: &syn::Type) -> Result { let shaped = shaped_name(returns)?; Ok(match &shaped { Shaped::Nodes => quote!(::std::vec::Vec<::quasi_router::Node>), Shaped::Acts => quote!(::std::vec::Vec<::quasi_router::Act>), Shaped::Single { name, optional } => { let shaped_type = format_ident!("{}", name, span = returns.span()); if *optional { quote!(::std::option::Option<::quasi_router::#shaped_type>) } else { quote!(::quasi_router::#shaped_type) } } }) } /// What a shape returns. enum Shaped { /// One vocabulary type, which the body's single emission is. `optional` is /// the grammar's `single "?"`, and rule R10 is what makes it usable: a body /// whose emission is guarded away yields `None`. Single { name: String, optional: bool }, /// `Vec`: the members of a panel, in order, with nothing wrapping /// them. /// /// The one shape that is not a single emission, and it is not a container /// either. `user_support` demanded it: the dashboard strip draws the region /// and its `id`, so the panel's own fill must add no second one, and the /// tab that answers over htmx wraps the same members itself. 52 of the /// population's 484 shapes return this, third after `Slot` and `Node`. Nodes, /// `Vec`: a menu, which is the controls a row holds back. /// /// [`Nodes`](Self::Nodes)' twin and for its reason. `Row::menu` and /// `Row::menu` takes the whole list, so a menu that is built conditionally /// has nowhere to accrete, and audiofiles' file list has two of them -- /// what a row offers and what a chosen set does. Every member is an `act`, /// because a menu is nothing but controls. Acts, } /// The type a shape returns, read off the declaration. fn shaped_name(returns: &syn::Type) -> Result { let syn::Type::Path(path) = returns else { return Err(syn::Error::new_spanned( returns, "a shape returns one of the vocabulary types", )); }; let Some(last) = path.path.segments.last() else { return Err(syn::Error::new_spanned(returns, "an empty return type")); }; if last.ident == "Vec" { let Some(inner) = sole_argument(returns, &last.arguments)? else { return Err(syn::Error::new_spanned(returns, "`Vec` of what?")); }; return match shaped_name(inner)? { Shaped::Single { ref name, optional: false, } if name == "Node" => Ok(Shaped::Nodes), Shaped::Single { ref name, optional: false, } if name == "Act" => Ok(Shaped::Acts), _ => Err(syn::Error::new_spanned( returns, "a shape builds two lists: `Vec`, a panel's members, and \ `Vec`, a menu", )), }; } if last.ident == "Option" { let Some(inner) = sole_argument(returns, &last.arguments)? else { return Err(syn::Error::new_spanned(returns, "`Option` of what?")); }; return match shaped_name(inner)? { Shaped::Single { name, optional: false, } => Ok(Shaped::Single { name, optional: true, }), Shaped::Single { .. } => Err(syn::Error::new_spanned( returns, "a shape omits its result or does not; there is no second omission", )), Shaped::Nodes => Err(syn::Error::new_spanned( returns, "a panel with no members is an empty `Vec`, not a `None`", )), Shaped::Acts => Err(syn::Error::new_spanned( returns, "a menu with no entries is an empty `Vec`, not a `None`", )), }; } Ok(Shaped::Single { name: last.ident.to_string(), optional: false, }) } /// The one type inside `Option<..>` or `Vec<..>`. fn sole_argument<'a>( returns: &syn::Type, arguments: &'a syn::PathArguments, ) -> Result> { let syn::PathArguments::AngleBracketed(arguments) = arguments else { return Err(syn::Error::new_spanned(returns, "of what?")); }; Ok(match arguments.args.first() { Some(syn::GenericArgument::Type(inner)) => Some(inner), _ => None, }) } /// A shape's body: its bindings, then the single emission that is the value. /// /// Rule R2 for the two shapes converted so far. A `-> Node` shape's single /// emission IS the result and a `-> Act` shape's single `act` member is, so /// neither fabricates a container: what a caller gets is what the body said. /// /// Bindings are hoisted above the emissions of their own body. Nothing is /// reordered by that: a binding can only name bindings written before it, and /// an emission produces a value rather than an effect, so the two are /// independent within one block. fn value_body(items: &[Item], shaped: &Shaped, span: Span) -> Result { let shaped = match shaped { Shaped::Nodes => return nodes(items), Shaped::Acts => return acts(items), single @ Shaped::Single { .. } => single, }; let Shaped::Single { name: shaped, optional, } = shaped else { unreachable!("the two lists are answered above") }; let optional = *optional; let shaped = shaped.as_str(); let mut bindings = Vec::new(); let mut emissions = Vec::new(); for item in items { match item { Item::Bind { name, source } => { let value = source_value(source)?; bindings.push(quote!(let #name = #value;)); } Item::Attribute { name, .. } => { return Err(syn::Error::new( name.span(), "a setting needs something to set: put it in the member's body", )); } Item::For { binder, .. } => { return Err(syn::Error::new( binder.span(), "a loop emits many members, and this shape is its single one", )); } Item::Emit(emission) => emissions.push(emission), } } let [only] = emissions.as_slice() else { return Err(syn::Error::new( span, format!( "a `-> {shaped}` shape is its single emission, and this one has {}", emissions.len() ), )); }; let (guard, only) = match only { Emission::Guarded { guard, inner } => (Some(guard), &**inner), other => (None, *other), }; if guard.is_some() && !optional { return Err(syn::Error::new( span, "a guard on the whole result needs a shape that may omit it: `-> Option<_>`", )); } let built = match shaped { "Node" => node(only)?, "Act" => match only { Emission::Act { .. } => act(only)?, other => { return Err(syn::Error::new( emission_span(other), "a `-> Act` shape is its single `act` member", )); } }, "Screen" => match only { Emission::Screen { arrangement, args, body, } => { let args = args.iter().map(self::arg).collect::>>()?; accrete( body, Container::Screen, "e!(::quasi_router::Screen::#arrangement(#(#args),*)), )? } other => { return Err(syn::Error::new( emission_span(other), "a `-> Screen` shape is its single `screen` member", )); } }, "Slot" => match only { Emission::Region { name, kind, body } => slot(name, kind, body)?, other => { return Err(syn::Error::new( emission_span(other), "a `-> Slot` shape is its single `region` member", )); } }, // One arm, because there is one row type since the 2026-09-05 collapse. // The body says which spelling it is -- `row "primary" { .. }` names // roles of the default column set, `cells { .. }` names declared columns // -- and the return type was a second, redundant way to say the same // thing. It was worse than redundant: it had to agree with the body, so // a table row spelled `-> Row` was an error about the return type when // nothing was wrong with it. "Row" => match only { Emission::Row { primary, body } => row(Some(primary), body)?, Emission::Cells(body) => cells(body)?, other => { return Err(syn::Error::new( emission_span(other), "a `-> Row` shape is its single `row` or `cells` member", )); } }, "Field" => match only { Emission::Field { kind, name, label, body, } => field(kind, name, label, body)?, other => { return Err(syn::Error::new( emission_span(other), "a `-> Field` shape is its single `field` member", )); } }, other => { return Err(syn::Error::new( span, format!( "`-> {other}` is not a shape this form can build yet. \ Add the R2 case, and name the screen that demanded it in the commit." ), )); } }; let built = if optional { let test = match guard { Some(guard) => predicate(guard)?, None => quote!(true), }; quote!(if #test { ::std::option::Option::Some(#built) } else { ::std::option::Option::None }) } else { built }; Ok(quote!({ #(#bindings)* #built })) } /// A binding's value, with the dispatch's ownership already decided. fn source_value(source: &Source) -> Result { self::source(source, owned_arms(source)) } /// A body that accretes onto something already built. fn accrete(items: &[Item], container: Container, base: &TokenStream) -> Result { // A loop cannot be a link in a chain, and neither can a guard: both are // statements. A body with either accumulates instead. let statementish = items.iter().any(|item| { matches!( item, Item::For { .. } | Item::Emit(Emission::Guarded { .. }) | Item::Attribute { guard: Some(_), .. } ) }); if statementish { return accumulate(items, container, base); } let mut bindings = Vec::new(); let mut steps = Vec::new(); for item in items { match item { Item::Bind { name, source } => { let value = source_value(source)?; bindings.push(quote!(let #name = #value;)); } Item::Attribute { name, args, .. } => steps.push(attribute(name, args)?), Item::For { .. } => unreachable!("a loop takes the accumulating form"), Item::Emit(emission) => { // A member is evaluated into its own binding before the // container is built, because a container's own name is often // the same value one of its members borrows: follow.rs names // the region and then aims the press at it. Built inline, the // container would move the name before the member read it. let held = format_ident!("member_{}", bindings.len(), span = Span::call_site()); let (value, call) = step(emission, container, &held)?; bindings.push(quote!(let #held = #value;)); steps.push(call); } } } Ok(quote!({ #(#bindings)* #base #(#steps)* })) } /// The same body, for a container a loop adds to. /// /// A chain cannot hold a loop, so this accumulates instead. The two forms are /// kept apart rather than merged because the chain is what a container with no /// loop should read as, and a `let mut` that is never reassigned is a warning /// in the caller's crate that the caller cannot see the cause of. fn accumulate(items: &[Item], container: Container, base: &TokenStream) -> Result { let built = format_ident!("built", span = Span::call_site()); let statements = statements(items, container, &built)?; Ok(quote!({ let mut #built = #base; #(#statements)* #built })) } /// The statements one body contributes to an accumulating container. fn statements( items: &[Item], container: Container, built: &proc_macro2::Ident, ) -> Result> { let mut statements = Vec::new(); for (index, item) in items.iter().enumerate() { statements.push(match item { Item::Bind { name, source } => { let value = source_value(source)?; quote!(let #name = #value;) } Item::Attribute { name, args, guard } => { let call = attribute(name, args)?; match guard { // The same rule a guarded member follows: the guard decides // whether the setting is made, and the args are evaluated // either way. Some(guard) => { let test = predicate(guard)?; quote!(if #test { #built = #built #call; }) } None => quote!(#built = #built #call;), } } Item::For { dereferenced, binder, iterable, body, } => { let iterable = hole(iterable)?; let inner = self::statements(body, container, built)?; let binder = binder_pattern(*dereferenced, binder); quote!(for #binder in #iterable { #(#inner)* }) } Item::Emit(Emission::Guarded { guard, inner }) => { let test = predicate(guard)?; let held = format_ident!("member_{index}", span = Span::call_site()); let (value, call) = step(inner, container, &held)?; // R9: the guard decides whether the member is placed, not // whether its holes are evaluated. The value is built either // way, and a supplier that is asked for nothing answers with // nothing. quote!({ let #held = #value; if #test { #built = #built #call; } }) } Item::Emit(emission) => { let held = format_ident!("member_{index}", span = Span::call_site()); let (value, call) = step(emission, container, &held)?; quote!({ let #held = #value; #built = #built #call; }) } }); } Ok(statements) } /// One setting, as the builder call ATTRIBUTE NAMING says it is. fn attribute(name: &proc_macro2::Ident, args: &[Arg]) -> Result { let slot = name.to_string(); let enumeration = VOCABULARY .iter() .find(|(attribute, _)| *attribute == slot) .map(|(_, enumeration)| *enumeration); let args = args .iter() .map(|value| match enumeration { Some(enumeration) => variant(value, enumeration), None => arg(value), }) .collect::>>()?; Ok(quote!(.#name(#(#args),*))) } /// Rule R1(4): a bare uppercase ident in a vocabulary slot is that enum's /// variant. A `SCREAMING_CASE` ident never is, because 19 of 19 real uses of /// the `measured` slot pass a module const rather than a variant. fn variant(value: &Arg, enumeration: &str) -> Result { let Arg::Hole(hole) = value else { return arg(value); }; let HoleRoot::Path(path) = &hole.root else { return arg(value); }; if !hole.steps.is_empty() || path.leading_colon.is_some() || path.segments.len() != 1 { return arg(value); } let name = path.segments[0].ident.to_string(); if name.chars().all(|letter| !letter.is_lowercase()) { return arg(value); } let enumeration = format_ident!("{}", enumeration); let name = &path.segments[0].ident; Ok(quote!(::quasi_router::layout::#enumeration::#name)) } /// One emission, as the value it builds and the call that places it. fn step( emission: &Emission, container: Container, held: &proc_macro2::Ident, ) -> Result<(TokenStream, TokenStream)> { match (container, emission) { (Container::Slot, Emission::Across { fallback, body }) => { let run = accrete( body, Container::Run, "e!(::quasi_router::Run::new(::quasi_router::layout::Fallback::#fallback)), )?; Ok((run, quote!(.across(#held)))) } // A question standing in a region rather than in a form. A region that // consults gathers the dials it contains and sends them itself, so // there is no form to put them in and `Node::field` is what the // vocabulary has for it. `pricing`'s calculator is the site: five dials // and no submit button anywhere on the page. // // Narrow on purpose. Everywhere else a field is still refused with the // message that names the two legal homes, because a question in a cell // or a row is a question nothing will ever read. // // A ranked member, which is `beside`'s third container and its second // meaning of `Priority`: a run ranks everything it holds and a region // ranks the members that say so. audiofiles' status band is the site -- // every fact in it is stated nowhere else on the window except the // focused sample's tags, which the detail panel repeats in full, so a // window with no room loses the repetition and keeps the rest. ( Container::Slot, Emission::Beside { priority, width, inner, }, ) => { if width.is_some() { return Err(syn::Error::new( Span::call_site(), "a region's body is a stack and every member gets the whole width: \ say a width on a row's member, inside `across`", )); } let rank = variant(priority, "Priority")?; Ok((panel_member(inner)?, quote!(.with_ranked(#held, #rank)))) } // The control one slot of a repeating question carries to take itself // away, and the question the slots are of. Both are `Act`s the region is // told, and neither could be said at all: written where the vocabulary // takes them they are expressions in argument position, which no guard // reaches and no reader of the description sees. audiofiles' rule editor // is the site for both, twice each. ( Container::Slot, Emission::Removes { label, action, body, }, ) => { let label = arg(label)?; let called = self::action(action)?; let taken = accrete( body, Container::Act, "e!(::quasi_router::Act::new(#label, #called)), )?; Ok((taken, quote!(.removes(#held)))) } ( Container::Slot, Emission::Repeats { one, label, action, body, }, ) => { let one = arg(one)?; let label = arg(label)?; let called = self::action(action)?; let question = accrete( body, Container::Repeats, "e!(::quasi_router::Repeating::new( #one, ::quasi_router::Act::new(#label, #called) )), )?; Ok((question, quote!(.repeating(#held)))) } // A named member of a region: the label is hoisted out of the child and // onto the placement, because that is where `Slot::frame` takes it. // // The word stays written inside the child, which is where it reads -- // "this region is called Bio" -- but it is no longer something the child // carries. Since quasicoherent `2cdc6761` a `Slot` has no label at all, // so a label on a member that nothing reveals cannot be built rather // than being built and dropped. See [`labelled`] for what is refused. // A member placed as a named frame, whatever it is. `include` is the // reason this exists: a shape in another `declare!` answers a `Slot`, // which since `2cdc6761` cannot carry a label, so the name has to be // said where the placing happens. (Container::Slot, Emission::Framed { label, inner }) => { let label = arg(label)?; let (built, _) = step(inner, container, held)?; Ok((built, quote!(.frame(#label, #held)))) } (Container::Slot, Emission::Region { name, kind, body }) => { let (label, body) = labelled(body)?; let slot = slot(name, kind, &body)?; let node = quote!(::quasi_router::Node::Region(#slot)); match label { Some(label) => Ok((node, quote!(.frame(#label, #held)))), None => Ok((node, quote!(.with(#held)))), } } // A shape that answers many members, spliced whole. The plural of // `include`, and only a container that holds many can take one. (Container::Slot, Emission::IncludeEach(supplier)) => { let supplier = hole(supplier)?; Ok((quote!(#supplier), quote!(.with_all(#held)))) } (Container::Slot, other) => Ok((panel_member(other)?, quote!(.with(#held)))), (Container::Screen, Emission::Region { name, kind, body }) => { Ok((slot(name, kind, body)?, quote!(.with(#held)))) } // A region another shape built. `custom_page`'s three screens carry the // same platform strip top and bottom, and every link in it is per // request, so the band is a shape rather than `Chrome`. A table already // takes a row this way and a row a control; this is a document taking a // region. (Container::Screen, Emission::Include(supplier)) => { let supplier = hole(supplier)?; Ok(( quote!(::std::convert::Into::into(#supplier)), quote!(.with(#held)), )) } (Container::Screen, other) => Err(syn::Error::new( emission_span(other), "a document holds regions: write `region as { .. }` \ or `include ;`", )), (Container::Cell | Container::Row, Emission::Activate(action)) => { Ok((self::action(action)?, quote!(.activate(#held)))) } (Container::Row, Emission::Act { .. }) => Ok((act(emission)?, quote!(.act(#held)))), // `act`'s held-back twin. A row shows what `act` puts in it and keeps // what `offers` gives it until the host is asked, which is the whole of // the difference and is why the two are one word apart. A member rather // than a setting because a menu control is told the same things an // inline one is: audiofiles' sidebar greys a vault's Delete when it is // the last vault, and a control written as an expression in an argument // cannot say that. (Container::Row, Emission::Offers { .. }) => Ok((act(emission)?, quote!(.offers(#held)))), // A control another shape built. `export_act::control` is the export // portal's one sentence about saving a file, said once and offered // under each card's own label, and a row holds acts rather than nodes. (Container::Row, Emission::Include(supplier)) => { let supplier = hole(supplier)?; Ok(( quote!(::std::convert::Into::into(#supplier)), quote!(.act(#held)), )) } // A row's parts are placed by role rather than ranked by priority, // which is the other thing `beside` means and the container is what // says which. embeds' button strip is the site: a thumbnail and a title // in `Primary`, the buy control in `Actions`, the price a setting // between them. ( Container::Row, Emission::Beside { priority, width, inner, }, ) => { if width.is_some() { return Err(syn::Error::new( Span::call_site(), "a row of a list places its parts by name and not by width: \ say a width on a region row's member, inside `across`", )); } let part = variant(priority, "RowPart")?; Ok((node(inner)?, quote!(.part(#part, #held)))) } // A cell names its column instead of taking a role, and that is the // whole of what a table row spells differently. One container since the // 2026-09-05 collapse, so this sits beside `beside` rather than in a // second settings table. ( Container::Row, Emission::Cell { column, value, body, }, ) => { let built = cell(value, body)?; Ok(match column { Some(column) => { let column = arg(column)?; (built, quote!(.at(#column, #held))) } None => (built, quote!(.cell(#held))), }) } // One message, because there is one container. A row places its content // by key and the two spellings are the two kinds of key: `beside ` // names a role of the default column set, `cell at ` names a // declared column. (Container::Row, other) => Err(syn::Error::new( emission_span(other), "a row holds controls, places its content and says what opening it does: write \ `act