max / quasi
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
16 files changed,
+860 insertions,
-29 deletions
| @@ -3529,8 +3529,10 @@ | |||
| 3529 | 3529 | version = "0.1.8" | |
| 3530 | 3530 | dependencies = [ | |
| 3531 | 3531 | "proc-macro2", | |
| 3532 | + | "quasi-router", | |
| 3532 | 3533 | "quote", | |
| 3533 | 3534 | "syn 2.0.119", | |
| 3535 | + | "trybuild", | |
| 3534 | 3536 | ] | |
| 3535 | 3537 | ||
| 3536 | 3538 | [[package]] | |
| @@ -3606,7 +3608,7 @@ | |||
| 3606 | 3608 | ||
| 3607 | 3609 | [[package]] | |
| 3608 | 3610 | name = "quasi-webview" | |
| 3609 | - | version = "0.101.1" | |
| 3611 | + | version = "0.101.2" | |
| 3610 | 3612 | dependencies = [ | |
| 3611 | 3613 | "docengine", | |
| 3612 | 3614 | "makeover-layout", | |
| @@ -4537,6 +4539,12 @@ | |||
| 4537 | 4539 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4538 | 4540 | checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" | |
| 4539 | 4541 | ||
| 4542 | + | [[package]] | |
| 4543 | + | name = "target-triple" | |
| 4544 | + | version = "1.0.1" | |
| 4545 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4546 | + | checksum = "c3a6bfce3d99adfa72d24750a61f782f3036a81e7f86d8841ee1326deaebd171" | |
| 4547 | + | ||
| 4540 | 4548 | [[package]] | |
| 4541 | 4549 | name = "tauri" | |
| 4542 | 4550 | version = "2.11.5" | |
| @@ -4818,6 +4826,15 @@ | |||
| 4818 | 4826 | "new_debug_unreachable", | |
| 4819 | 4827 | ] | |
| 4820 | 4828 | ||
| 4829 | + | [[package]] | |
| 4830 | + | name = "termcolor" | |
| 4831 | + | version = "1.4.1" | |
| 4832 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4833 | + | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" | |
| 4834 | + | dependencies = [ | |
| 4835 | + | "winapi-util", | |
| 4836 | + | ] | |
| 4837 | + | ||
| 4821 | 4838 | [[package]] | |
| 4822 | 4839 | name = "thiserror" | |
| 4823 | 4840 | version = "1.0.69" | |
| @@ -5184,6 +5201,21 @@ | |||
| 5184 | 5201 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5185 | 5202 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" | |
| 5186 | 5203 | ||
| 5204 | + | [[package]] | |
| 5205 | + | name = "trybuild" | |
| 5206 | + | version = "1.0.120" | |
| 5207 | + | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5208 | + | checksum = "1e605bf6b39357663d8ba4e984f8be8da8df6bb32e81031d6889024ea8fd68e4" | |
| 5209 | + | dependencies = [ | |
| 5210 | + | "glob", | |
| 5211 | + | "serde", | |
| 5212 | + | "serde_derive", | |
| 5213 | + | "serde_json", | |
| 5214 | + | "target-triple", | |
| 5215 | + | "termcolor", | |
| 5216 | + | "toml 1.1.4+spec-1.1.0", | |
| 5217 | + | ] | |
| 5218 | + | ||
| 5187 | 5219 | [[package]] | |
| 5188 | 5220 | name = "typeid" | |
| 5189 | 5221 | version = "1.0.3" | |
| @@ -6282,8 +6314,8 @@ | |||
| 6282 | 6314 | ] | |
| 6283 | 6315 | ||
| 6284 | 6316 | [[patch.unused]] | |
| 6285 | - | name = "synckit-client" | |
| 6286 | - | version = "0.10.0" | |
| 6317 | + | name = "quasi-type" | |
| 6318 | + | version = "0.1.3" | |
| 6287 | 6319 | ||
| 6288 | 6320 | [[patch.unused]] | |
| 6289 | 6321 | name = "kberg" | |
| @@ -6302,5 +6334,5 @@ | |||
| 6302 | 6334 | version = "0.4.1" | |
| 6303 | 6335 | ||
| 6304 | 6336 | [[patch.unused]] | |
| 6305 | - | name = "quasi-type" | |
| 6306 | - | version = "0.1.3" | |
| 6337 | + | name = "synckit-client" | |
| 6338 | + | version = "0.10.0" |
| @@ -19,3 +19,12 @@ | |||
| 19 | 19 | ||
| 20 | 20 | [lints] | |
| 21 | 21 | workspace = true | |
| 22 | + | ||
| 23 | + | [dev-dependencies] | |
| 24 | + | # The pair rule has to fail at the macro, naming the binding, rather than at | |
| 25 | + | # rustc naming a `let` the declaration never wrote. Those are two different | |
| 26 | + | # compile errors and only a compile-fail harness can tell them apart. | |
| 27 | + | trybuild = "1" | |
| 28 | + | # The generated code names the vocabulary, so a case that is meant to COMPILE | |
| 29 | + | # needs it. A proc-macro crate cannot depend on it outside dev. | |
| 30 | + | quasi-router = { path = "../quasi-router", version = "0.101" } |
| @@ -28,7 +28,7 @@ | |||
| 28 | 28 | declare! { | |
| 29 | 29 | /// Everything inside the tab pane. | |
| 30 | 30 | #[staged] | |
| 31 | - | shape pane(buyers: &[Buyer], shared: &[Shared]) -> Node; | |
| 31 | + | pub(crate) shape pane(buyers: &[Buyer], shared: &[Shared]) -> Node; | |
| 32 | 32 | ||
| 33 | 33 | region REGION as Pane { | |
| 34 | 34 | empty "No contacts yet." when buyers.is_empty() and shared.is_empty(); | |
| @@ -48,7 +48,7 @@ | |||
| 48 | 48 | declare! { | |
| 49 | 49 | /// The buyers who shared an email. | |
| 50 | 50 | #[staged] | |
| 51 | - | shape buyers_table(buyers: &[Buyer]) -> Node; | |
| 51 | + | pub(crate) shape buyers_table(buyers: &[Buyer]) -> Node; | |
| 52 | 52 | ||
| 53 | 53 | table { | |
| 54 | 54 | column "Username" { | |
| @@ -89,7 +89,7 @@ | |||
| 89 | 89 | declare! { | |
| 90 | 90 | /// The creators this reader has shared an email with. | |
| 91 | 91 | #[staged] | |
| 92 | - | shape shared_table(shared: &[Shared]) -> Node; | |
| 92 | + | pub(crate) shape shared_table(shared: &[Shared]) -> Node; | |
| 93 | 93 | ||
| 94 | 94 | table { | |
| 95 | 95 | column "Creator" { | |
| @@ -351,3 +351,68 @@ | |||
| 351 | 351 | } | |
| 352 | 352 | } | |
| 353 | 353 | } | |
| 354 | + | ||
| 355 | + | #[cfg(test)] | |
| 356 | + | mod filled_tests { | |
| 357 | + | use quasi_http::Serves as _; | |
| 358 | + | use quasi_router::stage::Residual; | |
| 359 | + | use quasi_webview::Webview; | |
| 360 | + | ||
| 361 | + | use super::*; | |
| 362 | + | use crate::fixture::SIZES; | |
| 363 | + | ||
| 364 | + | fn residual() -> Residual { | |
| 365 | + | quasi_webview::stage::derive(&Webview::new(), pane_staged) | |
| 366 | + | } | |
| 367 | + | ||
| 368 | + | /// The measurement this whole path exists for, stated as a test. | |
| 369 | + | /// | |
| 370 | + | /// Filling the residual builds no `Node`, and what comes out is what the | |
| 371 | + | /// renderer produces from the tree. Byte for byte here, which is stronger | |
| 372 | + | /// than the acceptance rule asks for: the rule allows markup to move, | |
| 373 | + | /// because an emitter may optimise, and this one does not optimise yet. | |
| 374 | + | #[test] | |
| 375 | + | fn a_filled_residual_is_what_the_renderer_would_have_produced() { | |
| 376 | + | let webview = Webview::new(); | |
| 377 | + | let residual = residual(); | |
| 378 | + | ||
| 379 | + | for size in SIZES { | |
| 380 | + | let rows = Rows::new(size); | |
| 381 | + | assert_eq!( | |
| 382 | + | webview.fragment(&describe(&rows)), | |
| 383 | + | pane_serve(&residual, &rows.buyers, &rows.shared), | |
| 384 | + | "the staged path and the renderer disagree at {size} rows" | |
| 385 | + | ); | |
| 386 | + | } | |
| 387 | + | } | |
| 388 | + | ||
| 389 | + | /// The branch, which is what the spike could not carry at all. | |
| 390 | + | #[test] | |
| 391 | + | fn the_empty_screen_goes_through_the_same_residual() { | |
| 392 | + | let webview = Webview::new(); | |
| 393 | + | let residual = residual(); | |
| 394 | + | let rows = Rows::new(0); | |
| 395 | + | ||
| 396 | + | assert_eq!( | |
| 397 | + | webview.fragment(&describe(&rows)), | |
| 398 | + | pane_serve(&residual, &rows.buyers, &rows.shared), | |
| 399 | + | ); | |
| 400 | + | } | |
| 401 | + | ||
| 402 | + | /// A value that would break the markup is escaped on the way in, through | |
| 403 | + | /// the renderer's own escaper rather than a second one. | |
| 404 | + | #[test] | |
| 405 | + | fn a_hostile_value_is_escaped_the_way_the_renderer_escapes_it() { | |
| 406 | + | let webview = Webview::new(); | |
| 407 | + | let residual = residual(); | |
| 408 | + | ||
| 409 | + | let mut rows = Rows::new(3); | |
| 410 | + | rows.buyers[1].username = r#"<script>alert("x")</script>"#.into(); | |
| 411 | + | rows.shared[0].name = "Bobby & <b>Tables</b>".into(); | |
| 412 | + | ||
| 413 | + | let filled = pane_serve(&residual, &rows.buyers, &rows.shared); | |
| 414 | + | assert_eq!(webview.fragment(&describe(&rows)), filled); | |
| 415 | + | assert!(!filled.contains("<script>"), "{filled}"); | |
| 416 | + | assert!(filled.contains("&"), "{filled}"); | |
| 417 | + | } | |
| 418 | + | } |
| @@ -183,12 +183,13 @@ | |||
| 183 | 183 | println!("nanoseconds per render, best of {PASSES} passes of {ITERATIONS}\n"); | |
| 184 | 184 | ||
| 185 | 185 | println!( | |
| 186 | - | "{:>5} {:>13} {:>13} {:>13} {:>13} {:>13} {:>13} {:>13}", | |
| 186 | + | "{:>5} {:>13} {:>13} {:>13} {:>13} {:>13} {:>13} {:>13} {:>13}", | |
| 187 | 187 | "rows", | |
| 188 | 188 | "build tree", | |
| 189 | 189 | "render tree", | |
| 190 | 190 | "build+render", | |
| 191 | 191 | "staged", | |
| 192 | + | "declared", | |
| 192 | 193 | COMPILED_HEAD, | |
| 193 | 194 | "terminal", | |
| 194 | 195 | "immediate" | |
| @@ -209,7 +210,15 @@ | |||
| 209 | 210 | return; | |
| 210 | 211 | } | |
| 211 | 212 | ||
| 213 | + | // The declared screen's residual, read off its staged twin. The `staged` | |
| 214 | + | // column beside it is the spike's hand-written copy and stays for | |
| 215 | + | // comparison: that copy dropped `navigating` and three settings on Revoke, | |
| 216 | + | // so it renders a slightly smaller screen and the two columns are not | |
| 217 | + | // measuring the same markup. See `declared`. | |
| 218 | + | let residual = quasi_webview::stage::derive(&webview, declared::pane_staged); | |
| 219 | + | ||
| 212 | 220 | check_staged_matches(&webview, &program); | |
| 221 | + | check_declared_matches(&webview, &residual); | |
| 213 | 222 | ||
| 214 | 223 | for size in fixture::SIZES { | |
| 215 | 224 | let rows = fixture::Rows::new(size); | |
| @@ -231,6 +240,15 @@ | |||
| 231 | 240 | staging::exec(&program, black_box(&rows), &mut out); | |
| 232 | 241 | black_box(out); | |
| 233 | 242 | }); | |
| 243 | + | // The same thing from a declaration rather than from a hand-written | |
| 244 | + | // `describe`: no `Node` is built, and the markup is the renderer's own. | |
| 245 | + | let declared = measure(|| { | |
| 246 | + | black_box(declared::pane_serve( | |
| 247 | + | &residual, | |
| 248 | + | black_box(&rows.buyers), | |
| 249 | + | black_box(&rows.shared), | |
| 250 | + | )); | |
| 251 | + | }); | |
| 234 | 252 | ||
| 235 | 253 | let fragment = measure(|| { | |
| 236 | 254 | let node = fixture::pane(&rows); | |
| @@ -336,7 +354,8 @@ | |||
| 336 | 354 | let compiled = measure_compiled(&webview, &rows, &screen); | |
| 337 | 355 | ||
| 338 | 356 | println!( | |
| 339 | - | "{size:>5} {build:>13} {render:>13} {fragment:>13} {staged:>13} {compiled:>13} {terminal:>13} {immediate:>13}" | |
| 357 | + | "{size:>5} {build:>13} {render:>13} {fragment:>13} {staged:>13} \ | |
| 358 | + | {declared:>13} {compiled:>13} {terminal:>13} {immediate:>13}" | |
| 340 | 359 | ); | |
| 341 | 360 | } | |
| 342 | 361 | ||
| @@ -347,6 +366,26 @@ | |||
| 347 | 366 | ); | |
| 348 | 367 | } | |
| 349 | 368 | ||
| 369 | + | /// The declared screen's residual serves what the renderer serves. | |
| 370 | + | /// | |
| 371 | + | /// The same check `check_staged_matches` makes, against the declared path. A | |
| 372 | + | /// staged number for a program that does not reproduce the screen would be a | |
| 373 | + | /// number about nothing, and the empty case is included because a residual | |
| 374 | + | /// carries branches now: a screen with no rows takes the guards the other way | |
| 375 | + | /// and is the case the spike could not express at all. | |
| 376 | + | fn check_declared_matches(webview: &Webview, residual: &quasi_router::stage::Residual) { | |
| 377 | + | for size in [0].into_iter().chain(fixture::SIZES) { | |
| 378 | + | let rows = fixture::Rows::new(size); | |
| 379 | + | let direct = webview.fragment(&declared::describe(&rows)); | |
| 380 | + | let filled = declared::pane_serve(residual, &rows.buyers, &rows.shared); | |
| 381 | + | assert!( | |
| 382 | + | direct == filled, | |
| 383 | + | "the declared residual differs from the renderer at {size} rows" | |
| 384 | + | ); | |
| 385 | + | } | |
| 386 | + | println!("declared residual verified against the renderer, empty screen included\n"); | |
| 387 | + | } | |
| 388 | + | ||
| 350 | 389 | /// A renderer in a shipped theme, at full colour. | |
| 351 | 390 | /// | |
| 352 | 391 | /// Through a bundled theme file rather than a literal because |
| @@ -28,11 +28,13 @@ | |||
| 28 | 28 | pub items: Vec<Item>, | |
| 29 | 29 | } | |
| 30 | 30 | ||
| 31 | + | #[derive(Clone)] | |
| 31 | 32 | pub struct Param { | |
| 32 | 33 | pub name: Ident, | |
| 33 | 34 | pub ty: Type, | |
| 34 | 35 | } | |
| 35 | 36 | ||
| 37 | + | #[derive(Clone)] | |
| 36 | 38 | pub enum Item { | |
| 37 | 39 | /// `let name = <source>;` | |
| 38 | 40 | Bind { name: Ident, source: Source }, | |
| @@ -70,6 +72,7 @@ | |||
| 70 | 72 | Emit(Emission), | |
| 71 | 73 | } | |
| 72 | 74 | ||
| 75 | + | #[derive(Clone)] | |
| 73 | 76 | pub enum Source { | |
| 74 | 77 | /// A string literal, which may carry `{hole}` interpolations. | |
| 75 | 78 | Str(Interpolated), | |
| @@ -91,6 +94,7 @@ | |||
| 91 | 94 | ||
| 92 | 95 | /// A pattern in value-dispatch position. Literals only: a binding pattern would | |
| 93 | 96 | /// need a scope, and a scope is how an arm becomes a block. | |
| 97 | + | #[derive(Clone)] | |
| 94 | 98 | pub enum Pattern { | |
| 95 | 99 | Int(i64), | |
| 96 | 100 | Str(String), | |
| @@ -101,11 +105,13 @@ | |||
| 101 | 105 | ||
| 102 | 106 | /// A string with `{hole}` interpolations, compiled to a literal push or a | |
| 103 | 107 | /// `format!` depending on whether it has any. | |
| 108 | + | #[derive(Clone)] | |
| 104 | 109 | pub struct Interpolated { | |
| 105 | 110 | pub parts: Vec<StrPart>, | |
| 106 | 111 | pub span: Span, | |
| 107 | 112 | } | |
| 108 | 113 | ||
| 114 | + | #[derive(Clone)] | |
| 109 | 115 | pub enum StrPart { | |
| 110 | 116 | Lit(String), | |
| 111 | 117 | Hole(Hole), | |
| @@ -116,11 +122,13 @@ | |||
| 116 | 122 | /// A hole is deliberately not an expression. It cannot contain an operator, a | |
| 117 | 123 | /// closure, a turbofish, an index or a block, so the macro can place its result | |
| 118 | 124 | /// in a field without reasoning about evaluation order. | |
| 125 | + | #[derive(Clone)] | |
| 119 | 126 | pub struct Hole { | |
| 120 | 127 | pub root: HoleRoot, | |
| 121 | 128 | pub steps: Vec<Step>, | |
| 122 | 129 | } | |
| 123 | 130 | ||
| 131 | + | #[derive(Clone)] | |
| 124 | 132 | pub enum HoleRoot { | |
| 125 | 133 | /// A bare lowercase ident: a binding, innermost first. | |
| 126 | 134 | Binding(Ident), | |
| @@ -130,11 +138,13 @@ | |||
| 130 | 138 | Call { path: syn::Path, args: Vec<Arg> }, | |
| 131 | 139 | } | |
| 132 | 140 | ||
| 141 | + | #[derive(Clone)] | |
| 133 | 142 | pub enum Step { | |
| 134 | 143 | Field(Ident), | |
| 135 | 144 | Method { name: Ident, args: Vec<Arg> }, | |
| 136 | 145 | } | |
| 137 | 146 | ||
| 147 | + | #[derive(Clone)] | |
| 138 | 148 | pub enum Arg { | |
| 139 | 149 | Str(Interpolated), | |
| 140 | 150 | /// `[ <arg>, .. ]`, which is an array literal and never a collection: what | |
| @@ -149,6 +159,7 @@ | |||
| 149 | 159 | } | |
| 150 | 160 | ||
| 151 | 161 | /// Everything that emits into the enclosing container. | |
| 162 | + | #[derive(Clone)] | |
| 152 | 163 | pub enum Emission { | |
| 153 | 164 | /// `<member> <arg>* ( "{" { item } "}" | ";" )` -- one of `Node`'s own | |
| 154 | 165 | /// constructors, by its own name, and whatever it is told afterwards. | |
| @@ -354,18 +365,21 @@ | |||
| 354 | 365 | /// | |
| 355 | 366 | /// Told apart by R1's rule and not by a keyword: a bare uppercase ident is a | |
| 356 | 367 | /// variant, and anything else is a hole. | |
| 368 | + | #[derive(Clone)] | |
| 357 | 369 | pub enum RegionKind { | |
| 358 | 370 | Variant(Ident), | |
| 359 | 371 | Supplied(Hole), | |
| 360 | 372 | } | |
| 361 | 373 | ||
| 362 | 374 | /// `when <predicate>`, or `unless <predicate>`, which is its negation. | |
| 375 | + | #[derive(Clone)] | |
| 363 | 376 | pub struct Guard { | |
| 364 | 377 | pub negated: bool, | |
| 365 | 378 | pub predicate: Predicate, | |
| 366 | 379 | pub span: Span, | |
| 367 | 380 | } | |
| 368 | 381 | ||
| 382 | + | #[derive(Clone)] | |
| 369 | 383 | pub enum Predicate { | |
| 370 | 384 | /// `<clause> and <clause>`, or `<clause> or <clause>`. One connective per | |
| 371 | 385 | /// predicate: mixing them would need precedence, and precedence is how a | |
| @@ -387,6 +401,7 @@ | |||
| 387 | 401 | }, | |
| 388 | 402 | } | |
| 389 | 403 | ||
| 404 | + | #[derive(Clone)] | |
| 390 | 405 | pub struct Action { | |
| 391 | 406 | /// One of the verbs, or `doing`, which takes the whole action from a | |
| 392 | 407 | /// supplier. Amendment 6: a screen whose action is decided by something | |
| @@ -397,6 +412,7 @@ | |||
| 397 | 412 | } | |
| 398 | 413 | ||
| 399 | 414 | /// One word after the verb, and whatever it needs to say it. | |
| 415 | + | #[derive(Clone)] | |
| 400 | 416 | pub struct Modifier { | |
| 401 | 417 | pub name: Ident, | |
| 402 | 418 | pub args: Vec<Arg>, |
| @@ -859,7 +859,7 @@ | |||
| 859 | 859 | } | |
| 860 | 860 | ||
| 861 | 861 | /// One guard, as the `bool` it tests. | |
| 862 | - | fn predicate(guard: &Guard) -> Result<TokenStream> { | |
| 862 | + | pub(crate) fn predicate(guard: &Guard) -> Result<TokenStream> { | |
| 863 | 863 | let test = clause(&guard.predicate)?; | |
| 864 | 864 | Ok(if guard.negated { | |
| 865 | 865 | quote!(!(#test)) | |
| @@ -1810,7 +1810,7 @@ | |||
| 1810 | 1810 | quote!(::std::format!(#literal, #(#holes),*)) | |
| 1811 | 1811 | } | |
| 1812 | 1812 | ||
| 1813 | - | fn hole(hole: &Hole) -> Result<TokenStream> { | |
| 1813 | + | pub(crate) fn hole(hole: &Hole) -> Result<TokenStream> { | |
| 1814 | 1814 | let mut built = match &hole.root { | |
| 1815 | 1815 | HoleRoot::Binding(name) => quote!(#name), | |
| 1816 | 1816 | HoleRoot::Path(path) => quote!(#path), | |
| @@ -1831,7 +1831,7 @@ | |||
| 1831 | 1831 | Ok(built) | |
| 1832 | 1832 | } | |
| 1833 | 1833 | ||
| 1834 | - | fn arg(arg: &Arg) -> Result<TokenStream> { | |
| 1834 | + | pub(crate) fn arg(arg: &Arg) -> Result<TokenStream> { | |
| 1835 | 1835 | match arg { | |
| 1836 | 1836 | Arg::Str(text) => Ok(interpolated(text, false)), | |
| 1837 | 1837 | Arg::Hole(hole) => self::hole(hole), |
| @@ -20,6 +20,8 @@ | |||
| 20 | 20 | ||
| 21 | 21 | mod ast; | |
| 22 | 22 | mod emit; | |
| 23 | + | mod fill; | |
| 24 | + | mod pairs; | |
| 23 | 25 | mod parse; | |
| 24 | 26 | mod symbolic; | |
| 25 | 27 | ||
| @@ -53,6 +55,7 @@ | |||
| 53 | 55 | ||
| 54 | 56 | /// The shape's function, and its staged twin if it asked for one. | |
| 55 | 57 | fn expand(declaration: &ast::Declaration) -> syn::Result<proc_macro2::TokenStream> { | |
| 58 | + | pairs::check(&declaration.items)?; | |
| 56 | 59 | let shape = emit::declaration(declaration)?; | |
| 57 | 60 | if !symbolic::wanted(declaration) { | |
| 58 | 61 | return Ok(shape); | |
| @@ -60,6 +63,7 @@ | |||
| 60 | 63 | ||
| 61 | 64 | let staged = symbolic::stage(declaration)?; | |
| 62 | 65 | let twin = emit::declaration(&staged.declaration)?; | |
| 66 | + | let filler = fill::filler(declaration, &staged.fill)?; | |
| 63 | 67 | let (guards, loops, holes, sites) = staged.counts; | |
| 64 | 68 | let counts = &staged.counts_ident; | |
| 65 | 69 | let vis = declaration | |
| @@ -74,6 +78,7 @@ | |||
| 74 | 78 | Ok(quote::quote! { | |
| 75 | 79 | #shape | |
| 76 | 80 | #twin | |
| 81 | + | #filler | |
| 77 | 82 | #[doc = #doc] | |
| 78 | 83 | #vis const #counts: ::quasi_router::stage::Shape = ::quasi_router::stage::Shape { | |
| 79 | 84 | guards: #guards, |
| @@ -71,6 +71,31 @@ | |||
| 71 | 71 | declaration.flags.iter().any(|flag| flag == FLAG) | |
| 72 | 72 | } | |
| 73 | 73 | ||
| 74 | + | /// One instruction of the fill program: what a request writes, and where. | |
| 75 | + | /// | |
| 76 | + | /// Collected by the same traversal that numbers the holes, so the filler and | |
| 77 | + | /// the residual cannot disagree about order. Written as a second traversal they | |
| 78 | + | /// would drift the first time a production was added to one and not the other. | |
| 79 | + | pub enum Fill { | |
| 80 | + | /// One value, at the hole with this number. | |
| 81 | + | /// | |
| 82 | + | /// Numbered rather than positioned because render order and declaration | |
| 83 | + | /// order genuinely differ: a cell's `activate` address is written after the | |
| 84 | + | /// cell's value and renders before it, inside the anchor that wraps it. | |
| 85 | + | Hole { id: u16, hole: Hole }, | |
| 86 | + | /// What one guard places, and the predicate that decides it. | |
| 87 | + | Branch { guard: Guard, body: Vec<Fill> }, | |
| 88 | + | /// One loop, over the collection the declaration named. | |
| 89 | + | Repeat { | |
| 90 | + | dereferenced: bool, | |
| 91 | + | binder: Ident, | |
| 92 | + | iterable: Hole, | |
| 93 | + | body: Vec<Fill>, | |
| 94 | + | }, | |
| 95 | + | /// Another shape's filler, called where its markup was spliced in. | |
| 96 | + | Include { callee: syn::Path, args: Vec<Arg> }, | |
| 97 | + | } | |
| 98 | + | ||
| 74 | 99 | /// Numbering, one counter per thing a plan answers. | |
| 75 | 100 | /// | |
| 76 | 101 | /// Shape-local on purpose. A residual is derived per shape and composed by | |
| @@ -84,6 +109,27 @@ | |||
| 84 | 109 | holes: u16, | |
| 85 | 110 | /// One per `include`, which is what scopes the shape it reaches. | |
| 86 | 111 | sites: u16, | |
| 112 | + | /// The fill program under construction, innermost body last. | |
| 113 | + | stack: Vec<Vec<Fill>>, | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | impl Counters { | |
| 117 | + | /// Start a body: a loop's, or what a guard places. | |
| 118 | + | fn enter(&mut self) { | |
| 119 | + | self.stack.push(Vec::new()); | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | /// Finish the innermost body. | |
| 123 | + | fn exit(&mut self) -> Vec<Fill> { | |
| 124 | + | self.stack.pop().unwrap_or_default() | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | /// Add one instruction to the innermost body. | |
| 128 | + | fn wrote(&mut self, fill: Fill) { | |
| 129 | + | if let Some(body) = self.stack.last_mut() { | |
| 130 | + | body.push(fill); | |
| 131 | + | } | |
| 132 | + | } | |
| 87 | 133 | } | |
| 88 | 134 | ||
| 89 | 135 | /// A hole reading the plan: `plan.<method>(<id>)`. | |
| @@ -110,6 +156,8 @@ | |||
| 110 | 156 | /// The staged declaration, and the counts a plan needs to drive it. | |
| 111 | 157 | pub struct Staged { | |
| 112 | 158 | pub declaration: Declaration, | |
| 159 | + | /// What a request writes into the residual, in declaration order. | |
| 160 | + | pub fill: Vec<Fill>, | |
| 113 | 161 | pub counts: (u16, u16, u16, u16), | |
| 114 | 162 | pub counts_ident: Ident, | |
| 115 | 163 | } | |
| @@ -117,7 +165,9 @@ | |||
| 117 | 165 | /// Rewrite one declaration into its staged twin. | |
| 118 | 166 | pub fn stage(declaration: &Declaration) -> Result<Staged> { | |
| 119 | 167 | let mut counters = Counters::default(); | |
| 168 | + | counters.enter(); | |
| 120 | 169 | let items = items(&declaration.items, &mut counters)?; | |
| 170 | + | let fill = counters.exit(); | |
| 121 | 171 | ||
| 122 | 172 | let name = staged_name(&declaration.name); | |
| 123 | 173 | let doc = format!( | |
| @@ -126,6 +176,7 @@ | |||
| 126 | 176 | ); | |
| 127 | 177 | ||
| 128 | 178 | Ok(Staged { | |
| 179 | + | fill, | |
| 129 | 180 | counts: ( | |
| 130 | 181 | counters.guards, | |
| 131 | 182 | counters.loops, | |
| @@ -168,11 +219,26 @@ | |||
| 168 | 219 | name: name.clone(), | |
| 169 | 220 | source: self::source(source, counters)?, | |
| 170 | 221 | }, | |
| 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 | - | }, | |
| 222 | + | // A guarded member renders where it was written; a guarded SETTING | |
| 223 | + | // renders in its container's opening tag, which is before every member | |
| 224 | + | // regardless of where the declaration put it. The residual marks | |
| 225 | + | // branches in render order and the filler walks them in declaration | |
| 226 | + | // order, so the two would disagree about which branch is which. | |
| 227 | + | Item::Attribute { name, args, guard } => { | |
| 228 | + | if let Some(guard) = guard { | |
| 229 | + | return Err(syn::Error::new( | |
| 230 | + | guard.span, | |
| 231 | + | "a staged shape cannot guard a setting: a setting renders in \ | |
| 232 | + | its container's opening tag, so the branch it makes is not \ | |
| 233 | + | where the declaration wrote it", | |
| 234 | + | )); | |
| 235 | + | } | |
| 236 | + | Item::Attribute { | |
| 237 | + | name: name.clone(), | |
| 238 | + | args: self::args(args, counters)?, | |
| 239 | + | guard: None, | |
| 240 | + | } | |
| 241 | + | } | |
| 176 | 242 | // The binder is never read: every hole rooted at it was rewritten to a | |
| 177 | 243 | // sentinel above, so the loop runs for its count and nothing else. It | |
| 178 | 244 | // keeps its name with an underscore so a reader can still see which | |
| @@ -180,16 +246,25 @@ | |||
| 180 | 246 | Item::For { | |
| 181 | 247 | binder, | |
| 182 | 248 | body, | |
| 183 | - | dereferenced: _, | |
| 184 | - | iterable: _, | |
| 249 | + | dereferenced, | |
| 250 | + | iterable, | |
| 185 | 251 | } => { | |
| 186 | 252 | let id = counters.loops; | |
| 187 | 253 | counters.loops += 1; | |
| 254 | + | counters.enter(); | |
| 255 | + | let staged = items(body, counters)?; | |
| 256 | + | let inner = counters.exit(); | |
| 257 | + | counters.wrote(Fill::Repeat { | |
| 258 | + | dereferenced: *dereferenced, | |
| 259 | + | binder: binder.clone(), | |
| 260 | + | iterable: iterable.clone(), | |
| 261 | + | body: inner, | |
| 262 | + | }); | |
| 188 | 263 | Item::For { | |
| 189 | 264 | dereferenced: false, | |
| 190 | 265 | binder: format_ident!("_{}", binder, span = binder.span()), | |
| 191 | 266 | iterable: plan_read("rows", id), | |
| 192 | - | body: items(body, counters)?, | |
| 267 | + | body: staged, | |
| 193 | 268 | } | |
| 194 | 269 | } | |
| 195 | 270 | Item::Emit(emission) => Item::Emit(self::emission(emission, counters)?), | |
| @@ -272,6 +347,10 @@ | |||
| 272 | 347 | } | |
| 273 | 348 | let id = counters.holes; | |
| 274 | 349 | counters.holes += 1; | |
| 350 | + | counters.wrote(Fill::Hole { | |
| 351 | + | id, | |
| 352 | + | hole: hole.clone(), | |
| 353 | + | }); | |
| 275 | 354 | sentinel(id) | |
| 276 | 355 | } | |
| 277 | 356 | ||
| @@ -472,10 +551,20 @@ | |||
| 472 | 551 | at: arg(at, counters)?, | |
| 473 | 552 | inner: Box::new(self::emission(inner, counters)?), | |
| 474 | 553 | }, | |
| 475 | - | Emission::Guarded { guard, inner } => Emission::Guarded { | |
| 476 | - | guard: self::guard(guard, counters), | |
| 477 | - | inner: Box::new(self::emission(inner, counters)?), | |
| 478 | - | }, | |
| 554 | + | Emission::Guarded { guard, inner } => { | |
| 555 | + | let staged = self::guard(guard, counters); | |
| 556 | + | counters.enter(); | |
| 557 | + | let inner = Box::new(self::emission(inner, counters)?); | |
| 558 | + | let body = counters.exit(); | |
| 559 | + | counters.wrote(Fill::Branch { | |
| 560 | + | guard: (*guard).clone(), | |
| 561 | + | body, | |
| 562 | + | }); | |
| 563 | + | Emission::Guarded { | |
| 564 | + | guard: staged, | |
| 565 | + | inner, | |
| 566 | + | } | |
| 567 | + | } | |
| 479 | 568 | // The arms become integers, so the plan chooses one by position and the | |
| 480 | 569 | // scrutinee's own type never has to be reproduced. That is also why an | |
| 481 | 570 | // `otherwise` stops being optional here: a match on a `usize` against | |
| @@ -545,6 +634,17 @@ | |||
| 545 | 634 | )); | |
| 546 | 635 | } | |
| 547 | 636 | ||
| 637 | + | let HoleRoot::Call { args, .. } = &hole.root else { | |
| 638 | + | unreachable!("checked above"); | |
| 639 | + | }; | |
| 640 | + | // The filler calls the shape's own name, not the twin's: the twin renders | |
| 641 | + | // markup and the filler fills it, and they are two different functions | |
| 642 | + | // beside one declaration. | |
| 643 | + | counters.wrote(Fill::Include { | |
| 644 | + | callee: path.clone(), | |
| 645 | + | args: args.clone(), | |
| 646 | + | }); | |
| 647 | + | ||
| 548 | 648 | let mut path = path.clone(); | |
| 549 | 649 | let last = path | |
| 550 | 650 | .segments |
| @@ -32,7 +32,7 @@ | |||
| 32 | 32 | ||
| 33 | 33 | use std::cell::RefCell; | |
| 34 | 34 | use std::collections::BTreeSet; | |
| 35 | - | use std::iter::{Repeat, Take, repeat}; | |
| 35 | + | use std::iter::RepeatN; | |
| 36 | 36 | use std::rc::Rc; | |
| 37 | 37 | ||
| 38 | 38 | /// The three-character opening of a sentinel. | |
| @@ -305,11 +305,10 @@ | |||
| 305 | 305 | /// Yields `()` rather than elements: the body's holes were rewritten to | |
| 306 | 306 | /// sentinels when the shape was staged, so nothing in it reads the binder | |
| 307 | 307 | /// and there is no element for it to read. | |
| 308 | - | #[must_use] | |
| 309 | - | pub fn rows(&self, id: u16) -> Take<Repeat<()>> { | |
| 308 | + | pub fn rows(&self, id: u16) -> RepeatN<()> { | |
| 310 | 309 | self.trace.saw(|sites| &mut sites.loops, (self.scope, id)); | |
| 311 | 310 | let count = look(&self.row_overrides, self.scope, id).unwrap_or(self.rows); | |
| 312 | - | repeat(()).take(count) | |
| 311 | + | std::iter::repeat_n((), count) | |
| 313 | 312 | } | |
| 314 | 313 | ||
| 315 | 314 | /// Which arm one dispatch takes. Read by generated code. | |
| @@ -424,6 +423,32 @@ | |||
| 424 | 423 | } | |
| 425 | 424 | } | |
| 426 | 425 | ||
| 426 | + | /// Every literal and every hole up to the next branch or loop. | |
| 427 | + | /// | |
| 428 | + | /// `value` is asked for each hole by its number rather than handed them in | |
| 429 | + | /// order, because a screen's holes do not render in the order they were | |
| 430 | + | /// written: a cell's address renders inside the anchor that wraps its text | |
| 431 | + | /// and is declared after it. | |
| 432 | + | pub fn fill<F>(&mut self, out: &mut String, value: &mut F) | |
| 433 | + | where | |
| 434 | + | F: FnMut(u16, &mut String), | |
| 435 | + | { | |
| 436 | + | loop { | |
| 437 | + | match self.ops.get(self.at) { | |
| 438 | + | Some(Op::Lit(text)) => { | |
| 439 | + | out.push_str(text); | |
| 440 | + | self.at += 1; | |
| 441 | + | } | |
| 442 | + | Some(Op::Hole { id, .. }) => { | |
| 443 | + | let id = *id; | |
| 444 | + | self.at += 1; | |
| 445 | + | value(id, out); | |
| 446 | + | } | |
| 447 | + | Some(Op::Branch(_) | Op::Loop(_)) | None => return, | |
| 448 | + | } | |
| 449 | + | } | |
| 450 | + | } | |
| 451 | + | ||
| 427 | 452 | /// The markup up to the next hole, then the hole itself. | |
| 428 | 453 | /// | |
| 429 | 454 | /// Returns nothing: the caller writes the value, because the caller is the |
| @@ -35,6 +35,66 @@ | |||
| 35 | 35 | ||
| 36 | 36 | use crate::Webview; | |
| 37 | 37 | ||
| 38 | + | /// What a request's value does on its way into a residual's gap. | |
| 39 | + | /// | |
| 40 | + | /// A small closed set rather than a blanket over `Display`, for two reasons. | |
| 41 | + | /// A string is the common case by a wide margin and must not go near the | |
| 42 | + | /// formatting machinery: it goes straight to the renderer's escaper. And a | |
| 43 | + | /// number cannot carry markup, so it needs no escaping at all and can be | |
| 44 | + | /// written into the output directly. | |
| 45 | + | /// | |
| 46 | + | /// A value whose type is not here is a compile error naming it, which is the | |
| 47 | + | /// same bargain the rest of this crate makes: one entry per thing a real | |
| 48 | + | /// screen turned out to read, and nothing specified ahead of one. | |
| 49 | + | pub trait Fill { | |
| 50 | + | /// Write this value into the output, escaped if it needs to be. | |
| 51 | + | fn fill(&self, out: &mut String); | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | impl Fill for str { | |
| 55 | + | /// Through `makeover_webview::form::escape_into`, which is the function | |
| 56 | + | /// `Node::Text` uses. Not reimplemented here and not to be: the guarantee a | |
| 57 | + | /// described screen carries is that function's, and a staged path escaping | |
| 58 | + | /// by its own rules would be a different guarantee under the same name. | |
| 59 | + | fn fill(&self, out: &mut String) { | |
| 60 | + | makeover_webview::form::escape_into(self, out); | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | impl Fill for String { | |
| 65 | + | fn fill(&self, out: &mut String) { | |
| 66 | + | makeover_webview::form::escape_into(self, out); | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | impl<T: Fill + ?Sized> Fill for &T { | |
| 71 | + | fn fill(&self, out: &mut String) { | |
| 72 | + | (**self).fill(out); | |
| 73 | + | } | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | /// Numbers and booleans, which have nothing to escape. | |
| 77 | + | /// | |
| 78 | + | /// A decimal integer, a float and `true`/`false` cannot carry a delimiter, so | |
| 79 | + | /// they are written straight into the output. Sending them through the escaper | |
| 80 | + | /// would be a scan over bytes that are known in advance to pass it. | |
| 81 | + | macro_rules! plainly { | |
| 82 | + | ($($ty:ty),* $(,)?) => { | |
| 83 | + | $( | |
| 84 | + | impl Fill for $ty { | |
| 85 | + | fn fill(&self, out: &mut String) { | |
| 86 | + | use ::std::fmt::Write as _; | |
| 87 | + | let _ = write!(out, "{self}"); | |
| 88 | + | } | |
| 89 | + | } | |
| 90 | + | )* | |
| 91 | + | }; | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | plainly!( | |
| 95 | + | u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, bool, char | |
| 96 | + | ); | |
| 97 | + | ||
| 38 | 98 | /// A span of the base render, and what decides it. | |
| 39 | 99 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] | |
| 40 | 100 | struct Span { |
| @@ -1,0 +1,222 @@ | |||
| 1 | + | //! Writing a request's values into a residual. | |
| 2 | + | //! | |
| 3 | + | //! The third thing a `#[staged]` shape gets. The first is the ordinary | |
| 4 | + | //! function, the second is the twin that renders with sentinels, and this is | |
| 5 | + | //! what a request actually calls: a walk over the residual that pushes the | |
| 6 | + | //! renderer's own literals and writes the request's values into the gaps. | |
| 7 | + | //! | |
| 8 | + | //! No `Node` is built. That is the whole point of the task this belongs to, and | |
| 9 | + | //! it is why this is generated rather than interpreted: the values come from | |
| 10 | + | //! the same expressions the declaration wrote, evaluated in place. | |
| 11 | + | //! | |
| 12 | + | //! # Why holes are filled by number and not by position | |
| 13 | + | //! | |
| 14 | + | //! Render order and declaration order are not the same. A cell's `activate` | |
| 15 | + | //! address is written after the cell's value and renders before it, inside the | |
| 16 | + | //! anchor that wraps it. So a filler that walked the residual writing values in | |
| 17 | + | //! the order it had them would put the address in the text and the text in the | |
| 18 | + | //! address. | |
| 19 | + | //! | |
| 20 | + | //! The residual carries each hole's number, and [`crate::symbolic`] recorded | |
| 21 | + | //! the expression for each number in the same traversal that assigned it. So a | |
| 22 | + | //! straight run of markup is filled by asking for holes by number, and only the | |
| 23 | + | //! things that change control flow -- a guard, a loop, another shape -- have to | |
| 24 | + | //! appear in the same order in both. Those do, because a container renders its | |
| 25 | + | //! members in the order it was told them. | |
| 26 | + | ||
| 27 | + | use proc_macro2::{Span, TokenStream}; | |
| 28 | + | use quote::{format_ident, quote}; | |
| 29 | + | use syn::{Ident, Result}; | |
| 30 | + | ||
| 31 | + | use crate::ast::{Declaration, Hole, Step}; | |
| 32 | + | use crate::symbolic::Fill; | |
| 33 | + | ||
| 34 | + | /// The filler's name, which is what an `include` calls. | |
| 35 | + | pub fn fill_name(name: &Ident) -> Ident { | |
| 36 | + | format_ident!("{}_fill", name, span = name.span()) | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | /// The one-call entry point, which makes a cursor and closes it. | |
| 40 | + | fn serve_name(name: &Ident) -> Ident { | |
| 41 | + | format_ident!("{}_serve", name, span = name.span()) | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | /// The filler for one shape, and the entry point beside it. | |
| 45 | + | pub fn filler(declaration: &Declaration, fill: &[Fill]) -> Result<TokenStream> { | |
| 46 | + | let name = fill_name(&declaration.name); | |
| 47 | + | let serve = serve_name(&declaration.name); | |
| 48 | + | let vis = &declaration.vis; | |
| 49 | + | ||
| 50 | + | let params: Vec<TokenStream> = declaration | |
| 51 | + | .params | |
| 52 | + | .iter() | |
| 53 | + | .map(|param| { | |
| 54 | + | let name = ¶m.name; | |
| 55 | + | let ty = ¶m.ty; | |
| 56 | + | quote!(#name: #ty) | |
| 57 | + | }) | |
| 58 | + | .collect(); | |
| 59 | + | let forwarded: Vec<&Ident> = declaration.params.iter().map(|param| ¶m.name).collect(); | |
| 60 | + | ||
| 61 | + | let body = self::body(fill)?; | |
| 62 | + | ||
| 63 | + | let fill_doc = format!( | |
| 64 | + | " Write a request's values into [`{}`]'s residual.", | |
| 65 | + | declaration.name | |
| 66 | + | ); | |
| 67 | + | let serve_doc = format!( | |
| 68 | + | " Serve [`{}`] from a residual, building no `Node`.", | |
| 69 | + | declaration.name | |
| 70 | + | ); | |
| 71 | + | ||
| 72 | + | Ok(quote! { | |
| 73 | + | #[doc = #fill_doc] | |
| 74 | + | /// | |
| 75 | + | /// Continues on the cursor it is given and does not close it, so a | |
| 76 | + | /// shape that includes this one carries on where it left off. | |
| 77 | + | #vis fn #name( | |
| 78 | + | cursor: &mut ::quasi_router::stage::Cursor<'_>, | |
| 79 | + | out: &mut ::std::string::String, | |
| 80 | + | #(#params),* | |
| 81 | + | ) { | |
| 82 | + | #body | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | #[doc = #serve_doc] | |
| 86 | + | #vis fn #serve( | |
| 87 | + | residual: &::quasi_router::stage::Residual, | |
| 88 | + | #(#params),* | |
| 89 | + | ) -> ::std::string::String { | |
| 90 | + | let mut out = ::std::string::String::with_capacity(residual.literal_len() + 64); | |
| 91 | + | let mut cursor = residual.cursor(); | |
| 92 | + | #name(&mut cursor, &mut out, #(#forwarded),*); | |
| 93 | + | ::quasi_router::stage::Cursor::finish(&mut cursor, &mut out); | |
| 94 | + | out | |
| 95 | + | } | |
| 96 | + | }) | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | /// One body: a shape's, a branch's, or a loop's. | |
| 100 | + | /// | |
| 101 | + | /// A run of consecutive holes becomes one walk of the residual answering each | |
| 102 | + | /// by number. Anything that changes control flow ends the run, because it also | |
| 103 | + | /// ends the residual's straight stretch. | |
| 104 | + | fn body(fill: &[Fill]) -> Result<TokenStream> { | |
| 105 | + | let mut out = TokenStream::new(); | |
| 106 | + | let mut holes: Vec<&Fill> = Vec::new(); | |
| 107 | + | ||
| 108 | + | for one in fill { | |
| 109 | + | match one { | |
| 110 | + | Fill::Hole { .. } => holes.push(one), | |
| 111 | + | _ => { | |
| 112 | + | out.extend(run(&mut holes)?); | |
| 113 | + | out.extend(control(one)?); | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | } | |
| 117 | + | out.extend(run(&mut holes)?); | |
| 118 | + | Ok(out) | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | /// One straight stretch of markup, with its holes answered by number. | |
| 122 | + | fn run(holes: &mut Vec<&Fill>) -> Result<TokenStream> { | |
| 123 | + | if holes.is_empty() { | |
| 124 | + | return Ok(TokenStream::new()); | |
| 125 | + | } | |
| 126 | + | let arms = holes | |
| 127 | + | .iter() | |
| 128 | + | .map(|one| { | |
| 129 | + | let Fill::Hole { id, hole } = one else { | |
| 130 | + | unreachable!("only holes are collected here"); | |
| 131 | + | }; | |
| 132 | + | let value = crate::emit::hole(&owned(hole))?; | |
| 133 | + | Ok(quote!(#id => ::quasi_webview::stage::Fill::fill(&(#value), out),)) | |
| 134 | + | }) | |
| 135 | + | .collect::<Result<Vec<_>>>()?; | |
| 136 | + | holes.clear(); | |
| 137 | + | ||
| 138 | + | Ok(quote! { | |
| 139 | + | ::quasi_router::stage::Cursor::fill(cursor, out, &mut |which, out| match which { | |
| 140 | + | #(#arms)* | |
| 141 | + | // A hole the residual has and this stretch does not. The two came | |
| 142 | + | // from one declaration, so this is unreachable rather than a case. | |
| 143 | + | _ => ::core::unreachable!("the residual has a hole the filler does not"), | |
| 144 | + | }); | |
| 145 | + | }) | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | /// A guard, a loop, or another shape. | |
| 149 | + | fn control(one: &Fill) -> Result<TokenStream> { | |
| 150 | + | Ok(match one { | |
| 151 | + | Fill::Hole { .. } => unreachable!("handled by the caller"), | |
| 152 | + | Fill::Branch { guard, body } => { | |
| 153 | + | let predicate = crate::emit::predicate(guard)?; | |
| 154 | + | let inner = self::body(body)?; | |
| 155 | + | quote! { | |
| 156 | + | { | |
| 157 | + | let mut cursor = &mut ::quasi_router::stage::Cursor::branch(cursor, out); | |
| 158 | + | if #predicate { | |
| 159 | + | #inner | |
| 160 | + | ::quasi_router::stage::Cursor::finish(cursor, out); | |
| 161 | + | } | |
| 162 | + | } | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | Fill::Repeat { | |
| 166 | + | dereferenced, | |
| 167 | + | binder, | |
| 168 | + | iterable, | |
| 169 | + | body, | |
| 170 | + | } => { | |
| 171 | + | let over = crate::emit::hole(iterable)?; | |
| 172 | + | let inner = self::body(body)?; | |
| 173 | + | let bound = if *dereferenced { | |
| 174 | + | quote!(&#binder) | |
| 175 | + | } else { | |
| 176 | + | quote!(#binder) | |
| 177 | + | }; | |
| 178 | + | quote! { | |
| 179 | + | { | |
| 180 | + | let repeated = ::quasi_router::stage::Cursor::repeat(cursor, out); | |
| 181 | + | for #bound in #over { | |
| 182 | + | let mut cursor = &mut ::quasi_router::stage::Cursor::over(repeated); | |
| 183 | + | #inner | |
| 184 | + | ::quasi_router::stage::Cursor::finish(cursor, out); | |
| 185 | + | } | |
| 186 | + | } | |
| 187 | + | } | |
| 188 | + | } | |
| 189 | + | // The callee's markup was spliced into this residual where its shape | |
| 190 | + | // was included, so its filler carries on with the same cursor. | |
| 191 | + | Fill::Include { callee, args } => { | |
| 192 | + | let mut path = callee.clone(); | |
| 193 | + | let last = path | |
| 194 | + | .segments | |
| 195 | + | .last_mut() | |
| 196 | + | .ok_or_else(|| syn::Error::new(Span::call_site(), "an empty path"))?; | |
| 197 | + | last.ident = fill_name(&last.ident); | |
| 198 | + | let args = args | |
| 199 | + | .iter() | |
| 200 | + | .map(crate::emit::arg) | |
| 201 | + | .collect::<Result<Vec<_>>>()?; | |
| 202 | + | quote!(#path(cursor, out, #(#args),*);) | |
| 203 | + | } | |
| 204 | + | }) | |
| 205 | + | } | |
| 206 | + | ||
| 207 | + | /// The hole with a trailing `.clone()` taken off. | |
| 208 | + | /// | |
| 209 | + | /// A declaration clones because the vocabulary takes the value by value. A | |
| 210 | + | /// filler only writes it, so the clone would be one allocation per hole per row | |
| 211 | + | /// bought for nothing, which at two hundred rows is the whole allocation | |
| 212 | + | /// budget. | |
| 213 | + | fn owned(hole: &Hole) -> Hole { | |
| 214 | + | let mut trimmed = hole.clone(); | |
| 215 | + | if let Some(Step::Method { name, args }) = trimmed.steps.last() | |
| 216 | + | && name == "clone" | |
| 217 | + | && args.is_empty() | |
| 218 | + | { | |
| 219 | + | trimmed.steps.pop(); | |
| 220 | + | } | |
| 221 | + | trimmed | |
| 222 | + | } |