Skip to main content

max / quasi

79.2 KB · 2037 lines History Blame Raw
1 //! Turning a parsed declaration into the Rust a shape function used to be.
2 //!
3 //! Every construct emitted here is a constructor `quasi_router` already has, so
4 //! nothing in the generated code is reachable only through this macro. The
5 //! expansion is the same tree the hand-written shape built, made at the same
6 //! moment for the same request; what changes is who wrote it.
7 //!
8 //! Two rules decide most of this file. Rule R2 says what the shape's return
9 //! type makes: a `Node` shape's single emission IS the result, so there is no
10 //! fabricated container to fill. And rule R8 says nothing borrows implicitly,
11 //! so an `&` appears in the output only where the declaration wrote one.
12
13 use proc_macro2::{Span, TokenStream};
14 use quote::{format_ident, quote};
15 use syn::spanned::Spanned as _;
16 use syn::{LitStr, Result};
17
18 use crate::ast::{
19 Action, Arg, Declaration, Emission, Guard, Hole, HoleRoot, Interpolated, Item, Pattern,
20 Predicate, RegionKind, Source, Step, StrPart,
21 };
22 use crate::parse::{COMPARISONS, VOCABULARY};
23
24 /// What the items of a body accrete onto.
25 #[derive(Clone, Copy, PartialEq, Eq)]
26 enum Container {
27 /// A region: emissions accrete onto a `Slot`.
28 Slot,
29 /// A row: every emission is `beside` and carries a priority.
30 Run,
31 /// One control: its body sets, and emits nothing.
32 Act,
33 /// One field: its body sets, and emits nothing.
34 Field,
35 /// A document: its members are regions, held as slots rather than nodes.
36 Screen,
37 /// One row of a list: its members are the controls it offers.
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 cell: its body says what opening it does.
44 Cell,
45 /// One node member: its body says what it is told afterwards.
46 Node,
47 /// One picture: its body sets on the `Image`, and it emits nothing.
48 Image,
49 /// One canvas: its body sets on the `Canvas` and draws inside its scope.
50 Canvas,
51 /// One tag: its body says what it is like, and it emits nothing.
52 Tag,
53 /// An axis: its members are `at`, each a placement and the row on it.
54 Timeline,
55 /// One meter: its body says what it is like, and it emits nothing.
56 Meter,
57 /// One figure: its body says what it is like, and it emits nothing.
58 Figure,
59 /// One repeating question: its body says how few may be left, and it emits
60 /// nothing.
61 Repeats,
62 }
63
64 pub fn declaration(declaration: &Declaration) -> Result<TokenStream> {
65 let Declaration {
66 docs,
67 flags,
68 vis,
69 name,
70 params,
71 returns,
72 items,
73 } = declaration;
74
75 let shaped = shaped_name(returns)?;
76
77 let docs = docs.iter().map(|line| {
78 let line = LitStr::new(line, Span::call_site());
79 quote!(#[doc = #line])
80 });
81 // `staged` asks for the twin in `symbolic` and `constant` asks for the
82 // shim beside it. Neither is an attribute rustc has ever heard of.
83 let flags = flags
84 .iter()
85 .filter(|flag| *flag != crate::symbolic::FLAG && *flag != crate::symbolic::CONSTANT)
86 .map(|flag| quote!(#[#flag]));
87 let params = params.iter().map(|param| {
88 let name = &param.name;
89 let ty = &param.ty;
90 quote!(#name: #ty)
91 });
92 let body = value_body(items, &shaped, name.span())?;
93
94 let returns = returns_type(returns)?;
95
96 Ok(quote! {
97 #(#docs)*
98 #(#flags)*
99 #vis fn #name(#(#params),*) -> #returns #body
100 })
101 }
102
103 /// The vocabulary type a shape returns, written out.
104 ///
105 /// The vocabulary type, not the path the file happened to import it under:
106 /// team.rs takes `Screen as Described` because it has a function called
107 /// `screen`, and a declaration names what it returns rather than what the module
108 /// around it calls that.
109 ///
110 /// Public to the crate because `symbolic`'s `#[constant]` shim has the same
111 /// signature as the shape it stands for, and a second spelling of this mapping
112 /// would drift from the first the moment a shaped type was added.
113 pub fn returns_type(returns: &syn::Type) -> Result<TokenStream> {
114 let shaped = shaped_name(returns)?;
115 Ok(match &shaped {
116 Shaped::Nodes => quote!(::std::vec::Vec<::quasi_router::Node>),
117 Shaped::Acts => quote!(::std::vec::Vec<::quasi_router::Act>),
118 Shaped::Single { name, optional } => {
119 let shaped_type = format_ident!("{}", name, span = returns.span());
120 if *optional {
121 quote!(::std::option::Option<::quasi_router::#shaped_type>)
122 } else {
123 quote!(::quasi_router::#shaped_type)
124 }
125 }
126 })
127 }
128
129 /// What a shape returns.
130 enum Shaped {
131 /// One vocabulary type, which the body's single emission is. `optional` is
132 /// the grammar's `single "?"`, and rule R10 is what makes it usable: a body
133 /// whose emission is guarded away yields `None`.
134 Single { name: String, optional: bool },
135 /// `Vec<Node>`: the members of a panel, in order, with nothing wrapping
136 /// them.
137 ///
138 /// The one shape that is not a single emission, and it is not a container
139 /// either. `user_support` demanded it: the dashboard strip draws the region
140 /// and its `id`, so the panel's own fill must add no second one, and the
141 /// tab that answers over htmx wraps the same members itself. 52 of the
142 /// population's 484 shapes return this, third after `Slot` and `Node`.
143 Nodes,
144 /// `Vec<Act>`: a menu, which is the controls a row holds back.
145 ///
146 /// [`Nodes`](Self::Nodes)' twin and for its reason. `Row::menu` and
147 /// `Row::menu` takes the whole list, so a menu that is built conditionally
148 /// has nowhere to accrete, and audiofiles' file list has two of them --
149 /// what a row offers and what a chosen set does. Every member is an `act`,
150 /// because a menu is nothing but controls.
151 Acts,
152 }
153
154 /// The type a shape returns, read off the declaration.
155 fn shaped_name(returns: &syn::Type) -> Result<Shaped> {
156 let syn::Type::Path(path) = returns else {
157 return Err(syn::Error::new_spanned(
158 returns,
159 "a shape returns one of the vocabulary types",
160 ));
161 };
162 let Some(last) = path.path.segments.last() else {
163 return Err(syn::Error::new_spanned(returns, "an empty return type"));
164 };
165 if last.ident == "Vec" {
166 let Some(inner) = sole_argument(returns, &last.arguments)? else {
167 return Err(syn::Error::new_spanned(returns, "`Vec` of what?"));
168 };
169 return match shaped_name(inner)? {
170 Shaped::Single {
171 ref name,
172 optional: false,
173 } if name == "Node" => Ok(Shaped::Nodes),
174 Shaped::Single {
175 ref name,
176 optional: false,
177 } if name == "Act" => Ok(Shaped::Acts),
178 _ => Err(syn::Error::new_spanned(
179 returns,
180 "a shape builds two lists: `Vec<Node>`, a panel's members, and \
181 `Vec<Act>`, a menu",
182 )),
183 };
184 }
185 if last.ident == "Option" {
186 let Some(inner) = sole_argument(returns, &last.arguments)? else {
187 return Err(syn::Error::new_spanned(returns, "`Option` of what?"));
188 };
189 return match shaped_name(inner)? {
190 Shaped::Single {
191 name,
192 optional: false,
193 } => Ok(Shaped::Single {
194 name,
195 optional: true,
196 }),
197 Shaped::Single { .. } => Err(syn::Error::new_spanned(
198 returns,
199 "a shape omits its result or does not; there is no second omission",
200 )),
201 Shaped::Nodes => Err(syn::Error::new_spanned(
202 returns,
203 "a panel with no members is an empty `Vec<Node>`, not a `None`",
204 )),
205 Shaped::Acts => Err(syn::Error::new_spanned(
206 returns,
207 "a menu with no entries is an empty `Vec<Act>`, not a `None`",
208 )),
209 };
210 }
211 Ok(Shaped::Single {
212 name: last.ident.to_string(),
213 optional: false,
214 })
215 }
216
217 /// The one type inside `Option<..>` or `Vec<..>`.
218 fn sole_argument<'a>(
219 returns: &syn::Type,
220 arguments: &'a syn::PathArguments,
221 ) -> Result<Option<&'a syn::Type>> {
222 let syn::PathArguments::AngleBracketed(arguments) = arguments else {
223 return Err(syn::Error::new_spanned(returns, "of what?"));
224 };
225 Ok(match arguments.args.first() {
226 Some(syn::GenericArgument::Type(inner)) => Some(inner),
227 _ => None,
228 })
229 }
230
231 /// A shape's body: its bindings, then the single emission that is the value.
232 ///
233 /// Rule R2 for the two shapes converted so far. A `-> Node` shape's single
234 /// emission IS the result and a `-> Act` shape's single `act` member is, so
235 /// neither fabricates a container: what a caller gets is what the body said.
236 ///
237 /// Bindings are hoisted above the emissions of their own body. Nothing is
238 /// reordered by that: a binding can only name bindings written before it, and
239 /// an emission produces a value rather than an effect, so the two are
240 /// independent within one block.
241 fn value_body(items: &[Item], shaped: &Shaped, span: Span) -> Result<TokenStream> {
242 let shaped = match shaped {
243 Shaped::Nodes => return nodes(items),
244 Shaped::Acts => return acts(items),
245 single @ Shaped::Single { .. } => single,
246 };
247 let Shaped::Single {
248 name: shaped,
249 optional,
250 } = shaped
251 else {
252 unreachable!("the two lists are answered above")
253 };
254 let optional = *optional;
255 let shaped = shaped.as_str();
256 let mut bindings = Vec::new();
257 let mut emissions = Vec::new();
258 for item in items {
259 match item {
260 Item::Bind { name, source } => {
261 let value = source_value(source)?;
262 bindings.push(quote!(let #name = #value;));
263 }
264 Item::Attribute { name, .. } => {
265 return Err(syn::Error::new(
266 name.span(),
267 "a setting needs something to set: put it in the member's body",
268 ));
269 }
270 Item::For { binder, .. } => {
271 return Err(syn::Error::new(
272 binder.span(),
273 "a loop emits many members, and this shape is its single one",
274 ));
275 }
276 Item::Emit(emission) => emissions.push(emission),
277 }
278 }
279
280 let [only] = emissions.as_slice() else {
281 return Err(syn::Error::new(
282 span,
283 format!(
284 "a `-> {shaped}` shape is its single emission, and this one has {}",
285 emissions.len()
286 ),
287 ));
288 };
289
290 let (guard, only) = match only {
291 Emission::Guarded { guard, inner } => (Some(guard), &**inner),
292 other => (None, *other),
293 };
294 if guard.is_some() && !optional {
295 return Err(syn::Error::new(
296 span,
297 "a guard on the whole result needs a shape that may omit it: `-> Option<_>`",
298 ));
299 }
300
301 let built = match shaped {
302 "Node" => node(only)?,
303 "Act" => match only {
304 Emission::Act { .. } => act(only)?,
305 other => {
306 return Err(syn::Error::new(
307 emission_span(other),
308 "a `-> Act` shape is its single `act` member",
309 ));
310 }
311 },
312 "Screen" => match only {
313 Emission::Screen {
314 arrangement,
315 args,
316 body,
317 } => {
318 let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?;
319 accrete(
320 body,
321 Container::Screen,
322 &quote!(::quasi_router::Screen::#arrangement(#(#args),*)),
323 )?
324 }
325 other => {
326 return Err(syn::Error::new(
327 emission_span(other),
328 "a `-> Screen` shape is its single `screen` member",
329 ));
330 }
331 },
332 "Slot" => match only {
333 Emission::Region { name, kind, body } => slot(name, kind, body)?,
334 other => {
335 return Err(syn::Error::new(
336 emission_span(other),
337 "a `-> Slot` shape is its single `region` member",
338 ));
339 }
340 },
341 // One arm, because there is one row type since the 2026-09-05 collapse.
342 // The body says which spelling it is -- `row "primary" { .. }` names
343 // roles of the default column set, `cells { .. }` names declared columns
344 // -- and the return type was a second, redundant way to say the same
345 // thing. It was worse than redundant: it had to agree with the body, so
346 // a table row spelled `-> Row` was an error about the return type when
347 // nothing was wrong with it.
348 "Row" => match only {
349 Emission::Row { primary, body } => row(Some(primary), body)?,
350 Emission::Cells(body) => cells(body)?,
351 other => {
352 return Err(syn::Error::new(
353 emission_span(other),
354 "a `-> Row` shape is its single `row` or `cells` member",
355 ));
356 }
357 },
358 "Field" => match only {
359 Emission::Field {
360 kind,
361 name,
362 label,
363 body,
364 } => field(kind, name, label, body)?,
365 other => {
366 return Err(syn::Error::new(
367 emission_span(other),
368 "a `-> Field` shape is its single `field` member",
369 ));
370 }
371 },
372 other => {
373 return Err(syn::Error::new(
374 span,
375 format!(
376 "`-> {other}` is not a shape this form can build yet. \
377 Add the R2 case, and name the screen that demanded it in the commit."
378 ),
379 ));
380 }
381 };
382
383 let built = if optional {
384 let test = match guard {
385 Some(guard) => predicate(guard)?,
386 None => quote!(true),
387 };
388 quote!(if #test { ::std::option::Option::Some(#built) } else { ::std::option::Option::None })
389 } else {
390 built
391 };
392
393 Ok(quote!({ #(#bindings)* #built }))
394 }
395
396 /// A binding's value, with the dispatch's ownership already decided.
397 fn source_value(source: &Source) -> Result<TokenStream> {
398 self::source(source, owned_arms(source))
399 }
400
401 /// A body that accretes onto something already built.
402 fn accrete(items: &[Item], container: Container, base: &TokenStream) -> Result<TokenStream> {
403 // A loop cannot be a link in a chain, and neither can a guard: both are
404 // statements. A body with either accumulates instead.
405 let statementish = items.iter().any(|item| {
406 matches!(
407 item,
408 Item::For { .. }
409 | Item::Emit(Emission::Guarded { .. })
410 | Item::Attribute { guard: Some(_), .. }
411 )
412 });
413 if statementish {
414 return accumulate(items, container, base);
415 }
416
417 let mut bindings = Vec::new();
418 let mut steps = Vec::new();
419 for item in items {
420 match item {
421 Item::Bind { name, source } => {
422 let value = source_value(source)?;
423 bindings.push(quote!(let #name = #value;));
424 }
425 Item::Attribute { name, args, .. } => steps.push(attribute(name, args)?),
426 Item::For { .. } => unreachable!("a loop takes the accumulating form"),
427 Item::Emit(emission) => {
428 // A member is evaluated into its own binding before the
429 // container is built, because a container's own name is often
430 // the same value one of its members borrows: follow.rs names
431 // the region and then aims the press at it. Built inline, the
432 // container would move the name before the member read it.
433 let held = format_ident!("member_{}", bindings.len(), span = Span::call_site());
434 let (value, call) = step(emission, container, &held)?;
435 bindings.push(quote!(let #held = #value;));
436 steps.push(call);
437 }
438 }
439 }
440 Ok(quote!({ #(#bindings)* #base #(#steps)* }))
441 }
442
443 /// The same body, for a container a loop adds to.
444 ///
445 /// A chain cannot hold a loop, so this accumulates instead. The two forms are
446 /// kept apart rather than merged because the chain is what a container with no
447 /// loop should read as, and a `let mut` that is never reassigned is a warning
448 /// in the caller's crate that the caller cannot see the cause of.
449 fn accumulate(items: &[Item], container: Container, base: &TokenStream) -> Result<TokenStream> {
450 let built = format_ident!("built", span = Span::call_site());
451 let statements = statements(items, container, &built)?;
452 Ok(quote!({
453 let mut #built = #base;
454 #(#statements)*
455 #built
456 }))
457 }
458
459 /// The statements one body contributes to an accumulating container.
460 fn statements(
461 items: &[Item],
462 container: Container,
463 built: &proc_macro2::Ident,
464 ) -> Result<Vec<TokenStream>> {
465 let mut statements = Vec::new();
466 for (index, item) in items.iter().enumerate() {
467 statements.push(match item {
468 Item::Bind { name, source } => {
469 let value = source_value(source)?;
470 quote!(let #name = #value;)
471 }
472 Item::Attribute { name, args, guard } => {
473 let call = attribute(name, args)?;
474 match guard {
475 // The same rule a guarded member follows: the guard decides
476 // whether the setting is made, and the args are evaluated
477 // either way.
478 Some(guard) => {
479 let test = predicate(guard)?;
480 quote!(if #test { #built = #built #call; })
481 }
482 None => quote!(#built = #built #call;),
483 }
484 }
485 Item::For {
486 dereferenced,
487 binder,
488 iterable,
489 body,
490 } => {
491 let iterable = hole(iterable)?;
492 let inner = self::statements(body, container, built)?;
493 let binder = binder_pattern(*dereferenced, binder);
494 quote!(for #binder in #iterable { #(#inner)* })
495 }
496 Item::Emit(Emission::Guarded { guard, inner }) => {
497 let test = predicate(guard)?;
498 let held = format_ident!("member_{index}", span = Span::call_site());
499 let (value, call) = step(inner, container, &held)?;
500 // R9: the guard decides whether the member is placed, not
501 // whether its holes are evaluated. The value is built either
502 // way, and a supplier that is asked for nothing answers with
503 // nothing.
504 quote!({
505 let #held = #value;
506 if #test {
507 #built = #built #call;
508 }
509 })
510 }
511 Item::Emit(emission) => {
512 let held = format_ident!("member_{index}", span = Span::call_site());
513 let (value, call) = step(emission, container, &held)?;
514 quote!({ let #held = #value; #built = #built #call; })
515 }
516 });
517 }
518 Ok(statements)
519 }
520
521 /// One setting, as the builder call ATTRIBUTE NAMING says it is.
522 fn attribute(name: &proc_macro2::Ident, args: &[Arg]) -> Result<TokenStream> {
523 let slot = name.to_string();
524 let enumeration = VOCABULARY
525 .iter()
526 .find(|(attribute, _)| *attribute == slot)
527 .map(|(_, enumeration)| *enumeration);
528 let args = args
529 .iter()
530 .map(|value| match enumeration {
531 Some(enumeration) => variant(value, enumeration),
532 None => arg(value),
533 })
534 .collect::<Result<Vec<_>>>()?;
535 Ok(quote!(.#name(#(#args),*)))
536 }
537
538 /// Rule R1(4): a bare uppercase ident in a vocabulary slot is that enum's
539 /// variant. A `SCREAMING_CASE` ident never is, because 19 of 19 real uses of
540 /// the `measured` slot pass a module const rather than a variant.
541 fn variant(value: &Arg, enumeration: &str) -> Result<TokenStream> {
542 let Arg::Hole(hole) = value else {
543 return arg(value);
544 };
545 let HoleRoot::Path(path) = &hole.root else {
546 return arg(value);
547 };
548 if !hole.steps.is_empty() || path.leading_colon.is_some() || path.segments.len() != 1 {
549 return arg(value);
550 }
551 let name = path.segments[0].ident.to_string();
552 if name.chars().all(|letter| !letter.is_lowercase()) {
553 return arg(value);
554 }
555 let enumeration = format_ident!("{}", enumeration);
556 let name = &path.segments[0].ident;
557 Ok(quote!(::quasi_router::layout::#enumeration::#name))
558 }
559
560 /// One emission, as the value it builds and the call that places it.
561 fn step(
562 emission: &Emission,
563 container: Container,
564 held: &proc_macro2::Ident,
565 ) -> Result<(TokenStream, TokenStream)> {
566 match (container, emission) {
567 (Container::Slot, Emission::Across { fallback, body }) => {
568 let run = accrete(
569 body,
570 Container::Run,
571 &quote!(::quasi_router::Run::new(::quasi_router::layout::Fallback::#fallback)),
572 )?;
573 Ok((run, quote!(.across(#held))))
574 }
575 // A question standing in a region rather than in a form. A region that
576 // consults gathers the dials it contains and sends them itself, so
577 // there is no form to put them in and `Node::field` is what the
578 // vocabulary has for it. `pricing`'s calculator is the site: five dials
579 // and no submit button anywhere on the page.
580 //
581 // Narrow on purpose. Everywhere else a field is still refused with the
582 // message that names the two legal homes, because a question in a cell
583 // or a row is a question nothing will ever read.
584 //
585 // A ranked member, which is `beside`'s third container and its second
586 // meaning of `Priority`: a run ranks everything it holds and a region
587 // ranks the members that say so. audiofiles' status band is the site --
588 // every fact in it is stated nowhere else on the window except the
589 // focused sample's tags, which the detail panel repeats in full, so a
590 // window with no room loses the repetition and keeps the rest.
591 (
592 Container::Slot,
593 Emission::Beside {
594 priority,
595 width,
596 inner,
597 },
598 ) => {
599 if width.is_some() {
600 return Err(syn::Error::new(
601 Span::call_site(),
602 "a region's body is a stack and every member gets the whole width: \
603 say a width on a row's member, inside `across`",
604 ));
605 }
606 let rank = variant(priority, "Priority")?;
607 Ok((panel_member(inner)?, quote!(.with_ranked(#held, #rank))))
608 }
609 // The control one slot of a repeating question carries to take itself
610 // away, and the question the slots are of. Both are `Act`s the region is
611 // told, and neither could be said at all: written where the vocabulary
612 // takes them they are expressions in argument position, which no guard
613 // reaches and no reader of the description sees. audiofiles' rule editor
614 // is the site for both, twice each.
615 (
616 Container::Slot,
617 Emission::Removes {
618 label,
619 action,
620 body,
621 },
622 ) => {
623 let label = arg(label)?;
624 let called = self::action(action)?;
625 let taken = accrete(
626 body,
627 Container::Act,
628 &quote!(::quasi_router::Act::new(#label, #called)),
629 )?;
630 Ok((taken, quote!(.removes(#held))))
631 }
632 (
633 Container::Slot,
634 Emission::Repeats {
635 one,
636 label,
637 action,
638 body,
639 },
640 ) => {
641 let one = arg(one)?;
642 let label = arg(label)?;
643 let called = self::action(action)?;
644 let question = accrete(
645 body,
646 Container::Repeats,
647 &quote!(::quasi_router::Repeating::new(
648 #one,
649 ::quasi_router::Act::new(#label, #called)
650 )),
651 )?;
652 Ok((question, quote!(.repeating(#held))))
653 }
654 // A named member of a region: the label is hoisted out of the child and
655 // onto the placement, because that is where `Slot::frame` takes it.
656 //
657 // The word stays written inside the child, which is where it reads --
658 // "this region is called Bio" -- but it is no longer something the child
659 // carries. Since quasicoherent `2cdc6761` a `Slot` has no label at all,
660 // so a label on a member that nothing reveals cannot be built rather
661 // than being built and dropped. See [`labelled`] for what is refused.
662 // A member placed as a named frame, whatever it is. `include` is the
663 // reason this exists: a shape in another `declare!` answers a `Slot`,
664 // which since `2cdc6761` cannot carry a label, so the name has to be
665 // said where the placing happens.
666 (Container::Slot, Emission::Framed { label, inner }) => {
667 let label = arg(label)?;
668 let (built, _) = step(inner, container, held)?;
669 Ok((built, quote!(.frame(#label, #held))))
670 }
671 (Container::Slot, Emission::Region { name, kind, body }) => {
672 let (label, body) = labelled(body)?;
673 let slot = slot(name, kind, &body)?;
674 let node = quote!(::quasi_router::Node::Region(#slot));
675 match label {
676 Some(label) => Ok((node, quote!(.frame(#label, #held)))),
677 None => Ok((node, quote!(.with(#held)))),
678 }
679 }
680 // A shape that answers many members, spliced whole. The plural of
681 // `include`, and only a container that holds many can take one.
682 (Container::Slot, Emission::IncludeEach(supplier)) => {
683 let supplier = hole(supplier)?;
684 Ok((quote!(#supplier), quote!(.with_all(#held))))
685 }
686 (Container::Slot, other) => Ok((panel_member(other)?, quote!(.with(#held)))),
687 (Container::Screen, Emission::Region { name, kind, body }) => {
688 Ok((slot(name, kind, body)?, quote!(.with(#held))))
689 }
690 // A region another shape built. `custom_page`'s three screens carry the
691 // same platform strip top and bottom, and every link in it is per
692 // request, so the band is a shape rather than `Chrome`. A table already
693 // takes a row this way and a row a control; this is a document taking a
694 // region.
695 (Container::Screen, Emission::Include(supplier)) => {
696 let supplier = hole(supplier)?;
697 Ok((
698 quote!(::std::convert::Into::into(#supplier)),
699 quote!(.with(#held)),
700 ))
701 }
702 (Container::Screen, other) => Err(syn::Error::new(
703 emission_span(other),
704 "a document holds regions: write `region <name> as <kind> { .. }` \
705 or `include <shape>;`",
706 )),
707 (Container::Cell | Container::Row, Emission::Activate(action)) => {
708 Ok((self::action(action)?, quote!(.activate(#held))))
709 }
710 (Container::Row, Emission::Act { .. }) => Ok((act(emission)?, quote!(.act(#held)))),
711 // `act`'s held-back twin. A row shows what `act` puts in it and keeps
712 // what `offers` gives it until the host is asked, which is the whole of
713 // the difference and is why the two are one word apart. A member rather
714 // than a setting because a menu control is told the same things an
715 // inline one is: audiofiles' sidebar greys a vault's Delete when it is
716 // the last vault, and a control written as an expression in an argument
717 // cannot say that.
718 (Container::Row, Emission::Offers { .. }) => Ok((act(emission)?, quote!(.offers(#held)))),
719 // A control another shape built. `export_act::control` is the export
720 // portal's one sentence about saving a file, said once and offered
721 // under each card's own label, and a row holds acts rather than nodes.
722 (Container::Row, Emission::Include(supplier)) => {
723 let supplier = hole(supplier)?;
724 Ok((
725 quote!(::std::convert::Into::into(#supplier)),
726 quote!(.act(#held)),
727 ))
728 }
729 // A row's parts are placed by role rather than ranked by priority,
730 // which is the other thing `beside` means and the container is what
731 // says which. embeds' button strip is the site: a thumbnail and a title
732 // in `Primary`, the buy control in `Actions`, the price a setting
733 // between them.
734 (
735 Container::Row,
736 Emission::Beside {
737 priority,
738 width,
739 inner,
740 },
741 ) => {
742 if width.is_some() {
743 return Err(syn::Error::new(
744 Span::call_site(),
745 "a row of a list places its parts by name and not by width: \
746 say a width on a region row's member, inside `across`",
747 ));
748 }
749 let part = variant(priority, "RowPart")?;
750 Ok((node(inner)?, quote!(.part(#part, #held))))
751 }
752 // A cell names its column instead of taking a role, and that is the
753 // whole of what a table row spells differently. One container since the
754 // 2026-09-05 collapse, so this sits beside `beside` rather than in a
755 // second settings table.
756 (
757 Container::Row,
758 Emission::Cell {
759 column,
760 value,
761 body,
762 },
763 ) => {
764 let built = cell(value, body)?;
765 Ok(match column {
766 Some(column) => {
767 let column = arg(column)?;
768 (built, quote!(.at(#column, #held)))
769 }
770 None => (built, quote!(.cell(#held))),
771 })
772 }
773 // One message, because there is one container. A row places its content
774 // by key and the two spellings are the two kinds of key: `beside <part>`
775 // names a role of the default column set, `cell at <column>` names a
776 // declared column.
777 (Container::Row, other) => Err(syn::Error::new(
778 emission_span(other),
779 "a row holds controls, places its content and says what opening it does: write \
780 `act <label> to <action>;`, `include <shape>;`, `beside <part> <emission>`, \
781 `cell [at <column>] <value>;` or `activate to <action>;`",
782 )),
783 (Container::Image, other) => Err(syn::Error::new(
784 emission_span(other),
785 "a picture holds nothing: its body says how it sits in its box",
786 )),
787 // Inside the scope and after the markup, which is the only place a
788 // canvas can hold anything: the markup is opaque, so nothing is
789 // injected into the middle of it.
790 (Container::Canvas, other) => Ok((node(other)?, quote!(.with(#held)))),
791 (Container::Tag, other) => Err(syn::Error::new(
792 emission_span(other),
793 "a tag holds nothing: its body says what it is like",
794 )),
795 (Container::Figure, other) => Err(syn::Error::new(
796 emission_span(other),
797 "a figure holds nothing: its body says what it is like",
798 )),
799 (Container::Meter, other) => Err(syn::Error::new(
800 emission_span(other),
801 "a meter holds nothing: its body says what it is like",
802 )),
803 (Container::Repeats, other) => Err(syn::Error::new(
804 emission_span(other),
805 "a repeating question holds nothing: its body says how few may be left",
806 )),
807 // `panel_member` rather than `node`, which is what a region's `beside`
808 // already used: a question standing in a row is the same narrow
809 // allowance as a question standing in a region, and audiofiles' search
810 // box is a field in a toolbar. The two spellings of one keyword had
811 // drifted apart, and nothing but the drift was stopping it.
812 (
813 Container::Run,
814 Emission::Beside {
815 priority,
816 width,
817 inner,
818 },
819 ) => {
820 let rank = variant(priority, "Priority")?;
821 let member = panel_member(inner)?;
822 let Some(width) = width else {
823 return Ok((member, quote!(.beside(#held, #rank))));
824 };
825 let share = variant(width, "Width")?;
826 Ok((member, quote!(.spread(#held, #rank, #share))))
827 }
828 (Container::Run, other) => Err(syn::Error::new(
829 emission_span(other),
830 "a row ranks what it holds: write `beside <priority> <emission>`",
831 )),
832 (Container::Table, Emission::Column { name, body }) => {
833 Ok((column(name, body)?, quote!(.column(#held))))
834 }
835 (Container::Table, Emission::Cells(body)) => Ok((cells(body)?, quote!(.row(#held)))),
836 // A row another shape built. `item_sales` splits its columns from its
837 // cells on purpose, so the row arrives whole rather than being written
838 // inside the table.
839 (Container::Table, Emission::Include(supplier)) => {
840 let supplier = hole(supplier)?;
841 Ok((
842 quote!(::std::convert::Into::into(#supplier)),
843 quote!(.row(#held)),
844 ))
845 }
846 (Container::Table, other) => Err(syn::Error::new(
847 emission_span(other),
848 "a table holds columns and rows: write `column <name>;`, `cells { .. }` \
849 or `include <shape>;`",
850 )),
851 (Container::Cell, Emission::Act { .. }) => Ok((act(emission)?, quote!(.act(#held)))),
852 // A control another shape built, as a row takes one. `item_files`'s
853 // acts column holds a Download written here and a Delete that is
854 // `version_delete_act`'s whole subject.
855 (Container::Cell, Emission::Include(supplier)) => {
856 let supplier = hole(supplier)?;
857 Ok((
858 quote!(::std::convert::Into::into(#supplier)),
859 quote!(.act(#held)),
860 ))
861 }
862 // A cell is a run of leaves, which is what `Cell::part` is for.
863 // `git_blame`'s commit cell carries the short oid and, when the commit
864 // has annotations, a second link to its notes; its code cell carries the
865 // line. A control is still `.act`, so a supplier reached by `include`
866 // stays a control and a node member is a part.
867 (Container::Cell, other) => Ok((node(other)?, quote!(.part(#held)))),
868 (
869 Container::Node,
870 Emission::Offering {
871 label,
872 action,
873 body,
874 },
875 ) => {
876 let label = arg(label)?;
877 let called = self::action(action)?;
878 let way_out = accrete(
879 body,
880 Container::Act,
881 &quote!(::quasi_router::Act::new(#label, #called)),
882 )?;
883 Ok((way_out, quote!(.offering(#held))))
884 }
885 (Container::Node, other) => Err(syn::Error::new(
886 emission_span(other),
887 "a node member is told settings and a way out: write `offering <label> to <action>;`",
888 )),
889 (Container::Column, other) => Err(syn::Error::new(
890 emission_span(other),
891 "a column holds no members: its body says how it narrows",
892 )),
893 (Container::Timeline, Emission::At { at, inner }) => {
894 let at = arg(at)?;
895 // A supplier, the same courtesy `include` is everywhere else:
896 // goingson's day view draws the axis and the all-day strip from
897 // one row shape, so the row is a shape away from both. An inline
898 // row is not a form until a screen wants one.
899 let row = match &**inner {
900 Emission::Include(supplier) => {
901 let supplier = hole(supplier)?;
902 quote!(::std::convert::Into::into(#supplier))
903 }
904 other => {
905 return Err(syn::Error::new(
906 emission_span(other),
907 "an axis holds rows another shape builds: write `at <placement> include <supplier>;`",
908 ));
909 }
910 };
911 Ok((
912 quote!(::quasi_router::screen::Placed {
913 placement: #at,
914 row: #row,
915 }),
916 quote!(.placed(#held)),
917 ))
918 }
919 (Container::Timeline, other) => Err(syn::Error::new(
920 emission_span(other),
921 "an axis holds what sits on it: write `at <placement> include <supplier>;`",
922 )),
923 (
924 Container::Act,
925 Emission::Field {
926 kind,
927 name,
928 label,
929 body,
930 },
931 ) => {
932 // `Act::asking`: a control that wants a value before it acts.
933 // goingson's day view is the site -- placing a task asks which slot
934 // and, when the task carries no estimate, how long it takes.
935 let asked = field(kind, name, label, body)?;
936 Ok((asked, quote!(.asking(#held))))
937 }
938 (Container::Act | Container::Field, other) => Err(syn::Error::new(
939 emission_span(other),
940 "this holds no members: its body says what it is like",
941 )),
942 }
943 }
944
945 /// One guard, as the `bool` it tests.
946 pub(crate) fn predicate(guard: &Guard) -> Result<TokenStream> {
947 let test = clause(&guard.predicate)?;
948 Ok(if guard.negated {
949 quote!(!(#test))
950 } else {
951 test
952 })
953 }
954
955 fn clause(predicate: &Predicate) -> Result<TokenStream> {
956 Ok(match predicate {
957 Predicate::Joined {
958 connective,
959 clauses,
960 } => {
961 let operator: TokenStream = if connective == "and" { "&&" } else { "||" }
962 .parse()
963 .expect("a connective");
964 let clauses = clauses.iter().map(clause).collect::<Result<Vec<_>>>()?;
965 let mut joined = TokenStream::new();
966 for (index, one) in clauses.into_iter().enumerate() {
967 if index > 0 {
968 joined.extend(operator.clone());
969 }
970 joined.extend(quote!((#one)));
971 }
972 joined
973 }
974 Predicate::Not(inner) => {
975 let inner = clause(inner)?;
976 quote!(!(#inner))
977 }
978 Predicate::Truth(hole) => self::hole(hole)?,
979 Predicate::Comparison {
980 left,
981 compare,
982 right,
983 } => {
984 let spelling = compare.to_string();
985 let operator = COMPARISONS
986 .iter()
987 .find(|(known, _)| *known == spelling)
988 .map(|(_, operator)| *operator)
989 .ok_or_else(|| {
990 syn::Error::new(compare.span(), format!("`{spelling}` is not a comparison"))
991 })?;
992 let operator: TokenStream = operator.parse().expect("a comparison operator");
993 let left = self::hole(left)?;
994 let right = arg(right)?;
995 quote!(#left #operator #right)
996 }
997 })
998 }
999
1000 /// One member of a region or of a panel, as the `Node` it is.
1001 ///
1002 /// [`node`] plus the one emission that is a node only in these two places. A
1003 /// question standing in a region rather than in a form is what a consulting
1004 /// region is made of -- it gathers the dials it contains and sends them itself,
1005 /// so there is no form to put them in -- and a `-> Vec<Node>` panel is a
1006 /// region's members with the region spread off, so the same holds there.
1007 ///
1008 /// Narrow on purpose. A field is still refused everywhere else, because a
1009 /// question in a cell or a row is a question nothing will ever read.
1010 fn panel_member(emission: &Emission) -> Result<TokenStream> {
1011 match emission {
1012 Emission::Field {
1013 kind,
1014 name,
1015 label,
1016 body,
1017 } => {
1018 let built = field(kind, name, label, body)?;
1019 Ok(quote!(::quasi_router::Node::field(#built)))
1020 }
1021 other => node(other),
1022 }
1023 }
1024
1025 /// One emission as a `Node` value.
1026 fn node(emission: &Emission) -> Result<TokenStream> {
1027 match emission {
1028 // A run of members is not a node, so it can only be written where a
1029 // container holds many. Refused where it is written rather than
1030 // reported against generated code.
1031 Emission::IncludeEach(_) => Err(syn::Error::new(
1032 Span::call_site(),
1033 "`include each` splices a run of members, so it belongs in a body \
1034 that holds many: write `include` for one node",
1035 )),
1036 // `framed` says how a member is placed, so it has no meaning where a
1037 // bare node is wanted. Refused where it is written rather than silently
1038 // losing the name.
1039 Emission::Framed { inner, .. } => Err(syn::Error::new(
1040 emission_span(inner),
1041 "`framed` names a member of a region that shows one at a time, \
1042 so it belongs where the member is placed",
1043 )),
1044 Emission::Simple { member, args, body } if member == "picture" => {
1045 // The one member whose body does not set on a `Node`. `Node::Image`
1046 // holds an `Image`, and `fit`, `lazy`, `caption` and `intrinsic`
1047 // are that type's builders, so the body accretes onto the picture
1048 // and the variant goes on afterwards. embeds' cover is the site: a
1049 // 40-pixel thumbnail whose art is any shape, which is `Fit::Cover`
1050 // and is unsayable without a body.
1051 let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?;
1052 let picture = accrete(
1053 body,
1054 Container::Image,
1055 &quote!(::quasi_router::Image::new(#(#args),*)),
1056 )?;
1057 Ok(quote!(::quasi_router::Node::Image(#picture)))
1058 }
1059 Emission::Simple { member, args, body } if member == "badge" => {
1060 // The third member whose body does not set on a `Node`, and the
1061 // first of two over `Tag`. `Node::Token` holds a `Tag`, and `token`
1062 // is a setting on `Row` and on `Cell` so it cannot be a member at
1063 // all, so the members are named for `Tag`'s constructors instead.
1064 // quasicoherent `e2030032` decided it over 33 sites.
1065 let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?;
1066 let tag = accrete(
1067 body,
1068 Container::Tag,
1069 &quote!(::quasi_router::screen::Tag::badge(#(#args),*)),
1070 )?;
1071 Ok(quote!(::quasi_router::Node::token(#tag)))
1072 }
1073 Emission::Chip {
1074 value,
1075 action,
1076 removable,
1077 body,
1078 } => {
1079 // The other two thirds of `badge`. A chip goes somewhere, so it
1080 // carries an action, which is why it is spelled
1081 // `chip <value> to <action>` rather than as a member with two
1082 // arguments. `removable` is the same member and the same shape with
1083 // the third constructor: a tag whose going takes it off. audiofiles'
1084 // detail panel is the site and `Tag` has no fourth constructor, so
1085 // the form now says all of them.
1086 let value = arg(value)?;
1087 let called = self::action(action)?;
1088 let tag = accrete(
1089 body,
1090 Container::Tag,
1091 &if *removable {
1092 quote!(::quasi_router::screen::Tag::removable(#value, #called))
1093 } else {
1094 quote!(::quasi_router::screen::Tag::chip(#value, #called))
1095 },
1096 )?;
1097 Ok(quote!(::quasi_router::Node::token(#tag)))
1098 }
1099 Emission::Simple { member, args, body } if member == "proportion" => {
1100 // A meter standing on its own. `Node::Meter` holds a `Meter`, so
1101 // the body sets on the meter the way `badge`'s sets on a `Tag`, and
1102 // the member is named for the row part rather than for the type:
1103 // `meter` is a setting on `Row` and on `Cell`, so it cannot also be
1104 // a member.
1105 let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?;
1106 let meter = accrete(
1107 body,
1108 Container::Meter,
1109 &quote!(::quasi_router::screen::Meter::new(#(#args),*)),
1110 )?;
1111 Ok(quote!(::quasi_router::Node::Meter(#meter)))
1112 }
1113 Emission::Simple { member, args, body } if member == "tally" => {
1114 // One number and what it counts, standing on its own. `Node::Figure`
1115 // holds a `Figure`, so the body sets on the figure the way
1116 // `proportion`'s sets on a `Meter`, and the member is named for what
1117 // it says rather than for the type: `figure` is the setting a
1118 // `stats` strip accretes with, so it cannot also be a member.
1119 let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?;
1120 let figure = accrete(
1121 body,
1122 Container::Figure,
1123 &quote!(::quasi_router::screen::Figure::new(#(#args),*)),
1124 )?;
1125 Ok(quote!(::quasi_router::Node::Figure(#figure)))
1126 }
1127 Emission::Simple { member, args, body } if member == "underway" => {
1128 // The one member whose name is not its constructor's. `Node::pending`
1129 // is the third `StandIn` state, beside `empty` and `failed`, and
1130 // `pending` is a setting on `Slot`, so the member is named for what
1131 // it says instead. The body still sets on a `Node`, which is what
1132 // `offering` wants: a stand-in with a way out of it.
1133 let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?;
1134 accrete(
1135 body,
1136 Container::Node,
1137 &quote!(::quasi_router::Node::pending(#(#args),*)),
1138 )
1139 }
1140 Emission::Simple { member, args, body } if member == "canvas" => {
1141 // The second member whose body does not set on a `Node`, and for
1142 // `picture`'s reason: `Node::Canvas` holds a `Canvas`, and
1143 // `classed`, `identified` and the nodes drawn in the scope are that
1144 // type's. `custom_page` is the site -- a creator's markup under the
1145 // scope `css_sanitizer` rewrote every one of their rules for.
1146 //
1147 // One argument always, because `Canvas::new("")` is
1148 // `Canvas::default()`: an item page has no creator markup and the
1149 // scope is still what carries the stylesheet.
1150 let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?;
1151 let canvas = accrete(
1152 body,
1153 Container::Canvas,
1154 &quote!(::quasi_router::screen::Canvas::new(#(#args),*)),
1155 )?;
1156 Ok(quote!(::quasi_router::Node::Canvas(::std::boxed::Box::new(#canvas))))
1157 }
1158 Emission::Simple { member, args, body } if member == "timeline" => {
1159 // The fifth member whose body is not `Node`'s own, and the only one
1160 // that holds members rather than settings: an axis is told what
1161 // sits on it and where. `Node::timeline` answers with the variant
1162 // already, so nothing is wrapped afterwards.
1163 let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?;
1164 accrete(
1165 body,
1166 Container::Timeline,
1167 &quote!(::quasi_router::Node::timeline(#(#args),*)),
1168 )
1169 }
1170 Emission::Simple { member, args, body } => {
1171 let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?;
1172 accrete(
1173 body,
1174 Container::Node,
1175 &quote!(::quasi_router::Node::#member(#(#args),*)),
1176 )
1177 }
1178 Emission::List(items) => list(items),
1179 Emission::Table(items) => {
1180 let table = self::table(items)?;
1181 Ok(quote!(::std::convert::Into::into(#table)))
1182 }
1183 Emission::Form { action, body } => form(action, body),
1184 Emission::Field { kind, .. } => Err(syn::Error::new(
1185 kind.span(),
1186 "a field is not a node: put it in a `form`, or give the shape `-> Field`",
1187 )),
1188 Emission::Row { .. } => Err(syn::Error::new(
1189 emission_span(emission),
1190 "a row is not a node: put it in a `list`",
1191 )),
1192 Emission::Screen { arrangement, .. } => Err(syn::Error::new(
1193 arrangement.span(),
1194 "a document is not a node",
1195 )),
1196 Emission::Link { text, action } => {
1197 let text = arg(text)?;
1198 let action = self::action(action)?;
1199 Ok(quote! {
1200 ::quasi_router::Node::Link {
1201 text: ::std::convert::Into::into(#text),
1202 action: #action,
1203 }
1204 })
1205 }
1206 Emission::Act { .. } => {
1207 let control = act(emission)?;
1208 Ok(quote!(::quasi_router::Node::Act(#control)))
1209 }
1210 Emission::Offers { action, .. } => Err(syn::Error::new(
1211 action.verb.span(),
1212 "`offers` is a control a row holds back; outside a row write `act`",
1213 )),
1214 Emission::Include(supplier) => {
1215 let supplier = hole(supplier)?;
1216 Ok(quote!(::std::convert::Into::into(#supplier)))
1217 }
1218 Emission::Region { name, kind, body } => {
1219 let slot = slot(name, kind, body)?;
1220 Ok(quote!(::quasi_router::Node::Region(#slot)))
1221 }
1222 Emission::Given {
1223 scrutinee,
1224 arms,
1225 otherwise,
1226 } => {
1227 let scrutinee = hole(scrutinee)?;
1228 let arms = arms
1229 .iter()
1230 .map(|(pattern, arm)| {
1231 let pattern = self::pattern(pattern);
1232 let arm = node(arm)?;
1233 Ok(quote!(#pattern => #arm,))
1234 })
1235 .collect::<Result<Vec<_>>>()?;
1236 let otherwise = match otherwise {
1237 Some(otherwise) => {
1238 let otherwise = node(otherwise)?;
1239 quote!(_ => #otherwise,)
1240 }
1241 None => quote!(),
1242 };
1243 Ok(quote!(match #scrutinee { #(#arms)* #otherwise }))
1244 }
1245 Emission::Guarded { guard, .. } => Err(syn::Error::new(
1246 guard.span,
1247 "a guarded member needs a container that may hold nothing; \
1248 only a `-> Option<_>` shape can drop its whole result so far",
1249 )),
1250 Emission::Across { fallback, .. } => Err(syn::Error::new(
1251 fallback.span(),
1252 "a row is not a node: `across` belongs in a region",
1253 )),
1254 Emission::Column { .. } | Emission::Cells(_) | Emission::Cell { .. } => {
1255 Err(syn::Error::new(
1256 emission_span(emission),
1257 "a column, a row of cells and a cell are a table's, not a node's",
1258 ))
1259 }
1260 Emission::Activate(action) => Err(syn::Error::new(
1261 action.verb.span(),
1262 "`activate` says what opening a row or a cell does; it is not a node",
1263 )),
1264 Emission::Offering { action, .. } => Err(syn::Error::new(
1265 action.verb.span(),
1266 "`offering` is the way out an empty state offers; it is not a node",
1267 )),
1268 Emission::Beside { inner, .. } => Err(syn::Error::new(
1269 emission_span(inner),
1270 "`beside` needs a run in scope, which is rule R3",
1271 )),
1272 Emission::At { inner, .. } => Err(syn::Error::new(
1273 emission_span(inner),
1274 "`at` places a row on an axis; it needs a `timeline` in scope",
1275 )),
1276 Emission::Removes { action, .. } | Emission::Repeats { action, .. } => {
1277 Err(syn::Error::new(
1278 action.verb.span(),
1279 "a repeating question and the control that takes one away are a \
1280 region's, not a node's",
1281 ))
1282 }
1283 }
1284 }
1285
1286 /// One region, as the `Slot` it is. A document holds these; a body wraps them
1287 /// in [`Node::Region`].
1288 fn slot(name: &Arg, kind: &RegionKind, body: &[Item]) -> Result<TokenStream> {
1289 let name = arg(name)?;
1290 let kind = match kind {
1291 RegionKind::Variant(variant) => quote!(::quasi_router::RegionKind::#variant),
1292 RegionKind::Supplied(hole) => self::hole(hole)?,
1293 };
1294 accrete(
1295 body,
1296 Container::Slot,
1297 &quote!(::quasi_router::Slot::new(#name, #kind)),
1298 )
1299 }
1300
1301 /// A region's `label`, taken out of its body.
1302 ///
1303 /// Answers the label and the rest of the body without it. The label is not a
1304 /// setting on the region any more: it names the control that reveals the region,
1305 /// so it belongs to the placement, and [`Slot::frame`] is the only thing that
1306 /// takes one.
1307 ///
1308 /// A guarded `label` is refused rather than hoisted. A frame's name is not
1309 /// something a request decides -- a tab whose text comes and goes is a strip
1310 /// that changes shape under the reader -- and hoisting it would move the guard
1311 /// somewhere it no longer guards what it was written next to.
1312 fn labelled(body: &[Item]) -> Result<(Option<TokenStream>, Vec<Item>)> {
1313 let mut label = None;
1314 let mut rest = Vec::with_capacity(body.len());
1315
1316 for item in body {
1317 let Item::Attribute { name, args, guard } = item else {
1318 rest.push(item.clone());
1319 continue;
1320 };
1321 if name != "label" {
1322 rest.push(item.clone());
1323 continue;
1324 }
1325 if guard.is_some() {
1326 return Err(syn::Error::new(
1327 name.span(),
1328 "a frame's name is not something a request decides: \
1329 drop the guard, or guard the whole region",
1330 ));
1331 }
1332 let [only] = args.as_slice() else {
1333 return Err(syn::Error::new(
1334 name.span(),
1335 "`label` names the control that reveals this region: one name",
1336 ));
1337 };
1338 if label.is_some() {
1339 return Err(syn::Error::new(name.span(), "a region has one name"));
1340 }
1341 label = Some(arg(only)?);
1342 }
1343
1344 Ok((label, rest))
1345 }
1346
1347 /// The loop's binder, dereferenced where the declaration said to.
1348 fn binder_pattern(dereferenced: bool, binder: &proc_macro2::Ident) -> TokenStream {
1349 if dereferenced {
1350 quote!(&#binder)
1351 } else {
1352 quote!(#binder)
1353 }
1354 }
1355
1356 /// One form: where it writes, what its button says, and what it asks.
1357 ///
1358 /// The fields are pushed rather than written into a `vec![]` literal because a
1359 /// field may be guarded, which is what `settings::email`'s advanced block asked
1360 /// for: six server questions that are on the form only while the disclosure is
1361 /// open. R9 holds here as everywhere else -- the field is built whether or not
1362 /// it is asked -- so a supplier feeding a question nobody sees is still called
1363 /// and still answers.
1364 fn form(action: &Action, body: &[Item]) -> Result<TokenStream> {
1365 let mut submit = None;
1366 let mut asked = Vec::new();
1367 let held = format_ident!("asked", span = Span::call_site());
1368 for (index, item) in body.iter().enumerate() {
1369 match item {
1370 Item::Attribute { name, args, guard } if name == "submit" => {
1371 let [label] = args.as_slice() else {
1372 return Err(syn::Error::new(name.span(), "`submit` says one thing"));
1373 };
1374 if let Some(guard) = guard {
1375 return Err(syn::Error::new(
1376 guard.span,
1377 "a form always has a button: `submit` takes no guard",
1378 ));
1379 }
1380 submit = Some(arg(label)?);
1381 }
1382 Item::Emit(Emission::Field {
1383 kind,
1384 name,
1385 label,
1386 body,
1387 }) => {
1388 let built = field(kind, name, label, body)?;
1389 asked.push(quote!(#held.push(#built);));
1390 }
1391 // A question another shape built, which is the courtesy every other
1392 // container already extends: a table takes a row, a cell takes a
1393 // control, a region takes a field. A form was the one that did not,
1394 // so two ends of an interval or four bounded millisecond boxes had
1395 // to be written out per form. audiofiles' editor is the site --
1396 // `span` twice in one form and `milliseconds` four times across two.
1397 Item::Emit(Emission::Include(supplier)) => {
1398 let supplier = self::hole(supplier)?;
1399 asked.push(quote!(#held.push(::std::convert::Into::into(#supplier));));
1400 }
1401 Item::Emit(Emission::Guarded { guard, inner }) => {
1402 let Emission::Field {
1403 kind,
1404 name,
1405 label,
1406 body,
1407 } = &**inner
1408 else {
1409 return Err(syn::Error::new(
1410 emission_span(inner),
1411 "a form holds fields, and only a field may be guarded here",
1412 ));
1413 };
1414 let built = field(kind, name, label, body)?;
1415 let test = predicate(guard)?;
1416 let question = format_ident!("question_{index}", span = Span::call_site());
1417 asked.push(quote!({
1418 let #question = #built;
1419 if #test {
1420 #held.push(#question);
1421 }
1422 }));
1423 }
1424 Item::Attribute { name, .. } => {
1425 return Err(syn::Error::new(
1426 name.span(),
1427 "a form says `submit` and asks fields, and nothing else yet",
1428 ));
1429 }
1430 Item::Bind { name, .. } => {
1431 return Err(syn::Error::new(name.span(), "a form binds nothing"));
1432 }
1433 Item::For { binder, .. } => {
1434 return Err(syn::Error::new(
1435 binder.span(),
1436 "a form's fields are written out; a loop over them is not a production yet",
1437 ));
1438 }
1439 Item::Emit(other) => {
1440 return Err(syn::Error::new(
1441 emission_span(other),
1442 "a form holds fields: write `field <kind> <name> <label>` or \
1443 `include <shape>;`",
1444 ));
1445 }
1446 }
1447 }
1448 let Some(submit) = submit else {
1449 return Err(syn::Error::new(
1450 action.verb.span(),
1451 "a form says what its button reads: `submit <text>;`",
1452 ));
1453 };
1454 let action = self::action(action)?;
1455 Ok(quote! {
1456 ::quasi_router::Node::Form {
1457 action: #action,
1458 submit: ::std::convert::Into::into(#submit),
1459 fields: {
1460 let mut #held = ::std::vec::Vec::new();
1461 #(#asked)*
1462 #held
1463 },
1464 }
1465 })
1466 }
1467
1468 /// One field: what it asks for, by name, under a label.
1469 fn field(kind: &proc_macro2::Ident, name: &Arg, label: &Arg, body: &[Item]) -> Result<TokenStream> {
1470 let name = arg(name)?;
1471 let label = arg(label)?;
1472 accrete(
1473 body,
1474 Container::Field,
1475 &quote!(::quasi_router::Field::new(
1476 ::quasi_router::layout::FieldKind::#kind,
1477 #name,
1478 #label
1479 )),
1480 )
1481 }
1482
1483 /// A panel's members, in order, with nothing wrapping them.
1484 ///
1485 /// The `-> Vec<Node>` shape. Kept apart from [`accumulate`] rather than folded
1486 /// into it: every other container is told `.with(member)` and answers with
1487 /// itself, and a `Vec` is told `push` and answers with nothing, so one of the
1488 /// two spellings would have had to be special-cased inside the general form
1489 /// anyway.
1490 fn nodes(items: &[Item]) -> Result<TokenStream> {
1491 let held = format_ident!("nodes", span = Span::call_site());
1492 let statements = node_statements(items, &held)?;
1493 Ok(quote!({
1494 let mut #held = ::std::vec::Vec::new();
1495 #(#statements)*
1496 #held
1497 }))
1498 }
1499
1500 /// A menu's entries, in order.
1501 ///
1502 /// [`nodes`]' twin, and narrower: every member is an `act`, because a menu is
1503 /// nothing but controls. A guard and a loop work the way they do everywhere
1504 /// else, which is the whole reason this exists -- audiofiles' row menu is
1505 /// fourteen entries of which nine are conditional.
1506 fn acts(items: &[Item]) -> Result<TokenStream> {
1507 let held = format_ident!("entries", span = Span::call_site());
1508 let statements = act_statements(items, &held)?;
1509 Ok(quote!({
1510 let mut #held = ::std::vec::Vec::new();
1511 #(#statements)*
1512 #held
1513 }))
1514 }
1515
1516 fn act_statements(items: &[Item], entries: &proc_macro2::Ident) -> Result<Vec<TokenStream>> {
1517 items
1518 .iter()
1519 .map(|item| match item {
1520 Item::Bind { name, source } => {
1521 let value = source_value(source)?;
1522 Ok(quote!(let #name = #value;))
1523 }
1524 Item::For {
1525 dereferenced,
1526 binder,
1527 iterable,
1528 body,
1529 } => {
1530 let iterable = hole(iterable)?;
1531 let inner = act_statements(body, entries)?;
1532 let binder = binder_pattern(*dereferenced, binder);
1533 Ok(quote!(for #binder in #iterable { #(#inner)* }))
1534 }
1535 // R9, as everywhere else: the entry is built whether or not the
1536 // guard places it.
1537 Item::Emit(Emission::Guarded { guard, inner }) => {
1538 let test = predicate(guard)?;
1539 let value = act(inner)?;
1540 let held = format_ident!("entry", span = Span::call_site());
1541 Ok(quote!({
1542 let #held = #value;
1543 if #test {
1544 #entries.push(#held);
1545 }
1546 }))
1547 }
1548 Item::Emit(emission) => {
1549 let value = act(emission)?;
1550 Ok(quote!(#entries.push(#value);))
1551 }
1552 Item::Attribute { name, .. } => Err(syn::Error::new(
1553 name.span(),
1554 "a menu sets nothing: say it on the entry it belongs to",
1555 )),
1556 })
1557 .collect()
1558 }
1559
1560 fn node_statements(items: &[Item], nodes: &proc_macro2::Ident) -> Result<Vec<TokenStream>> {
1561 items
1562 .iter()
1563 .map(|item| match item {
1564 Item::Bind { name, source } => {
1565 let value = source_value(source)?;
1566 Ok(quote!(let #name = #value;))
1567 }
1568 Item::For {
1569 dereferenced,
1570 binder,
1571 iterable,
1572 body,
1573 } => {
1574 let iterable = hole(iterable)?;
1575 let inner = node_statements(body, nodes)?;
1576 let binder = binder_pattern(*dereferenced, binder);
1577 Ok(quote!(for #binder in #iterable { #(#inner)* }))
1578 }
1579 Item::Emit(Emission::Guarded { guard, inner }) => {
1580 let test = predicate(guard)?;
1581 let value = panel_member(inner)?;
1582 // R9, as everywhere else: the guard decides whether the member
1583 // is placed, and the holes inside it are evaluated either way.
1584 let member = format_ident!("member", span = Span::call_site());
1585 Ok(quote!({
1586 let #member = #value;
1587 if #test {
1588 #nodes.push(#member);
1589 }
1590 }))
1591 }
1592 Item::Emit(emission) => {
1593 let value = panel_member(emission)?;
1594 Ok(quote!(#nodes.push(#value);))
1595 }
1596 // The one attribute a panel takes, and it means in a panel what it
1597 // means on a region: splice another shape's nodes in here. A panel
1598 // built out of panels had no spelling until goingson's Sharing
1599 // section, which is eight of them under one heading.
1600 Item::Attribute { name, args, guard } if name == "extend" => {
1601 let spliced = args.iter().map(arg).collect::<Result<Vec<_>>>()?;
1602 let extending = quote!(#(#nodes.extend(#spliced);)*);
1603 guard.as_ref().map_or_else(
1604 || Ok(extending.clone()),
1605 |guard| {
1606 let test = predicate(guard)?;
1607 Ok(quote!(if #test { #extending }))
1608 },
1609 )
1610 }
1611 Item::Attribute { name, .. } => Err(syn::Error::new(
1612 name.span(),
1613 "a panel sets nothing but `extend`: say it on the member it belongs to",
1614 )),
1615 })
1616 .collect()
1617 }
1618
1619 /// The rows of a list, built where they are read.
1620 ///
1621 /// `more` is the one thing a list sets rather than says on a row, because it is
1622 /// about the list and not about any row in it: what is left over, and the
1623 /// address that fetches it. goingson's mail list is the site -- the query hands
1624 /// back the page and the total in one call, so the description carries both
1625 /// numbers and `Rest` derives the difference.
1626 fn list(items: &[Item]) -> Result<TokenStream> {
1627 let rows = format_ident!("rows", span = Span::call_site());
1628 let rest = format_ident!("rest", span = Span::call_site());
1629 let mut more = Vec::new();
1630 let mut body = Vec::new();
1631 for item in items {
1632 match item {
1633 Item::Attribute { name, args, guard } if name == "more" => {
1634 let [only] = args.as_slice() else {
1635 return Err(syn::Error::new(
1636 name.span(),
1637 "`more` says what is left over: one `Rest`",
1638 ));
1639 };
1640 let value = arg(only)?;
1641 let setting = quote!(#rest = ::std::option::Option::Some(#value););
1642 more.push(match guard {
1643 None => setting,
1644 Some(guard) => {
1645 let test = predicate(guard)?;
1646 quote!(if #test { #setting })
1647 }
1648 });
1649 }
1650 other => body.push(other),
1651 }
1652 }
1653 let statements = list_statements(&body, &rows)?;
1654 if more.is_empty() {
1655 return Ok(quote!({
1656 let mut #rows = ::std::vec::Vec::new();
1657 #(#statements)*
1658 ::quasi_router::Node::list(#rows)
1659 }));
1660 }
1661 Ok(quote!({
1662 let mut #rows = ::std::vec::Vec::new();
1663 #(#statements)*
1664 let mut #rest = ::std::option::Option::None;
1665 #(#more)*
1666 ::quasi_router::Node::Table {
1667 columns: ::std::vec::Vec::new(),
1668 rows: #rows,
1669 more: #rest,
1670 }
1671 }))
1672 }
1673
1674 fn list_statements(items: &[&Item], rows: &proc_macro2::Ident) -> Result<Vec<TokenStream>> {
1675 items
1676 .iter()
1677 .map(|item| match item {
1678 Item::Bind { name, source } => {
1679 let value = source_value(source)?;
1680 Ok(quote!(let #name = #value;))
1681 }
1682 Item::For {
1683 dereferenced,
1684 binder,
1685 iterable,
1686 body,
1687 } => {
1688 let iterable = hole(iterable)?;
1689 let inner = list_statements(&body.iter().collect::<Vec<_>>(), rows)?;
1690 let binder = binder_pattern(*dereferenced, binder);
1691 Ok(quote!(for #binder in #iterable { #(#inner)* }))
1692 }
1693 // R9 again: the row is built whether or not the guard places it.
1694 Item::Emit(Emission::Guarded { guard, inner }) => {
1695 let test = predicate(guard)?;
1696 let value = list_row(inner)?;
1697 let held = format_ident!("member", span = Span::call_site());
1698 Ok(quote!({
1699 let #held = #value;
1700 if #test {
1701 #rows.push(#held);
1702 }
1703 }))
1704 }
1705 Item::Emit(emission) => {
1706 let row = list_row(emission)?;
1707 Ok(quote!(#rows.push(#row);))
1708 }
1709 Item::Attribute { name, .. } => Err(syn::Error::new(
1710 name.span(),
1711 "a list sets nothing but `more`: say it on the row",
1712 )),
1713 })
1714 .collect()
1715 }
1716
1717 /// One row of a list, written here or built by another shape.
1718 ///
1719 /// `export_portal` demanded the second: five of its six cards come off a const
1720 /// and the sixth is the asynchronous export, which is a shape of its own and is
1721 /// drawn only for a reader who has files. The table already took an `include`
1722 /// for the same reason, and this is the list saying it the same way.
1723 fn list_row(emission: &Emission) -> Result<TokenStream> {
1724 match emission {
1725 Emission::Row { primary, body } => row(Some(primary), body),
1726 Emission::Include(supplier) => {
1727 let supplier = hole(supplier)?;
1728 Ok(quote!(::std::convert::Into::into(#supplier)))
1729 }
1730 other => Err(syn::Error::new(
1731 emission_span(other),
1732 "a list holds rows: write `row <primary> { .. }` or `include <shape>;`",
1733 )),
1734 }
1735 }
1736
1737 /// One table: its columns, its rows, and what it has not shown.
1738 ///
1739 /// Both halves accrete. `Table::new` takes its columns as a list and the form
1740 /// has no expression to hold one in, so `Table::column` was added beside it
1741 /// rather than the form growing a production that admits a list of built
1742 /// values. Columns before rows is the vocabulary's own rule and this emits them
1743 /// in the order they were written, so a declaration that gets it wrong trips
1744 /// `Table::row`'s assertion rather than losing a cell quietly.
1745 fn table(items: &[Item]) -> Result<TokenStream> {
1746 accrete(
1747 items,
1748 Container::Table,
1749 &quote!(::quasi_router::screen::Table::new(::std::iter::empty())),
1750 )
1751 }
1752
1753 /// One column: what it is called, and how it narrows.
1754 fn column(name: &Arg, body: &[Item]) -> Result<TokenStream> {
1755 let name = arg(name)?;
1756 accrete(
1757 body,
1758 Container::Column,
1759 &quote!(::quasi_router::screen::Column::new(#name)),
1760 )
1761 }
1762
1763 /// One row of a table, by position. `row` with no primary.
1764 fn cells(body: &[Item]) -> Result<TokenStream> {
1765 row(None, body)
1766 }
1767
1768 /// One cell: what it says, what it holds, and what opening it does.
1769 fn cell(value: &Arg, body: &[Item]) -> Result<TokenStream> {
1770 let value = arg(value)?;
1771 accrete(
1772 body,
1773 Container::Cell,
1774 &quote!(::quasi_router::screen::Cell::new(#value)),
1775 )
1776 }
1777
1778 /// One row: what it says, what it holds, and what it offers.
1779 ///
1780 /// One function since the 2026-09-05 collapse. `row "x" { .. }` and `cells {
1781 /// .. }` were two builders onto what is now one type, differing only in whether
1782 /// a primary was named: a list row opens with its primary text and a table row
1783 /// opens empty and fills by key. That is an argument, not a second function.
1784 fn row(primary: Option<&Arg>, body: &[Item]) -> Result<TokenStream> {
1785 let base = match primary {
1786 Some(primary) => {
1787 let primary = arg(primary)?;
1788 quote!(::quasi_router::Row::new(#primary))
1789 }
1790 None => quote!(<::quasi_router::Row as ::std::default::Default>::default()),
1791 };
1792 accrete(body, Container::Row, &base)
1793 }
1794
1795 /// One control: what it is called, what it does, and what it is like.
1796 fn act(emission: &Emission) -> Result<TokenStream> {
1797 let (Emission::Act {
1798 label,
1799 action,
1800 body,
1801 }
1802 | Emission::Offers {
1803 label,
1804 action,
1805 body,
1806 }) = emission
1807 else {
1808 return Err(syn::Error::new(
1809 emission_span(emission),
1810 "expected a control here",
1811 ));
1812 };
1813 let label = arg(label)?;
1814 let called = self::action(action)?;
1815 accrete(
1816 body,
1817 Container::Act,
1818 &quote!(::quasi_router::Act::new(#label, #called)),
1819 )
1820 }
1821
1822 fn action(action: &Action) -> Result<TokenStream> {
1823 let Action {
1824 verb,
1825 target,
1826 modifiers,
1827 } = action;
1828 let called = if verb == "doing" {
1829 // Amendment 6: the supplier IS the action.
1830 let Some(target) = target else {
1831 return Err(syn::Error::new(verb.span(), "`doing` names a supplier"));
1832 };
1833 arg(target)?
1834 } else {
1835 let target = match target {
1836 Some(target) => arg(target)?,
1837 None => quote!(),
1838 };
1839 quote!(::quasi_router::Action::#verb(#target))
1840 };
1841 let modifiers = modifiers
1842 .iter()
1843 .map(|modifier| {
1844 let name = &modifier.name;
1845 let args = modifier.args.iter().map(arg).collect::<Result<Vec<_>>>()?;
1846 Ok(quote!(.#name(#(#args),*)))
1847 })
1848 .collect::<Result<Vec<_>>>()?;
1849 Ok(quote!(#called #(#modifiers)*))
1850 }
1851
1852 fn source(source: &Source, owned: bool) -> Result<TokenStream> {
1853 match source {
1854 Source::Str(text) => Ok(interpolated(text, owned)),
1855 Source::Hole(hole) => self::hole(hole),
1856 Source::Choose {
1857 scrutinee,
1858 arms,
1859 otherwise,
1860 } => {
1861 let scrutinee = self::hole(scrutinee)?;
1862 let arms = arms
1863 .iter()
1864 .map(|(pattern, value)| {
1865 let pattern = self::pattern(pattern);
1866 let value = self::source(value, owned)?;
1867 Ok(quote!(#pattern => #value,))
1868 })
1869 .collect::<Result<Vec<_>>>()?;
1870 let otherwise = self::source(otherwise, owned)?;
1871 Ok(quote! {
1872 match #scrutinee {
1873 #(#arms)*
1874 _ => #otherwise,
1875 }
1876 })
1877 }
1878 }
1879 }
1880
1881 /// Whether a source's arms have to agree on `String`.
1882 ///
1883 /// A dispatch whose arms are all strings and at least one of which interpolates
1884 /// mixes a `&'static str` with a `String`, which is the split the record's
1885 /// amendment 10 reached for with a `&`/`owned` spelling. It does not need one:
1886 /// the arms of one dispatch have to agree on a type, so the dispatch decides
1887 /// the ownership for all of them, and nothing outside a dispatch has an
1888 /// ambiguity to settle.
1889 fn owned_arms(source: &Source) -> bool {
1890 let Source::Choose {
1891 arms, otherwise, ..
1892 } = source
1893 else {
1894 return false;
1895 };
1896 let every = arms
1897 .iter()
1898 .map(|(_, value)| value)
1899 .chain(std::iter::once(&**otherwise));
1900 let mut all_strings = true;
1901 let mut any_interpolates = false;
1902 for value in every {
1903 match value {
1904 Source::Str(text) => {
1905 any_interpolates |= text
1906 .parts
1907 .iter()
1908 .any(|part| matches!(part, StrPart::Hole(_)));
1909 }
1910 _ => all_strings = false,
1911 }
1912 }
1913 all_strings && any_interpolates
1914 }
1915
1916 fn pattern(pattern: &Pattern) -> TokenStream {
1917 match pattern {
1918 Pattern::Int(value) => {
1919 let value = proc_macro2::Literal::i64_unsuffixed(*value);
1920 quote!(#value)
1921 }
1922 Pattern::Str(value) => {
1923 let value = LitStr::new(value, Span::call_site());
1924 quote!(#value)
1925 }
1926 Pattern::Bool(value) => quote!(#value),
1927 Pattern::Path(path) => quote!(#path),
1928 }
1929 }
1930
1931 fn interpolated(text: &Interpolated, owned: bool) -> TokenStream {
1932 let mut format = String::new();
1933 let mut holes = Vec::new();
1934 for part in &text.parts {
1935 match part {
1936 StrPart::Lit(literal) => {
1937 for character in literal.chars() {
1938 if character == '{' || character == '}' {
1939 format.push(character);
1940 }
1941 format.push(character);
1942 }
1943 }
1944 StrPart::Hole(hole) => {
1945 format.push_str("{}");
1946 holes.push(hole);
1947 }
1948 }
1949 }
1950 let literal = LitStr::new(&format, text.span);
1951
1952 if holes.is_empty() {
1953 return if owned {
1954 quote!(::std::string::String::from(#literal))
1955 } else {
1956 quote!(#literal)
1957 };
1958 }
1959
1960 let holes = holes
1961 .iter()
1962 .map(|hole| self::hole(hole).unwrap_or_else(syn::Error::into_compile_error));
1963 quote!(::std::format!(#literal, #(#holes),*))
1964 }
1965
1966 pub(crate) fn hole(hole: &Hole) -> Result<TokenStream> {
1967 let mut built = match &hole.root {
1968 HoleRoot::Binding(name) => quote!(#name),
1969 HoleRoot::Path(path) => quote!(#path),
1970 HoleRoot::Call { path, args } => {
1971 let args = args.iter().map(arg).collect::<Result<Vec<_>>>()?;
1972 quote!(#path(#(#args),*))
1973 }
1974 };
1975 for step in &hole.steps {
1976 built = match step {
1977 Step::Field(name) => quote!(#built.#name),
1978 Step::Method { name, args } => {
1979 let args = args.iter().map(arg).collect::<Result<Vec<_>>>()?;
1980 quote!(#built.#name(#(#args),*))
1981 }
1982 };
1983 }
1984 Ok(built)
1985 }
1986
1987 pub(crate) fn arg(arg: &Arg) -> Result<TokenStream> {
1988 match arg {
1989 Arg::Str(text) => Ok(interpolated(text, false)),
1990 Arg::Hole(hole) => self::hole(hole),
1991 Arg::Int(value) => {
1992 let value = proc_macro2::Literal::i64_unsuffixed(*value);
1993 Ok(quote!(#value))
1994 }
1995 Arg::Bool(value) => Ok(quote!(#value)),
1996 Arg::List(items) => {
1997 let items = items.iter().map(self::arg).collect::<Result<Vec<_>>>()?;
1998 Ok(quote!([#(#items),*]))
1999 }
2000 Arg::Borrow(inner) => {
2001 let inner = self::arg(inner)?;
2002 Ok(quote!(&#inner))
2003 }
2004 }
2005 }
2006
2007 fn emission_span(emission: &Emission) -> Span {
2008 match emission {
2009 Emission::Framed { inner, .. } => emission_span(inner),
2010 Emission::Include(_) | Emission::IncludeEach(_) | Emission::Link { .. } => {
2011 Span::call_site()
2012 }
2013 Emission::Simple { member, .. } => member.span(),
2014 Emission::Screen { arrangement, .. } => arrangement.span(),
2015 Emission::Row { .. } | Emission::List(_) => Span::call_site(),
2016 Emission::Form { action, .. } | Emission::Chip { action, .. } => action.verb.span(),
2017 Emission::Field { kind, .. } => kind.span(),
2018 Emission::Act { action, .. }
2019 | Emission::Offers { action, .. }
2020 | Emission::Removes { action, .. }
2021 | Emission::Repeats { action, .. } => action.verb.span(),
2022 Emission::Guarded { guard, .. } => guard.span,
2023 Emission::Given { .. } => Span::call_site(),
2024 Emission::Region { kind, .. } => match kind {
2025 RegionKind::Variant(variant) => variant.span(),
2026 RegionKind::Supplied(_) => Span::call_site(),
2027 },
2028 Emission::Across { fallback, .. } => fallback.span(),
2029 Emission::Beside { inner, .. } => emission_span(inner),
2030 Emission::At { inner, .. } => emission_span(inner),
2031 Emission::Table(_) | Emission::Cells(_) => Span::call_site(),
2032 Emission::Column { .. } | Emission::Cell { .. } => Span::call_site(),
2033 Emission::Activate(action) => action.verb.span(),
2034 Emission::Offering { action, .. } => action.verb.span(),
2035 }
2036 }
2037