Skip to main content

max / quasi

Grow the form for the use-cases page and the tip offer use_cases: a `-> Slot` shape, a bodyless `row`, and a loop binder that dereferences. The last is a pattern rather than an expression, and it is written where the code writes it, which is what R8 asks: a slice of `&str` yields `&&str` and a row takes a value. tip: `form` and `field`, and `doing` from amendment 6, which is the remedy for an action the form cannot spell. The tip's project rides on the action only on a project page, and a form has no way to say "and this value too, sometimes", so the action comes from a supplier beside the declaration. `From<Slot> for Node` joins `From<Act>`: use_cases builds nine cards as slots and puts each in a region.
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-03 17:52 UTC
Signed with PGP, not checked
Commit: c4bbd0788067d1ca5ab63d75a2ca547bc7e47753
Parent: c91b0c4
5 files changed, +188 insertions, -21 deletions
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-router"
3 - version = "0.101.3"
3 + version = "0.101.4"
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
@@ -48,6 +48,10 @@
48 48 /// A loop is the one item that cannot be hoisted above its container: its
49 49 /// members name the binder, which does not exist until the loop does.
50 50 For {
51 + /// `&name` where the elements are references to values the members
52 + /// take by value. Written where the code writes it, as R8 requires,
53 + /// and a pattern rather than an expression.
54 + dereferenced: bool,
51 55 binder: Ident,
52 56 iterable: Hole,
53 57 body: Vec<Item>,
@@ -146,6 +150,15 @@
146 150 },
147 151 /// `row <arg> { .. }`
148 152 Row { primary: Arg, body: Vec<Item> },
153 + /// `form <action> { submit <arg>; field .. }`
154 + Form { action: Action, body: Vec<Item> },
155 + /// `field <kind> <name> <label> ( { .. } | ; )`
156 + Field {
157 + kind: Ident,
158 + name: Arg,
159 + label: Arg,
160 + body: Vec<Item>,
161 + },
149 162 /// `list { .. }` -- the rows built in place rather than supplied.
150 163 ///
151 164 /// Its own member rather than a container in the general sense: a `Vec`
@@ -222,6 +235,9 @@
222 235 }
223 236
224 237 pub struct Action {
238 + /// One of the verbs, or `doing`, which takes the whole action from a
239 + /// supplier. Amendment 6: a screen whose action is decided by something
240 + /// the form cannot say gets a function beside it rather than a production.
225 241 pub verb: Ident,
226 242 pub target: Option<Arg>,
227 243 pub modifiers: Vec<Modifier>,
@@ -30,6 +30,8 @@
30 30 Run,
31 31 /// One control: its body sets, and emits nothing.
32 32 Act,
33 + /// One field: its body sets, and emits nothing.
34 + Field,
33 35 /// A document: its members are regions, held as slots rather than nodes.
34 36 Screen,
35 37 /// One row of a list: its members are the controls it offers.
@@ -199,6 +201,15 @@
199 201 ));
200 202 }
201 203 },
204 + "Slot" => match only {
205 + Emission::Region { name, kind, body } => slot(name, kind, body)?,
206 + other => {
207 + return Err(syn::Error::new(
208 + emission_span(other),
209 + "a `-> Slot` shape is its single `region` member",
210 + ));
211 + }
212 + },
202 213 "Row" => match only {
203 214 Emission::Row { primary, body } => row(primary, body)?,
204 215 other => {
@@ -311,12 +322,14 @@
311 322 quote!(#built = #built #call;)
312 323 }
313 324 Item::For {
325 + dereferenced,
314 326 binder,
315 327 iterable,
316 328 body,
317 329 } => {
318 330 let iterable = hole(iterable)?;
319 331 let inner = self::statements(body, container, built)?;
332 + let binder = binder_pattern(*dereferenced, binder);
320 333 quote!(for #binder in #iterable { #(#inner)* })
321 334 }
322 335 Item::Emit(Emission::Guarded { guard, inner }) => {
@@ -419,9 +432,9 @@
419 432 emission_span(other),
420 433 "a row ranks what it holds: write `beside <priority> <emission>`",
421 434 )),
422 - (Container::Act, other) => Err(syn::Error::new(
435 + (Container::Act | Container::Field, other) => Err(syn::Error::new(
423 436 emission_span(other),
424 - "a control holds no members: its body says what it is like",
437 + "this holds no members: its body says what it is like",
425 438 )),
426 439 }
427 440 }
@@ -489,6 +502,11 @@
489 502 Ok(quote!(::quasi_router::Node::#member(#(#args),*)))
490 503 }
491 504 Emission::List(items) => list(items),
505 + Emission::Form { action, body } => form(action, body),
506 + Emission::Field { kind, .. } => Err(syn::Error::new(
507 + kind.span(),
508 + "a field is not a node: put it in a `form`",
509 + )),
492 510 Emission::Row { .. } => Err(syn::Error::new(
493 511 emission_span(emission),
494 512 "a row is not a node: put it in a `list`",
@@ -569,6 +587,84 @@
569 587 )
570 588 }
571 589
590 + /// The loop's binder, dereferenced where the declaration said to.
591 + fn binder_pattern(dereferenced: bool, binder: &proc_macro2::Ident) -> TokenStream {
592 + if dereferenced {
593 + quote!(&#binder)
594 + } else {
595 + quote!(#binder)
596 + }
597 + }
598 +
599 + /// One form: where it writes, what its button says, and what it asks.
600 + fn form(action: &Action, body: &[Item]) -> Result<TokenStream> {
601 + let mut submit = None;
602 + let mut fields = Vec::new();
603 + for item in body {
604 + match item {
605 + Item::Attribute { name, args } if name == "submit" => {
606 + let [label] = args.as_slice() else {
607 + return Err(syn::Error::new(name.span(), "`submit` says one thing"));
608 + };
609 + submit = Some(arg(label)?);
610 + }
611 + Item::Emit(Emission::Field {
612 + kind,
613 + name,
614 + label,
615 + body,
616 + }) => fields.push(field(kind, name, label, body)?),
617 + Item::Attribute { name, .. } => {
618 + return Err(syn::Error::new(
619 + name.span(),
620 + "a form says `submit` and asks fields, and nothing else yet",
621 + ));
622 + }
623 + Item::Bind { name, .. } => {
624 + return Err(syn::Error::new(name.span(), "a form binds nothing"));
625 + }
626 + Item::For { binder, .. } => {
627 + return Err(syn::Error::new(
628 + binder.span(),
629 + "a form's fields are written out; a loop over them is not a production yet",
630 + ));
631 + }
632 + Item::Emit(other) => {
633 + return Err(syn::Error::new(emission_span(other), "a form holds fields"));
634 + }
635 + }
636 + }
637 + let Some(submit) = submit else {
638 + return Err(syn::Error::new(
639 + action.verb.span(),
640 + "a form says what its button reads: `submit <text>;`",
641 + ));
642 + };
643 + let action = self::action(action)?;
644 + Ok(quote! {
645 + ::quasi_router::Node::Form {
646 + action: #action,
647 + submit: ::std::convert::Into::into(#submit),
648 + fields: ::std::vec![#(#fields),*],
649 + }
650 + })
651 + }
652 +
653 + /// One field: what it asks for, by name, under a label.
654 + fn field(kind: &proc_macro2::Ident, name: &Arg, label: &Arg, body: &[Item]) -> Result<TokenStream> {
655 + let name = arg(name)?;
656 + let label = arg(label)?;
657 + accrete(
658 + body,
659 + Container::Field,
660 + &quote!(::quasi_router::Field::new(
661 + ::quasi_router::layout::FieldKind::#kind,
662 + #name,
663 + #label
664 + )),
665 + )
666 + }
667 +
572 668 /// The rows of a list, built where they are read.
573 669 fn list(items: &[Item]) -> Result<TokenStream> {
574 670 let rows = format_ident!("rows", span = Span::call_site());
@@ -589,12 +685,14 @@
589 685 Ok(quote!(let #name = #value;))
590 686 }
591 687 Item::For {
688 + dereferenced,
592 689 binder,
593 690 iterable,
594 691 body,
595 692 } => {
596 693 let iterable = hole(iterable)?;
597 694 let inner = list_statements(body, rows)?;
695 + let binder = binder_pattern(*dereferenced, binder);
598 696 Ok(quote!(for #binder in #iterable { #(#inner)* }))
599 697 }
600 698 Item::Emit(Emission::Row { primary, body }) => {
@@ -651,13 +749,18 @@
651 749 target,
652 750 modifiers,
653 751 } = action;
654 - let verb = format_ident!("{}", verb);
655 - let target = match target {
656 - Some(target) => {
657 - let target = arg(target)?;
658 - quote!(#target)
659 - }
660 - None => quote!(),
752 + let called = if verb == "doing" {
753 + // Amendment 6: the supplier IS the action.
754 + let Some(target) = target else {
755 + return Err(syn::Error::new(verb.span(), "`doing` names a supplier"));
756 + };
757 + arg(target)?
758 + } else {
759 + let target = match target {
760 + Some(target) => arg(target)?,
761 + None => quote!(),
762 + };
763 + quote!(::quasi_router::Action::#verb(#target))
661 764 };
662 765 let modifiers = modifiers
663 766 .iter()
@@ -667,7 +770,7 @@
667 770 Ok(quote!(.#name(#(#args),*)))
668 771 })
669 772 .collect::<Result<Vec<_>>>()?;
670 - Ok(quote!(::quasi_router::Action::#verb(#target) #(#modifiers)*))
773 + Ok(quote!(#called #(#modifiers)*))
671 774 }
672 775
673 776 fn source(source: &Source, owned: bool) -> Result<TokenStream> {
@@ -831,6 +934,8 @@
831 934 Emission::Simple { member, .. } => member.span(),
832 935 Emission::Screen { arrangement, .. } => arrangement.span(),
833 936 Emission::Row { .. } | Emission::List(_) => Span::call_site(),
937 + Emission::Form { action, .. } => action.verb.span(),
938 + Emission::Field { kind, .. } => kind.span(),
834 939 Emission::Act { action, .. } => action.verb.span(),
835 940 Emission::Guarded { guard, .. } => guard.span,
836 941 Emission::Given { .. } => Span::call_site(),
@@ -42,7 +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", "given", "include", "link", "region", "row", "screen",
45 + "act", "across", "beside", "field", "form", "given", "include", "link", "region", "row",
46 + "screen",
46 47 ];
47 48
48 49 /// The members that are one of `Node`'s own constructors, called by its name.
@@ -195,10 +196,15 @@
195 196 }
196 197 if input.peek(Token![for]) {
197 198 input.parse::<Token![for]>()?;
199 + let dereferenced = input.peek(Token![&]);
200 + if dereferenced {
201 + input.parse::<Token![&]>()?;
202 + }
198 203 let binder: Ident = input.parse()?;
199 204 input.parse::<Token![in]>()?;
200 205 let iterable: Hole = input.parse()?;
201 206 return Ok(Self::For {
207 + dereferenced,
202 208 binder,
203 209 iterable,
204 210 body: block(input)?,
@@ -471,12 +477,39 @@
471 477 }
472 478 "row" => {
473 479 let primary: Arg = input.parse()?;
474 - Ok(Self::Row {
475 - primary,
480 + let body = if input.peek(token::Brace) {
481 + block(input)?
482 + } else {
483 + input.parse::<Token![;]>()?;
484 + Vec::new()
485 + };
486 + Ok(Self::Row { primary, body })
487 + }
488 + "list" if input.peek(token::Brace) => Ok(Self::List(block(input)?)),
489 + "form" => {
490 + let action: Action = input.parse()?;
491 + Ok(Self::Form {
492 + action,
476 493 body: block(input)?,
477 494 })
478 495 }
479 - "list" if input.peek(token::Brace) => Ok(Self::List(block(input)?)),
496 + "field" => {
497 + let kind: Ident = input.parse()?;
498 + let name: Arg = input.parse()?;
499 + let label: Arg = input.parse()?;
500 + let body = if input.peek(token::Brace) {
501 + block(input)?
502 + } else {
503 + input.parse::<Token![;]>()?;
504 + Vec::new()
505 + };
506 + Ok(Self::Field {
507 + kind,
508 + name,
509 + label,
510 + body,
511 + })
512 + }
480 513 name if NODE_MEMBERS.contains(&name) => {
481 514 let mut args = Vec::new();
482 515 while !input.peek(Token![;]) && guard_ahead(input)?.is_none() {
@@ -638,14 +671,19 @@
638 671 fn parse(input: ParseStream) -> Result<Self> {
639 672 let verb: Ident = input.parse()?;
640 673 let name = verb.to_string();
641 - let Some((_, addresses)) = VERBS.iter().find(|(known, _)| *known == name) else {
642 - return Err(syn::Error::new(
643 - verb.span(),
644 - format!("`{name}` is not one of the verbs"),
645 - ));
674 + let addresses = if name == "doing" {
675 + true
676 + } else {
677 + let Some((_, addresses)) = VERBS.iter().find(|(known, _)| *known == name) else {
678 + return Err(syn::Error::new(
679 + verb.span(),
680 + format!("`{name}` is not one of the verbs"),
681 + ));
682 + };
683 + *addresses
646 684 };
647 685
648 - let target = if *addresses {
686 + let target = if addresses {
649 687 Some(input.parse()?)
650 688 } else {
651 689 None
@@ -7581,6 +7581,14 @@
7581 7581 }
7582 7582 }
7583 7583
7584 + impl From<Slot> for Node {
7585 + /// A region inside a region, without the enclosing one naming the variant.
7586 + /// `From<Act>` below is the same courtesy for the same reason.
7587 + fn from(slot: Slot) -> Self {
7588 + Self::Region(slot)
7589 + }
7590 + }
7591 +
7584 7592 impl From<Act> for Node {
7585 7593 /// The wrapping every caller of a control-returning function writes by
7586 7594 /// hand. `From<Table>` below is the same courtesy for the same reason, and