Skip to main content

max / quasi

Let a field stand in a panel, and let an action carry a value Two productions, both from goingson's first converted screens. A field may be a member of a `-> Vec<Node>` panel as well as of a region. A panel is a region's members with the region spread off, so the argument that admitted one admits the other: settings/about and settings/sync are both panels of facts and questions with no form anywhere on the screen. Still refused in a cell or a row, where nothing would read it. `with` joins the modifier table. It was refused in wave 3 and the refusal was too wide: what cannot be said is a *form* carrying "and this value too, sometimes", and `doing` is still the remedy for that. An unconditional payload is Action::with(name, value) and nothing else, which is a modifier by this table's own rule. Three suppliers existed only to write it and two come out here; tip::checkout stays, because its value really is conditional. progress.py's derived not-work rule was wrong in two ways and both came out the same day it went in. It called `-> Result<T, E>` unemittable, which flagged 49 ordinary conversions: the read is hoisted into the handler and the shape becomes infallible, which is what every converted MNW screen does. And it called `-> Vec<Row>` unemittable, which flagged 19 more: those rows are written at the call site inside the list that consumes them. The test is whether it is work, not whether it compiles today, and the docstring now says so.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-04 16:29 UTC
Signed with PGP, not checked
Commit: 94101e223f569c97814d0ac5f634a41402c06d2c
Parent: 78dcaa6
3 files changed, +61 insertions, -23 deletions
@@ -95,12 +95,31 @@
95 95 def unemittable(shape):
96 96 """Whether `declare!` could never emit this shape, and why.
97 97
98 - Two independent reasons, and `feeds.rs`'s `Surface::address` has both. A
99 - return type outside `SHAPED` has no R2 case and cannot get one, because the
100 - macro emits a free function returning a vocabulary type. A receiver is the
101 - other: a declaration is a `fn`, never an `impl` item.
98 + THE TEST IS WHETHER IT IS WORK, NOT WHETHER IT COMPILES TODAY. A shape no
99 + restructuring inside its own file can make declarable is not work; one that
100 + only needs restructuring is work, and most of the transition has been
101 + exactly that.
102 +
103 + So this is one rule and deliberately not three. A return type outside
104 + `SHAPED` is a leaf the form has to be handed -- an `Action`, a `Tag`, a
105 + `Choice`, a `Column` -- and no production reaches it. `feeds.rs`'s
106 + `Surface::address` is also a method, which is a second reason for the same
107 + shape: a declaration is a `fn`, never an `impl` item.
108 +
109 + Two rules were here on 2026-09-04 and came out the same day, because both
110 + answered the wrong question:
111 +
112 + - `-> Result<T, E>` was called unemittable. Not work-free: the read is
113 + hoisted out and the shape becomes infallible, which is what every
114 + converted MNW screen does (`user_analytics::read`, then `pane`). It
115 + flagged 49 ordinary conversions.
116 + - `-> Vec<Row>` was called unemittable because R2 has no case for it. Also
117 + not work-free: the rows are written at the call site inside the `list`
118 + that consumes them and the supplier is deleted, which is wave 4's rule.
119 + It flagged 19.
102 120 """
103 - head = pop.head_type(shape["returns"]) or ""
121 + ret = " ".join(shape["returns"].split())
122 + head = pop.head_type(ret) or ""
104 123 if head not in SHAPED:
105 124 return f"`declare!` returns one of {'/'.join(sorted(SHAPED))}, not `{head}`"
106 125 return None
@@ -534,22 +534,7 @@
534 534 // Narrow on purpose. Everywhere else a field is still refused with the
535 535 // message that names the two legal homes, because a question in a cell
536 536 // or a row is a question nothing will ever read.
537 - (
538 - Container::Slot,
539 - Emission::Field {
540 - kind,
541 - name,
542 - label,
543 - body,
544 - },
545 - ) => {
546 - let built = field(kind, name, label, body)?;
547 - Ok((
548 - quote!(::quasi_router::Node::field(#built)),
549 - quote!(.with(#held)),
550 - ))
551 - }
552 - (Container::Slot, other) => Ok((node(other)?, quote!(.with(#held)))),
537 + (Container::Slot, other) => Ok((panel_member(other)?, quote!(.with(#held)))),
553 538 (Container::Screen, Emission::Region { name, kind, body }) => {
554 539 Ok((slot(name, kind, body)?, quote!(.with(#held))))
555 540 }
@@ -763,6 +748,31 @@
763 748 })
764 749 }
765 750
751 + /// One member of a region or of a panel, as the `Node` it is.
752 + ///
753 + /// [`node`] plus the one emission that is a node only in these two places. A
754 + /// question standing in a region rather than in a form is what a consulting
755 + /// region is made of -- it gathers the dials it contains and sends them itself,
756 + /// so there is no form to put them in -- and a `-> Vec<Node>` panel is a
757 + /// region's members with the region spread off, so the same holds there.
758 + ///
759 + /// Narrow on purpose. A field is still refused everywhere else, because a
760 + /// question in a cell or a row is a question nothing will ever read.
761 + fn panel_member(emission: &Emission) -> Result<TokenStream> {
762 + match emission {
763 + Emission::Field {
764 + kind,
765 + name,
766 + label,
767 + body,
768 + } => {
769 + let built = field(kind, name, label, body)?;
770 + Ok(quote!(::quasi_router::Node::field(#built)))
771 + }
772 + other => node(other),
773 + }
774 + }
775 +
766 776 /// One emission as a `Node` value.
767 777 fn node(emission: &Emission) -> Result<TokenStream> {
768 778 match emission {
@@ -1068,7 +1078,7 @@
1068 1078 }
1069 1079 Item::Emit(Emission::Guarded { guard, inner }) => {
1070 1080 let test = predicate(guard)?;
1071 - let value = node(inner)?;
1081 + let value = panel_member(inner)?;
1072 1082 // R9, as everywhere else: the guard decides whether the member
1073 1083 // is placed, and the holes inside it are evaluated either way.
1074 1084 let member = format_ident!("member", span = Span::call_site());
@@ -1080,7 +1090,7 @@
1080 1090 }))
1081 1091 }
1082 1092 Item::Emit(emission) => {
1083 - let value = node(emission)?;
1093 + let value = panel_member(emission)?;
1084 1094 Ok(quote!(#nodes.push(#value);))
1085 1095 }
1086 1096 Item::Attribute { name, .. } => Err(syn::Error::new(
@@ -28,6 +28,14 @@
28 28 /// `Action`; `copying` is `Act::copying`, which sets the destination to
29 29 /// `Action::local` itself, and `collections`' Copy link is the site. A word
30 30 /// belongs here only if `Action` has the method.
31 + ///
32 + /// **`with` was refused in wave 3 and the refusal was too wide.** What was
33 + /// refused is a *form* saying "and this value too, sometimes" -- a conditional
34 + /// payload, which the form still cannot say and which `doing` is still the
35 + /// remedy for. An unconditional one is `Action::with(name, value)` and nothing
36 + /// else, so it is a modifier by this table's own rule. Four suppliers existed
37 + /// only to write it: `tip::checkout`, `auth_pages::reset_action`,
38 + /// `git_commit::note_delete` and goingson's board move.
31 39 const MODIFIERS: &[(&str, usize)] = &[
32 40 ("navigating", 0),
33 41 ("awaiting", 0),
@@ -38,6 +46,7 @@
38 46 ("saving", 1),
39 47 ("replacing", 1),
40 48 ("carrying", 2),
49 + ("with", 2),
41 50 ];
42 51
43 52 /// The members: everything that emits rather than sets.