Skip to main content

max / quasi

Grow the form for the payout summary card Guards on members, which the card needs three of, and predicates joined by one connective. A body with a guard accumulates rather than chaining, for the reason a body with a loop does: both are statements. R9 is emitted rather than described. The guarded member's value is built whether or not it is placed, so the figures supplier is asked even when the strip is not drawn and answers the absent case with nothing. That is the git_nav arrangement, at the cost of one empty allocation. `Node::subsection` goes in beside `page` and `section`. Three heading levels had two constructors, so every subsection in the tree wrote the variant out with its `level` and its `text`, which is the struct rather than the vocabulary.
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:40 UTC
Signed with PGP, not checked
Commit: c91b0c4f1f5ff38054a0e8bf6e9a997286d89f40
Parent: 6890a9b
5 files changed, +155 insertions, -30 deletions
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-router"
3 - version = "0.101.2"
3 + version = "0.101.3"
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
@@ -201,6 +201,15 @@
201 201 }
202 202
203 203 pub enum Predicate {
204 + /// `<clause> and <clause>`, or `<clause> or <clause>`. One connective per
205 + /// predicate: mixing them would need precedence, and precedence is how a
206 + /// predicate becomes an expression.
207 + Joined {
208 + connective: Ident,
209 + clauses: Vec<Predicate>,
210 + },
211 + /// `not <clause>`.
212 + Not(Box<Predicate>),
204 213 /// A hole that is already a `bool`.
205 214 Truth(Hole),
206 215 /// `<hole> <compare> <arg>`. The comparison is a word rather than an
@@ -239,7 +239,15 @@
239 239
240 240 /// A body that accretes onto something already built.
241 241 fn accrete(items: &[Item], container: Container, base: &TokenStream) -> Result<TokenStream> {
242 - if items.iter().any(|item| matches!(item, Item::For { .. })) {
242 + // A loop cannot be a link in a chain, and neither can a guard: both are
243 + // statements. A body with either accumulates instead.
244 + let statementish = items.iter().any(|item| {
245 + matches!(
246 + item,
247 + Item::For { .. } | Item::Emit(Emission::Guarded { .. })
248 + )
249 + });
250 + if statementish {
243 251 return accumulate(items, container, base);
244 252 }
245 253
@@ -311,6 +319,21 @@
311 319 let inner = self::statements(body, container, built)?;
312 320 quote!(for #binder in #iterable { #(#inner)* })
313 321 }
322 + Item::Emit(Emission::Guarded { guard, inner }) => {
323 + let test = predicate(guard)?;
324 + let held = format_ident!("member_{index}", span = Span::call_site());
325 + let (value, call) = step(inner, container, &held)?;
326 + // R9: the guard decides whether the member is placed, not
327 + // whether its holes are evaluated. The value is built either
328 + // way, and a supplier that is asked for nothing answers with
329 + // nothing.
330 + quote!({
331 + let #held = #value;
332 + if #test {
333 + #built = #built #call;
334 + }
335 + })
336 + }
314 337 Item::Emit(emission) => {
315 338 let held = format_ident!("member_{index}", span = Span::call_site());
316 339 let (value, call) = step(emission, container, &held)?;
@@ -405,7 +428,37 @@
405 428
406 429 /// One guard, as the `bool` it tests.
407 430 fn predicate(guard: &Guard) -> Result<TokenStream> {
408 - let test = match &guard.predicate {
431 + let test = clause(&guard.predicate)?;
432 + Ok(if guard.negated {
433 + quote!(!(#test))
434 + } else {
435 + test
436 + })
437 + }
438 +
439 + fn clause(predicate: &Predicate) -> Result<TokenStream> {
440 + Ok(match predicate {
441 + Predicate::Joined {
442 + connective,
443 + clauses,
444 + } => {
445 + let operator: TokenStream = if connective == "and" { "&&" } else { "||" }
446 + .parse()
447 + .expect("a connective");
448 + let clauses = clauses.iter().map(clause).collect::<Result<Vec<_>>>()?;
449 + let mut joined = TokenStream::new();
450 + for (index, one) in clauses.into_iter().enumerate() {
451 + if index > 0 {
452 + joined.extend(operator.clone());
453 + }
454 + joined.extend(quote!((#one)));
455 + }
456 + joined
457 + }
458 + Predicate::Not(inner) => {
459 + let inner = clause(inner)?;
460 + quote!(!(#inner))
461 + }
409 462 Predicate::Truth(hole) => self::hole(hole)?,
410 463 Predicate::Comparison {
411 464 left,
@@ -425,11 +478,6 @@
425 478 let right = arg(right)?;
426 479 quote!(#left #operator #right)
427 480 }
428 - };
429 - Ok(if guard.negated {
430 - quote!(!(#test))
431 - } else {
432 - test
433 481 })
434 482 }
435 483
@@ -47,7 +47,16 @@
47 47
48 48 /// The members that are one of `Node`'s own constructors, called by its name.
49 49 /// Growing this list is the whole of adding one.
50 - const NODE_MEMBERS: &[&str] = &["text", "page", "section", "list"];
50 + const NODE_MEMBERS: &[&str] = &[
51 + "text",
52 + "page",
53 + "section",
54 + "subsection",
55 + "list",
56 + "stats",
57 + "banner",
58 + "failed",
59 + ];
51 60
52 61 /// The arrangements a screen may be laid out in.
53 62 const ARRANGEMENTS: &[&str] = &["single", "list_detail", "sidebar_content"];
@@ -520,33 +529,79 @@
520 529 return Ok(None);
521 530 };
522 531 let word: Ident = input.parse()?;
523 -
524 - let left: Hole = input.parse()?;
525 - let predicate = if input.peek(Ident) {
526 - let compare: Ident = input.parse()?;
527 - let spelling = compare.to_string();
528 - if !COMPARISONS.iter().any(|(known, _)| *known == spelling) {
529 - return Err(syn::Error::new(
530 - compare.span(),
531 - format!("`{spelling}` is not one of the comparisons"),
532 - ));
533 - }
534 - Predicate::Comparison {
535 - left,
536 - compare,
537 - right: input.parse()?,
538 - }
539 - } else {
540 - Predicate::Truth(left)
541 - };
542 -
543 532 Ok(Some(Guard {
544 533 negated,
545 - predicate,
534 + predicate: predicate(input)?,
546 535 span: word.span(),
547 536 }))
548 537 }
549 538
539 + /// One predicate: clauses joined by one connective, and never two.
540 + fn predicate(input: ParseStream) -> Result<Predicate> {
541 + let first = clause(input)?;
542 + let Some(connective) = connective_ahead(input)? else {
543 + return Ok(first);
544 + };
545 + let mut clauses = vec![first];
546 + loop {
547 + let Some(next) = connective_ahead(input)? else {
548 + break;
549 + };
550 + if next != connective {
551 + return Err(syn::Error::new(
552 + input.span(),
553 + "one connective per predicate: parenthesise to mix `and` with `or`",
554 + ));
555 + }
556 + input.parse::<Ident>()?;
557 + clauses.push(clause(input)?);
558 + }
559 + Ok(Predicate::Joined {
560 + connective,
561 + clauses,
562 + })
563 + }
564 +
565 + /// Whether `and` or `or` continues the predicate here.
566 + fn connective_ahead(input: ParseStream) -> Result<Option<Ident>> {
567 + if !input.peek(Ident) {
568 + return Ok(None);
569 + }
570 + let word = input.fork().parse::<Ident>()?;
571 + Ok(match word.to_string().as_str() {
572 + "and" | "or" => Some(word),
573 + _ => None,
574 + })
575 + }
576 +
577 + fn clause(input: ParseStream) -> Result<Predicate> {
578 + if input.peek(Ident) && input.fork().parse::<Ident>()? == "not" {
579 + input.parse::<Ident>()?;
580 + return Ok(Predicate::Not(Box::new(clause(input)?)));
581 + }
582 + if input.peek(token::Paren) {
583 + let inner;
584 + parenthesized!(inner in input);
585 + return predicate(&inner);
586 + }
587 +
588 + let left: Hole = input.parse()?;
589 + if !input.peek(Ident) {
590 + return Ok(Predicate::Truth(left));
591 + }
592 + let word = input.fork().parse::<Ident>()?;
593 + let spelling = word.to_string();
594 + if !COMPARISONS.iter().any(|(known, _)| *known == spelling) {
595 + return Ok(Predicate::Truth(left));
596 + }
597 + let compare: Ident = input.parse()?;
598 + Ok(Predicate::Comparison {
599 + left,
600 + compare,
601 + right: input.parse()?,
602 + })
603 + }
604 +
550 605 fn guarded(guard: Option<Guard>, inner: Emission) -> Emission {
551 606 match guard {
552 607 Some(guard) => Emission::Guarded {
@@ -8554,6 +8554,19 @@
8554 8554 }
8555 8555 }
8556 8556
8557 + /// A subsection title.
8558 + ///
8559 + /// The third of the three heading levels, which had no constructor while
8560 + /// the other two did. Every site that wanted one wrote the variant out
8561 + /// with its `level` and its `text`, which is the struct rather than the
8562 + /// vocabulary.
8563 + pub fn subsection(text: impl Into<String>) -> Self {
8564 + Self::Heading {
8565 + level: layout::Heading::Subsection,
8566 + text: text.into(),
8567 + }
8568 + }
8569 +
8557 8570 /// Ordinary prose.
8558 8571 pub fn text(text: impl Into<String>) -> Self {
8559 8572 Self::Text {