Skip to main content

max / quasi

Read a residual off a staged shape, with its branches and repeats marked A staged twin renders the screen with sentinels where values go. This turns that render into a residual: the same markup with every branch and every loop marked, so filling it is a walk rather than a render. Boundaries are asked for rather than searched for. The macro numbered every guard and every loop, so the derivation renders twice with exactly one of them changed and reads the difference. A loop run once and then twice grows by its body; a guard that stops passing shrinks by whatever it placed. The spike had to hunt for something that repeated, on the assumption the output was prefix + body * n + suffix, and its own docs record that assumption as the source of its one bug. Two things this cost, both worth writing down. Walking the call tree by scope to find the sites is combinatorial: the first attempt probed 32 sites deep to six levels and wedged the test runner. The plan now writes down what it was asked, so one traced render names every site exactly, including a shape that carries only markup and a loop and would leave no sentinel to be found by. And the two renders a loop is measured with are not in the same coordinates. `between` locates the inserted copy in the WIDER render, which is the second one; the body's span in the base is the first. Using one for the other put a table's rows halfway down the next section.
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 17:51 UTC
Signed with PGP, not checked
Commit: bf0190c72b5d6964fdc81d6489ff50a5bd4cb7eb
Parent: e49177c
6 files changed, +588 insertions, -10 deletions
M Cargo.lock +4 -4
@@ -6281,10 +6281,6 @@
6281 6281 "winnow 1.0.4",
6282 6282 ]
6283 6283
6284 - [[patch.unused]]
6285 - name = "quasi-type"
6286 - version = "0.1.3"
6287 -
6288 6284 [[patch.unused]]
6289 6285 name = "synckit-client"
6290 6286 version = "0.10.0"
@@ -6304,3 +6300,7 @@
6304 6300 [[patch.unused]]
6305 6301 name = "tagtree"
6306 6302 version = "0.4.1"
6303 +
6304 + [[patch.unused]]
6305 + name = "quasi-type"
6306 + version = "0.1.3"
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-webview"
3 - version = "0.101.1"
3 + version = "0.101.2"
4 4 description = "The webview renderer for quasi: a screen description in, an htmx document out"
5 5 edition.workspace = true
6 6 rust-version.workspace = true
@@ -270,3 +270,84 @@
270 270 assert_eq!(two.matches("table-row").count(), 4);
271 271 }
272 272 }
273 +
274 + #[cfg(test)]
275 + mod residual_tests {
276 + use quasi_http::Serves as _;
277 + use quasi_router::stage::{Op, Plan, Residual};
278 + use quasi_webview::Webview;
279 +
280 + use super::*;
281 +
282 + /// Replay a residual with sentinels still in it, which is what the staged
283 + /// twin would have rendered at this many rows.
284 + fn replay(ops: &[Op], rows: usize, out: &mut String) {
285 + for op in ops {
286 + match op {
287 + Op::Lit(text) => out.push_str(text),
288 + Op::Hole { scope, id } => {
289 + out.push_str(&quasi_router::stage::sentinel_at(*scope, *id));
290 + }
291 + Op::Branch(body) => replay(body, rows, out),
292 + Op::Loop(body) => {
293 + for _ in 0..rows {
294 + replay(body, rows, out);
295 + }
296 + }
297 + }
298 + }
299 + }
300 +
301 + fn residual() -> Residual {
302 + quasi_webview::stage::derive(&Webview::new(), pane_staged)
303 + }
304 +
305 + /// The derivation found structure, rather than one flat run of markup.
306 + #[test]
307 + fn the_residual_has_a_branch_and_a_loop_in_it() {
308 + let residual = residual();
309 +
310 + fn count(ops: &[Op], branches: &mut usize, loops: &mut usize, holes: &mut usize) {
311 + for op in ops {
312 + match op {
313 + Op::Lit(_) => {}
314 + Op::Hole { .. } => *holes += 1,
315 + Op::Branch(body) => {
316 + *branches += 1;
317 + count(body, branches, loops, holes);
318 + }
319 + Op::Loop(body) => {
320 + *loops += 1;
321 + count(body, branches, loops, holes);
322 + }
323 + }
324 + }
325 + }
326 +
327 + let (mut branches, mut loops, mut holes) = (0, 0, 0);
328 + count(residual.ops(), &mut branches, &mut loops, &mut holes);
329 +
330 + // Seven guards in the pane and two loops, one per table.
331 + assert_eq!(loops, 2, "{:#?}", residual.ops());
332 + assert!(branches >= 6, "branches {branches}: {:#?}", residual.ops());
333 + assert!(holes > 0, "no holes at all");
334 + }
335 +
336 + /// The residual is the renderer's own output with the repeats marked, so
337 + /// replaying it at any row count has to give the twin's render back.
338 + #[test]
339 + fn replaying_the_residual_reproduces_the_staged_render() {
340 + let webview = Webview::new();
341 + let residual = residual();
342 +
343 + for rows in [1, 2, 5, 25] {
344 + let mut replayed = String::new();
345 + replay(residual.ops(), rows, &mut replayed);
346 + assert_eq!(
347 + webview.fragment(&pane_staged(&Plan::full(rows))),
348 + replayed,
349 + "the residual and the renderer disagree at {rows} rows"
350 + );
351 + }
352 + }
353 + }
@@ -30,7 +30,10 @@
30 30 //! which `ZQH`/`HQZ` is by construction rather than by hope: a residual carries
31 31 //! its own check that no literal chunk contains one.
32 32
33 + use std::cell::RefCell;
34 + use std::collections::BTreeSet;
33 35 use std::iter::{Repeat, Take, repeat};
36 + use std::rc::Rc;
34 37
35 38 /// The three-character opening of a sentinel.
36 39 const OPEN: &str = "ZQH";
@@ -55,8 +58,11 @@
55 58 type Scope = u32;
56 59
57 60 /// The stand-in for one value a request would have brought.
61 + ///
62 + /// Public because reading a residual back means writing the sentinel a hole
63 + /// stands for, which is how a derivation checks itself.
58 64 #[must_use]
59 - fn sentinel_at(scope: Scope, id: u16) -> String {
65 + pub fn sentinel_at(scope: u32, id: u16) -> String {
60 66 format!("{OPEN}{:04x}{id:04x}{CLOSE}", scope & 0xffff)
61 67 }
62 68
@@ -94,6 +100,42 @@
94 100 pub sites: u16,
95 101 }
96 102
103 + /// Every site one evaluation actually read, by scope and id.
104 + ///
105 + /// A derivation has to know where the guards and the loops are before it can
106 + /// vary one, and guessing is not available: a scope is built by descending the
107 + /// call tree, so the set of them is only knowable by descending it. Rather than
108 + /// probe, the plan writes down what it was asked. One render names every site
109 + /// exactly, including the ones in shapes that carry no values of their own and
110 + /// would therefore leave no sentinel to be found by.
111 + #[derive(Clone, Debug, Default)]
112 + pub struct Sites {
113 + /// The guards that were consulted.
114 + pub guards: BTreeSet<(u32, u16)>,
115 + /// The loops that were run.
116 + pub loops: BTreeSet<(u32, u16)>,
117 + /// The holes that were filled.
118 + pub holes: BTreeSet<(u32, u16)>,
119 + }
120 +
121 + /// Where a plan writes down what it was asked, when anyone is listening.
122 + #[derive(Clone, Default)]
123 + struct Trace(Option<Rc<RefCell<Sites>>>);
124 +
125 + impl std::fmt::Debug for Trace {
126 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 + f.write_str(if self.0.is_some() { "tracing" } else { "quiet" })
128 + }
129 + }
130 +
131 + impl Trace {
132 + fn saw(&self, which: fn(&mut Sites) -> &mut BTreeSet<(u32, u16)>, at: (u32, u16)) {
133 + if let Some(sites) = &self.0 {
134 + which(&mut sites.borrow_mut()).insert(at);
135 + }
136 + }
137 + }
138 +
97 139 /// What one symbolic evaluation of a shape is told.
98 140 ///
99 141 /// A derivation asks the same two questions over and over: run everything, or
@@ -113,6 +155,8 @@
113 155 guard_overrides: Vec<((Scope, u16), bool)>,
114 156 /// Which arm each dispatch takes, by scope and id. Absent means the first.
115 157 arms: Vec<((Scope, u16), usize)>,
158 + /// Where to write down what this plan is asked, if anyone is listening.
159 + trace: Trace,
116 160 }
117 161
118 162 impl Default for Plan {
@@ -132,9 +176,27 @@
132 176 row_overrides: Vec::new(),
133 177 guard_overrides: Vec::new(),
134 178 arms: Vec::new(),
179 + trace: Trace::default(),
135 180 }
136 181 }
137 182
183 + /// This plan, and the record of every site it is asked about.
184 + ///
185 + /// One render through the returned plan names every guard, loop and hole
186 + /// the screen actually reaches, which is what a derivation needs before it
187 + /// can vary one of them.
188 + #[must_use]
189 + pub fn tracing(self) -> (Self, Rc<RefCell<Sites>>) {
190 + let sites = Rc::new(RefCell::new(Sites::default()));
191 + (
192 + Self {
193 + trace: Trace(Some(Rc::clone(&sites))),
194 + ..self
195 + },
196 + sites,
197 + )
198 + }
199 +
138 200 /// Every guard passing and every loop running `rows` times.
139 201 ///
140 202 /// The shape a screen has when it is full, which is the render a residual
@@ -157,10 +219,7 @@
157 219 #[must_use]
158 220 pub fn enter(&self, site: u16) -> Self {
159 221 Self {
160 - scope: self
161 - .scope
162 - .wrapping_mul(31)
163 - .wrapping_add(u32::from(site) + 1),
222 + scope: Self::scope_of(self.scope, site),
164 223 ..self.clone()
165 224 }
166 225 }
@@ -186,6 +245,40 @@
186 245 self
187 246 }
188 247
248 + /// Say how many times one loop runs, naming the scope it is read at.
249 + ///
250 + /// A derivation walks the whole call tree from one root plan, so it needs
251 + /// to say something about a loop inside an included shape without being at
252 + /// that shape's scope itself.
253 + #[must_use]
254 + pub fn with_rows_at(mut self, scope: u32, id: u16, count: usize) -> Self {
255 + self.row_overrides.push(((scope, id), count));
256 + self
257 + }
258 +
259 + /// Say what one guard answers, naming the scope it is read at.
260 + #[must_use]
261 + pub fn with_guard_at(mut self, scope: u32, id: u16, passes: bool) -> Self {
262 + self.guard_overrides.push(((scope, id), passes));
263 + self
264 + }
265 +
266 + /// Say which arm one dispatch takes, naming the scope it is read at.
267 + #[must_use]
268 + pub fn with_arm_at(mut self, scope: u32, id: u16, arm: usize) -> Self {
269 + self.arms.push(((scope, id), arm));
270 + self
271 + }
272 +
273 + /// The scope one `include` reaches from here, without entering it.
274 + ///
275 + /// The same mixing [`enter`](Self::enter) does, exposed so a derivation can
276 + /// walk the call tree by scope rather than by holding a plan per shape.
277 + #[must_use]
278 + pub fn scope_of(scope: u32, site: u16) -> u32 {
279 + scope.wrapping_mul(31).wrapping_add(u32::from(site) + 1)
280 + }
281 +
189 282 /// This plan's scope, which a derivation needs to name a loop it wants to
190 283 /// vary inside an included shape.
191 284 #[must_use]
@@ -196,12 +289,14 @@
196 289 /// One value a request would have brought. Read by generated code.
197 290 #[must_use]
198 291 pub fn hole(&self, id: u16) -> String {
292 + self.trace.saw(|sites| &mut sites.holes, (self.scope, id));
199 293 sentinel_at(self.scope, id)
200 294 }
201 295
202 296 /// Whether one guard passes. Read by generated code.
203 297 #[must_use]
204 298 pub fn guard(&self, id: u16) -> bool {
299 + self.trace.saw(|sites| &mut sites.guards, (self.scope, id));
205 300 look(&self.guard_overrides, self.scope, id).unwrap_or(self.guards)
206 301 }
207 302
@@ -212,6 +307,7 @@
212 307 /// and there is no element for it to read.
213 308 #[must_use]
214 309 pub fn rows(&self, id: u16) -> Take<Repeat<()>> {
310 + self.trace.saw(|sites| &mut sites.loops, (self.scope, id));
215 311 let count = look(&self.row_overrides, self.scope, id).unwrap_or(self.rows);
216 312 repeat(()).take(count)
217 313 }
@@ -236,6 +332,160 @@
236 332 .map(|(_, value)| *value)
237 333 }
238 334
335 + /// One instruction of a residual program.
336 + ///
337 + /// A residual is markup with the request taken out of it. Everything the
338 + /// renderer decided is a [`Lit`](Op::Lit); everything a request brings is a
339 + /// hole, a branch it may not take, or a loop it says the length of.
340 + #[derive(Clone, Debug, PartialEq, Eq)]
341 + pub enum Op {
342 + /// Markup the renderer produced that no request reaches.
343 + Lit(Box<str>),
344 + /// One value, escaped on the way in.
345 + ///
346 + /// The scope and id are the sentinel's, kept for reading a residual back
347 + /// rather than for filling it: a filler walks the same body in the same
348 + /// order, so what a hole holds is decided by where it is, not by its
349 + /// number.
350 + Hole { scope: u32, id: u16 },
351 + /// What one guard places, which a request may decide not to.
352 + Branch(Vec<Op>),
353 + /// One pass of a loop body, run once per element a request brought.
354 + Loop(Vec<Op>),
355 + }
356 +
357 + /// A screen's markup with the request taken out of it.
358 + #[derive(Clone, Debug, Default, PartialEq, Eq)]
359 + pub struct Residual {
360 + ops: Vec<Op>,
361 + }
362 +
363 + impl Residual {
364 + /// A residual over these instructions.
365 + #[must_use]
366 + pub fn new(ops: Vec<Op>) -> Self {
367 + Self { ops }
368 + }
369 +
370 + /// The instructions, for a reader or a check.
371 + #[must_use]
372 + pub fn ops(&self) -> &[Op] {
373 + &self.ops
374 + }
375 +
376 + /// How much markup this holds, which is what a filler reserves.
377 + #[must_use]
378 + pub fn literal_len(&self) -> usize {
379 + fn walk(ops: &[Op]) -> usize {
380 + ops.iter()
381 + .map(|op| match op {
382 + Op::Lit(text) => text.len(),
383 + Op::Hole { .. } => 0,
384 + Op::Branch(body) | Op::Loop(body) => walk(body),
385 + })
386 + .sum()
387 + }
388 + walk(&self.ops)
389 + }
390 +
391 + /// A walk over this residual, for generated code to fill.
392 + #[must_use]
393 + pub fn cursor(&self) -> Cursor<'_> {
394 + Cursor::new(&self.ops)
395 + }
396 + }
397 +
398 + /// A walk over a residual, alternating with the code that fills it.
399 + ///
400 + /// Generated filling code and the residual were produced from one declaration,
401 + /// so they agree on order by construction: the filler asks for the literals up
402 + /// to the next hole, writes the value it has, and carries on. Nothing is looked
403 + /// up and nothing is matched.
404 + ///
405 + /// A cursor that is asked for something the residual does not have next is a
406 + /// bug in the pairing rather than in the request, so it says so loudly instead
407 + /// of writing markup that is quietly wrong.
408 + pub struct Cursor<'a> {
409 + ops: &'a [Op],
410 + at: usize,
411 + }
412 +
413 + impl<'a> Cursor<'a> {
414 + #[must_use]
415 + fn new(ops: &'a [Op]) -> Self {
416 + Self { ops, at: 0 }
417 + }
418 +
419 + /// Push every literal up to the next thing a request decides.
420 + fn literals(&mut self, out: &mut String) {
421 + while let Some(Op::Lit(text)) = self.ops.get(self.at) {
422 + out.push_str(text);
423 + self.at += 1;
424 + }
425 + }
426 +
427 + /// The markup up to the next hole, then the hole itself.
428 + ///
429 + /// Returns nothing: the caller writes the value, because the caller is the
430 + /// only thing that has it.
431 + ///
432 + /// # Panics
433 + ///
434 + /// If the next instruction is not a hole, which means the filler and the
435 + /// residual came from different declarations.
436 + pub fn hole(&mut self, out: &mut String) {
437 + self.literals(out);
438 + assert!(
439 + matches!(self.ops.get(self.at), Some(Op::Hole { .. })),
440 + "the residual has no hole where the filler has a value"
441 + );
442 + self.at += 1;
443 + }
444 +
445 + /// The body of the next branch, or `None` if this branch was not placed.
446 + ///
447 + /// # Panics
448 + ///
449 + /// If the next instruction is not a branch.
450 + pub fn branch(&mut self, out: &mut String) -> Cursor<'a> {
451 + self.literals(out);
452 + let Some(Op::Branch(body)) = self.ops.get(self.at) else {
453 + panic!("the residual has no branch where the filler has a guard");
454 + };
455 + self.at += 1;
456 + Cursor::new(body)
457 + }
458 +
459 + /// The body of the next loop, to be walked once per element.
460 + ///
461 + /// # Panics
462 + ///
463 + /// If the next instruction is not a loop.
464 + pub fn repeat(&mut self, out: &mut String) -> &'a [Op] {
465 + self.literals(out);
466 + let Some(Op::Loop(body)) = self.ops.get(self.at) else {
467 + panic!("the residual has no loop where the filler has one");
468 + };
469 + self.at += 1;
470 + body
471 + }
472 +
473 + /// A fresh walk over one loop body, for one element.
474 + #[must_use]
475 + pub fn over(body: &'a [Op]) -> Self {
476 + Cursor::new(body)
477 + }
478 +
479 + /// Whatever markup is left, which is what closes a body.
480 + pub fn finish(&mut self, out: &mut String) {
481 + self.literals(out);
482 + assert!(
483 + self.at == self.ops.len(),
484 + "the filler stopped before the residual did"
485 + );
486 + }
487 + }
488 +
239 489 #[cfg(test)]
240 490 mod tests {
241 491 use super::*;
@@ -70,6 +70,7 @@
70 70 mod hyperscript;
71 71 mod node;
72 72 mod shell;
73 + pub mod stage;
73 74 pub mod vocabulary;
74 75
75 76 #[cfg(test)]
@@ -1,0 +1,246 @@
1 + //! Reading a residual off a staged shape.
2 + //!
3 + //! A shape's staged twin renders the screen with a sentinel wherever a request
4 + //! would have put a value, and with each guard and each loop answered by a
5 + //! [`Plan`]. This turns that into a [`Residual`]: the same markup with the
6 + //! branches and the repeats marked, so that filling it is a walk rather than a
7 + //! render.
8 + //!
9 + //! # How a boundary is found
10 + //!
11 + //! By asking, not by guessing. The macro numbered every guard and every loop,
12 + //! so a derivation can render the screen twice with exactly one of them changed
13 + //! and read the difference. A loop rendered once and then twice grows by its
14 + //! own body; a guard that passes and then does not shrinks by whatever it
15 + //! placed. Either way the span is the region between the two renders' common
16 + //! prefix and their common suffix, and either way it is checked by putting it
17 + //! back and comparing.
18 + //!
19 + //! This is the spike's arithmetic with the search taken out of it.
20 + //! `quasi-bench`'s `staging` module had to recover a loop body by looking for
21 + //! something that repeated, on the assumption that the output was `prefix +
22 + //! body * n + suffix`, and its own docs record that the assumption is where its
23 + //! one bug came from. Nothing here searches: the site being varied is named
24 + //! before the render, and the only question is where its span landed.
25 + //!
26 + //! # What it refuses
27 + //!
28 + //! Spans have to nest. Two that overlap without one containing the other are
29 + //! not a tree, so there is no residual to build, and a derivation that met one
30 + //! would be reporting a structure the screen does not have. It stops instead.
31 +
32 + use quasi_http::Serves as _;
33 + use quasi_router::stage::{Op, Plan, Residual, find_sentinel, read_sentinel};
34 + use quasi_router::{Node, stage};
35 +
36 + use crate::Webview;
37 +
38 + /// A span of the base render, and what decides it.
39 + #[derive(Clone, Copy, PartialEq, Eq, Debug)]
40 + struct Span {
41 + at: usize,
42 + to: usize,
43 + repeats: bool,
44 + }
45 +
46 + /// The residual of one staged shape.
47 + ///
48 + /// `shape` is the staged twin, which takes a plan and answers a `Node`. It is
49 + /// called many times and must be free of side effects, which it is: a staged
50 + /// twin reads nothing but its plan.
51 + ///
52 + /// # Panics
53 + ///
54 + /// If the screen's branches and loops do not nest, or if a span does not put
55 + /// back the way it came out. Both mean the derivation has read a structure the
56 + /// screen does not have, and a residual built on one would serve markup that is
57 + /// quietly wrong. Loud is the only safe answer.
58 + #[must_use]
59 + pub fn derive<F>(webview: &Webview, shape: F) -> Residual
60 + where
61 + F: Fn(&Plan) -> Node,
62 + {
63 + let render = |plan: &Plan| webview.fragment(&shape(plan));
64 +
65 + // One row per loop and every guard passing. A guard that does not pass
66 + // places nothing, so a residual read off a plan that failed one would be
67 + // missing a branch rather than carrying it.
68 + let base_plan = Plan::full(1);
69 + let base = render(&base_plan);
70 +
71 + // One traced render names every guard and every loop the screen reaches,
72 + // so the spans below are read for sites that are known to exist rather than
73 + // hunted for. A shape carrying only markup and a loop leaves no sentinel to
74 + // be found by, which is why this is a record and not a scan of the output.
75 + let (traced, sites) = base_plan.clone().tracing();
76 + let _ = render(&traced);
77 + let sites = sites.borrow();
78 +
79 + let mut spans = Vec::new();
80 + for &(scope, id) in &sites.loops {
81 + let wider = render(&base_plan.clone().with_rows_at(scope, id, 2));
82 + if let Some(span) = grown(&base, &wider) {
83 + spans.push(Span {
84 + repeats: true,
85 + ..span
86 + });
87 + }
88 + }
89 + for &(scope, id) in &sites.guards {
90 + let without = render(&base_plan.clone().with_guard_at(scope, id, false));
91 + if let Some(span) = shrunk(&base, &without) {
92 + spans.push(Span {
93 + repeats: false,
94 + ..span
95 + });
96 + }
97 + }
98 +
99 + Residual::new(tree(&base, &mut spans))
100 + }
101 +
102 + /// The span `wide` holds one more time than `narrow` does, in `narrow`.
103 + ///
104 + /// `narrow` is `P R Q` and `wide` is `P R R Q`, so the extra copy is the length
105 + /// difference. [`between`] finds where that copy sits in `wide`, which is the
106 + /// SECOND one; the body's own span in `narrow` is the first, ending where the
107 + /// two renders stop agreeing. Getting that conversion wrong is what put a
108 + /// table's rows halfway down the next section.
109 + ///
110 + /// Checked by taking the copy back out of `wide`. The two copies are the same
111 + /// string, so removing either gives `narrow` back, which is what makes the
112 + /// check a check on the length and the position rather than on which one.
113 + fn grown(narrow: &str, wide: &str) -> Option<Span> {
114 + let length = wide.len().checked_sub(narrow.len())?;
115 + if length == 0 {
116 + return None;
117 + }
118 + let found = between(narrow, wide, length)?;
119 + let mut rebuilt = String::with_capacity(narrow.len());
120 + rebuilt.push_str(&wide[..found.at]);
121 + rebuilt.push_str(&wide[found.at + length..]);
122 + if rebuilt != narrow {
123 + return None;
124 + }
125 +
126 + let at = found.at.checked_sub(length)?;
127 + let to = found.at;
128 + (narrow.is_char_boundary(at) && narrow.is_char_boundary(to)).then_some(Span {
129 + at,
130 + to,
131 + repeats: true,
132 + })
133 + }
134 +
135 + /// The span `full` holds and `without` does not.
136 + ///
137 + /// The same arithmetic the other way round: a guard that stops passing deletes
138 + /// what it placed, and the deletion is between the common prefix and the common
139 + /// suffix. Checked by taking it back out of `full`.
140 + fn shrunk(full: &str, without: &str) -> Option<Span> {
141 + let length = full.len().checked_sub(without.len())?;
142 + if length == 0 {
143 + return None;
144 + }
145 + let span = between(without, full, length)?;
146 + let mut rebuilt = String::with_capacity(without.len());
147 + rebuilt.push_str(&full[..span.at]);
148 + rebuilt.push_str(&full[span.at + length..]);
149 + (rebuilt == without).then_some(span)
150 + }
151 +
152 + /// Where `long` differs from `short`, given that it is longer by `length`.
153 + ///
154 + /// Between the common prefix and the common suffix. Both are needed: a prefix
155 + /// alone cannot tell a body from the text that follows it when the two begin
156 + /// the same way, which is the ambiguity that makes a search necessary and a
157 + /// named site unnecessary.
158 + fn between(short: &str, long: &str, length: usize) -> Option<Span> {
159 + let (s, l) = (short.as_bytes(), long.as_bytes());
160 +
161 + let mut head = 0;
162 + while head < s.len() && s[head] == l[head] {
163 + head += 1;
164 + }
165 + let mut tail = 0;
166 + while tail < s.len() - head && s[s.len() - 1 - tail] == l[l.len() - 1 - tail] {
167 + tail += 1;
168 + }
169 + if head + tail + length != long.len() {
170 + return None;
171 + }
172 + let (at, to) = (head, head + length);
173 + (long.is_char_boundary(at) && long.is_char_boundary(to)).then_some(Span {
174 + at,
175 + to,
176 + repeats: false,
177 + })
178 + }
179 +
180 + /// The spans, as a tree over the base render.
181 + ///
182 + /// Outermost first and then by position, so a span is placed inside the last
183 + /// one still open. Anything that leaves an open span without closing it inside
184 + /// is an overlap rather than a nesting, and there is no tree for that.
185 + fn tree(base: &str, spans: &mut Vec<Span>) -> Vec<Op> {
186 + spans.sort_by_key(|span| (span.at, std::cmp::Reverse(span.to)));
187 + spans.dedup();
188 + build(base, 0, base.len(), spans, &mut 0)
189 + }
190 +
191 + fn build(base: &str, at: usize, to: usize, spans: &[Span], next: &mut usize) -> Vec<Op> {
192 + let mut ops = Vec::new();
193 + let mut cut = at;
194 +
195 + while *next < spans.len() {
196 + let span = spans[*next];
197 + if span.at >= to {
198 + break;
199 + }
200 + assert!(
201 + span.to <= to,
202 + "a branch and a loop overlap without one holding the other, \
203 + so the screen's structure is not a tree"
204 + );
205 + assert!(span.at >= cut, "two spans start in the same place");
206 +
207 + ops.extend(literal(&base[cut..span.at]));
208 + *next += 1;
209 + let body = build(base, span.at, span.to, spans, next);
210 + ops.push(if span.repeats {
211 + Op::Loop(body)
212 + } else {
213 + Op::Branch(body)
214 + });
215 + cut = span.to;
216 + }
217 +
218 + ops.extend(literal(&base[cut..to]));
219 + ops
220 + }
221 +
222 + /// One chunk of markup, split on the sentinels in it.
223 + fn literal(chunk: &str) -> Vec<Op> {
224 + let mut ops = Vec::new();
225 + let mut rest = chunk;
226 + while let Some(at) = find_sentinel(rest) {
227 + match read_sentinel(&rest[at..]) {
228 + Some((scope, id)) => {
229 + if at > 0 {
230 + ops.push(Op::Lit(rest[..at].into()));
231 + }
232 + ops.push(Op::Hole { scope, id });
233 + rest = &rest[at + stage::SENTINEL_LEN..];
234 + }
235 + // An opening that is not a sentinel. Keep it and carry on past it.
236 + None => {
237 + ops.push(Op::Lit(rest[..at + 3].into()));
238 + rest = &rest[at + 3..];
239 + }
240 + }
241 + }
242 + if !rest.is_empty() {
243 + ops.push(Op::Lit(rest.into()));
244 + }
245 + ops
246 + }