max / quasi
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
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. |