max / quasi
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
6 files changed,
+588 insertions,
-10 deletions
| @@ -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 | + | } |