Skip to main content

max / quasi

Serve a declared screen from its residual, building no tree The third function a `#[staged]` shape gets. The twin renders the screen with sentinels, the derivation reads a residual off that, and this walks the residual writing a request's values into it. No `Node` is built. fw13, library_contacts at 5/25/200 rows. Against the spike's staged column, which is the floor the task named: ns 991 / 4313 / 31273 -> 845 / 3702 / 24231 allocs 7 / 9 / 12 -> 3 / 5 / 8 Under it on both axes, on a bigger screen: the spike's hand-written copy dropped `navigating` and three settings on Revoke, and carries no guards at all. Against building and rendering a tree it is 9.1x at 200 rows, and 1.13x a compiled Askama template. Holes are filled by number, not by position. A cell's `activate` address is written after the cell's value and renders before it, inside the anchor that wraps it, so a filler walking the residual in order would put the address in the text. Only guards, loops and includes have to agree on order, and they do, because a container renders its members in the order it was told them. The last of the timing came from how a value reaches the output. Going through `Display` into a scratch buffer costs more than the escaping does; `Fill` sends a string straight to the renderer's own escaper and writes a number directly, since a decimal integer has nothing to escape. A type not in that set is a compile error naming it, which is the bargain the rest of this crate makes. R4(b) is enforced here too, and its zero-incidence measurement was stale. Section 11 counted zero written-out complementary pairs across the hand-written shapes, but the conversion is what creates the split form: a declaration has no `if`/`else`, so `unless x` beside `when x` is how a description says one. 56 bodies hold one now, 21/24/11 across the three trees. What is still zero is a pair whose arms share an owned binding, which the trees compiling guarantees. So the rule stops guarding a form nobody writes and starts guarding one written 56 times. Verified not to fire on any of them.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
Author: Max Johnson <me@maxj.phd> · 2026-09-05 18:09 UTC
Signed with PGP, not checked
Commit: 03d05a2cbf733e47a31d5672dc149bb35539b3c4
Parent: bf0190c
16 files changed, +860 insertions, -29 deletions
M Cargo.lock +37 -5
@@ -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("&amp;"), "{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 = &param.name;
55 + let ty = &param.ty;
56 + quote!(#name: #ty)
57 + })
58 + .collect();
59 + let forwarded: Vec<&Ident> = declaration.params.iter().map(|param| &param.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 + }