max / quasi
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
4 files changed,
+181 insertions,
-8 deletions
| @@ -48,6 +48,63 @@ | |||
| 48 | 48 | # reproduced by population.py. | |
| 49 | 49 | BASELINE = {"mnw": 173, "goingson": 184, "audiofiles": 157} | |
| 50 | 50 | ||
| 51 | + | # COUNTED, AND NOT WORK. | |
| 52 | + | # | |
| 53 | + | # `population.py`'s predicate counts a function returning a description type as | |
| 54 | + | # a shape to convert. Three kinds of thing match it and never will be converted, | |
| 55 | + | # so they sit at the head of `--next` forever and a session not told to skip | |
| 56 | + | # them converts nothing and reports a stall. Settled 2026-09-04 (GoingsOn | |
| 57 | + | # `95017e31`, option (a)): the predicate does not change and the denominator | |
| 58 | + | # stays the ratified 514, because the number's whole value is that it is | |
| 59 | + | # comparable across waves and reproducible from the tree. They are reported | |
| 60 | + | # instead. | |
| 61 | + | # | |
| 62 | + | # Two of the three categories cannot be detected mechanically and are listed | |
| 63 | + | # here by hand, with the reason. The third -- a shape `declare!` cannot emit at | |
| 64 | + | # all -- is detected in `unemittable()` below. | |
| 65 | + | NOT_WORK = { | |
| 66 | + | "MNW/server/src/quasi/tip.rs": { | |
| 67 | + | "checkout": "`Action::with` is refused as a production; `doing` is the remedy", | |
| 68 | + | }, | |
| 69 | + | "MNW/server/src/quasi/auth_pages.rs": { | |
| 70 | + | "reset_action": "`Action::with` is refused as a production", | |
| 71 | + | }, | |
| 72 | + | "MNW/server/src/quasi/git_commit.rs": { | |
| 73 | + | "note_delete": "`Action::with` is refused as a production", | |
| 74 | + | }, | |
| 75 | + | "MNW/server/src/quasi/pricing.rs": { | |
| 76 | + | "crossover_meter": "`meter` is a setting on `Row`, so it cannot also be a member", | |
| 77 | + | }, | |
| 78 | + | "MNW/server/src/quasi/item_sales.rs": {"badge": "a supplier a conversion wrote"}, | |
| 79 | + | "MNW/server/src/quasi/media_picker.rs": { | |
| 80 | + | "folder_choices": "a supplier a conversion wrote", | |
| 81 | + | }, | |
| 82 | + | "MNW/server/src/quasi/payout_summary.rs": { | |
| 83 | + | "figures": "a supplier a conversion wrote", | |
| 84 | + | }, | |
| 85 | + | "MNW/server/src/quasi/widgets/carousel.rs": { | |
| 86 | + | "region": "refused on the hard limit: a closure holding a `let mut` and an `if let`", | |
| 87 | + | }, | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | # The shaped types `declare!` can return. A shape function whose return type is | |
| 91 | + | # not one of these cannot be emitted at all, whatever the form grows. | |
| 92 | + | SHAPED = {"Node", "Screen", "Slot", "Row", "Cells", "Field", "Act"} | |
| 93 | + | ||
| 94 | + | ||
| 95 | + | def unemittable(shape): | |
| 96 | + | """Whether `declare!` could never emit this shape, and why. | |
| 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. | |
| 102 | + | """ | |
| 103 | + | head = pop.head_type(shape["returns"]) or "" | |
| 104 | + | if head not in SHAPED: | |
| 105 | + | return f"`declare!` returns one of {'/'.join(sorted(SHAPED))}, not `{head}`" | |
| 106 | + | return None | |
| 107 | + | ||
| 51 | 108 | ||
| 52 | 109 | def scan(): | |
| 53 | 110 | per_file = {} | |
| @@ -62,6 +119,7 @@ | |||
| 62 | 119 | hand = pop.shapes_in(f) | |
| 63 | 120 | per_file[key] = { | |
| 64 | 121 | "tree": TREES[rel], | |
| 122 | + | "shapes": hand, | |
| 65 | 123 | "hand": len(hand), | |
| 66 | 124 | "declared": len(DECLARED.findall(blanked)), | |
| 67 | 125 | "lines": sum(h.get("lines", 0) for h in hand), | |
| @@ -69,10 +127,28 @@ | |||
| 69 | 127 | return per_file | |
| 70 | 128 | ||
| 71 | 129 | ||
| 130 | + | def counted_but_not_work(per_file): | |
| 131 | + | """Every counted shape that no conversion will ever remove, with its reason. | |
| 132 | + | ||
| 133 | + | `NOT_WORK` holds the two categories that need a human to say so; the third | |
| 134 | + | is derived, so a new method returning an `Action` is reported the day it is | |
| 135 | + | written rather than the day somebody notices. | |
| 136 | + | """ | |
| 137 | + | out = [] | |
| 138 | + | for path, v in sorted(per_file.items()): | |
| 139 | + | for shape in v["shapes"]: | |
| 140 | + | why = NOT_WORK.get(path, {}).get(shape["fn"]) or unemittable(shape) | |
| 141 | + | if why: | |
| 142 | + | out.append((path, shape["fn"], why)) | |
| 143 | + | return out | |
| 144 | + | ||
| 145 | + | ||
| 72 | 146 | def main() -> int: | |
| 73 | 147 | ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | |
| 74 | 148 | ap.add_argument("--files", action="store_true", help="per file, fully converted files hidden") | |
| 75 | 149 | ap.add_argument("--next", action="store_true", help="the smallest unconverted files first") | |
| 150 | + | ap.add_argument("--parked", action="store_true", | |
| 151 | + | help="the shapes the predicate counts that are not work, and why") | |
| 76 | 152 | args = ap.parse_args() | |
| 77 | 153 | ||
| 78 | 154 | per_file = scan() | |
| @@ -87,12 +163,27 @@ | |||
| 87 | 163 | print(f" {tree:11} declared {d:4}, remaining {h:4} of {base}") | |
| 88 | 164 | print(f"\ndeclared {declared}, remaining {remaining} of {total_base} ({pct:.1f}%)") | |
| 89 | 165 | ||
| 166 | + | parked = counted_but_not_work(per_file) | |
| 167 | + | if parked: | |
| 168 | + | print(f"of which counted but not work: {len(parked)}") | |
| 169 | + | ||
| 170 | + | if args.parked: | |
| 171 | + | print("\nCounted, and not work. Never take one of these off the queue.\n") | |
| 172 | + | for path, name, why in parked: | |
| 173 | + | print(f" {path}:{name}\n {why}") | |
| 174 | + | ||
| 90 | 175 | if args.next: | |
| 91 | - | todo = [(k, v) for k, v in per_file.items() if v["hand"]] | |
| 92 | - | todo.sort(key=lambda kv: (kv[1]["lines"], kv[1]["hand"])) | |
| 93 | - | print("\nSmallest unconverted files first. A whole file is the unit worth taking.\n") | |
| 94 | - | for k, v in todo[:20]: | |
| 95 | - | print(f" {v['hand']:3} shapes {v['lines']:5}L {k}") | |
| 176 | + | skip = {(path, name) for path, name, _ in parked} | |
| 177 | + | todo = [] | |
| 178 | + | for k, v in per_file.items(): | |
| 179 | + | left = [s for s in v["shapes"] if (k, s["fn"]) not in skip] | |
| 180 | + | if left: | |
| 181 | + | todo.append((k, len(left), sum(s.get("lines", 0) for s in left))) | |
| 182 | + | todo.sort(key=lambda row: (row[2], row[1])) | |
| 183 | + | print("\nSmallest unconverted files first. A whole file is the unit worth taking.") | |
| 184 | + | print("The counted-but-not-work shapes are already out; `--parked` lists them.\n") | |
| 185 | + | for path, hand, lines in todo[:20]: | |
| 186 | + | print(f" {hand:3} shapes {lines:5}L {path}") | |
| 96 | 187 | ||
| 97 | 188 | if args.files: | |
| 98 | 189 | print() |
| @@ -161,6 +161,15 @@ | |||
| 161 | 161 | args: Vec<Arg>, | |
| 162 | 162 | body: Vec<Item>, | |
| 163 | 163 | }, | |
| 164 | + | /// `chip <value> to <action> { .. }` | |
| 165 | + | /// | |
| 166 | + | /// Its own emission rather than a `Simple` member because `Tag::chip` takes | |
| 167 | + | /// an `Action`, and an action is spelled `to <verb>` rather than as an arg. | |
| 168 | + | Chip { | |
| 169 | + | value: Arg, | |
| 170 | + | action: Action, | |
| 171 | + | body: Vec<Item>, | |
| 172 | + | }, | |
| 164 | 173 | /// `screen <arrangement> <arg> { .. }` | |
| 165 | 174 | Screen { | |
| 166 | 175 | arrangement: Ident, |
| @@ -50,6 +50,8 @@ | |||
| 50 | 50 | Image, | |
| 51 | 51 | /// One canvas: its body sets on the `Canvas` and draws inside its scope. | |
| 52 | 52 | Canvas, | |
| 53 | + | /// One tag: its body says what it is like, and it emits nothing. | |
| 54 | + | Tag, | |
| 53 | 55 | } | |
| 54 | 56 | ||
| 55 | 57 | pub fn declaration(declaration: &Declaration) -> Result<TokenStream> { | |
| @@ -605,6 +607,10 @@ | |||
| 605 | 607 | // canvas can hold anything: the markup is opaque, so nothing is | |
| 606 | 608 | // injected into the middle of it. | |
| 607 | 609 | (Container::Canvas, other) => Ok((node(other)?, quote!(.with(#held)))), | |
| 610 | + | (Container::Tag, other) => Err(syn::Error::new( | |
| 611 | + | emission_span(other), | |
| 612 | + | "a tag holds nothing: its body says what it is like", | |
| 613 | + | )), | |
| 608 | 614 | (Container::Run, Emission::Beside { priority, inner }) => Ok(( | |
| 609 | 615 | node(inner)?, | |
| 610 | 616 | quote!(.beside(#held, ::quasi_router::layout::Priority::#priority)), | |
| @@ -775,6 +781,37 @@ | |||
| 775 | 781 | )?; | |
| 776 | 782 | Ok(quote!(::quasi_router::Node::Image(#picture))) | |
| 777 | 783 | } | |
| 784 | + | Emission::Simple { member, args, body } if member == "badge" => { | |
| 785 | + | // The third member whose body does not set on a `Node`, and the | |
| 786 | + | // first of two over `Tag`. `Node::Token` holds a `Tag`, and `token` | |
| 787 | + | // is a setting on `Row` and on `Cell` so it cannot be a member at | |
| 788 | + | // all, so the members are named for `Tag`'s constructors instead. | |
| 789 | + | // quasicoherent `e2030032` decided it over 33 sites. | |
| 790 | + | let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?; | |
| 791 | + | let tag = accrete( | |
| 792 | + | body, | |
| 793 | + | Container::Tag, | |
| 794 | + | "e!(::quasi_router::screen::Tag::badge(#(#args),*)), | |
| 795 | + | )?; | |
| 796 | + | Ok(quote!(::quasi_router::Node::token(#tag))) | |
| 797 | + | } | |
| 798 | + | Emission::Chip { | |
| 799 | + | value, | |
| 800 | + | action, | |
| 801 | + | body, | |
| 802 | + | } => { | |
| 803 | + | // The other half of `badge`. A chip goes somewhere, so it carries an | |
| 804 | + | // action, which is why it is spelled `chip <value> to <action>` | |
| 805 | + | // rather than as a member with two arguments. | |
| 806 | + | let value = arg(value)?; | |
| 807 | + | let called = self::action(action)?; | |
| 808 | + | let tag = accrete( | |
| 809 | + | body, | |
| 810 | + | Container::Tag, | |
| 811 | + | "e!(::quasi_router::screen::Tag::chip(#value, #called)), | |
| 812 | + | )?; | |
| 813 | + | Ok(quote!(::quasi_router::Node::token(#tag))) | |
| 814 | + | } | |
| 778 | 815 | Emission::Simple { member, args, body } if member == "canvas" => { | |
| 779 | 816 | // The second member whose body does not set on a `Node`, and for | |
| 780 | 817 | // `picture`'s reason: `Node::Canvas` holds a `Canvas`, and | |
| @@ -1396,7 +1433,7 @@ | |||
| 1396 | 1433 | Emission::Simple { member, .. } => member.span(), | |
| 1397 | 1434 | Emission::Screen { arrangement, .. } => arrangement.span(), | |
| 1398 | 1435 | Emission::Row { .. } | Emission::List(_) => Span::call_site(), | |
| 1399 | - | Emission::Form { action, .. } => action.verb.span(), | |
| 1436 | + | Emission::Form { action, .. } | Emission::Chip { action, .. } => action.verb.span(), | |
| 1400 | 1437 | Emission::Field { kind, .. } => kind.span(), | |
| 1401 | 1438 | Emission::Act { action, .. } => action.verb.span(), | |
| 1402 | 1439 | Emission::Guarded { guard, .. } => guard.span, |
| @@ -47,8 +47,8 @@ | |||
| 47 | 47 | /// without a production is a parse error naming it, which is the failure worth | |
| 48 | 48 | /// having. | |
| 49 | 49 | const MEMBERS: &[&str] = &[ | |
| 50 | - | "act", "across", "activate", "beside", "cell", "cells", "column", "field", "form", "given", | |
| 51 | - | "include", "link", "offering", "region", "row", "screen", "table", | |
| 50 | + | "act", "across", "activate", "beside", "cell", "cells", "chip", "column", "field", "form", | |
| 51 | + | "given", "include", "link", "offering", "region", "row", "screen", "table", | |
| 52 | 52 | ]; | |
| 53 | 53 | ||
| 54 | 54 | /// The members that are one of `Node`'s own constructors, called by its name. | |
| @@ -68,12 +68,24 @@ | |||
| 68 | 68 | /// supplier, which is the remedy the deferred table already names, and | |
| 69 | 69 | /// `project_analytics`'s `chip` is that supplier. | |
| 70 | 70 | /// | |
| 71 | + | /// **Where a variant holds a builder type, the member is named for the | |
| 72 | + | /// constructor rather than for the variant.** That is the rule the exceptions | |
| 73 | + | /// below share, and it is what `badge` and `chip` settled: `Node::Token` holds a | |
| 74 | + | /// `Tag`, `token` is a setting on `Row` and on `Cell` so it cannot be a member | |
| 75 | + | /// at all, and `Tag`'s two constructors are what a screen actually reaches for. | |
| 76 | + | /// So there is no `token` member and there never will be. quasicoherent | |
| 77 | + | /// `e2030032` is the decision, measured over 33 sites in the three trees. | |
| 78 | + | /// | |
| 71 | 79 | /// `canvas` is the second exception and is the last entry for that reason: | |
| 72 | 80 | /// `Canvas` is a builder type of its own, the way `Image` is, so the member's | |
| 73 | 81 | /// body accretes onto the canvas and the emitter wraps the variant afterwards. | |
| 74 | 82 | /// `custom_page` is the site -- a creator's opaque markup under the scope its | |
| 75 | 83 | /// stylesheet was rewritten for, with the platform's own blocks drawn inside | |
| 76 | 84 | /// it. | |
| 85 | + | /// | |
| 86 | + | /// `badge` is the third and `chip` the fourth, and `chip` is not on this list | |
| 87 | + | /// because it carries an action: it has a parse arm of its own. Both accrete | |
| 88 | + | /// onto a `Tag` under `Container::Tag` and the emitter wraps `Node::token`. | |
| 77 | 89 | const NODE_MEMBERS: &[&str] = &[ | |
| 78 | 90 | "text", | |
| 79 | 91 | "toned", | |
| @@ -91,6 +103,7 @@ | |||
| 91 | 103 | "code", | |
| 92 | 104 | "picture", | |
| 93 | 105 | "canvas", | |
| 106 | + | "badge", | |
| 94 | 107 | ]; | |
| 95 | 108 | ||
| 96 | 109 | /// The arrangements a screen may be laid out in. | |
| @@ -677,6 +690,29 @@ | |||
| 677 | 690 | }, | |
| 678 | 691 | )) | |
| 679 | 692 | } | |
| 693 | + | // A chip is a tag that goes somewhere, so it takes an action, and | |
| 694 | + | // an action is spelled `to <verb>` rather than as an argument. That | |
| 695 | + | // is why it is not in NODE_MEMBERS with `badge`. | |
| 696 | + | "chip" => { | |
| 697 | + | let value: Arg = input.parse()?; | |
| 698 | + | preposition(input, "to")?; | |
| 699 | + | let action: Action = input.parse()?; | |
| 700 | + | let guard = guard(input)?; | |
| 701 | + | let body = if input.peek(token::Brace) { | |
| 702 | + | block(input)? | |
| 703 | + | } else { | |
| 704 | + | input.parse::<Token![;]>()?; | |
| 705 | + | Vec::new() | |
| 706 | + | }; | |
| 707 | + | Ok(guarded( | |
| 708 | + | guard, | |
| 709 | + | Self::Chip { | |
| 710 | + | value, | |
| 711 | + | action, | |
| 712 | + | body, | |
| 713 | + | }, | |
| 714 | + | )) | |
| 715 | + | } | |
| 680 | 716 | "field" => { | |
| 681 | 717 | let kind: Ident = input.parse()?; | |
| 682 | 718 | let name: Arg = input.parse()?; |