//! The staged twin of a declaration. //! //! A shape marked `#[staged]` gets a second function beside it, which is the //! same description evaluated with no request: every value is a sentinel, every //! guard is a bool a [`Plan`](quasi_router::stage::Plan) chooses, and every //! loop runs as many times as the plan says. Handing its result to the ordinary //! renderer produces the screen's markup with sentinels where the data would //! be, and that string is the residual a request later fills. //! //! # Why this is a rewrite and not a second emitter //! //! Nothing here emits Rust. It rewrites the parsed [`Declaration`] into another //! [`Declaration`] and hands that to [`crate::emit`], so the staged function is //! built by the same code that builds the ordinary one. //! //! That is the whole reason to do it this way. The residual's literals have to //! be the renderer's own output, or the staged path is a second renderer //! wearing the first one's name, and a second emitter here would be the same //! mistake one level up: two spellings of every production, drifting apart one //! conversion at a time. A rewrite over a closed grammar cannot drift, because //! a production it does not know about is a production it cannot silently //! mistranslate -- it refuses instead. //! //! # What it refuses, and why refusing is right //! //! `#[staged]` is a request, so a shape that cannot be staged is a compile //! error naming the construct rather than a silently unstaged shape. Three //! things are refused today, each because the value it needs has no sentinel: //! //! - a region whose kind comes from a supplier, which needs a `RegionKind` //! - a dispatch with no `otherwise`, whose arms become integers and would stop //! being exhaustive //! - a value hole in a position that is not a string, which rustc reports //! against the generated call rather than here //! //! The flag is opt-in for exactly this reason: a shape is staged when someone //! has looked at it, and the rest keep the runtime renderer, which is how the //! two paths coexist during the migration without a switch. use proc_macro2::Span; use quote::format_ident; use syn::{Ident, Result}; use crate::ast::{ Arg, Declaration, Emission, Guard, Hole, HoleRoot, Interpolated, Item, Param, Predicate, RegionKind, Source, StrPart, }; /// The flag that asks for a staged twin. pub const FLAG: &str = "staged"; /// The flag that says a shape answers the same thing every time. /// /// A promise about the body, made where the body is, which is the only place /// anybody can check it. See [`constant_name`] for how a caller in another /// `declare!` (and possibly another crate) is held to it without the two macros /// ever seeing each other. pub const CONSTANT: &str = "constant"; /// The shim a `#[constant]` shape gets, which is how a caller opts in. /// /// A proc macro sees one invocation and no other, so a caller cannot read the /// callee's flags: two `declare!`s are two separate expansions, often in two /// crates. The flag therefore has to leave something behind that a call site /// can name and that does not exist otherwise. /// /// That is this function. It takes the shape's own parameters and calls the /// shape, so it is the shape under a second name, and it is emitted only under /// the flag. An `include` whose arguments are all literals calls it instead of /// opening a staged scope; against a shape that made no such promise the call /// does not resolve, and rustc names the function it could not find. The word /// in that name is the word to go and write. pub fn constant_name(name: &Ident) -> Ident { format_ident!("{}_constant", name, span = name.span()) } /// Whether an argument is settled where it is written. /// /// A literal, or a list of them, and nothing else. A hole is a read, and a /// borrow of one is the same read with an ampersand in front, so neither is /// fixed however constant it looks: `&SECTIONS` names a path this macro cannot /// evaluate. An interpolated string counts only when it interpolates nothing. fn fixed(arg: &Arg) -> bool { match arg { Arg::Str(text) => text .parts .iter() .all(|part| matches!(part, StrPart::Lit(_))), Arg::Int(_) | Arg::Bool(_) => true, Arg::List(items) => items.iter().all(fixed), Arg::Hole(_) | Arg::Borrow(_) => false, } } /// Whether this declaration promised to answer the same thing every time. pub fn constant(declaration: &Declaration) -> bool { declaration.flags.iter().any(|flag| flag == CONSTANT) } /// The shim, or nothing where no promise was made. pub fn promise(declaration: &Declaration) -> proc_macro2::TokenStream { if !constant(declaration) { return proc_macro2::TokenStream::new(); } let name = &declaration.name; let shim = constant_name(name); let vis = declaration .vis .clone() .unwrap_or(syn::Visibility::Inherited); let params: Vec<_> = declaration .params .iter() .map(|param| { let name = ¶m.name; let ty = ¶m.ty; quote::quote!(#name: #ty) }) .collect(); let arguments = declaration.params.iter().map(|param| ¶m.name); let Ok(returns) = crate::emit::returns_type(&declaration.returns) else { // A return type this crate does not know is refused by `emit` with a // message naming it, and that error is the one worth reporting. Emit no // shim rather than a second complaint about the same line. return proc_macro2::TokenStream::new(); }; let doc = format!( " [`{name}`], and the promise that it answers the same thing every time.\n\n Emitted by `#[constant]`. A staged `include` whose arguments are all\n literals calls this rather than opening a scope, so what this returns is\n built once while a residual is derived instead of once per request. A\n shape that made no such promise has no function here, and the call site\n fails to resolve rather than quietly baking one evaluation in forever." ); quote::quote! { #[doc = #doc] #[allow(dead_code)] #vis fn #shim(#(#params),*) -> #returns { #name(#(#arguments),*) } } } /// The parameter the staged function takes in place of the request's reads. pub(crate) const PLAN: &str = "plan"; /// The staged twin of `name`, which is what an `include` retargets to. pub fn staged_name(name: &Ident) -> Ident { format_ident!("{}_staged", name, span = name.span()) } /// The const holding the shape's counts, beside the staged function. fn counts_name(name: &Ident) -> Ident { format_ident!( "{}_STAGED", name.to_string().to_uppercase(), span = name.span() ) } /// Whether this declaration asked to be staged. pub fn wanted(declaration: &Declaration) -> bool { declaration.flags.iter().any(|flag| flag == FLAG) } /// One instruction of the fill program: what a request writes, and where. /// /// Collected by the same traversal that numbers the holes, so the filler and /// the residual cannot disagree about order. Written as a second traversal they /// would drift the first time a production was added to one and not the other. pub enum Fill { /// One value, at the hole with this number. /// /// Numbered rather than positioned because render order and declaration /// order genuinely differ: a cell's `activate` address is written after the /// cell's value and renders before it, inside the anchor that wraps it. Hole { id: u16, hole: Hole }, /// What one guard places, and the predicate that decides it. Branch { guard: Guard, body: Vec }, /// One loop, over the collection the declaration named. Repeat { dereferenced: bool, binder: Ident, iterable: Hole, body: Vec, }, /// Two markups at one position, and the predicate that picks between them. /// /// What a guarded SETTLING setting compiles to. `chosen`, `here` and /// `latched` change the markup the member their own statement produces was /// going to write anyway, so there is no run of members for a branch to /// cover; the member is built both ways instead and a request picks one. /// /// Arm 0 is the setting made, matching [`Plan::swap`][swap], which is what /// the twin reads to decide both which arm it is drawing and whether to /// make the setting. No body, for [`Arms`](Self::Arms)'s reason: both arms' /// holes are numbered at the level around them. /// /// [swap]: quasi_router::stage::Plan::swap Swap { guard: Guard }, /// One dispatch, and which arm the request takes. /// /// No bodies. Every arm's holes are staged at the level around the /// dispatch, so the closure that fills the stretch these arms sit in is the /// one that fills the arm -- the same arrangement a guard that swaps markup /// uses, and for the same reason: one arm carries holes another does not, /// so an arm cannot number its own. Arms { scrutinee: Hole, /// One pattern per written arm, in the order they were written. The /// index a pattern maps to is its position, and anything unmatched /// takes the last one, which is `otherwise`. patterns: Vec, }, /// Another shape's filler, called where its markup was spliced in. /// /// `site` is the include's ordinal in the shape that holds it, which is /// what the staged twin hands `Plan::enter`. The filler passes the same /// number to `Cursor::enter`, so a caller's walk stops at the boundary /// rather than answering the callee's holes with its own. Include { callee: syn::Path, args: Vec, site: u16, }, } /// Numbering, one counter per thing a plan answers. /// /// Shape-local on purpose. A residual is derived per shape and composed by /// reference, so an included shape's holes are never renumbered against its /// caller's, which is what keeps the derivation linear in the tree rather than /// in the tree's expansion. #[derive(Default)] struct Counters { guards: u16, loops: u16, holes: u16, /// One per `include`, which is what scopes the shape it reaches. sites: u16, /// One per dispatch, which is what the plan answers with an arm index. arms: u16, /// The fill program under construction, innermost body last. stack: Vec>, /// The arm site of the member currently being built both ways, if any. /// /// Set while a member carrying a guarded settling setting is staged, and /// read by that setting: its guard becomes a read of this site's arm rather /// than a refusal, and it writes no fill of its own because the wrapper /// already wrote one. swapping: Option, } impl Counters { /// Start a body: a loop's, or what a guard places. fn enter(&mut self) { self.stack.push(Vec::new()); } /// Finish the innermost body. fn exit(&mut self) -> Vec { self.stack.pop().unwrap_or_default() } /// Add one instruction to the innermost body. fn wrote(&mut self, fill: Fill) { if let Some(body) = self.stack.last_mut() { body.push(fill); } } } /// A hole reading the plan: `plan.()`. fn plan_read(method: &str, id: u16) -> Hole { Hole { root: HoleRoot::Binding(Ident::new(PLAN, Span::call_site())), steps: vec![crate::ast::Step::Method { name: Ident::new(method, Span::call_site()), args: vec![Arg::Int(i64::from(id))], }], } } /// A hole answering with one sentinel: `plan.hole()`. /// /// Through the plan rather than a free function because a sentinel carries the /// scope it was read at, and the plan is what knows the scope. Two tables on /// one screen each number their holes from zero, so without the scope they /// would both claim hole 0 and the residual would fill one from the other. fn sentinel(id: u16) -> Hole { plan_read("hole", id) } /// Which arm a dispatch takes: `plan.arm(, )`. /// /// The count rides along so a traced render records it. See /// `quasi_router::stage::Plan::arm`. fn plan_arm(id: u16, count: usize) -> Hole { Hole { root: HoleRoot::Binding(Ident::new(PLAN, Span::call_site())), steps: vec![crate::ast::Step::Method { name: Ident::new("arm", Span::call_site()), args: vec![Arg::Int(i64::from(id)), Arg::Int(count as i64)], }], } } /// A hole answering with one numeric stand-in: `plan.number()`. /// /// See `quasi_router::stage::number_at`. A number has no `ZQH` to carry, so a /// slot typed `usize` was refused a hole until this existed, and a pager is /// three of them. fn counted(id: u16) -> Hole { plan_read("number", id) } /// The staged declaration, and the counts a plan needs to drive it. pub struct Staged { pub declaration: Declaration, /// What a request writes into the residual, in declaration order. pub fill: Vec, pub counts: (u16, u16, u16, u16), pub counts_ident: Ident, } /// Rewrite one declaration into its staged twin. pub fn stage(declaration: &Declaration) -> Result { let mut counters = Counters::default(); counters.enter(); let items = items(&declaration.items, &mut counters)?; let fill = counters.exit(); let name = staged_name(&declaration.name); let doc = format!( " The staged twin of [`{}`], evaluated with no request.", declaration.name ); Ok(Staged { fill, counts: ( counters.guards, counters.loops, counters.holes, counters.sites, ), counts_ident: counts_name(&declaration.name), declaration: Declaration { docs: vec![doc], // `must_use` and `inline` carry over; the flag that asked for this // does not, or the twin would ask for a twin of its own. flags: declaration .flags .iter() .filter(|flag| *flag != FLAG) .cloned() .collect(), vis: declaration.vis.clone(), name, params: vec![Param { name: Ident::new(PLAN, Span::call_site()), ty: syn::parse_quote!(&::quasi_router::stage::Plan), }], // A shape answering a bare list answers `Staged>` here: a // `Vec` has nowhere to keep a mark, so the marks ride beside the // value until whatever splices its members in absorbs them. Free to // do, because nothing but the derivation calls a twin. returns: listed(&declaration.returns), items, }, }) } /// A twin's return type: a bare list gains its marks, everything else is itself. /// /// Read syntactically rather than through `emit`'s reader, because this runs /// before the twin is emitted and the answer is one shape of type. A container /// keeps its own marks inside it and needs nothing here. fn listed(returns: &syn::Type) -> syn::Type { let syn::Type::Path(path) = returns else { return returns.clone(); }; match path.path.segments.last() { Some(last) if last.ident == "Vec" => { syn::parse_quote!(::quasi_router::stage::Staged<#returns>) } _ => returns.clone(), } } fn items(items: &[Item], counters: &mut Counters) -> Result> { items .iter() .map(|item| self::item(item, counters)) .collect() } fn item(item: &Item, counters: &mut Counters) -> Result { Ok(match item { Item::Bind { name, source } => Item::Bind { name: name.clone(), source: self::source(source, counters)?, }, // Nothing an author wrote. A twin is built from a declaration, and a // declaration cannot carry one of these, so meeting one means the // rewrite has been run over its own output. Item::Marked { .. } => { return Err(syn::Error::new( Span::call_site(), "a staged twin cannot be staged again", )); } // A setting is one of two things, and which one decides whether a // guard on it can be a mark at all. See [`PLACING`]. Item::Attribute { name, args, guard, body, } => { if let Some(guard) = guard { // The member around this one is being built both ways, and this // setting is what differs between them. Its guard becomes the // arm the plan asks for; the wrapper wrote the fill. if !placing(name) && let Some(site) = counters.swapping { return Ok(Item::Attribute { name: name.clone(), args: staged_args(name, args, counters)?, guard: Some(Guard { negated: false, predicate: Predicate::Truth(plan_read("swap", site)), span: guard.span, }), body: items(body, counters)?, }); } if !placing(name) { return Err(syn::Error::new( guard.span, "a staged shape cannot guard a settling setting: it varies \ markup INSIDE the member its own statement produces, and \ a mark covers a run of members. The answer is two arms \ over that member -- built with the setting and without it \ -- which is the shape `given` already compiles to", )); } // A placing setting is a member in all but name, so its guard // is an ordinary mark over the run it places. The setting is // made unconditionally and the mark says what a request // decides, exactly as for a guarded member. let site = counters.guards; counters.guards += 1; counters.enter(); let args = staged_args(name, args, counters)?; // Inside the branch, because a setting that is not made places // none of what its body would have settled either. let inner = items(body, counters)?; let recorded = counters.exit(); counters.wrote(Fill::Branch { guard: (*guard).clone(), body: recorded, }); return Ok(Item::Marked { site, varies: crate::ast::Varies::Absent, body: vec![Item::Attribute { name: name.clone(), args, guard: None, body: inner, }], }); } // A body may settle the argument under a guard, and that guard // is the settling case: `chosen` is ` selected` inside the // `