Skip to main content

max / quasi

Grow the form for the git landing, the project blog and the buyer contacts Wave 2. Six screens: git_explore's page_screen and listing, project_blog's screen, buyer_contacts' pane, export and table. Earned by them: - `empty` as a node member. All three files draw an empty state. - `table`, `column`, `cells`, `cell`. git_explore's listing was the first table; buyer_contacts' is the second and needed nothing new. - `activate to <action>;`. A row of a list, a row of a table and a cell all carry one, and the vocabulary spells all three the same. - A guard on a setting, not only on a member. git_explore says `more rest(loaded) when loaded.page over 1 or loaded.has_more`, which is a setting that is sometimes not made. Forces the accumulating form the same way a guarded member does. `submit` refuses one: a form always has a button. - `width` and `priority` in the vocabulary table, so a column narrows by naming a variant rather than writing the path. Two gaps closed in quasi-router (0.101.5) rather than in the form. `Table::new` and `Cells::new` take lists and every other container accretes; the form has no expression to hold a list in, and a production that admitted one is the door this grammar keeps shut. So `Table::column` beside `Table::row`, asserting no row has been pushed yet because a later column is invisible to a pushed row's named cells, and `Cells::cell` beside `Cells::at`, positional to pair with it. `for <binder> in &<hole>` is refused, the second of section 10's candidates to be ruled on contact. `.iter()` is a method step the hole grammar already admits, so `for repo in loaded.repos.iter()` says the whole of it, and `&` would be a second spelling of one thing.
Record
wiki quasi-declare-form section 13.
Author: Max Johnson <me@maxj.phd> · 2026-09-03 18:49 UTC
Signed with PGP, not checked
Commit: b7ecad9f2856c99836bbfbbd312551ac12d4d406
Parent: c4bbd07
5 files changed, +231 insertions, -18 deletions
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-router"
3 - version = "0.101.4"
3 + version = "0.101.5"
4 4 description = "Host-agnostic router: a request in, a renderer-agnostic description out"
5 5 edition.workspace = true
6 6 rust-version.workspace = true
@@ -36,13 +36,23 @@
36 36 pub enum Item {
37 37 /// `let name = <source>;`
38 38 Bind { name: Ident, source: Source },
39 - /// `<builder> <arg>*;` -- one setting on the enclosing container.
39 + /// `<builder> <arg>* <guard>?;` -- one setting on the enclosing container.
40 40 ///
41 41 /// The ident is the **builder method's** name and not the struct field's,
42 42 /// which is what ATTRIBUTE NAMING settles: `Slot::named` writes `name`, and
43 43 /// a form that spelled the field would be naming something no caller can
44 44 /// reach.
45 - Attribute { name: Ident, args: Vec<Arg> },
45 + ///
46 + /// A setting takes a guard for the same reason a member does: the rule is
47 + /// that a guard is allowed where something may be absent, and a setting
48 + /// that is sometimes not made is exactly that. `git_explore`'s `more` is
49 + /// the live instance -- a table says what it has not shown only when there
50 + /// is another page.
51 + Attribute {
52 + name: Ident,
53 + args: Vec<Arg>,
54 + guard: Option<Guard>,
55 + },
46 56 /// `for <binder> in <hole> { .. }` -- the same body once per element.
47 57 ///
48 58 /// A loop is the one item that cannot be hoisted above its container: its
@@ -171,6 +181,29 @@
171 181 action: Action,
172 182 body: Vec<Item>,
173 183 },
184 + /// `table { column ..; cells { .. } }`
185 + ///
186 + /// Columns and rows accrete rather than arriving as two lists, which is
187 + /// what `Table::column` and `Cells::cell` were added to `quasi-router` for.
188 + /// The form has no expression to hold a list in, and a production that
189 + /// admitted one would be the door this grammar exists to keep shut.
190 + Table(Vec<Item>),
191 + /// `column <name> ( "{" .. "}" | ";" )` -- one column and how it narrows.
192 + Column { name: Arg, body: Vec<Item> },
193 + /// `cells { cell ..; }` -- one row of a table, by position.
194 + ///
195 + /// Positional only. A cell that names its column (`Cells::at`) is not a
196 + /// production yet: `git_blame`'s row is the one site that wants it, and it
197 + /// is still hand-written.
198 + Cells(Vec<Item>),
199 + /// `cell <value> ( "{" .. "}" | ";" )`
200 + Cell { value: Arg, body: Vec<Item> },
201 + /// `activate to <action>;` -- what opening this row or cell does.
202 + ///
203 + /// A setting in shape, and a member in the tree, because the thing it sets
204 + /// is an action and a setting's arguments are args. Rows, rows of cells and
205 + /// single cells all carry one.
206 + Activate(Action),
174 207 /// `include <hole>;` -- whatever another shape built.
175 208 Include(Hole),
176 209 /// `link <arg> to <action>;`
@@ -36,6 +36,14 @@
36 36 Screen,
37 37 /// One row of a list: its members are the controls it offers.
38 38 Row,
39 + /// A table: its members are its columns and its rows.
40 + Table,
41 + /// One column: its body says how it narrows, and it emits nothing.
42 + Column,
43 + /// One row of a table: its members are its cells, by position.
44 + Cells,
45 + /// One cell: its body says what opening it does.
46 + Cell,
39 47 }
40 48
41 49 pub fn declaration(declaration: &Declaration) -> Result<TokenStream> {
@@ -255,7 +263,9 @@
255 263 let statementish = items.iter().any(|item| {
256 264 matches!(
257 265 item,
258 - Item::For { .. } | Item::Emit(Emission::Guarded { .. })
266 + Item::For { .. }
267 + | Item::Emit(Emission::Guarded { .. })
268 + | Item::Attribute { guard: Some(_), .. }
259 269 )
260 270 });
261 271 if statementish {
@@ -270,7 +280,7 @@
270 280 let value = source_value(source)?;
271 281 bindings.push(quote!(let #name = #value;));
272 282 }
273 - Item::Attribute { name, args } => steps.push(attribute(name, args)?),
283 + Item::Attribute { name, args, .. } => steps.push(attribute(name, args)?),
274 284 Item::For { .. } => unreachable!("a loop takes the accumulating form"),
275 285 Item::Emit(emission) => {
276 286 // A member is evaluated into its own binding before the
@@ -317,9 +327,18 @@
317 327 let value = source_value(source)?;
318 328 quote!(let #name = #value;)
319 329 }
320 - Item::Attribute { name, args } => {
330 + Item::Attribute { name, args, guard } => {
321 331 let call = attribute(name, args)?;
322 - quote!(#built = #built #call;)
332 + match guard {
333 + // The same rule a guarded member follows: the guard decides
334 + // whether the setting is made, and the args are evaluated
335 + // either way.
336 + Some(guard) => {
337 + let test = predicate(guard)?;
338 + quote!(if #test { #built = #built #call; })
339 + }
340 + None => quote!(#built = #built #call;),
341 + }
323 342 }
324 343 Item::For {
325 344 dereferenced,
@@ -419,10 +438,14 @@
419 438 emission_span(other),
420 439 "a document holds regions: write `region <name> as <kind> { .. }`",
421 440 )),
441 + (Container::Cells | Container::Cell | Container::Row, Emission::Activate(action)) => {
442 + Ok((self::action(action)?, quote!(.activate(#held))))
443 + }
422 444 (Container::Row, Emission::Act { .. }) => Ok((act(emission)?, quote!(.act(#held)))),
423 445 (Container::Row, other) => Err(syn::Error::new(
424 446 emission_span(other),
425 - "a row holds controls: write `act <label> to <action>;`",
447 + "a row holds controls and says what opening it does: write \
448 + `act <label> to <action>;` or `activate to <action>;`",
426 449 )),
427 450 (Container::Run, Emission::Beside { priority, inner }) => Ok((
428 451 node(inner)?,
@@ -432,6 +455,29 @@
432 455 emission_span(other),
433 456 "a row ranks what it holds: write `beside <priority> <emission>`",
434 457 )),
458 + (Container::Table, Emission::Column { name, body }) => {
459 + Ok((column(name, body)?, quote!(.column(#held))))
460 + }
461 + (Container::Table, Emission::Cells(body)) => Ok((cells(body)?, quote!(.row(#held)))),
462 + (Container::Table, other) => Err(syn::Error::new(
463 + emission_span(other),
464 + "a table holds columns and rows: write `column <name>;` or `cells { .. }`",
465 + )),
466 + (Container::Cells, Emission::Cell { value, body }) => {
467 + Ok((cell(value, body)?, quote!(.cell(#held))))
468 + }
469 + (Container::Cells, other) => Err(syn::Error::new(
470 + emission_span(other),
471 + "a row of a table holds cells: write `cell <value>;`",
472 + )),
473 + (Container::Cell, other) => Err(syn::Error::new(
474 + emission_span(other),
475 + "a cell says what opening it does, and nothing else yet",
476 + )),
477 + (Container::Column, other) => Err(syn::Error::new(
478 + emission_span(other),
479 + "a column holds no members: its body says how it narrows",
480 + )),
435 481 (Container::Act | Container::Field, other) => Err(syn::Error::new(
436 482 emission_span(other),
437 483 "this holds no members: its body says what it is like",
@@ -502,6 +548,10 @@
502 548 Ok(quote!(::quasi_router::Node::#member(#(#args),*)))
503 549 }
504 550 Emission::List(items) => list(items),
551 + Emission::Table(items) => {
552 + let table = self::table(items)?;
553 + Ok(quote!(::std::convert::Into::into(#table)))
554 + }
505 555 Emission::Form { action, body } => form(action, body),
506 556 Emission::Field { kind, .. } => Err(syn::Error::new(
507 557 kind.span(),
@@ -569,6 +619,16 @@
569 619 fallback.span(),
570 620 "a row is not a node: `across` belongs in a region",
571 621 )),
622 + Emission::Column { .. } | Emission::Cells(_) | Emission::Cell { .. } => {
623 + Err(syn::Error::new(
624 + emission_span(emission),
625 + "a column, a row of cells and a cell are a table's, not a node's",
626 + ))
627 + }
628 + Emission::Activate(action) => Err(syn::Error::new(
629 + action.verb.span(),
630 + "`activate` says what opening a row or a cell does; it is not a node",
631 + )),
572 632 Emission::Beside { priority, .. } => Err(syn::Error::new(
573 633 priority.span(),
574 634 "`beside` needs a run in scope, which is rule R3",
@@ -602,10 +662,16 @@
602 662 let mut fields = Vec::new();
603 663 for item in body {
604 664 match item {
605 - Item::Attribute { name, args } if name == "submit" => {
665 + Item::Attribute { name, args, guard } if name == "submit" => {
606 666 let [label] = args.as_slice() else {
607 667 return Err(syn::Error::new(name.span(), "`submit` says one thing"));
608 668 };
669 + if let Some(guard) = guard {
670 + return Err(syn::Error::new(
671 + guard.span,
672 + "a form always has a button: `submit` takes no guard",
673 + ));
674 + }
609 675 submit = Some(arg(label)?);
610 676 }
611 677 Item::Emit(Emission::Field {
@@ -711,6 +777,51 @@
711 777 .collect()
712 778 }
713 779
780 + /// One table: its columns, its rows, and what it has not shown.
781 + ///
782 + /// Both halves accrete. `Table::new` takes its columns as a list and the form
783 + /// has no expression to hold one in, so `Table::column` was added beside it
784 + /// rather than the form growing a production that admits a list of built
785 + /// values. Columns before rows is the vocabulary's own rule and this emits them
786 + /// in the order they were written, so a declaration that gets it wrong trips
787 + /// `Table::row`'s assertion rather than losing a cell quietly.
788 + fn table(items: &[Item]) -> Result<TokenStream> {
789 + accrete(
790 + items,
791 + Container::Table,
792 + &quote!(::quasi_router::screen::Table::new(::std::iter::empty())),
793 + )
794 + }
795 +
796 + /// One column: what it is called, and how it narrows.
797 + fn column(name: &Arg, body: &[Item]) -> Result<TokenStream> {
798 + let name = arg(name)?;
799 + accrete(
800 + body,
801 + Container::Column,
802 + &quote!(::quasi_router::screen::Column::new(#name)),
803 + )
804 + }
805 +
806 + /// One row of a table, by position.
807 + fn cells(body: &[Item]) -> Result<TokenStream> {
808 + accrete(
809 + body,
810 + Container::Cells,
811 + &quote!(<::quasi_router::screen::Cells as ::std::default::Default>::default()),
812 + )
813 + }
814 +
815 + /// One cell: what it says, and what opening it does.
816 + fn cell(value: &Arg, body: &[Item]) -> Result<TokenStream> {
817 + let value = arg(value)?;
818 + accrete(
819 + body,
820 + Container::Cell,
821 + &quote!(::quasi_router::screen::Cell::new(#value)),
822 + )
823 + }
824 +
714 825 /// One row of a list: what it says, and what it offers.
715 826 fn row(primary: &Arg, body: &[Item]) -> Result<TokenStream> {
716 827 let primary = arg(primary)?;
@@ -942,5 +1053,8 @@
942 1053 Emission::Region { kind, .. } => kind.span(),
943 1054 Emission::Across { fallback, .. } => fallback.span(),
944 1055 Emission::Beside { priority, .. } => priority.span(),
1056 + Emission::Table(_) | Emission::Cells(_) => Span::call_site(),
1057 + Emission::Column { .. } | Emission::Cell { .. } => Span::call_site(),
1058 + Emission::Activate(action) => action.verb.span(),
945 1059 }
946 1060 }
@@ -42,8 +42,8 @@
42 42 /// without a production is a parse error naming it, which is the failure worth
43 43 /// having.
44 44 const MEMBERS: &[&str] = &[
45 - "act", "across", "beside", "field", "form", "given", "include", "link", "region", "row",
46 - "screen",
45 + "act", "across", "activate", "beside", "cell", "cells", "column", "field", "form", "given",
46 + "include", "link", "region", "row", "screen", "table",
47 47 ];
48 48
49 49 /// The members that are one of `Node`'s own constructors, called by its name.
@@ -57,6 +57,7 @@
57 57 "stats",
58 58 "banner",
59 59 "failed",
60 + "empty",
60 61 ];
61 62
62 63 /// The arrangements a screen may be laid out in.
@@ -74,7 +75,11 @@
74 75
75 76 /// The attributes whose argument is a variant of a vocabulary enum, and the
76 77 /// enum it belongs to. Rule R1(4).
77 - pub const VOCABULARY: &[(&str, &str)] = &[("tone", "Tone")];
78 + pub const VOCABULARY: &[(&str, &str)] = &[
79 + ("tone", "Tone"),
80 + ("width", "Width"),
81 + ("priority", "Priority"),
82 + ];
78 83
79 84 /// The attributes the generated function may carry.
80 85 const FLAGS: &[&str] = &["must_use", "inline"];
@@ -217,11 +222,12 @@
217 222 }
218 223 let name: Ident = input.parse()?;
219 224 let mut args = Vec::new();
220 - while !input.peek(Token![;]) {
225 + while !input.peek(Token![;]) && guard_ahead(input)?.is_none() {
221 226 args.push(input.parse()?);
222 227 }
228 + let guard = guard(input)?;
223 229 input.parse::<Token![;]>()?;
224 - Ok(Self::Attribute { name, args })
230 + Ok(Self::Attribute { name, args, guard })
225 231 }
226 232 }
227 233
@@ -486,6 +492,35 @@
486 492 Ok(Self::Row { primary, body })
487 493 }
488 494 "list" if input.peek(token::Brace) => Ok(Self::List(block(input)?)),
495 + "table" => Ok(Self::Table(block(input)?)),
496 + "column" => {
497 + let name: Arg = input.parse()?;
498 + let body = if input.peek(token::Brace) {
499 + block(input)?
500 + } else {
501 + input.parse::<Token![;]>()?;
502 + Vec::new()
503 + };
504 + Ok(Self::Column { name, body })
505 + }
506 + "cells" => Ok(Self::Cells(block(input)?)),
507 + "cell" => {
508 + let value: Arg = input.parse()?;
509 + let body = if input.peek(token::Brace) {
510 + block(input)?
511 + } else {
512 + input.parse::<Token![;]>()?;
513 + Vec::new()
514 + };
515 + Ok(Self::Cell { value, body })
516 + }
517 + "activate" => {
518 + preposition(input, "to")?;
519 + let action: Action = input.parse()?;
520 + let guard = guard(input)?;
521 + input.parse::<Token![;]>()?;
522 + Ok(guarded(guard, Self::Activate(action)))
523 + }
489 524 "form" => {
490 525 let action: Action = input.parse()?;
491 526 Ok(Self::Form {
@@ -576,10 +611,7 @@
576 611 return Ok(first);
577 612 };
578 613 let mut clauses = vec![first];
579 - loop {
580 - let Some(next) = connective_ahead(input)? else {
581 - break;
582 - };
614 + while let Some(next) = connective_ahead(input)? {
583 615 if next != connective {
584 616 return Err(syn::Error::new(
585 617 input.span(),
@@ -7324,6 +7324,20 @@
7324 7324 self
7325 7325 }
7326 7326
7327 + /// One cell, by position, appended after the ones already there.
7328 + ///
7329 + /// The accreting half of [`new`](Self::new), for the same reason
7330 + /// [`Table::column`] is the accreting half of [`Table::new`]: a caller with
7331 + /// no expression to hold a list in still has to be able to say a row of
7332 + /// cells. Positional, so it pairs with [`Table::column`] and not with
7333 + /// [`at`](Self::at); mixing the two in one row is what `at`'s own docs warn
7334 + /// against.
7335 + #[must_use]
7336 + pub fn cell(mut self, cell: impl Into<Cell>) -> Self {
7337 + self.values.push(cell.into());
7338 + self
7339 + }
7340 +
7327 7341 /// This is the row being shown elsewhere.
7328 7342 ///
7329 7343 /// Was reachable only by assigning the field, which is why a row built by
@@ -7502,6 +7516,26 @@
7502 7516 }
7503 7517 }
7504 7518
7519 + /// Add one column, for a caller building the table a piece at a time.
7520 + ///
7521 + /// Beside [`new`](Self::new) rather than instead of it: a table whose
7522 + /// columns are a literal list still says so in one expression, and a table
7523 + /// assembled by something that accretes (the declared form, which has no
7524 + /// expression to hold a list in) says the same thing one column at a time.
7525 + ///
7526 + /// **Columns before rows.** [`row`](Self::row) resolves a named cell
7527 + /// against the columns the table has when the row arrives, so a column
7528 + /// added afterwards is invisible to every row already pushed.
7529 + #[must_use]
7530 + pub fn column(mut self, column: Column) -> Self {
7531 + debug_assert!(
7532 + self.rows.is_empty(),
7533 + "a column added after a row cannot be seen by that row's named cells"
7534 + );
7535 + self.columns.push(column);
7536 + self
7537 + }
7538 +
7505 7539 /// Add a row, resolving any cell that named its column.
7506 7540 ///
7507 7541 /// A named cell whose column this table does not have is dropped: the