Skip to main content

max / quasi

Mark what a request decides, so a residual is read rather than recovered The derivation used to find a guard's branch by rendering the screen twice with that guard changed and measuring the difference, and a loop body by rendering it with one row and then two. Everything that went wrong in the staged path went wrong there: the window a repeat converts through, spans placed against each other, a prefix scan running past its own body, arms that had to stay flat because the arm not taken was never drawn. None of that information was ever missing. The macro parsed the guard, so it knows exactly which members it controls. It now says so. A staged twin marks the run of members each guard, loop and dispatch covers (quasi_router::stage::Mark). The renderer records where it wrote them (Cover). The derivation reads the record. One render answers every guard and every loop; a dispatch renders once per arm, because an arm nobody drew has nothing to compile, and each arm is read off its own cover -- so an arm may now hold loops and guards of its own. Gone with the comparisons: grown, shrunk, between, swapped, dispatched, placeable, placed, straddled, balanced, cuts, and the twelve tests that existed to argue about which placement a deletion should take. IN FLIGHT. The twin marks every container; four do not yet carry marks, and rustc names each one at the shape that tried: Table's rows, Field's options, the Node variants that hold members, and a bare Vec (which is what stage::Staged is for). A guarded setting is still refused unless it is in symbolic's PLACED list; the answer there is two arms over the member it settles, which also dissolves the Select and latched blockers.
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-08 19:31 UTC
Signed with PGP, not checked
Commit: 6140a79a8d631f31e124daeee96dd2a9177c5fd4
Parent: cceb968
11 files changed, +735 insertions, -259 deletions
M Cargo.lock +23 -25
@@ -2656,9 +2656,9 @@
2656 2656
2657 2657 [[package]]
2658 2658 name = "makeover-immediate"
2659 - version = "0.45.0"
2659 + version = "0.46.0"
2660 2660 source = "registry+https://github.com/rust-lang/crates.io-index"
2661 - checksum = "e50588ef96178a8873bc43c370fac6608f554b144b311769c8ca93bb237e3448"
2661 + checksum = "a9868c94a16b8b32c2a30d1e89cc0715fc0044a9663ccb6dffd2dee2464f39fc"
2662 2662 dependencies = [
2663 2663 "egui",
2664 2664 "egui_extras",
@@ -2668,9 +2668,9 @@
2668 2668
2669 2669 [[package]]
2670 2670 name = "makeover-layout"
2671 - version = "0.44.0"
2671 + version = "0.44.2"
2672 2672 source = "registry+https://github.com/rust-lang/crates.io-index"
2673 - checksum = "f4c5dd2718ddfe428033a2c55d810e3b567edf9273f148875c1aa0291735ae12"
2673 + checksum = "49914ec86a7383c3f868c22befdca45cae1e72fdfdfb97f988e4075419e2800d"
2674 2674
2675 2675 [[package]]
2676 2676 name = "makeover-timing"
@@ -2693,9 +2693,9 @@
2693 2693
2694 2694 [[package]]
2695 2695 name = "makeover-tui"
2696 - version = "0.44.0"
2696 + version = "0.44.1"
2697 2697 source = "registry+https://github.com/rust-lang/crates.io-index"
2698 - checksum = "a39ca8938ca1e082ce16ac772592bc81aa0a14c461c5ec0b60bf55b0728ae7e5"
2698 + checksum = "e0bedfcbe4dcd0635d4802d6e8ab65f5d847621a424a28ec8b1b0aa52a0efa1f"
2699 2699 dependencies = [
2700 2700 "makeover",
2701 2701 "makeover-layout",
@@ -2705,9 +2705,7 @@
2705 2705
2706 2706 [[package]]
2707 2707 name = "makeover-webview"
2708 - version = "0.74.0"
2709 - source = "registry+https://github.com/rust-lang/crates.io-index"
2710 - checksum = "19502720cc808bdf7eead1f2393f270c4418cb1cc5cf0060cc33b31df87af063"
2708 + version = "0.75.0"
2711 2709 dependencies = [
2712 2710 "makeover-geometry",
2713 2711 "makeover-layout",
@@ -3476,11 +3474,11 @@
3476 3474
3477 3475 [[package]]
3478 3476 name = "quasi"
3479 - version = "0.104.1"
3477 + version = "0.109.0"
3480 3478
3481 3479 [[package]]
3482 3480 name = "quasi-axum"
3483 - version = "0.104.1"
3481 + version = "0.109.0"
3484 3482 dependencies = [
3485 3483 "axum",
3486 3484 "http",
@@ -3494,7 +3492,7 @@
3494 3492
3495 3493 [[package]]
3496 3494 name = "quasi-basics"
3497 - version = "0.104.1"
3495 + version = "0.109.0"
3498 3496 dependencies = [
3499 3497 "makeover-layout",
3500 3498 "quasi-declare",
@@ -3505,7 +3503,7 @@
3505 3503
3506 3504 [[package]]
3507 3505 name = "quasi-bench"
3508 - version = "0.104.1"
3506 + version = "0.109.0"
3509 3507 dependencies = [
3510 3508 "askama",
3511 3509 "dhat",
@@ -3526,7 +3524,7 @@
3526 3524
3527 3525 [[package]]
3528 3526 name = "quasi-declare"
3529 - version = "0.1.13"
3527 + version = "0.1.15"
3530 3528 dependencies = [
3531 3529 "proc-macro2",
3532 3530 "quasi-router",
@@ -3538,7 +3536,7 @@
3538 3536
3539 3537 [[package]]
3540 3538 name = "quasi-http"
3541 - version = "0.104.1"
3539 + version = "0.109.0"
3542 3540 dependencies = [
3543 3541 "form_urlencoded",
3544 3542 "http",
@@ -3547,7 +3545,7 @@
3547 3545
3548 3546 [[package]]
3549 3547 name = "quasi-immediate"
3550 - version = "0.104.1"
3548 + version = "0.109.0"
3551 3549 dependencies = [
3552 3550 "docengine",
3553 3551 "egui",
@@ -3558,7 +3556,7 @@
3558 3556
3559 3557 [[package]]
3560 3558 name = "quasi-notifs"
3561 - version = "0.104.1"
3559 + version = "0.109.0"
3562 3560 dependencies = [
3563 3561 "quasi-router",
3564 3562 "synckit-config",
@@ -3568,7 +3566,7 @@
3568 3566
3569 3567 [[package]]
3570 3568 name = "quasi-router"
3571 - version = "0.104.1"
3569 + version = "0.109.0"
3572 3570 dependencies = [
3573 3571 "makeover-layout",
3574 3572 ]
@@ -3585,7 +3583,7 @@
3585 3583
3586 3584 [[package]]
3587 3585 name = "quasi-tauri"
3588 - version = "0.104.1"
3586 + version = "0.109.0"
3589 3587 dependencies = [
3590 3588 "http",
3591 3589 "quasi-http",
@@ -3596,7 +3594,7 @@
3596 3594
3597 3595 [[package]]
3598 3596 name = "quasi-tui"
3599 - version = "0.104.1"
3597 + version = "0.109.0"
3600 3598 dependencies = [
3601 3599 "docengine",
3602 3600 "makeover",
@@ -3609,7 +3607,7 @@
3609 3607
3610 3608 [[package]]
3611 3609 name = "quasi-webview"
3612 - version = "0.104.1"
3610 + version = "0.109.0"
3613 3611 dependencies = [
3614 3612 "docengine",
3615 3613 "makeover-layout",
@@ -6314,14 +6312,14 @@
6314 6312 "winnow 1.0.4",
6315 6313 ]
6316 6314
6317 - [[patch.unused]]
6318 - name = "synckit-client"
6319 - version = "0.10.0"
6320 -
6321 6315 [[patch.unused]]
6322 6316 name = "quasi-type"
6323 6317 version = "0.1.3"
6324 6318
6319 + [[patch.unused]]
6320 + name = "synckit-client"
6321 + version = "0.10.0"
6322 +
6325 6323 [[patch.unused]]
6326 6324 name = "kberg"
6327 6325 version = "0.1.0"
@@ -41,7 +41,7 @@
41 41 makeover-layout = "0.44"
42 42 # The renderer's own escaper, so the staged path fills a hole exactly the way
43 43 # `Node::Text` does and the guarantee is not quietly reimplemented.
44 - makeover-webview = "0.74"
44 + makeover-webview = "0.75"
45 45 makeover-tui = { version = "0.44", features = ["theme"] }
46 46 # Only to load a bundled theme file. `makeover_tui::Theme` is `#[non_exhaustive]`,
47 47 # so `Theme::from_theme` is the one way to get one, the same reason quasi-tui's
@@ -16,7 +16,7 @@
16 16 quasi-router = { path = "../quasi-router", version = "0.109" }
17 17 quasi-http = { path = "../quasi-http", version = "0.109" }
18 18 makeover-layout = "0.44"
19 - makeover-webview = "0.74"
19 + makeover-webview = "0.75"
20 20 # `Node::Rich` carries markdown source and this is what turns it into markup.
21 21 # Sanitising comes with it, which is why the node can carry what a user typed.
22 22 #
@@ -86,6 +86,42 @@
86 86 },
87 87 /// Anything that puts something into the enclosing container.
88 88 Emit(Emission),
89 + /// A run of members whose markup a request decides, and which site decides
90 + /// it.
91 + ///
92 + /// Written by no author and parsed from nothing. [`crate::symbolic`] puts
93 + /// these in when it rewrites a declaration into its staged twin: a guard
94 + /// becomes an unguarded body under a mark, a loop becomes one pass under a
95 + /// mark, and a dispatch becomes the arm the plan picked under a mark.
96 + ///
97 + /// # Why the twin marks rather than the renderer inferring
98 + ///
99 + /// The macro parsed the guard, so it knows exactly which members it
100 + /// controls. A derivation that rendered the screen twice and measured the
101 + /// difference would be recovering something that was never lost, and every
102 + /// bug that path had was in the recovery rather than in the description.
103 + Marked {
104 + /// The site, numbered by the same counter the plan uses.
105 + site: u16,
106 + /// What a request decides about the run.
107 + varies: Varies,
108 + body: Vec<Item>,
109 + },
110 + }
111 +
112 + /// What a request decides about a marked run, as the twin writes it.
113 + ///
114 + /// The compile-time twin of [`quasi_router::stage::Varies`], which is the
115 + /// runtime value this becomes. Separate because an arm's index is a hole read
116 + /// from the plan rather than a number the macro knows.
117 + #[derive(Clone)]
118 + pub enum Varies {
119 + /// A guard: the run is placed, or it is not.
120 + Absent,
121 + /// A loop: the run is one pass.
122 + Repeated,
123 + /// A dispatch: `of` arms, and the one the plan asked for.
124 + Arm { of: usize, taken: Hole },
89 125 }
90 126
91 127 #[derive(Clone)]
@@ -261,6 +261,16 @@
261 261 let value = source_value(source)?;
262 262 bindings.push(quote!(let #name = #value;));
263 263 }
264 + // A value is not a container, so there is no run of members for a
265 + // mark to cover. Reached only if the twin marked a body answering a
266 + // value, which is a bug in the rewrite rather than in a screen.
267 + Item::Marked { .. } => {
268 + return Err(syn::Error::new(
269 + span,
270 + "a marked run in a body that answers a value, which has no \
271 + members to mark",
272 + ));
273 + }
264 274 Item::Attribute { name, .. } => {
265 275 return Err(syn::Error::new(
266 276 name.span(),
@@ -406,6 +416,7 @@
406 416 matches!(
407 417 item,
408 418 Item::For { .. }
419 + | Item::Marked { .. }
409 420 | Item::Emit(Emission::Guarded { .. })
410 421 | Item::Attribute { guard: Some(_), .. }
411 422 )
@@ -425,7 +436,9 @@
425 436 Item::Attribute {
426 437 name, args, body, ..
427 438 } => steps.push(attribute(name, args, body)?),
428 - Item::For { .. } => unreachable!("a loop takes the accumulating form"),
439 + Item::For { .. } | Item::Marked { .. } => {
440 + unreachable!("a loop and a marked run take the accumulating form")
441 + }
429 442 Item::Emit(emission) => {
430 443 // A member is evaluated into its own binding before the
431 444 // container is built, because a container's own name is often
@@ -500,6 +513,31 @@
500 513 let binder = binder_pattern(*dereferenced, binder);
501 514 quote!(for #binder in #iterable { #(#inner)* })
502 515 }
516 + Item::Marked { site, varies, body } => {
517 + let inner = self::statements(body, container, built)?;
518 + let varies = self::varies(varies)?;
519 + let plan = format_ident!("{}", crate::symbolic::PLAN, span = Span::call_site());
520 + let at = format_ident!("marked_from_{index}", span = Span::call_site());
521 + // `placed` before and after, so the run is named by where its
522 + // members landed rather than by counting what was written. A
523 + // container that cannot be marked fails here, naming its own
524 + // type, which is how "can this shape be staged" stays a
525 + // question rustc answers rather than a list somebody keeps.
526 + quote!({
527 + let #at = ::quasi_router::stage::Marking::placed(&#built);
528 + #(#inner)*
529 + ::quasi_router::stage::Marking::mark(
530 + &mut #built,
531 + ::quasi_router::stage::Mark {
532 + scope: #plan.scope(),
533 + id: #site,
534 + varies: #varies,
535 + from: #at,
536 + to: ::quasi_router::stage::Marking::placed(&#built),
537 + },
538 + );
539 + })
540 + }
503 541 Item::Emit(Emission::Guarded { guard, inner }) => {
504 542 let test = predicate(guard)?;
505 543 let held = format_ident!("member_{index}", span = Span::call_site());
@@ -525,6 +563,22 @@
525 563 Ok(statements)
526 564 }
527 565
566 + /// What a marked run says at runtime.
567 + ///
568 + /// An arm's index is read from the plan rather than known here, which is the
569 + /// whole reason [`crate::ast::Varies`] is its own type: the other two are
570 + /// decided by the macro, and this one is decided per render.
571 + fn varies(varies: &crate::ast::Varies) -> Result<TokenStream> {
572 + Ok(match varies {
573 + crate::ast::Varies::Absent => quote!(::quasi_router::stage::Varies::Absent),
574 + crate::ast::Varies::Repeated => quote!(::quasi_router::stage::Varies::Repeated),
575 + crate::ast::Varies::Arm { of, taken } => {
576 + let taken = hole(taken)?;
577 + quote!(::quasi_router::stage::Varies::Arm { of: #of, at: #taken })
578 + }
579 + })
580 + }
581 +
528 582 /// One setting, as the builder call ATTRIBUTE NAMING says it is.
529 583 ///
530 584 /// A body is settings made on the argument before it is passed. See
@@ -579,6 +633,14 @@
579 633 items
580 634 .iter()
581 635 .map(|item| match item {
636 + // Settling an argument is not accreting members either. A guard on
637 + // a setting inside one of these varies markup INSIDE the member its
638 + // statement produces, so the mark belongs to the container that
639 + // member lands in, which is where the twin puts it.
640 + Item::Marked { .. } => Err(syn::Error::new(
641 + proc_macro2::Span::call_site(),
642 + "a marked run where an argument is settled, which has no members",
643 + )),
582 644 Item::Attribute {
583 645 name,
584 646 args,
@@ -657,7 +719,9 @@
657 719 ) -> Result<()> {
658 720 for item in items {
659 721 match item {
660 - Item::For { body, .. } => each(body, last, last_name)?,
722 + Item::For { body, .. } | Item::Marked { body, .. } => {
723 + each(body, last, last_name)?;
724 + }
661 725 Item::Attribute { name, .. } => {
662 726 let Some(at) = drawn(name) else { continue };
663 727 if at < *last {
@@ -1605,6 +1669,18 @@
1605 1669 let held = format_ident!("asked", span = Span::call_site());
1606 1670 for (index, item) in body.iter().enumerate() {
1607 1671 match item {
1672 + // A form's fields are a bare `Vec`, so there is no container
1673 + // carrying marks for them yet. Refused rather than dropped: a
1674 + // staged form with a guarded field would otherwise compile to a
1675 + // residual with no branch where the fill program has one, and the
1676 + // filler would find that out on a request.
1677 + Item::Marked { .. } => {
1678 + return Err(syn::Error::new(
1679 + Span::call_site(),
1680 + "a form's fields cannot be marked yet, so this shape cannot be \
1681 + staged; give `Node::Form` its own marks first",
1682 + ));
1683 + }
1608 1684 Item::Attribute {
1609 1685 name,
1610 1686 args,
@@ -1759,6 +1835,7 @@
1759 1835 ) {
1760 1836 for item in items {
1761 1837 match item {
1838 + Item::Marked { body, .. } => marking(body, value, marked),
1762 1839 Item::Attribute { name, body, .. } => {
1763 1840 if name == "value" {
1764 1841 value.get_or_insert(name);
@@ -1814,6 +1891,15 @@
1814 1891 items
1815 1892 .iter()
1816 1893 .map(|item| match item {
1894 + // A bare `Vec` has nowhere to keep a mark. `Staged<Vec<_>>` is the
1895 + // wrapper that fixes it, and until this builder uses one, a shape
1896 + // whose members accrete into a list cannot be staged. Refused
1897 + // rather than dropped: a residual missing a branch the fill program
1898 + // has is a fault a request discovers.
1899 + Item::Marked { .. } => Err(syn::Error::new(
1900 + Span::call_site(),
1901 + "a menu's entries cannot be marked yet, so this shape cannot be staged",
1902 + )),
1817 1903 Item::Bind { name, source } => {
1818 1904 let value = source_value(source)?;
1819 1905 Ok(quote!(let #name = #value;))
@@ -1858,6 +1944,15 @@
1858 1944 items
1859 1945 .iter()
1860 1946 .map(|item| match item {
1947 + // A bare `Vec` has nowhere to keep a mark. `Staged<Vec<_>>` is the
1948 + // wrapper that fixes it, and until this builder uses one, a shape
1949 + // whose members accrete into a list cannot be staged. Refused
1950 + // rather than dropped: a residual missing a branch the fill program
1951 + // has is a fault a request discovers.
1952 + Item::Marked { .. } => Err(syn::Error::new(
1953 + Span::call_site(),
1954 + "a panel's members cannot be marked yet, so this shape cannot be staged",
1955 + )),
1861 1956 Item::Bind { name, source } => {
1862 1957 let value = source_value(source)?;
1863 1958 Ok(quote!(let #name = #value;))
@@ -1998,6 +2093,15 @@
1998 2093 items
1999 2094 .iter()
2000 2095 .map(|item| match item {
2096 + // A bare `Vec` has nowhere to keep a mark. `Staged<Vec<_>>` is the
2097 + // wrapper that fixes it, and until this builder uses one, a shape
2098 + // whose members accrete into a list cannot be staged. Refused
2099 + // rather than dropped: a residual missing a branch the fill program
2100 + // has is a fault a request discovers.
2101 + Item::Marked { .. } => Err(syn::Error::new(
2102 + Span::call_site(),
2103 + "a list's rows cannot be marked yet, so this shape cannot be staged",
2104 + )),
2001 2105 Item::Bind { name, source } => {
2002 2106 let value = source_value(source)?;
2003 2107 Ok(quote!(let #name = #value;))
@@ -174,7 +174,7 @@
174 174 }
175 175 }
176 176 Item::Attribute { args, .. } => args.iter().for_each(|arg| walk_arg(arg, found)),
177 - Item::For { body, .. } => walk_items(body, found),
177 + Item::For { body, .. } | Item::Marked { body, .. } => walk_items(body, found),
178 178 Item::Emit(emission) => walk_emission(emission, found),
179 179 }
180 180 }
@@ -137,7 +137,7 @@
137 137 }
138 138
139 139 /// The parameter the staged function takes in place of the request's reads.
140 - const PLAN: &str = "plan";
140 + pub(crate) const PLAN: &str = "plan";
141 141
142 142 /// The staged twin of `name`, which is what an `include` retargets to.
143 143 pub fn staged_name(name: &Ident) -> Ident {
@@ -354,6 +354,15 @@
354 354 name: name.clone(),
355 355 source: self::source(source, counters)?,
356 356 },
357 + // Nothing an author wrote. A twin is built from a declaration, and a
358 + // declaration cannot carry one of these, so meeting one means the
359 + // rewrite has been run over its own output.
360 + Item::Marked { .. } => {
361 + return Err(syn::Error::new(
362 + Span::call_site(),
363 + "a staged twin cannot be staged again",
364 + ));
365 + }
357 366 // A guarded member renders where it was written; a guarded SETTING
358 367 // renders in its container's opening tag, which is before every member
359 368 // regardless of where the declaration put it. The residual marks
@@ -429,11 +438,92 @@
429 438 iterable: iterable.clone(),
430 439 body: inner,
431 440 });
432 - Item::For {
433 - dereferenced: false,
434 - binder: format_ident!("_{}", binder, span = binder.span()),
435 - iterable: plan_read("rows", id),
436 - body: staged,
441 + Item::Marked {
442 + site: id,
443 + varies: crate::ast::Varies::Repeated,
444 + body: vec![Item::For {
445 + dereferenced: false,
446 + binder: format_ident!("_{}", binder, span = binder.span()),
447 + iterable: plan_read("rows", id),
448 + body: staged,
449 + }],
450 + }
451 + }
452 + // A guarded member is placed unconditionally and marked instead.
453 + //
454 + // The guard is what named the members it controls, so the twin says so
455 + // and the renderer records where they landed. Rendering the screen a
456 + // second time with the guard off, to see what went missing, is the
457 + // thing this replaces.
458 + Item::Emit(Emission::Guarded { guard, inner }) => {
459 + let site = counters.guards;
460 + counters.guards += 1;
461 + counters.enter();
462 + let inner = self::emission(inner, counters)?;
463 + let body = counters.exit();
464 + counters.wrote(Fill::Branch {
465 + guard: (*guard).clone(),
466 + body,
467 + });
468 + Item::Marked {
469 + site,
470 + varies: crate::ast::Varies::Absent,
471 + body: vec![Item::Emit(inner)],
472 + }
473 + }
474 + // A dispatch draws the arm the plan asks for, and says how many there
475 + // are. The derivation renders it once per arm and reads each arm's own
476 + // cover, so an arm may hold loops and guards of its own -- which is
477 + // what "the arms are flat" used to be about.
478 + Item::Emit(Emission::Given {
479 + scrutinee,
480 + arms,
481 + otherwise,
482 + }) => {
483 + let Some(otherwise) = otherwise else {
484 + return Err(syn::Error::new(
485 + scrutinee.span(),
486 + "a staged dispatch says what it does when nothing matches: \
487 + a residual holds one arm per position and there has to be \
488 + one to hold",
489 + ));
490 + };
491 + let site = counters.arms;
492 + counters.arms += 1;
493 +
494 + // Every arm's holes are numbered at THIS level rather than inside
495 + // the arm, which is what lets one arm carry a hole another does not.
496 + // See `Fill::Arms`.
497 + let staged = arms
498 + .iter()
499 + .map(|(pattern, arm)| {
500 + Ok((pattern.clone(), Box::new(self::emission(arm, counters)?)))
501 + })
502 + .collect::<Result<Vec<_>>>()?;
503 + let last = Box::new(self::emission(otherwise, counters)?);
504 +
505 + counters.wrote(Fill::Arms {
506 + scrutinee: (*scrutinee).clone(),
507 + patterns: arms.iter().map(|(pattern, _)| pattern.clone()).collect(),
508 + });
509 +
510 + let of = staged.len() + 1;
511 + let taken = plan_arm(site, of);
512 + Item::Marked {
513 + site,
514 + varies: crate::ast::Varies::Arm {
515 + of,
516 + taken: taken.clone(),
517 + },
518 + body: vec![Item::Emit(Emission::Given {
519 + scrutinee: taken,
520 + arms: staged
521 + .into_iter()
522 + .enumerate()
523 + .map(|(at, (_, arm))| (crate::ast::Pattern::Int(at as i64), arm))
524 + .collect(),
525 + otherwise: Some(last),
526 + })],
437 527 }
438 528 }
439 529 Item::Emit(emission) => Item::Emit(self::emission(emission, counters)?),
@@ -5223,6 +5223,13 @@
5223 5223 /// a renderer narrows a region the way it already narrows a table: raise a
5224 5224 /// cutoff over a declared order. `Ranked`'s own docs carry the argument.
5225 5225 pub body: Body,
5226 + /// What a request decides about runs of [`body`](Self::body).
5227 + ///
5228 + /// Empty on every tree built to answer a request. A staged twin fills it,
5229 + /// and it is what lets a renderer bracket the markup a guard, a loop or a
5230 + /// dispatch controls rather than leaving a derivation to find those bounds
5231 + /// by comparing renders. See [`stage::Mark`](crate::stage::Mark).
5232 + pub marks: crate::stage::Marks,
5226 5233 /// How many of [`body`](Self::body) are visible at once.
5227 5234 ///
5228 5235 /// [`layout::Showing::All`] by default, so a description written against
@@ -5561,6 +5568,32 @@
5561 5568 }
5562 5569 }
5563 5570
5571 + impl crate::stage::Marking for Slot {
5572 + /// Members of the body, whichever shape it has.
5573 + fn placed(&self) -> usize {
5574 + self.body.len()
5575 + }
5576 +
5577 + fn mark(&mut self, mark: crate::stage::Mark) {
5578 + self.marks.add(mark);
5579 + }
5580 + }
5581 +
5582 + impl crate::stage::Marking for Row {
5583 + /// Cells first, then the menu, which is the order a row is drawn in.
5584 + ///
5585 + /// One index space over both lists, which is the rule for every container
5586 + /// that draws from more than one: the numbering is the render order, so a
5587 + /// mark reads the same to the renderer as it did to the twin that made it.
5588 + fn placed(&self) -> usize {
5589 + self.cells.len() + self.menu.len()
5590 + }
5591 +
5592 + fn mark(&mut self, mark: crate::stage::Mark) {
5593 + self.marks.add(mark);
5594 + }
5595 + }
5596 +
5564 5597 impl Slot {
5565 5598 /// An empty region under this address.
5566 5599 pub fn new(id: impl Into<String>, kind: RegionKind) -> Self {
@@ -5569,6 +5602,7 @@
5569 5602 kind,
5570 5603 readiness: layout::Readiness::Ready,
5571 5604 body: Body::All(Vec::new()),
5605 + marks: crate::stage::Marks::none(),
5572 5606 name: None,
5573 5607 fed_by: None,
5574 5608 live: false,
@@ -6860,6 +6894,11 @@
6860 6894 /// them. A cell keyed [`CellKey::Named`] is unresolved until
6861 6895 /// [`Table::row`] sees it.
6862 6896 pub cells: Vec<Cell>,
6897 + /// What a request decides about runs of [`cells`](Self::cells).
6898 + ///
6899 + /// Empty answering a request, filled by a staged twin. See
6900 + /// [`Slot::marks`], which carries the argument for both.
6901 + pub marks: crate::stage::Marks,
6863 6902 /// The route that selects this row, if selecting it does anything.
6864 6903 pub activate: Option<Action>,
6865 6904 /// Whether this is the row the detail side is currently showing.
@@ -192,6 +192,185 @@
192 192 pub arms: BTreeSet<(u32, u16, usize)>,
193 193 }
194 194
195 + /// What a request decides about the members one mark covers.
196 + ///
197 + /// The three ways markup varies, and there is no fourth: a run of members is
198 + /// there or is not, is drawn once per element a request brought, or is one of
199 + /// several drawings of which a request picks one.
200 + #[derive(Clone, Copy, Debug, PartialEq, Eq)]
201 + pub enum Varies {
202 + /// A guard: the members are placed, or they are not.
203 + Absent,
204 + /// A loop: the members are one pass, drawn once per element.
205 + Repeated,
206 + /// A dispatch: this is arm `at` of `of`, and a request picks one of them.
207 + Arm {
208 + /// How many arms the dispatch offers.
209 + of: usize,
210 + /// Which one this is.
211 + at: usize,
212 + },
213 + }
214 +
215 + /// One run of a container's members whose markup a request decides.
216 + ///
217 + /// # Why the container carries this and the member does not
218 + ///
219 + /// What varies is never one value in a node, it is a run of a container's
220 + /// members: a guard on a member covers one, a loop body covers however many
221 + /// the pass emitted, and a dispatch's arm covers whatever that arm wrote.
222 + /// Naming the run by its bounds in the container says all three the same way,
223 + /// and leaves every member type untouched.
224 + ///
225 + /// A renderer reads these only while it is staging. Answering a request, a
226 + /// container's marks are empty and nothing consults them.
227 + #[derive(Clone, Copy, Debug, PartialEq, Eq)]
228 + pub struct Mark {
229 + /// Which shape's numbering `id` belongs to. See [`Plan::scope`].
230 + pub scope: u32,
231 + /// The site, numbered by the macro that parsed it.
232 + pub id: u16,
233 + /// What a request decides about it.
234 + pub varies: Varies,
235 + /// The first member covered.
236 + pub from: usize,
237 + /// One past the last member covered.
238 + pub to: usize,
239 + }
240 +
241 + /// The marks a container carries, which is none while a request is answered.
242 + ///
243 + /// A `None` inside rather than an empty `Vec` because every container in the
244 + /// tree has one of these on the serving path and none of them uses it: the
245 + /// marks exist only in the tree a staged twin builds. This way that costs a
246 + /// null pointer per container rather than an allocation.
247 + #[derive(Clone, Debug, Default, PartialEq, Eq)]
248 + pub struct Marks(Option<Box<Vec<Mark>>>);
249 +
250 + impl Marks {
251 + /// Nothing marked, which is what answering a request builds.
252 + #[must_use]
253 + pub const fn none() -> Self {
254 + Self(None)
255 + }
256 +
257 + /// Whether anything here is marked.
258 + #[must_use]
259 + pub fn is_empty(&self) -> bool {
260 + self.0.as_ref().is_none_or(|marks| marks.is_empty())
261 + }
262 +
263 + /// Every mark, outermost first, in the order they were made.
264 + #[must_use]
265 + pub fn all(&self) -> &[Mark] {
266 + self.0.as_ref().map_or(&[], |marks| marks.as_slice())
267 + }
268 +
269 + /// Cover members `from..to` with one mark.
270 + pub fn add(&mut self, mark: Mark) {
271 + self.0.get_or_insert_with(Box::default).push(mark);
272 + }
273 + }
274 +
275 + /// A container a staged twin can mark a run of members in.
276 + ///
277 + /// One trait so the macro emits the same three lines whatever it is building:
278 + /// read where the container is up to, emit, then cover what was emitted. A
279 + /// container that has not implemented this fails to compile at the site that
280 + /// tried to mark it, naming the type, which is the answer to "can this shape be
281 + /// staged" being a compiler question rather than a list somebody maintains.
282 + pub trait Marking {
283 + /// How many members have been placed so far.
284 + ///
285 + /// The unit is whatever the renderer walks, so a container whose members
286 + /// are drawn from more than one list counts them the way it draws them.
287 + fn placed(&self) -> usize;
288 +
289 + /// Cover a run of members with one mark.
290 + fn mark(&mut self, mark: Mark);
291 +
292 + /// Take a spliced shape's own marks, shifted to where its members landed.
293 + ///
294 + /// What `include each` needs: the child numbered its members from zero and
295 + /// they are now at an offset in the parent, so every bound moves by the
296 + /// same amount. The child's scope is its own, so nothing else changes.
297 + fn absorb(&mut self, marks: &Marks, at: usize) {
298 + for mark in marks.all() {
299 + self.mark(Mark {
300 + from: mark.from + at,
301 + to: mark.to + at,
302 + ..*mark
303 + });
304 + }
305 + }
306 + }
307 +
308 + /// One mark, resolved to the bytes the renderer wrote for it.
309 + ///
310 + /// What a [`Mark`] becomes once something has drawn it: the same site, and the
311 + /// span of output it covers. This is renderer-agnostic on purpose -- a terminal
312 + /// or an immediate renderer that wanted a compiled path would record the same
313 + /// thing over its own output.
314 + #[derive(Clone, Copy, Debug, PartialEq, Eq)]
315 + pub struct Cover {
316 + /// The site, as the mark named it.
317 + pub scope: u32,
318 + /// The site, as the mark named it.
319 + pub id: u16,
320 + /// What a request decides about it.
321 + pub varies: Varies,
322 + /// Where the covered markup starts.
323 + pub at: usize,
324 + /// One past where it ends.
325 + pub to: usize,
326 + }
327 +
328 + /// A staged twin's answer: what it built, and what varies in it.
329 + ///
330 + /// A twin's return type is free, because nothing but the derivation calls one.
331 + /// That is what lets a shape answering a bare `Vec` be staged at all: the value
332 + /// keeps its own type and the marks ride beside it until the container that
333 + /// splices them in absorbs them.
334 + #[derive(Clone, Debug, Default, PartialEq, Eq)]
335 + pub struct Staged<T> {
336 + /// What the shape built.
337 + pub value: T,
338 + /// What a request decides about it, numbered from this value's own zero.
339 + pub marks: Marks,
340 + }
341 +
342 + impl<T> Marking for Staged<Vec<T>> {
343 + /// The members pushed so far.
344 + ///
345 + /// A bare `Vec` is the one container with nowhere to keep a mark, which is
346 + /// what this wrapper is for: the value keeps its own type and the marks
347 + /// ride beside it until whatever holds the list absorbs them.
348 + fn placed(&self) -> usize {
349 + self.value.len()
350 + }
351 +
352 + fn mark(&mut self, mark: Mark) {
353 + self.marks.add(mark);
354 + }
355 + }
356 +
357 + impl<T> Staged<T> {
358 + /// A value with nothing varying in it.
359 + #[must_use]
360 + pub const fn plain(value: T) -> Self {
361 + Self {
362 + value,
363 + marks: Marks::none(),
364 + }
365 + }
366 +
367 + /// A value and the marks that were made in it.
368 + #[must_use]
369 + pub const fn new(value: T, marks: Marks) -> Self {
370 + Self { value, marks }
371 + }
372 + }
373 +
195 374 /// Where a plan writes down what it was asked, when anyone is listening.
196 375 #[derive(Clone, Default)]
197 376 struct Trace(Option<Rc<RefCell<Sites>>>);
@@ -2675,7 +2675,19 @@
2675 2675 content: Markup(value),
2676 2676 })
2677 2677 .collect();
2678 - cells_html_into(columns, &emitted, opts, out);
2678 + let mut marked = crate::stage::Cursor::open(&cells.marks);
2679 + if marked.watching() {
2680 + // Told where each block went rather than looking for it. A cell that
2681 + // renders to nothing cannot be searched for, and a cell whose markup
2682 + // repeats in the same row would be found twice.
2683 + let mut placed = Vec::new();
2684 + makeover_webview::list::cells_html_placed(columns, &emitted, opts, out, &mut placed);
2685 + for block in &placed {
2686 + marked.wrote(block.start, block.end);
2687 + }
2688 + } else {
2689 + cells_html_into(columns, &emitted, opts, out);
2690 + }
2679 2691
2680 2692 // After the cells, and outside the grid: a menu is not a value in the row,
2681 2693 // so it takes no column and the head emits nothing to match it. That is the
@@ -2698,11 +2710,14 @@
2698 2710 class_attr(&["table-row-menu"], opts, out);
2699 2711 out.push_str(" data-menu=\"row\" hidden>");
2700 2712 for act in &cells.menu {
2713 + marked.starts(out);
2701 2714 act_html(act, opts, &Doc::bare(), out);
2715 + marked.ends(out);
2702 2716 }
2703 2717 out.push_str("</div>");
2704 2718 }
2705 2719
2720 + marked.close(&cells.marks);
2706 2721 out.push_str(RowLook::Table.close_tag());
2707 2722 }
2708 2723
@@ -4266,7 +4281,9 @@
4266 4281 // element it is. The wrapper appears only here, so a region that shows
4267 4282 // everything -- which is every region written before `Showing` existed
4268 4283 // -- emits exactly the markup it always did.
4284 + let mut marked = crate::stage::Cursor::open(&slot.marks);
4269 4285 for (at, placed) in slot.body.iter().enumerate() {
4286 + marked.starts(out);
4270 4287 out.push_str("<div");
4271 4288 frame_attrs(slot, at, current == Some(at), opts, out);
4272 4289 out.push('>');
@@ -4291,7 +4308,9 @@
4291 4308 _ => ranked_html(placed, opts, doc, out),
4292 4309 }
4293 4310 out.push_str("</div>");
4311 + marked.ends(out);
4294 4312 }
4313 + marked.close(&slot.marks);
4295 4314
4296 4315 if !labelled {
4297 4316 run_html(slot, true, opts, doc, out);
@@ -4301,9 +4320,13 @@
4301 4320 repeating_html(slot, repeating, opts, doc, out);
4302 4321 } else {
4303 4322 run_html(slot, false, opts, doc, out);
4323 + let mut marked = crate::stage::Cursor::open(&slot.marks);
4304 4324 for placed in slot.body.iter() {
4325 + marked.starts(out);
4305 4326 ranked_html(placed, opts, doc, out);
4327 + marked.ends(out);
4306 4328 }
4329 + marked.close(&slot.marks);
4307 4330 }
4308 4331
4309 4332 // The host's markup for this region, after whatever the description put
@@ -30,11 +30,156 @@
30 30 //! would be reporting a structure the screen does not have. It stops instead.
31 31
32 32 use quasi_http::Serves as _;
33 - use quasi_router::stage::{Op, Plan, Residual, find_sentinel, read_sentinel};
33 + use quasi_router::stage::{Cover, Op, Plan, Residual, Varies, find_sentinel, read_sentinel};
34 34 use quasi_router::{Node, stage};
35 35
36 36 use crate::Webview;
37 37
38 + /// Where a container's members landed in the output, while one is being staged.
39 + ///
40 + /// # Why this is a thread-local and not a parameter
41 + ///
42 + /// The renderer's functions take `opts: &Emit`, and `Emit` is
43 + /// `makeover_webview`'s: a published crate that knows nothing about staging and
44 + /// should not learn. The alternative is a second parameter on every rendering
45 + /// function in `node.rs`, which is over a hundred signatures and every call
46 + /// site, to carry a value that is `None` on every request this server answers.
47 + ///
48 + /// Staging is a build-time pass over one shape at a time, entered by
49 + /// [`derive`] and left before it returns, so there is no second thread and no
50 + /// re-entry. Serving reads the flag once per marked container and finds it
51 + /// empty.
52 + mod recording {
53 + use std::cell::RefCell;
54 +
55 + use quasi_router::stage::{Cover, Mark, Marks};
56 +
57 + thread_local! {
58 + static COVERS: RefCell<Option<Vec<Cover>>> = const { RefCell::new(None) };
59 + }
60 +
61 + /// Record covers for the duration of one derivation.
62 + pub(super) fn during<R>(body: impl FnOnce() -> R) -> (R, Vec<Cover>) {
63 + COVERS.with(|covers| *covers.borrow_mut() = Some(Vec::new()));
64 + let answer = body();
65 + let covers = COVERS.with(|covers| covers.borrow_mut().take());
66 + (
67 + answer,
68 + covers.expect("the derivation took its own recorder away"),
69 + )
70 + }
71 +
72 + /// Whether anything is listening.
73 + fn listening() -> bool {
74 + COVERS.with(|covers| covers.borrow().is_some())
75 + }
76 +
77 + fn record(cover: Cover) {
78 + COVERS.with(|covers| {
79 + if let Some(covers) = covers.borrow_mut().as_mut() {
80 + covers.push(cover);
81 + }
82 + });
83 + }
84 +
85 + /// Where one container's members landed, as they are written.
86 + ///
87 + /// Nothing at all when no derivation is running or the container carries no
88 + /// marks, which is every container on the serving path.
89 + ///
90 + /// Members are recorded as ranges rather than as boundaries because they
91 + /// are not always adjacent: a row's cells are written back to back, a
92 + /// region's members have the frame wrapper between them, and a menu comes
93 + /// after markup that belongs to neither. A range says what one member wrote
94 + /// and claims nothing about the gaps.
95 + pub struct Cursor(Option<Vec<(usize, usize)>>);
96 +
97 + impl Cursor {
98 + /// Start watching a container's members.
99 + #[must_use]
100 + pub fn open(marks: &Marks) -> Self {
101 + Self((!marks.is_empty() && listening()).then(Vec::new))
102 + }
103 +
104 + /// Whether anything is being recorded for this container.
105 + ///
106 + /// For a caller whose two ways of writing a container differ in cost:
107 + /// asking `makeover-webview` to say where every cell went is a `Vec`
108 + /// per row, and a request should not pay for one.
109 + #[must_use]
110 + pub const fn watching(&self) -> bool {
111 + self.0.is_some()
112 + }
113 +
114 + /// A member is about to be written.
115 + pub fn starts(&mut self, out: &str) {
116 + if let Some(members) = self.0.as_mut() {
117 + members.push((out.len(), out.len()));
118 + }
119 + }
120 +
121 + /// The member that was being written is finished.
122 + pub fn ends(&mut self, out: &str) {
123 + if let Some(members) = self.0.as_mut()
124 + && let Some(last) = members.last_mut()
125 + {
126 + last.1 = out.len();
127 + }
128 + }
129 +
130 + /// A member whose bounds the writer already knows.
131 + ///
132 + /// For a container that hands its assembly to `makeover-webview` and is
133 + /// told afterwards where each piece went.
134 + pub fn wrote(&mut self, at: usize, to: usize) {
135 + if let Some(members) = self.0.as_mut() {
136 + members.push((at, to));
137 + }
138 + }
139 +
140 + /// The container is finished: turn its marks into covers.
141 + ///
142 + /// A mark names members and the record says what each one wrote, so
143 + /// `from..to` is the start of the first and the end of the last, and
144 + /// nothing is searched for. A mark naming a member the container did
145 + /// not draw is a twin that marked what it did not emit, which is a bug
146 + /// rather than something to paper over, so it says so.
147 + pub fn close(self, marks: &Marks) {
148 + let Some(members) = self.0 else {
149 + return;
150 + };
151 + for &Mark {
152 + scope,
153 + id,
154 + varies,
155 + from,
156 + to,
157 + } in marks.all()
158 + {
159 + assert!(
160 + to > from,
161 + "mark {scope:x}/{id} covers no member at all ({from}..{to})"
162 + );
163 + let (Some(first), Some(last)) = (members.get(from), members.get(to - 1)) else {
164 + panic!(
165 + "mark {scope:x}/{id} covers members {from}..{to} and the container drew {}",
166 + members.len()
167 + );
168 + };
169 + record(Cover {
170 + scope,
171 + id,
172 + varies,
173 + at: first.0,
174 + to: last.1,
175 + });
176 + }
177 + }
178 + }
179 + }
180 +
181 + pub use recording::Cursor;
182 +
38 183 /// What a request's value does on its way into a residual's gap.
39 184 ///
40 185 /// A small closed set rather than a blanket over `Display`, for two reasons.
@@ -95,496 +240,144 @@
95 240 u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, bool, char
96 241 );
97 242
98 - /// A span of the base render, and what decides it.
243 + /// A run of the base render, and what a request decides about it.
99 244 ///
100 - /// `at` is where the span is finally placed; `first` is the earliest placement
101 - /// that is equally consistent with the two renders it was read from. See
102 - /// [`between`] for why a span has a range at all, and [`placed`] for what picks
103 - /// one point in it.
245 + /// Read off a [`Cover`] the renderer recorded, so `at` and `to` are where the
246 + /// markup was written and not where a comparison guessed it might be.
104 247 #[derive(Clone, PartialEq, Eq, Debug)]
105 248 struct Span {
106 249 at: usize,
107 250 to: usize,
108 - repeats: bool,
109 - /// The earliest `at` that rebuilds the same string. `at` is the latest.
110 - first: usize,
111 - /// What each of a dispatch's other cases puts in this range.
251 + varies: Varies,
252 + /// The other arms of a dispatch, already built, in declaration order.
112 253 ///
113 - /// Empty for everything that is not a dispatch. The base render holds the
114 - /// first case, so these are cases 1..n in declaration order.
115 - cases: Vec<String>,
116 - /// What stands in this range when the guard does not pass.
117 - ///
118 - /// `None` for every span that is a gap: a branch is markup that is there or
119 - /// is not, and a loop is a body that repeats. `Some` is a SWAP, where the
120 - /// render with the guard off holds different markup rather than less of it,
121 - /// and the two together become [`Op::Arms`]. See [`swapped`].
122 - other: Option<String>,
254 + /// Empty for a guard and for a loop. The base render holds arm 0, so these
255 + /// are arms 1..n, each built from its own render and therefore holding
256 + /// whatever structure is inside it.
257 + arms: Vec<Vec<Op>>,
123 258 }
124 259
125 260 /// The residual of one staged shape.
126 261 ///
127 262 /// `shape` is the staged twin, which takes a plan and answers a `Node`. It is
128 - /// called many times and must be free of side effects, which it is: a staged
129 - /// twin reads nothing but its plan.
263 + /// called once, plus once more per arm of each dispatch, and must be free of
264 + /// side effects, which it is: a staged twin reads nothing but its plan.
265 + ///
266 + /// # How a boundary is found
267 + ///
268 + /// It is not found. The twin marks the members each guard, loop and dispatch
269 + /// covers, the renderer records where it wrote them, and this reads the record.
270 + /// Nothing here compares two renders, measures a common prefix, or decides
271 + /// where a deletion "would have" gone.
272 + ///
273 + /// A dispatch is the one site that still renders more than once, and for a
274 + /// reason that is not about finding anything: the arm a request does not take
275 + /// was never drawn, so there is nothing to compile until it is. Each arm is
276 + /// rendered under its own plan and read off its own cover, and because the
277 + /// cover is exact, an arm may hold loops and guards of its own.
130 278 ///
131 279 /// # Panics
132 280 ///
133 - /// If the screen's branches and loops do not nest, or if a span does not put
134 - /// back the way it came out. Both mean the derivation has read a structure the
135 - /// screen does not have, and a residual built on one would serve markup that is
136 - /// quietly wrong. Loud is the only safe answer.
281 + /// If the marked runs do not nest, if an arm's render differs outside the arm,
282 + /// or if a mark names a member its container did not draw. Each means the tree
283 + /// and the markup disagree about the screen's structure, and a residual built
284 + /// on one would serve markup that is quietly wrong.
137 285 #[must_use]
138 286 pub fn derive<F>(webview: &Webview, shape: F) -> Residual
139 287 where
140 288 F: Fn(&Plan) -> Node,
141 289 {
142 - let render = |plan: &Plan| webview.fragment(&shape(plan));
290 + let plan = Plan::full(1);
291 + let render = |plan: &Plan| recording::during(|| webview.fragment(&shape(plan)));
143 292
144 - // One row per loop and every guard passing. A guard that does not pass
145 - // places nothing, so a residual read off a plan that failed one would be
146 - // missing a branch rather than carrying it.
147 - let base_plan = Plan::full(1);
148 - let base = render(&base_plan);
149 -
150 - // One traced render names every guard and every loop the screen reaches,
151 - // so the spans below are read for sites that are known to exist rather than
152 - // hunted for. A shape carrying only markup and a loop leaves no sentinel to
153 - // be found by, which is why this is a record and not a scan of the output.
154 - let (traced, sites) = base_plan.clone().tracing();
155 - let _ = render(&traced);
156 - let sites = sites.borrow();
293 + // Every guard passes and every loop runs once, so the base render holds one
294 + // of everything the screen can draw. A guard that did not pass would leave
295 + // a residual with no branch to fill.
296 + let (base, covers) = render(&plan);
157 297
158 298 let mut spans = Vec::new();
159 - for &(scope, id) in &sites.loops {
160 - let wider = render(&base_plan.clone().with_rows_at(scope, id, 2));
161 - let Some(span) = grown(&base, &wider) else {
162 - unheld("loop", scope, id, &base, &wider);
299 + for cover in &covers {
300 + let arms = match cover.varies {
301 + Varies::Arm { of, at: 0 } => other_arms(&render, &plan, &base, cover, of),
302 + // Arms past the first are read from their own render, below the one
303 + // that names them, so they are not spans of the base at all.
304 + Varies::Arm { .. } => continue,
305 + Varies::Absent | Varies::Repeated => Vec::new(),
163 306 };
164 307 spans.push(Span {
165 - repeats: true,
166 - ..span
308 + at: cover.at,
309 + to: cover.to,
310 + varies: cover.varies,
311 + arms,
167 312 });
168 313 }
169 - let marks = sentinels(&base);
170 - // A dispatch is arms by construction rather than by discovery: its cases
171 - // replace each other at one position, which is what `Op::Arms` is. Rendered
172 - // once per case, and the region they disagree over is the arms.
173 - for &(scope, id, count) in &sites.arms {
174 - let others = (1..count)
175 - .map(|arm| render(&base_plan.clone().with_arm_at(scope, id, arm)))
176 - .collect::<Vec<_>>();
177 - let Some(span) = dispatched(&base, &others, &marks) else {
178 - unheld(
179 - "dispatch",
180 - scope,
181 - id,
182 - &base,
183 - others.first().map_or(&base, |first| first),
184 - );
185 - };
186 - spans.push(span);
187 - }
188 - for &(scope, id) in &sites.guards {
189 - let without = render(&base_plan.clone().with_guard_at(scope, id, false));
190 - let span = match shrunk(&base, &without) {
191 - Some(span) => Span {
192 - repeats: false,
193 - ..span
194 - },
195 - // Not a deletion. It may still be a SWAP -- markup replaced rather
196 - // than removed -- which is a shape the residual has since
197 - // `Op::Arms`. Anything else is refused.
198 - None => match swapped(&base, &without, &marks) {
199 - Some(span) => span,
200 - None => unheld("guard", scope, id, &base, &without),
201 - },
202 - };
203 - spans.push(span);
204 - }
205 314
206 315 Residual::new(tree(&base, &mut spans))
207 316 }
208 317
209 - /// Refuse a site the residual has no shape for, and say which kind it is.
318 + /// Every arm of one dispatch but the first, compiled from its own render.
210 319 ///
211 - /// Reached when varying one guard or one loop does not change the render the
212 - /// way a gap changes it. Three things can be behind that, and the message
213 - /// separates them because the fix is different for each.
320 + /// The base render drew arm 0. Each of the others is drawn under a plan that
321 + /// asks for it, and its cover says exactly which bytes it wrote -- so the arm
322 + /// is compiled the same way the whole screen is, with its own marks inside it.
214 323 ///
215 - /// # Why this is loud here rather than left to the filler
324 + /// # What is checked, and why it is fatal
216 325 ///
217 - /// The filler already refuses the result: a residual with no branch where the
218 - /// fill program has a guard panics in [`Cursor::branch`]. But it refuses it on a
219 - /// REQUEST, so a screen without a filling test ships and takes its first reader
220 - /// down, and it can be quiet instead -- two sites that both fail this way and
221 - /// whose bodies happen to have the same shape line up, and the filler answers
222 - /// one site's branch with the other's arm. Refusing here moves both cases to the
223 - /// build machine, where the residual is generated.
224 - ///
225 - /// The commonest cause by a distance is a **substitution**: markup that replaces
226 - /// other markup rather than being absent. A guard derives as a branch only when
227 - /// turning it off deletes bytes, so a renderer that draws one thing when a
228 - /// value is present and a different thing when it is not gives the derivation
229 - /// nothing to measure. `rest_html` drew a disabled control for a pager direction
230 - /// with no address until 2026-09-08, and `rest_strip_html` still draws the page
231 - /// a reader is on as a readout rather than a control. Both are two markups at
232 - /// one position, which is what `Op::Arms` is for.
233 - #[cold]
234 - fn unheld(kind: &str, scope: u32, id: u16, base: &str, varied: &str) -> ! {
235 - let what = if base == varied {
236 - "it renders exactly the same either way, so it places nothing and the \
237 - residual has no gap to hold. A member that is sometimes nothing is a \
238 - member whose value is sometimes empty, and empty is a different shape \
239 - rather than a shorter one"
240 - } else if base.len() == varied.len() {
241 - "the two renders are the same length and differ in their bytes, which \
242 - is a substitution rather than a gap: the screen is saying two markups \
243 - at one position"
244 - } else {
245 - "what changes is not a clean insertion or deletion, so it is a \
246 - substitution rather than a gap: the screen is saying two markups at \
247 - one position, and a residual holds one"
248 - };
249 - panic!(
250 - "this screen cannot be staged: {kind} {id} in scope {scope} varies the \
251 - render in a way a residual cannot hold, because {what}.\n\nWith the \
252 - {kind} on, the render is {} bytes; with it off, {}.",
253 - base.len(),
254 - varied.len(),
255 - );
326 + /// That the two renders agree everywhere outside the arm. A dispatch is two
327 + /// markups at ONE position; if picking a different arm also changed something
328 + /// elsewhere, the residual would carry one arm's version of that change for
329 + /// every request, whichever arm it later took.
330 + fn other_arms<R>(render: &R, plan: &Plan, base: &str, cover: &Cover, of: usize) -> Vec<Vec<Op>>
331 + where
332 + R: Fn(&Plan) -> (String, Vec<Cover>),
333 + {
334 + let mut arms = Vec::with_capacity(of.saturating_sub(1));
335 + for arm in 1..of {
336 + let (drawn, covers) = render(&plan.clone().with_arm_at(cover.scope, cover.id, arm));
337 + let Some(here) = covers
338 + .iter()
339 + .find(|other| other.scope == cover.scope && other.id == cover.id)
340 + else {
341 + panic!(
342 + "dispatch {:x}/{} drew arm {arm} nowhere, so the screen has an arm \
343 + that no request can reach",
344 + cover.scope, cover.id
345 + );
346 + };
347 + assert!(
348 + base[..cover.at] == drawn[..here.at] && base[cover.to..] == drawn[here.to..],
349 + "dispatch {:x}/{} arm {arm} changed markup outside its own position, \
350 + so it is not two markups at one place",
351 + cover.scope,
352 + cover.id
353 + );
354 + let mut inside = spans_within(&covers, here);
355 + arms.push(build(&drawn, here.at, here.to, &mut inside, &mut 0));
356 + }
357 + arms
256 358 }
257 359
258 - /// The span `wide` holds one more time than `narrow` does, in `narrow`.
360 + /// The marked runs strictly inside one cover, as spans of that render.
259 361 ///
260 - /// `narrow` is `P R Q` and `wide` is `P R R Q`, so the extra copy is the length
261 - /// difference. [`between`] finds where that copy sits in `wide`, which is the
262 - /// SECOND one; the body's own span in `narrow` is the first, ending where the
263 - /// two renders stop agreeing. Getting that conversion wrong is what put a
264 - /// table's rows halfway down the next section.
265 - ///
266 - /// Checked by taking the copy back out of `wide`. The two copies are the same
267 - /// string, so removing either gives `narrow` back, which is what makes the
268 - /// check a check on the length and the position rather than on which one.
269 - fn grown(narrow: &str, wide: &str) -> Option<Span> {
270 - let length = wide.len().checked_sub(narrow.len())?;
271 - if length == 0 {
272 - return None;
273 - }
274 - let found = between(narrow, wide, length)?;
275 - let mut rebuilt = String::with_capacity(narrow.len());
276 - rebuilt.push_str(&wide[..found.at]);
277 - rebuilt.push_str(&wide[found.at + length..]);
278 - if rebuilt != narrow {
279 - return None;
280 - }
281 -
282 - let at = found.at.checked_sub(length)?;
283 - let to = found.at;
284 - // The two placements convert differently, and that is the whole of this
285 - // conversion rather than an optimisation of it.
286 - //
287 - // `wide` is `P F F Q` and the removable copy sits anywhere from `|P|` to
288 - // `|P| + |F|`. The LATEST of those is one copy after the body's own place,
289 - // so it converts by subtracting a body length. The EARLIEST is already the
290 - // body's own place -- `|P|` is where the first copy starts in `wide` and in
291 - // `narrow` alike -- so it converts by not moving at all.
292 - //
293 - // Subtracting from both is what this did until 2026-09-08 and it was wrong
294 - // twice over. It underflowed whenever the body was longer than the markup
295 - // before it, which a table whose rows outrun its heading row always is;
296 - // that was read as `?`, answered `None`, and dropped the loop, leaving the
297 - // residual holding one pass as a literal while the fill program still had
298 - // its `Repeat`. Answering `at` instead stopped the drop and left a window
299 - // one point wide, which is the second failure: `placed` had nowhere to
300 - // slide to.
301 - //
302 - // That matters because the prefix scan can run PAST the body. `</div>` and
303 - // `<div class="figure"` share their `<`, so the latest placement is one
304 - // byte beyond the copy and the converted `at` is one byte inside the
305 - // opening tag -- markup no emission produced. With a window that reaches
306 - // back to `first`, `placed` corrects it; without one, the loop sat a byte
307 - // off its own body and overlapped a guard inside it, which `build` then
308 - // refused as a structure that is not a tree. A guarded figure in a stats
309 - // strip is the site: MNW's `/dashboard/tabs/analytics`.
310 - let first = found.first.min(at);
311 - (narrow.is_char_boundary(at) && narrow.is_char_boundary(to)).then_some(Span {
312 - at,
313 - to,
314 - repeats: true,
315 - first,
316 - other: None,
317 - cases: Vec::new(),
318 - })
319 - }
320 -
321 - /// The span `full` holds and `without` does not.
322 - ///
323 - /// The same arithmetic the other way round: a guard that stops passing deletes
324 - /// what it placed, and the deletion is between the common prefix and the common
325 - /// suffix. Checked by taking it back out of `full`.
326 - fn shrunk(full: &str, without: &str) -> Option<Span> {
327 - let length = full.len().checked_sub(without.len())?;
328 - if length == 0 {
329 - return None;
330 - }
331 - let span = between(without, full, length)?;
332 - let mut rebuilt = String::with_capacity(without.len());
333 - rebuilt.push_str(&full[..span.at]);
334 - rebuilt.push_str(&full[span.at + length..]);
335 - (rebuilt == without).then_some(span)
336 - }
337 -
338 - /// Where `long` differs from `short`, given that it is longer by `length`.
339 - ///
340 - /// Between the common prefix and the common suffix. Both are needed: a prefix
341 - /// alone cannot tell a body from the text that follows it when the two begin
342 - /// the same way, which is the ambiguity that makes a search necessary and a
343 - /// named site unnecessary.
344 - ///
345 - /// # The deletion is a range, not a point
346 - ///
347 - /// When what is deleted shares a boundary with what follows it, several
348 - /// placements rebuild `short` from `long` **exactly**, so no check on one
349 - /// placement can tell them apart. Two adjacent described regions are the site,
350 - /// and they are not an edge case: every region opens `<section id="` and closes
351 - /// `</section>`, so a pair of guarded siblings always shares a prefix, and MNW's
352 - /// `/fan-plus` shares 22 bytes of it.
353 - ///
354 - /// So this answers with the whole range. `first` is the earliest placement, from
Lines truncated