Skip to main content

max / quasi

Stop a filler's walk at the shape it is about to include An `include` splices the callee's markup into the caller's, so the residual has no boundary between them: a caller's straight stretch runs straight on into the callee's holes. Every shape numbers its holes from zero, so the caller answers the callee's hole 0 with its own expression and the page is quietly wrong -- or, when the numbers do not line up, the cursor asks for one nothing has and the filler panics. MNW's SSH keys pane is where it surfaced, as the second. The residual already knows: `Op::Hole` carries the scope it was read at, because a derivation walks many shapes and each numbers from zero. The cursor did not. It does now, and it stops at a hole belonging to a shape it is not in -- which is always the include the caller is one statement away from making. The two halves are kept in step by the include's own ordinal. The staged twin passes it to `Plan::enter`; the filler passes the same number to `Cursor::enter`, and both mix it the same way through `Plan::scope_of`. A loop body opened inside a shape carries that shape's scope, since a loop belongs to the shape that wrote it.
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:29 UTC
Commit: da581a8a6bda8fbd225bc8ad0604855da01f7937
Parent: 6462070
3 files changed, +126 insertions, -80 deletions
@@ -199,8 +199,10 @@
199 199 quote! {
200 200 {
201 201 let repeated = ::quasi_router::stage::Cursor::repeat(cursor, out);
202 + let scope = ::quasi_router::stage::Cursor::scope(cursor);
202 203 for #bound in #over {
203 - let mut cursor = &mut ::quasi_router::stage::Cursor::over(repeated);
204 + let mut cursor =
205 + &mut ::quasi_router::stage::Cursor::over(repeated, scope);
204 206 #inner
205 207 ::quasi_router::stage::Cursor::finish(cursor, out);
206 208 }
@@ -209,7 +211,7 @@
209 211 }
210 212 // The callee's markup was spliced into this residual where its shape
211 213 // was included, so its filler carries on with the same cursor.
212 - Fill::Include { callee, args } => {
214 + Fill::Include { callee, args, site } => {
213 215 let mut path = callee.clone();
214 216 let last = path
215 217 .segments
@@ -220,7 +222,15 @@
220 222 .iter()
221 223 .map(crate::emit::arg)
222 224 .collect::<Result<Vec<_>>>()?;
223 - quote!(#path(cursor, out, #(#args),*);)
225 + quote! {
226 + {
227 + // The callee's holes are numbered in its own namespace, so
228 + // the walk is told whose they are before it reaches them.
229 + let held = ::quasi_router::stage::Cursor::enter(cursor, #site);
230 + #path(cursor, out, #(#args),*);
231 + ::quasi_router::stage::Cursor::leave(cursor, held);
232 + }
233 + }
224 234 }
225 235 })
226 236 }
@@ -42,8 +42,8 @@
42 42 use syn::{Ident, Result};
43 43
44 44 use crate::ast::{
45 - Arg, Declaration, Emission, Guard, Hole, HoleRoot, Interpolated, Item, Param, Pattern,
46 - Predicate, RegionKind, Source, StrPart,
45 + Arg, Declaration, Emission, Guard, Hole, HoleRoot, Interpolated, Item, Param, Predicate,
46 + RegionKind, Source, StrPart,
47 47 };
48 48
49 49 /// The flag that asks for a staged twin.
@@ -180,7 +180,16 @@
180 180 body: Vec<Fill>,
181 181 },
182 182 /// Another shape's filler, called where its markup was spliced in.
183 - Include { callee: syn::Path, args: Vec<Arg> },
183 + ///
184 + /// `site` is the include's ordinal in the shape that holds it, which is
185 + /// what the staged twin hands `Plan::enter`. The filler passes the same
186 + /// number to `Cursor::enter`, so a caller's walk stops at the boundary
187 + /// rather than answering the callee's holes with its own.
188 + Include {
189 + callee: syn::Path,
190 + args: Vec<Arg>,
191 + site: u16,
192 + },
184 193 }
185 194
186 195 /// Numbering, one counter per thing a plan answers.
@@ -383,43 +392,20 @@
383 392 Ok(match source {
384 393 Source::Str(text) => Source::Str(interpolated(text, counters)),
385 394 Source::Hole(hole) => Source::Hole(value(hole, counters)),
386 - Source::Choose {
387 - arms,
388 - otherwise,
389 - scrutinee: _,
390 - } => {
391 - let id = counters.arms_next();
392 - let arms = arms
393 - .iter()
394 - .enumerate()
395 - .map(|(at, (_, value))| {
396 - Ok((
397 - Pattern::Int(i64::try_from(at).unwrap_or(i64::MAX)),
398 - self::source(value, counters)?,
399 - ))
400 - })
401 - .collect::<Result<Vec<_>>>()?;
402 - Source::Choose {
403 - scrutinee: plan_read("arm", id),
404 - arms,
405 - otherwise: Box::new(self::source(otherwise, counters)?),
406 - }
395 + // A dispatch has no residual, and staging one bakes an arm. The
396 + // `Emission::Given` arm below carries the whole reason.
397 + Source::Choose { scrutinee, .. } => {
398 + return Err(syn::Error::new(
399 + scrutinee.span(),
400 + "a staged shape cannot dispatch: only one arm reaches the \
401 + residual, so the others are lost rather than compiled. Say \
402 + the arms as guards -- `A when x; B unless x` -- which renders \
403 + the same and derives a branch for each",
404 + ));
407 405 }
408 406 })
409 407 }
410 408
411 - impl Counters {
412 - /// A dispatch is numbered out of the guard counter's neighbour, and the
413 - /// plan keeps arms in their own list.
414 - fn arms_next(&mut self) -> u16 {
415 - let id = self.holes;
416 - // Arms and holes do not share a namespace in the plan; the counter is
417 - // reused so that a shape's numbering stays a single pass over its body.
418 - self.holes += 1;
419 - id
420 - }
421 - }
422 -
423 409 /// A value a request would have brought, which becomes one sentinel.
424 410 ///
425 411 /// A hole rooted at a path is left alone. `REGION` and `PATH` are consts, so
@@ -745,40 +731,32 @@
745 731 inner,
746 732 }
747 733 }
748 - // The arms become integers, so the plan chooses one by position and the
749 - // scrutinee's own type never has to be reproduced. That is also why an
750 - // `otherwise` stops being optional here: a match on a `usize` against
751 - // integer literals is not exhaustive without one, and rustc would
752 - // report that against generated code rather than against the
753 - // declaration that caused it.
754 - Emission::Given {
755 - scrutinee,
756 - arms,
757 - otherwise,
758 - } => {
759 - let Some(otherwise) = otherwise else {
760 - return Err(syn::Error::new(
761 - scrutinee.span(),
762 - "a staged dispatch needs an `otherwise`: its arms are chosen by \
763 - position, and a position nothing answers has to land somewhere",
764 - ));
765 - };
766 - let id = counters.arms_next();
767 - let arms = arms
768 - .iter()
769 - .enumerate()
770 - .map(|(at, (_, emission))| {
771 - Ok((
772 - Pattern::Int(i64::try_from(at).unwrap_or(i64::MAX)),
773 - Box::new(self::emission(emission, counters)?),
774 - ))
775 - })
776 - .collect::<Result<Vec<_>>>()?;
777 - Emission::Given {
778 - scrutinee: plan_read("arm", id),
779 - arms,
780 - otherwise: Some(Box::new(self::emission(otherwise, counters)?)),
781 - }
734 + // A dispatch has no residual, and staging one is silently wrong.
735 + //
736 + // The derivation finds a branch by rendering the screen twice with one
737 + // guard changed and reading the difference, and it finds a loop the same
738 + // way with a row count. A dispatch is neither: its arms replace each
739 + // other at one position, so there is no insertion to measure and no
740 + // deletion to put back, and every arm would claim the same bytes.
741 + //
742 + // What happened instead was worse than a refusal. The staged twin reads
743 + // `plan.arm(id)`, which answers zero, so exactly one arm rendered and
744 + // the residual held it as a literal with the others simply gone. MNW's
745 + // forum settings pane compiled to "You haven't joined any forum
746 + // communities yet." and would have served that to every reader.
747 + //
748 + // Refused rather than modelled. `Op::Arms` and a derivation that renders
749 + // once per arm is the general answer and is a feature rather than a fix;
750 + // every dispatch in the tree today is two-way over a bool, which is two
751 + // guards, which the residual already has a shape for.
752 + Emission::Given { scrutinee, .. } => {
753 + return Err(syn::Error::new(
754 + scrutinee.span(),
755 + "a staged shape cannot dispatch: only one arm reaches the \
756 + residual, so the others are lost rather than compiled. Say \
757 + the arms as guards -- `A when x; B unless x` -- which renders \
758 + the same and derives a branch for each",
759 + ));
782 760 }
783 761 })
784 762 }
@@ -850,9 +828,13 @@
850 828 // The filler calls the shape's own name, not the twin's: the twin renders
851 829 // markup and the filler fills it, and they are two different functions
852 830 // beside one declaration.
831 + let site = counters.sites;
832 + counters.sites += 1;
833 +
853 834 counters.wrote(Fill::Include {
854 835 callee: path.clone(),
855 836 args: args.clone(),
837 + site,
856 838 });
857 839
858 840 let mut path = path.clone();
@@ -862,9 +844,6 @@
862 844 .ok_or_else(|| syn::Error::new(hole.span(), "an empty path"))?;
863 845 last.ident = staged_name(&last.ident);
864 846
865 - let site = counters.sites;
866 - counters.sites += 1;
867 -
868 847 Ok(Hole {
869 848 root: HoleRoot::Call {
870 849 path,
@@ -570,12 +570,45 @@
570 570 pub struct Cursor<'a> {
571 571 ops: &'a [Op],
572 572 at: usize,
573 + /// Which shape's holes this walk may answer.
574 + ///
575 + /// An `include` splices the callee's markup into the caller's, so a
576 + /// caller's straight stretch runs straight into it and would answer the
577 + /// callee's holes with its own expressions -- silently, since every shape
578 + /// numbers its holes from zero. The scope is what tells them apart, and it
579 + /// is on the hole already: [`Op::Hole`] carries the one it was read at.
580 + ///
581 + /// Kept in step with [`Plan::enter`] by the same mixing, through
582 + /// [`Plan::scope_of`], so the number here is the number the derivation
583 + /// wrote into the residual.
584 + scope: u32,
573 585 }
574 586
575 587 impl<'a> Cursor<'a> {
576 588 #[must_use]
577 589 fn new(ops: &'a [Op]) -> Self {
578 - Self { ops, at: 0 }
590 + Self {
591 + ops,
592 + at: 0,
593 + scope: 0,
594 + }
595 + }
596 +
597 + /// Enter one `include`, answering the callee's holes rather than this
598 + /// shape's, and hand back what [`leave`](Self::leave) puts back.
599 + ///
600 + /// `site` is the include's ordinal in the shape that holds it, which is the
601 + /// number the staged twin passed to [`Plan::enter`]. Generated code passes
602 + /// it, so the two halves cannot drift.
603 + pub fn enter(&mut self, site: u16) -> u32 {
604 + let held = self.scope;
605 + self.scope = Plan::scope_of(self.scope, site);
606 + held
607 + }
608 +
609 + /// Leave an `include`, back to the caller's own holes.
610 + pub fn leave(&mut self, held: u32) {
611 + self.scope = held;
579 612 }
580 613
581 614 /// Push every literal up to the next thing a request decides.
@@ -602,6 +635,13 @@
602 635 out.push_str(text);
603 636 self.at += 1;
604 637 }
638 + // A hole belonging to a shape this walk is not in, which is
639 + // the next `include` reached before its call. Left where it is:
640 + // the caller's filler is about to enter that shape, and the
641 + // callee's own run answers it. Without this a caller writes its
642 + // hole 0 into the callee's hole 0, which is markup that is
643 + // quietly wrong rather than markup that is missing.
644 + Some(Op::Hole { scope, .. }) if *scope != self.scope => return,
605 645 Some(Op::Hole { id, .. }) => {
606 646 let id = *id;
607 647 self.at += 1;
@@ -642,7 +682,11 @@
642 682 panic!("the residual has no branch where the filler has a guard");
643 683 };
644 684 self.at += 1;
645 - Cursor::new(body)
685 + Cursor {
686 + ops: body,
687 + at: 0,
688 + scope: self.scope,
689 + }
646 690 }
647 691
648 692 /// The body of the next loop, to be walked once per element.
@@ -660,9 +704,22 @@
660 704 }
661 705
662 706 /// A fresh walk over one loop body, for one element.
707 + ///
708 + /// `scope` is the walk the loop was found in, since a loop body belongs to
709 + /// the shape that wrote it.
663 710 #[must_use]
664 - pub fn over(body: &'a [Op]) -> Self {
665 - Cursor::new(body)
711 + pub fn over(body: &'a [Op], scope: u32) -> Self {
712 + Cursor {
713 + ops: body,
714 + at: 0,
715 + scope,
716 + }
717 + }
718 +
719 + /// Which shape's holes this walk answers, for a loop body opened from it.
720 + #[must_use]
721 + pub const fn scope(&self) -> u32 {
722 + self.scope
666 723 }
667 724
668 725 /// Whatever markup is left, which is what closes a body.