Skip to main content

max / quasi

Let a shape answer many members, and relax two staging refusals Three changes, all found putting MNW's remaining screens on the residual seam. **`include each`.** A shape answering `Vec<Node>` has no region of its own, so before this a caller spread it by hand: a loop whose body was `include node;`. That reads as a loop over data and is a splice, and a staged shape could make nothing of it -- the `include` names a binding rather than a shape, so there is no twin to retarget to and no filler to call. Said as one `include each` the caller splices a reference the way every other `include` does, and `Slot::with_all` places the run. Three sites in MNW; a `-> Node` container still refuses it, since what it places is a run and not a node. **A supplied region kind may be constant.** `RegionKind::ceded("revenue-chart")` names a path and passes a literal, so it is the same value on every request and can be evaluated once while the residual is derived. The refusal now asks what `#[constant]` asks -- every argument fixed where it is written -- rather than refusing every supplier. **A setting that renders where it was written may be guarded.** The blanket refusal is right for `measured`, `width` and `priority`, which render in their container's opening tag: the branch a guard makes there is not where the declaration wrote it, so the residual's branches and the filler's would be in different orders. `figure` is not one of those. A stats strip accretes its figures in declaration order and the renderer draws them in that order, so a guarded figure's branch is exactly where it was written.
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 03:07 UTC
Commit: 636b392a79370fd0028940711400b4bb937acc9f
Parent: da581a8
6 files changed, +134 insertions, -12 deletions
@@ -310,6 +310,12 @@
310 310 Activate(Action),
311 311 /// `include <hole>;` -- whatever another shape built.
312 312 Include(Hole),
313 + /// `include each <shape>`: a shape answering many members, spliced whole.
314 + ///
315 + /// The plural of [`Include`](Self::Include), and a separate emission rather
316 + /// than a flag on it because what it places is a run and not a node: only
317 + /// a container that holds many members can take one.
318 + IncludeEach(Hole),
313 319 /// `link <arg> to <action>;`
314 320 Link { text: Arg, action: Action },
315 321 /// `region <arg> as <kind> { .. }`
@@ -677,6 +677,12 @@
677 677 None => Ok((node, quote!(.with(#held)))),
678 678 }
679 679 }
680 + // A shape that answers many members, spliced whole. The plural of
681 + // `include`, and only a container that holds many can take one.
682 + (Container::Slot, Emission::IncludeEach(supplier)) => {
683 + let supplier = hole(supplier)?;
684 + Ok((quote!(#supplier), quote!(.with_all(#held))))
685 + }
680 686 (Container::Slot, other) => Ok((panel_member(other)?, quote!(.with(#held)))),
681 687 (Container::Screen, Emission::Region { name, kind, body }) => {
682 688 Ok((slot(name, kind, body)?, quote!(.with(#held))))
@@ -1019,6 +1025,14 @@
1019 1025 /// One emission as a `Node` value.
1020 1026 fn node(emission: &Emission) -> Result<TokenStream> {
1021 1027 match emission {
1028 + // A run of members is not a node, so it can only be written where a
1029 + // container holds many. Refused where it is written rather than
1030 + // reported against generated code.
1031 + Emission::IncludeEach(_) => Err(syn::Error::new(
1032 + Span::call_site(),
1033 + "`include each` splices a run of members, so it belongs in a body \
1034 + that holds many: write `include` for one node",
1035 + )),
1022 1036 // `framed` says how a member is placed, so it has no meaning where a
1023 1037 // bare node is wanted. Refused where it is written rather than silently
1024 1038 // losing the name.
@@ -1993,7 +2007,9 @@
1993 2007 fn emission_span(emission: &Emission) -> Span {
1994 2008 match emission {
1995 2009 Emission::Framed { inner, .. } => emission_span(inner),
1996 - Emission::Include(_) | Emission::Link { .. } => Span::call_site(),
2010 + Emission::Include(_) | Emission::IncludeEach(_) | Emission::Link { .. } => {
2011 + Span::call_site()
2012 + }
1997 2013 Emission::Simple { member, .. } => member.span(),
1998 2014 Emission::Screen { arrangement, .. } => arrangement.span(),
1999 2015 Emission::Row { .. } | Emission::List(_) => Span::call_site(),
@@ -100,6 +100,7 @@
100 100 | Emission::At { inner, .. } => bodies(inner),
101 101 Emission::Guarded { inner, .. } => bodies(inner),
102 102 Emission::Include(_)
103 + | Emission::IncludeEach(_)
103 104 | Emission::Link { .. }
104 105 | Emission::Activate(_)
105 106 | Emission::Given { .. } => &[],
@@ -194,7 +195,7 @@
194 195 | Emission::Offers { label, .. }
195 196 | Emission::Offering { label, .. }
196 197 | Emission::Removes { label, .. } => walk_arg(label, found),
197 - Emission::Include(hole) => note(hole, found),
198 + Emission::Include(hole) | Emission::IncludeEach(hole) => note(hole, found),
198 199 Emission::Beside { inner, .. }
199 200 | Emission::Framed { inner, .. }
200 201 | Emission::At { inner, .. } => {
@@ -689,10 +689,31 @@
689 689 ))
690 690 }
691 691 "include" => {
692 + // `include each <shape>` splices a shape that answers many
693 + // members. `each` cannot be a shape name here: a hole starts
694 + // with a path or a binding and the next token would be `(`.
695 + let every = input.peek(Ident)
696 + && input.fork().parse::<Ident>()? == "each"
697 + && !input.fork().parse::<Hole>().is_ok_and(|_| false);
698 + let every = every && {
699 + let fork = input.fork();
700 + fork.parse::<Ident>()?;
701 + fork.peek(Ident) || fork.peek(Token![:])
702 + };
703 + if every {
704 + input.parse::<Ident>()?;
705 + }
692 706 let supplier: Hole = input.parse()?;
693 707 let guard = guard(input)?;
694 708 input.parse::<Token![;]>()?;
695 - Ok(guarded(guard, Self::Include(supplier)))
709 + Ok(guarded(
710 + guard,
711 + if every {
712 + Self::IncludeEach(supplier)
713 + } else {
714 + Self::Include(supplier)
715 + },
716 + ))
696 717 }
697 718 "given" => {
698 719 let scrutinee: Hole = input.parse()?;
@@ -321,7 +321,9 @@
321 321 // branches in render order and the filler walks them in declaration
322 322 // order, so the two would disagree about which branch is which.
323 323 Item::Attribute { name, args, guard } => {
324 - if let Some(guard) = guard {
324 + if let Some(guard) = guard
325 + && !placed(name)
326 + {
325 327 return Err(syn::Error::new(
326 328 guard.span,
327 329 "a staged shape cannot guard a setting: a setting renders in \
@@ -329,6 +331,25 @@
329 331 where the declaration wrote it",
330 332 ));
331 333 }
334 + if let Some(guard) = guard {
335 + let staged = self::guard(guard, counters);
336 + counters.enter();
337 + let args = if structured(name) {
338 + through(args, counters)?
339 + } else {
340 + self::args(args, counters)?
341 + };
342 + let body = counters.exit();
343 + counters.wrote(Fill::Branch {
344 + guard: (*guard).clone(),
345 + body,
346 + });
347 + return Ok(Item::Attribute {
348 + name: name.clone(),
349 + args,
350 + guard: Some(staged),
351 + });
352 + }
332 353 Item::Attribute {
333 354 name: name.clone(),
334 355 args: if structured(name) {
@@ -466,7 +487,26 @@
466 487 /// where the whole call is the value and staging through it would hand
467 488 /// `tier_line` a sentinel where it wants a `&TierPrices`. Only the slot knows
468 489 /// which of the two it is looking at.
469 - const STRUCTURED: &[&str] = &["token", "meter", "stats", "option"];
490 + const STRUCTURED: &[&str] = &["token", "meter", "stats", "option", "figure"];
491 +
492 + /// The settings that render where the declaration wrote them.
493 + ///
494 + /// A setting is refused a guard because most of them render in their
495 + /// container's opening tag: `measured`, `width`, `priority` are decided before
496 + /// any member is placed, so the branch a guard makes is not where it was
497 + /// written and the residual's branches and the filler's would be in different
498 + /// orders.
499 + ///
500 + /// `figure` is not one of those. A stats strip accretes its figures in
501 + /// declaration order and the renderer draws them in that order, so a guarded
502 + /// figure's branch is exactly where it was written. user_analytics has three,
503 + /// one per tone a delta can carry.
504 + const PLACED: &[&str] = &["figure"];
505 +
506 + /// Whether a guard on this setting lands where it was written.
507 + fn placed(name: &Ident) -> bool {
508 + PLACED.contains(&name.to_string().as_str())
509 + }
470 510
471 511 /// Whether this slot takes a structure rather than a value.
472 512 fn structured(name: &Ident) -> bool {
@@ -677,18 +717,36 @@
677 717 action: self::action(action, counters)?,
678 718 },
679 719 Emission::Include(hole) => Emission::Include(include(hole, counters)?),
720 + // The plural, staged the same way: the callee has a twin of its own and
721 + // a filler of its own, and the caller splices a reference to both.
722 + Emission::IncludeEach(hole) => Emission::IncludeEach(include(hole, counters)?),
680 723 Emission::Region { name, kind, body } => Emission::Region {
681 724 name: arg(name, counters)?,
682 725 kind: match kind {
683 726 RegionKind::Variant(variant) => RegionKind::Variant(variant.clone()),
684 - // A `RegionKind` has no sentinel, and guessing a variant would
685 - // put a kind in the residual that no request asked for.
727 + // A `RegionKind` has no sentinel, so a supplied one is refused
728 + // -- unless nothing about it is supplied. `RegionKind::ceded("revenue-chart")`
729 + // names a path and passes a literal, which is the same value on
730 + // every request, so it is evaluated once while the residual is
731 + // derived. That is `#[constant]`'s rule one level down, and it
732 + // is the same test: every argument fixed where it is written.
686 733 RegionKind::Supplied(hole) => {
687 - return Err(syn::Error::new(
688 - hole.span(),
689 - "a staged shape cannot take its region kind from a supplier: \
690 - a `RegionKind` has no sentinel to stand in for it",
691 - ));
734 + let fixed_kind = match &hole.root {
735 + HoleRoot::Path(_) => hole.steps.is_empty(),
736 + HoleRoot::Call { args, .. } => {
737 + hole.steps.is_empty() && args.iter().all(fixed)
738 + }
739 + HoleRoot::Binding(_) => false,
740 + };
741 + if !fixed_kind {
742 + return Err(syn::Error::new(
743 + hole.span(),
744 + "a staged shape cannot take its region kind from a supplier \
745 + that reads the request: a `RegionKind` has no sentinel to \
746 + stand in for it",
747 + ));
748 + }
749 + RegionKind::Supplied(hole.clone())
692 750 }
693 751 },
694 752 body: items(body, counters)?,
@@ -5492,6 +5492,26 @@
5492 5492 self
5493 5493 }
5494 5494
5495 + /// Add every node a shape answered with, chaining.
5496 + ///
5497 + /// [`with`](Self::with)'s plural, and the whole of what `include each`
5498 + /// emits. A shape answering `Vec<Node>` has no region of its own -- what it
5499 + /// returns is a run of members and not one node -- so before this a caller
5500 + /// spread it by hand, with a loop whose body was `include node;`.
5501 + ///
5502 + /// That loop is why this exists. It reads as a loop over data and is a
5503 + /// splice, and a staged shape cannot make sense of it: the `include` names
5504 + /// a binding rather than a shape, so there is no twin to retarget to and no
5505 + /// filler to call. Said as one `include each`, the caller splices a
5506 + /// reference the way every other `include` does.
5507 + #[must_use]
5508 + pub fn with_all(mut self, nodes: impl IntoIterator<Item = Node>) -> Self {
5509 + for node in nodes {
5510 + self = self.with(node);
5511 + }
5512 + self
5513 + }
5514 +
5495 5515 /// A named frame: a tab, or a disclosure's one panel.
5496 5516 ///
5497 5517 /// The only way to write a label, and it places the member at the same