max / quasi
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
10 files changed,
+933 insertions,
-7 deletions
| @@ -3526,7 +3526,7 @@ | |||
| 3526 | 3526 | ||
| 3527 | 3527 | [[package]] | |
| 3528 | 3528 | name = "quasi-declare" | |
| 3529 | - | version = "0.1.7" | |
| 3529 | + | version = "0.1.8" | |
| 3530 | 3530 | dependencies = [ | |
| 3531 | 3531 | "proc-macro2", | |
| 3532 | 3532 | "quote", | |
| @@ -3565,7 +3565,7 @@ | |||
| 3565 | 3565 | ||
| 3566 | 3566 | [[package]] | |
| 3567 | 3567 | name = "quasi-router" | |
| 3568 | - | version = "0.101.14" | |
| 3568 | + | version = "0.101.15" | |
| 3569 | 3569 | dependencies = [ | |
| 3570 | 3570 | "makeover-layout", | |
| 3571 | 3571 | ] |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "quasi-declare" | |
| 3 | - | version = "0.1.7" | |
| 3 | + | version = "0.1.8" | |
| 4 | 4 | description = "The declare! form: a screen description compiled to Rust at build time." | |
| 5 | 5 | edition.workspace = true | |
| 6 | 6 | rust-version.workspace = true |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "quasi-router" | |
| 3 | - | version = "0.101.14" | |
| 3 | + | version = "0.101.15" | |
| 4 | 4 | description = "Host-agnostic router: a request in, a renderer-agnostic description out" | |
| 5 | 5 | edition.workspace = true | |
| 6 | 6 | rust-version.workspace = true |
| @@ -27,6 +27,7 @@ | |||
| 27 | 27 | ||
| 28 | 28 | declare! { | |
| 29 | 29 | /// Everything inside the tab pane. | |
| 30 | + | #[staged] | |
| 30 | 31 | shape pane(buyers: &[Buyer], shared: &[Shared]) -> Node; | |
| 31 | 32 | ||
| 32 | 33 | region REGION as Pane { | |
| @@ -46,6 +47,7 @@ | |||
| 46 | 47 | ||
| 47 | 48 | declare! { | |
| 48 | 49 | /// The buyers who shared an email. | |
| 50 | + | #[staged] | |
| 49 | 51 | shape buyers_table(buyers: &[Buyer]) -> Node; | |
| 50 | 52 | ||
| 51 | 53 | table { | |
| @@ -86,6 +88,7 @@ | |||
| 86 | 88 | ||
| 87 | 89 | declare! { | |
| 88 | 90 | /// The creators this reader has shared an email with. | |
| 91 | + | #[staged] | |
| 89 | 92 | shape shared_table(shared: &[Shared]) -> Node; | |
| 90 | 93 | ||
| 91 | 94 | table { | |
| @@ -197,3 +200,73 @@ | |||
| 197 | 200 | assert!(!html.contains("Shared With"), "{html}"); | |
| 198 | 201 | } | |
| 199 | 202 | } | |
| 203 | + | ||
| 204 | + | #[cfg(test)] | |
| 205 | + | mod staged_tests { | |
| 206 | + | use quasi_http::Serves as _; | |
| 207 | + | use quasi_router::stage::{Plan, read_sentinel}; | |
| 208 | + | use quasi_webview::Webview; | |
| 209 | + | ||
| 210 | + | use super::*; | |
| 211 | + | ||
| 212 | + | #[test] | |
| 213 | + | fn the_staged_twin_renders_the_screen_with_sentinels_where_values_go() { | |
| 214 | + | let html = Webview::new().fragment(&pane_staged(&Plan::full(1))); | |
| 215 | + | ||
| 216 | + | // The literals are the renderer's own, so the markup is the real | |
| 217 | + | // screen's and not something the macro decided. | |
| 218 | + | assert!(html.contains(r#"id="tab-content""#), "{html}"); | |
| 219 | + | assert!(html.contains("Your Buyers ("), "{html}"); | |
| 220 | + | assert!(html.contains(r#"class="table-head""#), "{html}"); | |
| 221 | + | assert!(html.contains("Last Purchase"), "{html}"); | |
| 222 | + | assert!(html.contains("Revoke"), "{html}"); | |
| 223 | + | ||
| 224 | + | // The consts stayed real: a value that is the same for every request | |
| 225 | + | // belongs in the literal rather than in a hole. | |
| 226 | + | assert!(html.contains("/library/tabs/contacts/revoke/"), "{html}"); | |
| 227 | + | ||
| 228 | + | // Every per-request value became a sentinel, addresses included. | |
| 229 | + | let mut holes = Vec::new(); | |
| 230 | + | let mut rest = html.as_str(); | |
| 231 | + | while let Some(at) = quasi_router::stage::find_sentinel(rest) { | |
| 232 | + | if let Some(hole) = read_sentinel(&rest[at..]) { | |
| 233 | + | holes.push(hole); | |
| 234 | + | rest = &rest[at + quasi_router::stage::SENTINEL_LEN..]; | |
| 235 | + | } else { | |
| 236 | + | rest = &rest[at + 3..]; | |
| 237 | + | } | |
| 238 | + | } | |
| 239 | + | assert!(!holes.is_empty(), "no sentinels at all: {html}"); | |
| 240 | + | ||
| 241 | + | // The two tables were reached by two includes, so their holes are in | |
| 242 | + | // different scopes and nothing fills one from the other. | |
| 243 | + | let scopes: std::collections::BTreeSet<u32> = holes.iter().map(|(s, _)| *s).collect(); | |
| 244 | + | assert!( | |
| 245 | + | scopes.len() >= 3, | |
| 246 | + | "expected root and two tables: {scopes:?}" | |
| 247 | + | ); | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | /// The plan decides the branch, which is what the spike could not do. | |
| 251 | + | #[test] | |
| 252 | + | fn a_failed_guard_takes_its_emission_out_of_the_render() { | |
| 253 | + | let webview = Webview::new(); | |
| 254 | + | let full = webview.fragment(&pane_staged(&Plan::full(1))); | |
| 255 | + | let none = webview.fragment(&pane_staged(&Plan::empty())); | |
| 256 | + | ||
| 257 | + | assert!(full.contains("Your Buyers ("), "{full}"); | |
| 258 | + | assert!(!none.contains("Your Buyers ("), "{none}"); | |
| 259 | + | assert!(!none.contains("table-head"), "{none}"); | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | /// A loop runs as many times as the plan says, in the scope it is read at. | |
| 263 | + | #[test] | |
| 264 | + | fn one_loop_grows_without_moving_the_other() { | |
| 265 | + | let webview = Webview::new(); | |
| 266 | + | let one = webview.fragment(&pane_staged(&Plan::full(1))); | |
| 267 | + | let two = webview.fragment(&pane_staged(&Plan::full(2))); | |
| 268 | + | ||
| 269 | + | assert_eq!(one.matches("table-row").count(), 2); | |
| 270 | + | assert_eq!(two.matches("table-row").count(), 4); | |
| 271 | + | } | |
| 272 | + | } |
| @@ -80,7 +80,12 @@ | |||
| 80 | 80 | let line = LitStr::new(line, Span::call_site()); | |
| 81 | 81 | quote!(#[doc = #line]) | |
| 82 | 82 | }); | |
| 83 | - | let flags = flags.iter().map(|flag| quote!(#[#flag])); | |
| 83 | + | // `staged` asks for the twin in `symbolic` and is not an attribute | |
| 84 | + | // rustc has ever heard of. | |
| 85 | + | let flags = flags | |
| 86 | + | .iter() | |
| 87 | + | .filter(|flag| *flag != crate::symbolic::FLAG) | |
| 88 | + | .map(|flag| quote!(#[#flag])); | |
| 84 | 89 | let params = params.iter().map(|param| { | |
| 85 | 90 | let name = ¶m.name; | |
| 86 | 91 | let ty = ¶m.ty; |
| @@ -21,6 +21,7 @@ | |||
| 21 | 21 | mod ast; | |
| 22 | 22 | mod emit; | |
| 23 | 23 | mod parse; | |
| 24 | + | mod symbolic; | |
| 24 | 25 | ||
| 25 | 26 | use proc_macro::TokenStream; | |
| 26 | 27 | use syn::parse_macro_input; | |
| @@ -44,8 +45,41 @@ | |||
| 44 | 45 | #[proc_macro] | |
| 45 | 46 | pub fn declare(input: TokenStream) -> TokenStream { | |
| 46 | 47 | let declaration = parse_macro_input!(input as ast::Declaration); | |
| 47 | - | match emit::declaration(&declaration) { | |
| 48 | + | match expand(&declaration) { | |
| 48 | 49 | Ok(expansion) => expansion.into(), | |
| 49 | 50 | Err(error) => error.into_compile_error().into(), | |
| 50 | 51 | } | |
| 51 | 52 | } | |
| 53 | + | ||
| 54 | + | /// The shape's function, and its staged twin if it asked for one. | |
| 55 | + | fn expand(declaration: &ast::Declaration) -> syn::Result<proc_macro2::TokenStream> { | |
| 56 | + | let shape = emit::declaration(declaration)?; | |
| 57 | + | if !symbolic::wanted(declaration) { | |
| 58 | + | return Ok(shape); | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | let staged = symbolic::stage(declaration)?; | |
| 62 | + | let twin = emit::declaration(&staged.declaration)?; | |
| 63 | + | let (guards, loops, holes, sites) = staged.counts; | |
| 64 | + | let counts = &staged.counts_ident; | |
| 65 | + | let vis = declaration | |
| 66 | + | .vis | |
| 67 | + | .clone() | |
| 68 | + | .unwrap_or(syn::Visibility::Inherited); | |
| 69 | + | let doc = format!( | |
| 70 | + | " What a plan has to answer for to drive [`{}`].", | |
| 71 | + | staged.declaration.name | |
| 72 | + | ); | |
| 73 | + | ||
| 74 | + | Ok(quote::quote! { | |
| 75 | + | #shape | |
| 76 | + | #twin | |
| 77 | + | #[doc = #doc] | |
| 78 | + | #vis const #counts: ::quasi_router::stage::Shape = ::quasi_router::stage::Shape { | |
| 79 | + | guards: #guards, | |
| 80 | + | loops: #loops, | |
| 81 | + | holes: #holes, | |
| 82 | + | sites: #sites, | |
| 83 | + | }; | |
| 84 | + | }) | |
| 85 | + | } |
| @@ -190,7 +190,9 @@ | |||
| 190 | 190 | ]; | |
| 191 | 191 | ||
| 192 | 192 | /// The attributes the generated function may carry. | |
| 193 | - | const FLAGS: &[&str] = &["must_use", "inline"]; | |
| 193 | + | // `staged` is not an attribute the generated function carries: it asks for | |
| 194 | + | // a second function beside it. See `symbolic`. | |
| 195 | + | const FLAGS: &[&str] = &["must_use", "inline", "staged"]; | |
| 194 | 196 | ||
| 195 | 197 | /// The verbs, and whether each addresses something. | |
| 196 | 198 | /// |
| @@ -141,6 +141,7 @@ | |||
| 141 | 141 | pub mod response; | |
| 142 | 142 | pub mod router; | |
| 143 | 143 | pub mod screen; | |
| 144 | + | pub mod stage; | |
| 144 | 145 | ||
| 145 | 146 | /// The description layer, re-exported. | |
| 146 | 147 | /// |
| @@ -1,0 +1,581 @@ | |||
| 1 | + | //! The staged twin of a declaration. | |
| 2 | + | //! | |
| 3 | + | //! A shape marked `#[staged]` gets a second function beside it, which is the | |
| 4 | + | //! same description evaluated with no request: every value is a sentinel, every | |
| 5 | + | //! guard is a bool a [`Plan`](quasi_router::stage::Plan) chooses, and every | |
| 6 | + | //! loop runs as many times as the plan says. Handing its result to the ordinary | |
| 7 | + | //! renderer produces the screen's markup with sentinels where the data would | |
| 8 | + | //! be, and that string is the residual a request later fills. | |
| 9 | + | //! | |
| 10 | + | //! # Why this is a rewrite and not a second emitter | |
| 11 | + | //! | |
| 12 | + | //! Nothing here emits Rust. It rewrites the parsed [`Declaration`] into another | |
| 13 | + | //! [`Declaration`] and hands that to [`crate::emit`], so the staged function is | |
| 14 | + | //! built by the same code that builds the ordinary one. | |
| 15 | + | //! | |
| 16 | + | //! That is the whole reason to do it this way. The residual's literals have to | |
| 17 | + | //! be the renderer's own output, or the staged path is a second renderer | |
| 18 | + | //! wearing the first one's name, and a second emitter here would be the same | |
| 19 | + | //! mistake one level up: two spellings of every production, drifting apart one | |
| 20 | + | //! conversion at a time. A rewrite over a closed grammar cannot drift, because | |
| 21 | + | //! a production it does not know about is a production it cannot silently | |
| 22 | + | //! mistranslate -- it refuses instead. | |
| 23 | + | //! | |
| 24 | + | //! # What it refuses, and why refusing is right | |
| 25 | + | //! | |
| 26 | + | //! `#[staged]` is a request, so a shape that cannot be staged is a compile | |
| 27 | + | //! error naming the construct rather than a silently unstaged shape. Three | |
| 28 | + | //! things are refused today, each because the value it needs has no sentinel: | |
| 29 | + | //! | |
| 30 | + | //! - a region whose kind comes from a supplier, which needs a `RegionKind` | |
| 31 | + | //! - a dispatch with no `otherwise`, whose arms become integers and would stop | |
| 32 | + | //! being exhaustive | |
| 33 | + | //! - a value hole in a position that is not a string, which rustc reports | |
| 34 | + | //! against the generated call rather than here | |
| 35 | + | //! | |
| 36 | + | //! The flag is opt-in for exactly this reason: a shape is staged when someone | |
| 37 | + | //! has looked at it, and the rest keep the runtime renderer, which is how the | |
| 38 | + | //! two paths coexist during the migration without a switch. | |
| 39 | + | ||
| 40 | + | use proc_macro2::Span; | |
| 41 | + | use quote::format_ident; | |
| 42 | + | use syn::{Ident, Result}; | |
| 43 | + | ||
| 44 | + | use crate::ast::{ | |
| 45 | + | Arg, Declaration, Emission, Guard, Hole, HoleRoot, Interpolated, Item, Param, Pattern, | |
| 46 | + | Predicate, RegionKind, Source, StrPart, | |
| 47 | + | }; | |
| 48 | + | ||
| 49 | + | /// The flag that asks for a staged twin. | |
| 50 | + | pub const FLAG: &str = "staged"; | |
| 51 | + | ||
| 52 | + | /// The parameter the staged function takes in place of the request's reads. | |
| 53 | + | const PLAN: &str = "plan"; | |
| 54 | + | ||
| 55 | + | /// The staged twin of `name`, which is what an `include` retargets to. | |
| 56 | + | pub fn staged_name(name: &Ident) -> Ident { | |
| 57 | + | format_ident!("{}_staged", name, span = name.span()) | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | /// The const holding the shape's counts, beside the staged function. | |
| 61 | + | fn counts_name(name: &Ident) -> Ident { | |
| 62 | + | format_ident!( | |
| 63 | + | "{}_STAGED", | |
| 64 | + | name.to_string().to_uppercase(), | |
| 65 | + | span = name.span() | |
| 66 | + | ) | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | /// Whether this declaration asked to be staged. | |
| 70 | + | pub fn wanted(declaration: &Declaration) -> bool { | |
| 71 | + | declaration.flags.iter().any(|flag| flag == FLAG) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | /// Numbering, one counter per thing a plan answers. | |
| 75 | + | /// | |
| 76 | + | /// Shape-local on purpose. A residual is derived per shape and composed by | |
| 77 | + | /// reference, so an included shape's holes are never renumbered against its | |
| 78 | + | /// caller's, which is what keeps the derivation linear in the tree rather than | |
| 79 | + | /// in the tree's expansion. | |
| 80 | + | #[derive(Default)] | |
| 81 | + | struct Counters { | |
| 82 | + | guards: u16, | |
| 83 | + | loops: u16, | |
| 84 | + | holes: u16, | |
| 85 | + | /// One per `include`, which is what scopes the shape it reaches. | |
| 86 | + | sites: u16, | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | /// A hole reading the plan: `plan.<method>(<id>)`. | |
| 90 | + | fn plan_read(method: &str, id: u16) -> Hole { | |
| 91 | + | Hole { | |
| 92 | + | root: HoleRoot::Binding(Ident::new(PLAN, Span::call_site())), | |
| 93 | + | steps: vec![crate::ast::Step::Method { | |
| 94 | + | name: Ident::new(method, Span::call_site()), | |
| 95 | + | args: vec![Arg::Int(i64::from(id))], | |
| 96 | + | }], | |
| 97 | + | } | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | /// A hole answering with one sentinel: `plan.hole(<id>)`. | |
| 101 | + | /// | |
| 102 | + | /// Through the plan rather than a free function because a sentinel carries the | |
| 103 | + | /// scope it was read at, and the plan is what knows the scope. Two tables on | |
| 104 | + | /// one screen each number their holes from zero, so without the scope they | |
| 105 | + | /// would both claim hole 0 and the residual would fill one from the other. | |
| 106 | + | fn sentinel(id: u16) -> Hole { | |
| 107 | + | plan_read("hole", id) | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | /// The staged declaration, and the counts a plan needs to drive it. | |
| 111 | + | pub struct Staged { | |
| 112 | + | pub declaration: Declaration, | |
| 113 | + | pub counts: (u16, u16, u16, u16), | |
| 114 | + | pub counts_ident: Ident, | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | /// Rewrite one declaration into its staged twin. | |
| 118 | + | pub fn stage(declaration: &Declaration) -> Result<Staged> { | |
| 119 | + | let mut counters = Counters::default(); | |
| 120 | + | let items = items(&declaration.items, &mut counters)?; | |
| 121 | + | ||
| 122 | + | let name = staged_name(&declaration.name); | |
| 123 | + | let doc = format!( | |
| 124 | + | " The staged twin of [`{}`], evaluated with no request.", | |
| 125 | + | declaration.name | |
| 126 | + | ); | |
| 127 | + | ||
| 128 | + | Ok(Staged { | |
| 129 | + | counts: ( | |
| 130 | + | counters.guards, | |
| 131 | + | counters.loops, | |
| 132 | + | counters.holes, | |
| 133 | + | counters.sites, | |
| 134 | + | ), | |
| 135 | + | counts_ident: counts_name(&declaration.name), | |
| 136 | + | declaration: Declaration { | |
| 137 | + | docs: vec![doc], | |
| 138 | + | // `must_use` and `inline` carry over; the flag that asked for this | |
| 139 | + | // does not, or the twin would ask for a twin of its own. | |
| 140 | + | flags: declaration | |
| 141 | + | .flags | |
| 142 | + | .iter() | |
| 143 | + | .filter(|flag| *flag != FLAG) | |
| 144 | + | .cloned() | |
| 145 | + | .collect(), | |
| 146 | + | vis: declaration.vis.clone(), | |
| 147 | + | name, | |
| 148 | + | params: vec![Param { | |
| 149 | + | name: Ident::new(PLAN, Span::call_site()), | |
| 150 | + | ty: syn::parse_quote!(&::quasi_router::stage::Plan), | |
| 151 | + | }], | |
| 152 | + | returns: declaration.returns.clone(), | |
| 153 | + | items, | |
| 154 | + | }, | |
| 155 | + | }) | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | fn items(items: &[Item], counters: &mut Counters) -> Result<Vec<Item>> { | |
| 159 | + | items | |
| 160 | + | .iter() | |
| 161 | + | .map(|item| self::item(item, counters)) | |
| 162 | + | .collect() | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | fn item(item: &Item, counters: &mut Counters) -> Result<Item> { | |
| 166 | + | Ok(match item { | |
| 167 | + | Item::Bind { name, source } => Item::Bind { | |
| 168 | + | name: name.clone(), | |
| 169 | + | source: self::source(source, counters)?, | |
| 170 | + | }, | |
| 171 | + | Item::Attribute { name, args, guard } => Item::Attribute { | |
| 172 | + | name: name.clone(), | |
| 173 | + | args: self::args(args, counters)?, | |
| 174 | + | guard: guard.as_ref().map(|guard| self::guard(guard, counters)), | |
| 175 | + | }, | |
| 176 | + | // The binder is never read: every hole rooted at it was rewritten to a | |
| 177 | + | // sentinel above, so the loop runs for its count and nothing else. It | |
| 178 | + | // keeps its name with an underscore so a reader can still see which | |
| 179 | + | // loop this was. | |
| 180 | + | Item::For { | |
| 181 | + | binder, | |
| 182 | + | body, | |
| 183 | + | dereferenced: _, | |
| 184 | + | iterable: _, | |
| 185 | + | } => { | |
| 186 | + | let id = counters.loops; | |
| 187 | + | counters.loops += 1; | |
| 188 | + | Item::For { | |
| 189 | + | dereferenced: false, | |
| 190 | + | binder: format_ident!("_{}", binder, span = binder.span()), | |
| 191 | + | iterable: plan_read("rows", id), | |
| 192 | + | body: items(body, counters)?, | |
| 193 | + | } | |
| 194 | + | } | |
| 195 | + | Item::Emit(emission) => Item::Emit(self::emission(emission, counters)?), | |
| 196 | + | }) | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | /// A guard becomes one read of the plan, and stops being negated. | |
| 200 | + | /// | |
| 201 | + | /// `unless x` and `when not x` are the same question asked twice, and the plan | |
| 202 | + | /// answers the emission rather than the predicate, so the twin keeps neither | |
| 203 | + | /// spelling. The predicate's own holes are dropped: R9 says they are evaluated | |
| 204 | + | /// whether or not the emission is placed, and a sentinel evaluated for a guard | |
| 205 | + | /// nobody reads is a cost with no output. | |
| 206 | + | fn guard(guard: &Guard, counters: &mut Counters) -> Guard { | |
| 207 | + | let id = counters.guards; | |
| 208 | + | counters.guards += 1; | |
| 209 | + | Guard { | |
| 210 | + | negated: false, | |
| 211 | + | predicate: Predicate::Truth(plan_read("guard", id)), | |
| 212 | + | span: guard.span, | |
| 213 | + | } | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | fn source(source: &Source, counters: &mut Counters) -> Result<Source> { | |
| 217 | + | Ok(match source { | |
| 218 | + | Source::Str(text) => Source::Str(interpolated(text, counters)), | |
| 219 | + | Source::Hole(hole) => Source::Hole(value(hole, counters)), | |
| 220 | + | Source::Choose { | |
| 221 | + | arms, | |
| 222 | + | otherwise, | |
| 223 | + | scrutinee: _, | |
| 224 | + | } => { | |
| 225 | + | let id = counters.arms_next(); | |
| 226 | + | let arms = arms | |
| 227 | + | .iter() | |
| 228 | + | .enumerate() | |
| 229 | + | .map(|(at, (_, value))| { | |
| 230 | + | Ok(( | |
| 231 | + | Pattern::Int(i64::try_from(at).unwrap_or(i64::MAX)), | |
| 232 | + | self::source(value, counters)?, | |
| 233 | + | )) | |
| 234 | + | }) | |
| 235 | + | .collect::<Result<Vec<_>>>()?; | |
| 236 | + | Source::Choose { | |
| 237 | + | scrutinee: plan_read("arm", id), | |
| 238 | + | arms, | |
| 239 | + | otherwise: Box::new(self::source(otherwise, counters)?), | |
| 240 | + | } | |
| 241 | + | } | |
| 242 | + | }) | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | impl Counters { | |
| 246 | + | /// A dispatch is numbered out of the guard counter's neighbour, and the | |
| 247 | + | /// plan keeps arms in their own list. | |
| 248 | + | fn arms_next(&mut self) -> u16 { | |
| 249 | + | let id = self.holes; | |
| 250 | + | // Arms and holes do not share a namespace in the plan; the counter is | |
| 251 | + | // reused so that a shape's numbering stays a single pass over its body. | |
| 252 | + | self.holes += 1; | |
| 253 | + | id | |
| 254 | + | } | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | /// A value a request would have brought, which becomes one sentinel. | |
| 258 | + | /// | |
| 259 | + | /// A hole rooted at a path is left alone. `REGION` and `PATH` are consts, so | |
| 260 | + | /// they are the same string for every request and belong in the literal rather | |
| 261 | + | /// than in a hole: staging them would spend a sentinel to reproduce a value the | |
| 262 | + | /// renderer already baked in. | |
| 263 | + | fn value(hole: &Hole, counters: &mut Counters) -> Hole { | |
| 264 | + | if matches!(hole.root, HoleRoot::Path(_)) && hole.steps.is_empty() { | |
| 265 | + | return Hole { | |
| 266 | + | root: match &hole.root { | |
| 267 | + | HoleRoot::Path(path) => HoleRoot::Path(path.clone()), | |
| 268 | + | _ => unreachable!("checked above"), | |
| 269 | + | }, | |
| 270 | + | steps: Vec::new(), | |
| 271 | + | }; | |
| 272 | + | } | |
| 273 | + | let id = counters.holes; | |
| 274 | + | counters.holes += 1; | |
| 275 | + | sentinel(id) | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | fn interpolated(text: &Interpolated, counters: &mut Counters) -> Interpolated { | |
| 279 | + | Interpolated { | |
| 280 | + | parts: text | |
| 281 | + | .parts | |
| 282 | + | .iter() | |
| 283 | + | .map(|part| match part { | |
| 284 | + | StrPart::Lit(literal) => StrPart::Lit(literal.clone()), | |
| 285 | + | StrPart::Hole(hole) => StrPart::Hole(value(hole, counters)), | |
| 286 | + | }) | |
| 287 | + | .collect(), | |
| 288 | + | span: text.span, | |
| 289 | + | } | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | fn args(args: &[Arg], counters: &mut Counters) -> Result<Vec<Arg>> { | |
| 293 | + | args.iter().map(|arg| self::arg(arg, counters)).collect() | |
| 294 | + | } | |
| 295 | + | ||
| 296 | + | fn arg(arg: &Arg, counters: &mut Counters) -> Result<Arg> { | |
| 297 | + | Ok(match arg { | |
| 298 | + | Arg::Str(text) => Arg::Str(interpolated(text, counters)), | |
| 299 | + | Arg::Hole(hole) => Arg::Hole(value(hole, counters)), | |
| 300 | + | Arg::List(items) => Arg::List(args(items, counters)?), | |
| 301 | + | Arg::Borrow(inner) => Arg::Borrow(Box::new(self::arg(inner, counters)?)), | |
| 302 | + | Arg::Int(value) => Arg::Int(*value), | |
| 303 | + | Arg::Bool(value) => Arg::Bool(*value), | |
| 304 | + | }) | |
| 305 | + | } | |
| 306 | + | ||
| 307 | + | /// An action's target and its modifiers both carry addresses, and an address is | |
| 308 | + | /// a value a request brings. Rewriting them is what puts every `href` and every | |
| 309 | + | /// `hx-delete` into the residual as a hole rather than baking one request's | |
| 310 | + | /// into the literal. | |
| 311 | + | fn action(action: &crate::ast::Action, counters: &mut Counters) -> Result<crate::ast::Action> { | |
| 312 | + | Ok(crate::ast::Action { | |
| 313 | + | verb: action.verb.clone(), | |
| 314 | + | target: action | |
| 315 | + | .target | |
| 316 | + | .as_ref() | |
| 317 | + | .map(|target| arg(target, counters)) | |
| 318 | + | .transpose()?, | |
| 319 | + | modifiers: action | |
| 320 | + | .modifiers | |
| 321 | + | .iter() | |
| 322 | + | .map(|modifier| { | |
| 323 | + | Ok(crate::ast::Modifier { | |
| 324 | + | name: modifier.name.clone(), | |
| 325 | + | args: args(&modifier.args, counters)?, | |
| 326 | + | }) | |
| 327 | + | }) | |
| 328 | + | .collect::<Result<Vec<_>>>()?, | |
| 329 | + | }) | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | fn emission(emission: &Emission, counters: &mut Counters) -> Result<Emission> { | |
| 333 | + | Ok(match emission { | |
| 334 | + | Emission::Simple { member, args, body } => Emission::Simple { | |
| 335 | + | member: member.clone(), | |
| 336 | + | args: self::args(args, counters)?, | |
| 337 | + | body: items(body, counters)?, | |
| 338 | + | }, | |
| 339 | + | Emission::Chip { | |
| 340 | + | value, | |
| 341 | + | action, | |
| 342 | + | removable, | |
| 343 | + | body, | |
| 344 | + | } => Emission::Chip { | |
| 345 | + | value: arg(value, counters)?, | |
| 346 | + | action: self::action(action, counters)?, | |
| 347 | + | removable: *removable, | |
| 348 | + | body: items(body, counters)?, | |
| 349 | + | }, | |
| 350 | + | Emission::Screen { | |
| 351 | + | arrangement, | |
| 352 | + | args, | |
| 353 | + | body, | |
| 354 | + | } => Emission::Screen { | |
| 355 | + | arrangement: arrangement.clone(), | |
| 356 | + | args: self::args(args, counters)?, | |
| 357 | + | body: items(body, counters)?, | |
| 358 | + | }, | |
| 359 | + | Emission::Row { primary, body } => Emission::Row { | |
| 360 | + | primary: arg(primary, counters)?, | |
| 361 | + | body: items(body, counters)?, | |
| 362 | + | }, | |
| 363 | + | Emission::Form { action, body } => Emission::Form { | |
| 364 | + | action: self::action(action, counters)?, | |
| 365 | + | body: items(body, counters)?, | |
| 366 | + | }, | |
| 367 | + | Emission::Field { | |
| 368 | + | kind, | |
| 369 | + | name, | |
| 370 | + | label, | |
| 371 | + | body, | |
| 372 | + | } => Emission::Field { | |
| 373 | + | kind: kind.clone(), | |
| 374 | + | name: arg(name, counters)?, | |
| 375 | + | label: arg(label, counters)?, | |
| 376 | + | body: items(body, counters)?, | |
| 377 | + | }, | |
| 378 | + | Emission::List(body) => Emission::List(items(body, counters)?), | |
| 379 | + | Emission::Act { | |
| 380 | + | label, | |
| 381 | + | action, | |
| 382 | + | body, | |
| 383 | + | } => Emission::Act { | |
| 384 | + | label: arg(label, counters)?, | |
| 385 | + | action: self::action(action, counters)?, | |
| 386 | + | body: items(body, counters)?, | |
| 387 | + | }, | |
| 388 | + | Emission::Offers { | |
| 389 | + | label, | |
| 390 | + | action, | |
| 391 | + | body, | |
| 392 | + | } => Emission::Offers { | |
| 393 | + | label: arg(label, counters)?, | |
| 394 | + | action: self::action(action, counters)?, | |
| 395 | + | body: items(body, counters)?, | |
| 396 | + | }, | |
| 397 | + | Emission::Table(body) => Emission::Table(items(body, counters)?), | |
| 398 | + | Emission::Column { name, body } => Emission::Column { | |
| 399 | + | name: arg(name, counters)?, | |
| 400 | + | body: items(body, counters)?, | |
| 401 | + | }, | |
| 402 | + | Emission::Cells(body) => Emission::Cells(items(body, counters)?), | |
| 403 | + | Emission::Cell { | |
| 404 | + | column, | |
| 405 | + | value, | |
| 406 | + | body, | |
| 407 | + | } => Emission::Cell { | |
| 408 | + | column: column.as_ref().map(|it| arg(it, counters)).transpose()?, | |
| 409 | + | value: arg(value, counters)?, | |
| 410 | + | body: items(body, counters)?, | |
| 411 | + | }, | |
| 412 | + | Emission::Offering { | |
| 413 | + | label, | |
| 414 | + | action, | |
| 415 | + | body, | |
| 416 | + | } => Emission::Offering { | |
| 417 | + | label: arg(label, counters)?, | |
| 418 | + | action: self::action(action, counters)?, | |
| 419 | + | body: items(body, counters)?, | |
| 420 | + | }, | |
| 421 | + | Emission::Removes { | |
| 422 | + | label, | |
| 423 | + | action, | |
| 424 | + | body, | |
| 425 | + | } => Emission::Removes { | |
| 426 | + | label: arg(label, counters)?, | |
| 427 | + | action: self::action(action, counters)?, | |
| 428 | + | body: items(body, counters)?, | |
| 429 | + | }, | |
| 430 | + | Emission::Repeats { | |
| 431 | + | one, | |
| 432 | + | label, | |
| 433 | + | action, | |
| 434 | + | body, | |
| 435 | + | } => Emission::Repeats { | |
| 436 | + | one: arg(one, counters)?, | |
| 437 | + | label: arg(label, counters)?, | |
| 438 | + | action: self::action(action, counters)?, | |
| 439 | + | body: items(body, counters)?, | |
| 440 | + | }, | |
| 441 | + | Emission::Activate(action) => Emission::Activate(self::action(action, counters)?), | |
| 442 | + | Emission::Link { text, action } => Emission::Link { | |
| 443 | + | text: arg(text, counters)?, | |
| 444 | + | action: self::action(action, counters)?, | |
| 445 | + | }, | |
| 446 | + | Emission::Include(hole) => Emission::Include(include(hole, counters)?), | |
| 447 | + | Emission::Region { name, kind, body } => Emission::Region { | |
| 448 | + | name: arg(name, counters)?, | |
| 449 | + | kind: match kind { | |
| 450 | + | RegionKind::Variant(variant) => RegionKind::Variant(variant.clone()), | |
| 451 | + | // A `RegionKind` has no sentinel, and guessing a variant would | |
| 452 | + | // put a kind in the residual that no request asked for. | |
| 453 | + | RegionKind::Supplied(hole) => { | |
| 454 | + | return Err(syn::Error::new( | |
| 455 | + | hole.span(), | |
| 456 | + | "a staged shape cannot take its region kind from a supplier: \ | |
| 457 | + | a `RegionKind` has no sentinel to stand in for it", | |
| 458 | + | )); | |
| 459 | + | } | |
| 460 | + | }, | |
| 461 | + | body: items(body, counters)?, | |
| 462 | + | }, | |
| 463 | + | Emission::Across { fallback, body } => Emission::Across { | |
| 464 | + | fallback: fallback.clone(), | |
| 465 | + | body: items(body, counters)?, | |
| 466 | + | }, | |
| 467 | + | Emission::Beside { priority, inner } => Emission::Beside { | |
| 468 | + | priority: arg(priority, counters)?, | |
| 469 | + | inner: Box::new(self::emission(inner, counters)?), | |
| 470 | + | }, | |
| 471 | + | Emission::At { at, inner } => Emission::At { | |
| 472 | + | at: arg(at, counters)?, | |
| 473 | + | inner: Box::new(self::emission(inner, counters)?), | |
| 474 | + | }, | |
| 475 | + | Emission::Guarded { guard, inner } => Emission::Guarded { | |
| 476 | + | guard: self::guard(guard, counters), | |
| 477 | + | inner: Box::new(self::emission(inner, counters)?), | |
| 478 | + | }, | |
| 479 | + | // The arms become integers, so the plan chooses one by position and the | |
| 480 | + | // scrutinee's own type never has to be reproduced. That is also why an | |
| 481 | + | // `otherwise` stops being optional here: a match on a `usize` against | |
| 482 | + | // integer literals is not exhaustive without one, and rustc would | |
| 483 | + | // report that against generated code rather than against the | |
| 484 | + | // declaration that caused it. | |
| 485 | + | Emission::Given { | |
| 486 | + | scrutinee, | |
| 487 | + | arms, | |
| 488 | + | otherwise, | |
| 489 | + | } => { | |
| 490 | + | let Some(otherwise) = otherwise else { | |
| 491 | + | return Err(syn::Error::new( | |
| 492 | + | scrutinee.span(), | |
| 493 | + | "a staged dispatch needs an `otherwise`: its arms are chosen by \ | |
| 494 | + | position, and a position nothing answers has to land somewhere", | |
| 495 | + | )); | |
| 496 | + | }; | |
| 497 | + | let id = counters.arms_next(); | |
| 498 | + | let arms = arms | |
| 499 | + | .iter() | |
| 500 | + | .enumerate() |
Lines truncated
| @@ -1,0 +1,311 @@ | |||
| 1 | + | //! What a declared shape reads when it is evaluated with no request. | |
| 2 | + | //! | |
| 3 | + | //! A described screen is built from two kinds of thing: markup the renderer | |
| 4 | + | //! decides, which is the same for every request, and values a request brings. | |
| 5 | + | //! Staging separates them once, so that serving a screen is filling holes in a | |
| 6 | + | //! program rather than building a tree and rendering it. | |
| 7 | + | //! | |
| 8 | + | //! The separation is found by evaluating the description twice. `declare!` | |
| 9 | + | //! emits a second function beside every shape marked `#[staged]`, identical to | |
| 10 | + | //! the first except that it reads a [`Plan`] instead of a request: every value | |
| 11 | + | //! becomes a [`sentinel`], every guard becomes a bool the plan chooses, and | |
| 12 | + | //! every loop runs as many times as the plan says. Handing the result to the | |
| 13 | + | //! ordinary renderer produces the screen's markup with sentinels where the data | |
| 14 | + | //! would be, and that string is the residual. | |
| 15 | + | //! | |
| 16 | + | //! # Why the plan rather than a symbolic value | |
| 17 | + | //! | |
| 18 | + | //! A guard is a `bool` a request answers and a loop counts something a request | |
| 19 | + | //! brought, so neither has a sentinel to stand in for it. They are answered | |
| 20 | + | //! here instead, which means the derivation chooses them rather than inferring | |
| 21 | + | //! them: the emitter knows where every guard and every loop is, because the | |
| 22 | + | //! macro parsed them, and does not have to recover that by comparing two | |
| 23 | + | //! renders. Nothing about the shape of the output is guessed. | |
| 24 | + | //! | |
| 25 | + | //! # What a sentinel has to be | |
| 26 | + | //! | |
| 27 | + | //! Fixed width, so a scanner can step over one without parsing. Alphanumeric, | |
| 28 | + | //! so the renderer's escaper passes it through unchanged and a value in an | |
| 29 | + | //! attribute reads the same as a value in text. And unlikely in real markup, | |
| 30 | + | //! which `ZQH`/`HQZ` is by construction rather than by hope: a residual carries | |
| 31 | + | //! its own check that no literal chunk contains one. | |
| 32 | + | ||
| 33 | + | use std::iter::{Repeat, Take, repeat}; | |
| 34 | + | ||
| 35 | + | /// The three-character opening of a sentinel. | |
| 36 | + | const OPEN: &str = "ZQH"; | |
| 37 | + | ||
| 38 | + | /// The three-character closing of a sentinel. | |
| 39 | + | const CLOSE: &str = "HQZ"; | |
| 40 | + | ||
| 41 | + | /// How wide one sentinel is, which is what lets a scanner step over one. | |
| 42 | + | /// | |
| 43 | + | /// `ZQH` + four hex digits of scope + four of hole + `HQZ`. The scope is there | |
| 44 | + | /// because a residual is derived by calling the staged twins for real, so one | |
| 45 | + | /// derivation walks many shapes and each numbers its holes from zero. Without | |
| 46 | + | /// it, two tables on one screen would both claim hole 0. | |
| 47 | + | pub const SENTINEL_LEN: usize = 14; | |
| 48 | + | ||
| 49 | + | /// Where one shape sits in the call tree a derivation walks. | |
| 50 | + | /// | |
| 51 | + | /// Built by descending: each `include` in a shape has an ordinal, and entering | |
| 52 | + | /// it mixes that ordinal into the caller's own scope. Two different paths to | |
| 53 | + | /// the same shape get different scopes, which is what makes a hole's identity | |
| 54 | + | /// the place it is read rather than the shape it is written in. | |
| 55 | + | type Scope = u32; | |
| 56 | + | ||
| 57 | + | /// The stand-in for one value a request would have brought. | |
| 58 | + | #[must_use] | |
| 59 | + | fn sentinel_at(scope: Scope, id: u16) -> String { | |
| 60 | + | format!("{OPEN}{:04x}{id:04x}{CLOSE}", scope & 0xffff) | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | /// The scope and hole a sentinel stands for, or `None` if this is not one. | |
| 64 | + | #[must_use] | |
| 65 | + | pub fn read_sentinel(text: &str) -> Option<(u32, u16)> { | |
| 66 | + | let found = text.get(..SENTINEL_LEN)?; | |
| 67 | + | if !found.starts_with(OPEN) || !found.ends_with(CLOSE) { | |
| 68 | + | return None; | |
| 69 | + | } | |
| 70 | + | let scope = u32::from_str_radix(&found[3..7], 16).ok()?; | |
| 71 | + | let id = u16::from_str_radix(&found[7..11], 16).ok()?; | |
| 72 | + | Some((scope, id)) | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | /// Where a sentinel could begin, searching forward from the start. | |
| 76 | + | #[must_use] | |
| 77 | + | pub fn find_sentinel(text: &str) -> Option<usize> { | |
| 78 | + | text.find(OPEN) | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | /// How much of a shape a plan has to answer for. | |
| 82 | + | /// | |
| 83 | + | /// Emitted beside every staged shape, because the macro is what counted these | |
| 84 | + | /// and a derivation that recounted would be reading the same declaration twice. | |
| 85 | + | #[derive(Clone, Copy, Debug)] | |
| 86 | + | pub struct Shape { | |
| 87 | + | /// Guards, so a plan can pass or fail each in turn. | |
| 88 | + | pub guards: u16, | |
| 89 | + | /// Loops, so a plan can say how many times each runs. | |
| 90 | + | pub loops: u16, | |
| 91 | + | /// Value holes, which is how many sentinels this shape can put in a render. | |
| 92 | + | pub holes: u16, | |
| 93 | + | /// `include` sites, which is how many scopes descend from this one. | |
| 94 | + | pub sites: u16, | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | /// What one symbolic evaluation of a shape is told. | |
| 98 | + | /// | |
| 99 | + | /// A derivation asks the same two questions over and over: run everything, or | |
| 100 | + | /// run everything with one loop one row longer. So a plan is a pair of defaults | |
| 101 | + | /// plus the handful of overrides that make one render differ from another, | |
| 102 | + | /// rather than a table with an entry per site. | |
| 103 | + | #[derive(Clone, Debug)] | |
| 104 | + | pub struct Plan { | |
| 105 | + | scope: Scope, | |
| 106 | + | /// What a guard answers unless something says otherwise. | |
| 107 | + | guards: bool, | |
| 108 | + | /// How many times a loop runs unless something says otherwise. | |
| 109 | + | rows: usize, | |
| 110 | + | /// The loops that differ from [`Self::rows`], by scope and id. | |
| 111 | + | row_overrides: Vec<((Scope, u16), usize)>, | |
| 112 | + | /// The guards that differ from [`Self::guards`], by scope and id. | |
| 113 | + | guard_overrides: Vec<((Scope, u16), bool)>, | |
| 114 | + | /// Which arm each dispatch takes, by scope and id. Absent means the first. | |
| 115 | + | arms: Vec<((Scope, u16), usize)>, | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | impl Default for Plan { | |
| 119 | + | fn default() -> Self { | |
| 120 | + | Self::empty() | |
| 121 | + | } | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | impl Plan { | |
| 125 | + | /// A plan that says nothing: no guard passes and no loop runs. | |
| 126 | + | #[must_use] | |
| 127 | + | pub fn empty() -> Self { | |
| 128 | + | Self { | |
| 129 | + | scope: 0, | |
| 130 | + | guards: false, | |
| 131 | + | rows: 0, | |
| 132 | + | row_overrides: Vec::new(), | |
| 133 | + | guard_overrides: Vec::new(), | |
| 134 | + | arms: Vec::new(), | |
| 135 | + | } | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | /// Every guard passing and every loop running `rows` times. | |
| 139 | + | /// | |
| 140 | + | /// The shape a screen has when it is full, which is the render a residual | |
| 141 | + | /// is read off. A guard that never passes contributes no markup, so a | |
| 142 | + | /// residual derived from a plan that failed one would be missing a branch | |
| 143 | + | /// rather than carrying it. | |
| 144 | + | #[must_use] | |
| 145 | + | pub fn full(rows: usize) -> Self { | |
| 146 | + | Self { | |
| 147 | + | guards: true, | |
| 148 | + | rows, | |
| 149 | + | ..Self::empty() | |
| 150 | + | } | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | /// The same plan as seen by the shape one `include` reaches. | |
| 154 | + | /// | |
| 155 | + | /// Called by generated code, once per include site, with that site's | |
| 156 | + | /// ordinal in its caller. | |
| 157 | + | #[must_use] | |
| 158 | + | pub fn enter(&self, site: u16) -> Self { | |
| 159 | + | Self { | |
| 160 | + | scope: self | |
| 161 | + | .scope | |
| 162 | + | .wrapping_mul(31) | |
| 163 | + | .wrapping_add(u32::from(site) + 1), | |
| 164 | + | ..self.clone() | |
| 165 | + | } | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | /// Say how many times one loop runs, in the scope this plan is at. | |
| 169 | + | #[must_use] | |
| 170 | + | pub fn with_rows(mut self, id: u16, count: usize) -> Self { | |
| 171 | + | self.row_overrides.push(((self.scope, id), count)); | |
| 172 | + | self | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | /// Say what one guard answers, in the scope this plan is at. | |
| 176 | + | #[must_use] | |
| 177 | + | pub fn with_guard(mut self, id: u16, passes: bool) -> Self { | |
| 178 | + | self.guard_overrides.push(((self.scope, id), passes)); | |
| 179 | + | self | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | /// Say which arm one dispatch takes, in the scope this plan is at. | |
| 183 | + | #[must_use] | |
| 184 | + | pub fn with_arm(mut self, id: u16, arm: usize) -> Self { | |
| 185 | + | self.arms.push(((self.scope, id), arm)); | |
| 186 | + | self | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | /// This plan's scope, which a derivation needs to name a loop it wants to | |
| 190 | + | /// vary inside an included shape. | |
| 191 | + | #[must_use] | |
| 192 | + | pub fn scope(&self) -> u32 { | |
| 193 | + | self.scope | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | /// One value a request would have brought. Read by generated code. | |
| 197 | + | #[must_use] | |
| 198 | + | pub fn hole(&self, id: u16) -> String { | |
| 199 | + | sentinel_at(self.scope, id) | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | /// Whether one guard passes. Read by generated code. | |
| 203 | + | #[must_use] | |
| 204 | + | pub fn guard(&self, id: u16) -> bool { | |
| 205 | + | look(&self.guard_overrides, self.scope, id).unwrap_or(self.guards) | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | /// One loop's runs, as something a `for` can take. Read by generated code. | |
| 209 | + | /// | |
| 210 | + | /// Yields `()` rather than elements: the body's holes were rewritten to | |
| 211 | + | /// sentinels when the shape was staged, so nothing in it reads the binder | |
| 212 | + | /// and there is no element for it to read. | |
| 213 | + | #[must_use] | |
| 214 | + | pub fn rows(&self, id: u16) -> Take<Repeat<()>> { | |
| 215 | + | let count = look(&self.row_overrides, self.scope, id).unwrap_or(self.rows); | |
| 216 | + | repeat(()).take(count) | |
| 217 | + | } | |
| 218 | + | ||
| 219 | + | /// Which arm one dispatch takes. Read by generated code. | |
| 220 | + | #[must_use] | |
| 221 | + | pub fn arm(&self, id: u16) -> usize { | |
| 222 | + | look(&self.arms, self.scope, id).unwrap_or(0) | |
| 223 | + | } | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | /// The last thing said about one site, or nothing. | |
| 227 | + | /// | |
| 228 | + | /// Last rather than first so that `with_rows` can be applied over a plan that | |
| 229 | + | /// already said something about the same loop, which is what a derivation does | |
| 230 | + | /// when it varies one loop at a time. | |
| 231 | + | fn look<T: Copy>(overrides: &[((Scope, u16), T)], scope: Scope, id: u16) -> Option<T> { | |
| 232 | + | overrides | |
| 233 | + | .iter() | |
| 234 | + | .rev() | |
| 235 | + | .find(|(at, _)| *at == (scope, id)) | |
| 236 | + | .map(|(_, value)| *value) | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | #[cfg(test)] | |
| 240 | + | mod tests { | |
| 241 | + | use super::*; | |
| 242 | + | ||
| 243 | + | #[test] | |
| 244 | + | fn a_sentinel_is_fixed_width_and_reads_back() { | |
| 245 | + | let plan = Plan::full(1); | |
| 246 | + | for id in [0, 1, 42, 9999] { | |
| 247 | + | let text = plan.hole(id); | |
| 248 | + | assert_eq!(text.len(), SENTINEL_LEN, "{text}"); | |
| 249 | + | assert_eq!(read_sentinel(&text), Some((plan.scope(), id))); | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | ||
| 253 | + | /// The scanner steps over `ZQH` that is not a sentinel rather than reading | |
| 254 | + | /// a hole number out of whatever follows it. | |
| 255 | + | #[test] | |
| 256 | + | fn an_opening_that_is_not_a_sentinel_reads_as_nothing() { | |
| 257 | + | assert_eq!(read_sentinel("ZQHzzzzzzzzHQZ"), None); | |
| 258 | + | assert_eq!(read_sentinel("ZQH00000001XXX"), None); | |
| 259 | + | assert_eq!(read_sentinel("ZQH"), None); | |
| 260 | + | assert_eq!(read_sentinel(""), None); | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | /// The collision this scope exists to stop: two shapes reached by two | |
| 264 | + | /// includes each number their first hole zero, and the sentinels differ. | |
| 265 | + | #[test] | |
| 266 | + | fn two_included_shapes_do_not_share_a_hole() { | |
| 267 | + | let plan = Plan::full(1); | |
| 268 | + | let first = plan.enter(0); | |
| 269 | + | let second = plan.enter(1); | |
| 270 | + | ||
| 271 | + | assert_ne!(first.hole(0), second.hole(0)); | |
| 272 | + | assert_ne!(first.scope(), second.scope()); | |
| 273 | + | assert_ne!(first.hole(0), plan.hole(0)); | |
| 274 | + | } | |
| 275 | + | ||
| 276 | + | /// A loop is varied where it is read, so saying it in the caller's scope | |
| 277 | + | /// leaves the included shape's own loop alone. | |
| 278 | + | #[test] | |
| 279 | + | fn a_row_count_is_said_in_one_scope_only() { | |
| 280 | + | let plan = Plan::full(1); | |
| 281 | + | let inner = plan.enter(0).with_rows(0, 7); | |
| 282 | + | ||
| 283 | + | assert_eq!(inner.rows(0).count(), 7); | |
| 284 | + | assert_eq!(plan.rows(0).count(), 1); | |
| 285 | + | assert_eq!(plan.enter(1).rows(0).count(), 1); | |
| 286 | + | } | |
| 287 | + | ||
| 288 | + | /// A plan answers its defaults where nothing was said, which is what lets a | |
| 289 | + | /// derivation vary one loop without writing every other one down. | |
| 290 | + | #[test] | |
| 291 | + | fn an_unsaid_site_takes_the_default() { | |
| 292 | + | let plan = Plan::full(3).with_guard(2, false); | |
| 293 | + | ||
| 294 | + | assert!(plan.guard(0)); | |
| 295 | + | assert!(!plan.guard(2)); | |
| 296 | + | assert_eq!(plan.rows(0).count(), 3); | |
| 297 | + | assert_eq!(plan.rows(97).count(), 3); | |
| 298 | + | assert_eq!(plan.arm(0), 0); | |
| 299 | + | assert_eq!(Plan::empty().rows(0).count(), 0); | |
| 300 | + | assert!(!Plan::empty().guard(0)); | |
| 301 | + | } | |
| 302 | + | ||
| 303 | + | /// The last thing said wins, which is how a derivation layers one override | |
| 304 | + | /// over a plan that already said something about the same loop. | |
| 305 | + | #[test] | |
| 306 | + | fn the_last_word_on_a_site_is_the_one_that_counts() { | |
| 307 | + | let plan = Plan::full(1).with_rows(0, 4).with_rows(0, 9); | |
| 308 | + | ||
| 309 | + | assert_eq!(plan.rows(0).count(), 9); | |
| 310 | + | } | |
| 311 | + | } |