Skip to main content

max / quasi

Stage a structured slot through its constructor, and offer every hole Two things a staged shape could not do, both found putting MNW's /git/{owner} on the residual seam. **A slot that takes a structure had no sentinel to stand in for it.** `token Tag::badge(word)`, `option Choice::new(id, name)`, `stats [Figure::new(count, label)]`: staging the hole as a value hands the slot a string and fails with `the trait bound Tag: Fill is not satisfied`, against generated code. The arguments are a different matter -- each is an ordinary value a request brings, and each does have a sentinel. So a hole in one of these slots keeps its call and stages what the call is given, which puts the tag's markup in the residual as a literal and the word in it as a hole. Slot-directed rather than a rule about calls, and that is not a shortcut. `text tier_line(profile.priced, prices)` is a supplier answering a string, where the whole call is the value and staging through it would hand `tier_line` a sentinel where it wants a `&TierPrices`. Only the slot knows which of the two it is looking at. **A straight stretch was offered only the holes written since the last branch.** The residual is in render order and the fill program is in declaration order, and control flow moves between them: a row that declares two cells, then a guarded third, then `activate` renders the address first, in the row's opening tag. So the residual's first stretch held the address's holes, the first run had arms for the two cells, and the cursor asked for a number nothing answered. Every stretch now gets every hole at its level, and every stretch gets the call -- which stretch has no holes is exactly what declaration order cannot say. It costs a wider `match` and nothing at run time: an arm is evaluated only when the cursor asks for that number.
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 02:02 UTC
Commit: 6462070f50e71974876942bc0cbd2bff46d5c5cd
Parent: dc4f8cd
2 files changed, +114 insertions, -18 deletions
@@ -104,28 +104,43 @@
104 104
105 105 /// One body: a shape's, a branch's, or a loop's.
106 106 ///
107 - /// A run of consecutive holes becomes one walk of the residual answering each
108 - /// by number. Anything that changes control flow ends the run, because it also
109 - /// ends the residual's straight stretch.
107 + /// Every straight stretch is offered **every hole at this level**, not just the
108 + /// ones written since the last branch or loop. That is what makes the filler
109 + /// agree with the residual about which stretch a hole is in, and the two do not
110 + /// otherwise agree: the residual is in render order and the fill program is in
111 + /// declaration order, and control flow moves between them.
112 + ///
113 + /// MNW's `/git/{owner}` is the site. A row declares two cells, then a guarded
114 + /// third, then `activate` -- and `activate`'s address renders in the row's
115 + /// opening tag, before any cell. So the residual's first stretch holds the
116 + /// address's holes and the fill program's first run held the two cells', and
117 + /// the cursor asked for a number that run had no arm for.
118 + ///
119 + /// Offering all of them costs a wider `match` in the generated code and nothing
120 + /// at run time: an arm's expression is evaluated only when the cursor asks for
121 + /// that number, and the cursor asks only for the holes the stretch it is
122 + /// walking actually has. A stretch with no holes at all still gets the call,
123 + /// because which stretch that is is exactly what declaration order cannot say.
110 124 fn body(fill: &[Fill]) -> Result<TokenStream> {
111 - let mut out = TokenStream::new();
112 - let mut holes: Vec<&Fill> = Vec::new();
125 + let holes: Vec<&Fill> = fill
126 + .iter()
127 + .filter(|one| matches!(one, Fill::Hole { .. }))
128 + .collect();
113 129
130 + let mut out = TokenStream::new();
114 131 for one in fill {
115 - match one {
116 - Fill::Hole { .. } => holes.push(one),
117 - _ => {
118 - out.extend(run(&mut holes)?);
119 - out.extend(control(one)?);
120 - }
132 + if matches!(one, Fill::Hole { .. }) {
133 + continue;
121 134 }
135 + out.extend(run(&holes)?);
136 + out.extend(control(one)?);
122 137 }
123 - out.extend(run(&mut holes)?);
138 + out.extend(run(&holes)?);
124 139 Ok(out)
125 140 }
126 141
127 142 /// One straight stretch of markup, with its holes answered by number.
128 - fn run(holes: &mut Vec<&Fill>) -> Result<TokenStream> {
143 + fn run(holes: &[&Fill]) -> Result<TokenStream> {
129 144 if holes.is_empty() {
130 145 return Ok(TokenStream::new());
131 146 }
@@ -139,13 +154,13 @@
139 154 Ok(quote!(#id => ::quasi_webview::stage::Fill::fill(&(#value), out),))
140 155 })
141 156 .collect::<Result<Vec<_>>>()?;
142 - holes.clear();
143 157
144 158 Ok(quote! {
145 159 ::quasi_router::stage::Cursor::fill(cursor, out, &mut |which, out| match which {
146 160 #(#arms)*
147 - // A hole the residual has and this stretch does not. The two came
148 - // from one declaration, so this is unreachable rather than a case.
161 + // A hole at another level, or one this shape does not have. The
162 + // residual and this code came from one declaration, so a number
163 + // outside the level's own set is a pairing bug rather than a case.
149 164 _ => ::core::unreachable!("the residual has a hole the filler does not"),
150 165 });
151 166 })
@@ -322,7 +322,11 @@
322 322 }
323 323 Item::Attribute {
324 324 name: name.clone(),
325 - args: self::args(args, counters)?,
325 + args: if structured(name) {
326 + through(args, counters)?
327 + } else {
328 + self::args(args, counters)?
329 + },
326 330 guard: None,
327 331 }
328 332 }
@@ -455,6 +459,79 @@
455 459 }
456 460 }
457 461
462 + /// The slots whose value is a structure the description builds, not a string a
463 + /// request brings.
464 + ///
465 + /// A hole in one of these is a constructor call: `token Tag::badge(kind)`,
466 + /// `option Choice::new(id, name)`, `stats [Figure::new(count, "Items")]`. What
467 + /// the slot takes is a `Tag`, a `Choice`, a `Figure` -- none of which has a
468 + /// sentinel, so the call cannot become one, and staging it as a value fails
469 + /// with `the trait bound Tag: Fill is not satisfied` against generated code.
470 + ///
471 + /// The arguments are a different matter. Each of those is an ordinary value a
472 + /// request brings, and each does have a sentinel. So the call is kept and
473 + /// staged **through**: the derivation builds the tag with a sentinel in it, the
474 + /// renderer draws the tag's markup around the sentinel, and the residual holds
475 + /// the markup as a literal with the word as a hole. That is the same trade
476 + /// [`value`] makes for a `const` path, one level down.
477 + ///
478 + /// Slot-directed rather than a rule about calls, and that is not a shortcut.
479 + /// `text tier_line(profile.priced, prices)` is a supplier answering a string,
480 + /// where the whole call is the value and staging through it would hand
481 + /// `tier_line` a sentinel where it wants a `&TierPrices`. Only the slot knows
482 + /// which of the two it is looking at.
483 + const STRUCTURED: &[&str] = &["token", "meter", "stats", "option"];
484 +
485 + /// Whether this slot takes a structure rather than a value.
486 + fn structured(name: &Ident) -> bool {
487 + STRUCTURED.contains(&name.to_string().as_str())
488 + }
489 +
490 + /// Stage a structured slot's arguments, keeping the calls that build them.
491 + ///
492 + /// See [`STRUCTURED`]. A call is kept and its own arguments staged; anything
493 + /// else is an ordinary value and goes through [`arg`].
494 + fn through(args: &[Arg], counters: &mut Counters) -> Result<Vec<Arg>> {
495 + args.iter().map(|one| one_through(one, counters)).collect()
496 + }
497 +
498 + fn one_through(arg: &Arg, counters: &mut Counters) -> Result<Arg> {
499 + Ok(match arg {
500 + Arg::List(items) => Arg::List(through(items, counters)?),
501 + Arg::Borrow(inner) => Arg::Borrow(Box::new(one_through(inner, counters)?)),
502 + Arg::Hole(hole) => match &hole.root {
503 + HoleRoot::Call { path, args } => Arg::Hole(Hole {
504 + root: HoleRoot::Call {
505 + path: path.clone(),
506 + args: self::args(args, counters)?,
507 + },
508 + // A builder chain on the constructor -- `Tag::badge(kind).tone(..)`
509 + // -- carries values of its own, and they are staged the same way.
510 + steps: hole
511 + .steps
512 + .iter()
513 + .map(|step| {
514 + Ok(match step {
515 + crate::ast::Step::Field(name) => crate::ast::Step::Field(name.clone()),
516 + crate::ast::Step::Method { name, args } => crate::ast::Step::Method {
517 + name: name.clone(),
518 + args: self::args(args, counters)?,
519 + },
520 + })
521 + })
522 + .collect::<Result<Vec<_>>>()?,
523 + }),
524 + // Not a constructor, so there is nothing to stage through and the
525 + // slot is being handed a value from somewhere else. Left as
526 + // written: a path is a const the renderer bakes in, and a binding
527 + // or a method chain is refused by rustc against the slot's own
528 + // type, which names the site.
529 + _ => Arg::Hole(hole.clone()),
530 + },
531 + other => self::arg(other, counters)?,
532 + })
533 + }
534 +
458 535 fn args(args: &[Arg], counters: &mut Counters) -> Result<Vec<Arg>> {
459 536 args.iter().map(|arg| self::arg(arg, counters)).collect()
460 537 }
@@ -499,7 +576,11 @@
499 576 Ok(match emission {
500 577 Emission::Simple { member, args, body } => Emission::Simple {
501 578 member: member.clone(),
502 - args: self::args(args, counters)?,
579 + args: if structured(member) {
580 + through(args, counters)?
581 + } else {
582 + self::args(args, counters)?
583 + },
503 584 body: items(body, counters)?,
504 585 },
505 586 Emission::Chip {