Skip to main content

max / quasi

44.7 KB · 1258 lines History Blame Raw
1 //! Reading the declared form into [`crate::ast`].
2 //!
3 //! The parser admits exactly what the syntax tree can hold, which is what a
4 //! converted screen has demanded. Anything else is a compile error naming the
5 //! construct, never a silent skip: a declaration that parsed but described less
6 //! than it said would be worse than one that refused.
7 //!
8 //! Two things this deliberately cannot do. It never parses a Rust expression,
9 //! so there is no path by which an operator, a closure or a block reaches the
10 //! tree. And it resolves nothing: rule R1 decides binding-versus-path by the
11 //! shape of the ident alone, so the parser never needs to know what is in
12 //! scope.
13
14 use syn::parse::{Parse, ParseStream};
15 use syn::{Attribute, Ident, LitBool, LitInt, LitStr, Result, Token, Type, Visibility};
16 use syn::{braced, parenthesized, token};
17
18 use crate::ast::{
19 Action, Arg, Declaration, Emission, Guard, Hole, HoleRoot, Interpolated, Item, Modifier, Param,
20 Pattern, Predicate, RegionKind, Source, Step, StrPart,
21 };
22 use crate::copy;
23
24 /// The modifiers, and how many arguments each takes.
25 ///
26 /// **Two words were here that are settings on the control rather than modifiers
27 /// of the action, and both were caught by a screen rather than by reading.**
28 /// `filling` is `Act::filling` and made `media_picker`'s tiles fail against
29 /// `Action`; `copying` is `Act::copying`, which sets the destination to
30 /// `Action::local` itself, and `collections`' Copy link is the site. A word
31 /// belongs here only if `Action` has the method.
32 ///
33 /// **`with` was refused in wave 3 and the refusal was too wide.** What was
34 /// refused is a *form* saying "and this value too, sometimes" -- a conditional
35 /// payload, which the form still cannot say and which `doing` is still the
36 /// remedy for. An unconditional one is `Action::with(name, value)` and nothing
37 /// else, so it is a modifier by this table's own rule. Four suppliers existed
38 /// only to write it: `tip::checkout`, `auth_pages::reset_action`,
39 /// `git_commit::note_delete` and goingson's board move.
40 const MODIFIERS: &[(&str, usize)] = &[
41 ("navigating", 0),
42 ("awaiting", 0),
43 ("by_host", 0),
44 ("elsewhere", 0),
45 ("invalidating", 0),
46 ("replacing_enclosing", 0),
47 ("saving", 1),
48 ("replacing", 1),
49 ("carrying", 2),
50 ("with", 2),
51 ];
52
53 /// The members: everything that emits rather than sets.
54 ///
55 /// An ident inside a body is a member if it is here and an attribute if it is
56 /// not, which is the whole of how the two are told apart. A member added here
57 /// without a production is a parse error naming it, which is the failure worth
58 /// having.
59 const MEMBERS: &[&str] = &[
60 "act",
61 "across",
62 "activate",
63 "at",
64 "beside",
65 "framed",
66 "cell",
67 "cells",
68 "chip",
69 "column",
70 "field",
71 "form",
72 "given",
73 "include",
74 "link",
75 "offering",
76 "offers",
77 "region",
78 "removable",
79 "removes",
80 "repeats",
81 "row",
82 "screen",
83 "table",
84 ];
85
86 /// The members that are one of `Node`'s own constructors, called by its name.
87 /// Growing this list is the whole of adding one.
88 ///
89 /// `picture` is the one exception and it is the last entry for that reason:
90 /// [`Node::Image`] carries a builder type of its own, so the member's body sets
91 /// on the `Image` and the emitter wraps the result rather than calling a
92 /// constructor on `Node`. Everything else here is `Node::<name>`.
93 ///
94 /// **A name that is a setting on any container cannot go here.** An ident inside
95 /// a body is a member if it is in one of these two lists and a setting if it is
96 /// not, and that is the whole of how the two are told apart, so one name cannot
97 /// mean both. `token` is the measured case: `Node::Token` looks like a member,
98 /// and `Row::token` and `Cell::token` are settings at four converted sites. A
99 /// screen wanting a tag standing on its own reaches `Node::token` through a
100 /// supplier, which is the remedy the deferred table already names, and
101 /// `project_analytics`'s `chip` is that supplier.
102 ///
103 /// **Where a variant holds a builder type, the member is named for the
104 /// constructor rather than for the variant.** That is the rule the exceptions
105 /// below share, and it is what `badge` and `chip` settled: `Node::Token` holds a
106 /// `Tag`, `token` is a setting on `Row` and on `Cell` so it cannot be a member
107 /// at all, and `Tag`'s two constructors are what a screen actually reaches for.
108 /// So there is no `token` member and there never will be. quasicoherent
109 /// `e2030032` is the decision, measured over 33 sites in the three trees.
110 ///
111 /// `canvas` is the second exception and is the last entry for that reason:
112 /// `Canvas` is a builder type of its own, the way `Image` is, so the member's
113 /// body accretes onto the canvas and the emitter wraps the variant afterwards.
114 /// `custom_page` is the site -- a creator's opaque markup under the scope its
115 /// stylesheet was rewritten for, with the platform's own blocks drawn inside
116 /// it.
117 ///
118 /// `badge` is the third and `chip` the fourth, and `chip` is not on this list
119 /// because it carries an action: it has a parse arm of its own. Both accrete
120 /// onto a `Tag` under `Container::Tag` and the emitter wraps `Node::token`.
121 ///
122 /// `timeline` is the fifth, and the only one whose body holds members rather
123 /// than settings: an axis is told what sits on it and where, which is the `at`
124 /// member under `Container::Timeline`. It is a `Node` either way, so nothing is
125 /// wrapped afterwards.
126 ///
127 /// `proportion` is the seventh, and the second renamed rather than reshaped. It
128 /// is `Node::Meter`, which holds a `Meter` builder the way `badge` holds a
129 /// `Tag`, so the body accretes onto the meter and the emitter wraps the
130 /// variant. It cannot be spelled `meter` because `Row::meter` and `Cell::meter`
131 /// are settings; `Row` puts one in `RowPart::Proportion`, which is where the
132 /// name comes from. goingson's task drawer is the site, twice -- subtask
133 /// completion and time against estimate -- and MNW's pricing panel is the
134 /// third, where the reader's volume is drawn against the crossover.
135 ///
136 /// `underway` is the sixth, and the only one renamed rather than reshaped. It
137 /// is `Node::pending`, the third of the three `StandIn` states beside `empty`
138 /// and `failed`, and it cannot be spelled `pending` because `Slot::pending` is
139 /// a setting and one name cannot mean both. Renaming is what the rule leaves
140 /// open where `token` had to go to a supplier: `token` collided with a setting
141 /// that meant the same thing on a container that could hold one, so a second
142 /// spelling would have been two ways to say one thing, while `Slot::pending`
143 /// says a region is waiting and this says what to draw in it. goingson's
144 /// backups region is the site: it is `Slot::live`, so a swapping host has
145 /// nowhere to put an attribute and needs the node.
146 const NODE_MEMBERS: &[&str] = &[
147 "text",
148 "toned",
149 "page",
150 "section",
151 "subsection",
152 "list",
153 "stats",
154 "banner",
155 "toast",
156 "failed",
157 "empty",
158 "rich",
159 "literal",
160 "code",
161 "since",
162 "until",
163 "picture",
164 "canvas",
165 "badge",
166 "timeline",
167 "underway",
168 "proportion",
169 "tally",
170 ];
171
172 /// The arrangements a screen may be laid out in.
173 const ARRANGEMENTS: &[&str] = &["single", "list_detail", "sidebar_content"];
174
175 /// The comparisons, and the Rust operator each is.
176 pub const COMPARISONS: &[(&str, &str)] = &[
177 ("is", "=="),
178 ("is_not", "!="),
179 ("over", ">"),
180 ("under", "<"),
181 ("at_least", ">="),
182 ("at_most", "<="),
183 ];
184
185 /// The attributes whose argument is a variant of a vocabulary enum, and the
186 /// enum it belongs to. Rule R1(4).
187 pub const VOCABULARY: &[(&str, &str)] = &[
188 ("tone", "Tone"),
189 ("width", "Width"),
190 ("priority", "Priority"),
191 ("fit", "Fit"),
192 ];
193
194 /// The attributes the generated function may carry.
195 // `staged` is not an attribute the generated function carries: it asks for
196 // a second function beside it. See `symbolic`.
197 const FLAGS: &[&str] = &["must_use", "inline", "staged", "constant"];
198
199 /// The verbs, and whether each addresses something.
200 ///
201 /// **`leaving` said it addressed nothing and it addresses a url.** Same class
202 /// of mistake as `filling` and `copying` above, and it survived for the same
203 /// reason: no declared screen had used the word, so the table was wrong with
204 /// nothing looking. `custom_page` is the site -- every link off a custom page
205 /// replaces it rather than opening a tab, which is what `Action::leaving(url)`
206 /// says. Read the signature, not the neighbours: `local` and `back` take
207 /// nothing and `leaving` sits between them.
208 const VERBS: &[(&str, bool)] = &[
209 ("get", true),
210 ("post", true),
211 ("put", true),
212 ("delete", true),
213 ("external", true),
214 ("local", false),
215 ("leaving", true),
216 ("back", false),
217 ];
218
219 impl Parse for Declaration {
220 fn parse(input: ParseStream) -> Result<Self> {
221 let attrs = Attribute::parse_outer(input)?;
222 let (docs, flags) = docs_and_flags(&attrs)?;
223
224 let vis: Visibility = input.parse()?;
225 let vis = match vis {
226 Visibility::Inherited => None,
227 other => Some(other),
228 };
229
230 let keyword: Ident = input.parse()?;
231 if keyword != "shape" {
232 return Err(syn::Error::new(
233 keyword.span(),
234 "a declaration begins with `shape`",
235 ));
236 }
237 let name: Ident = input.parse()?;
238
239 let signature;
240 parenthesized!(signature in input);
241 let params = signature.parse_terminated(Param::parse, Token![,])?;
242
243 input.parse::<Token![->]>()?;
244 let returns: Type = input.parse()?;
245 input.parse::<Token![;]>()?;
246
247 let items = items(input)?;
248
249 Ok(Self {
250 docs,
251 flags,
252 vis,
253 name,
254 params: params.into_iter().collect(),
255 returns,
256 items,
257 })
258 }
259 }
260
261 /// The `///` lines and the bare flags, in order.
262 fn docs_and_flags(attrs: &[Attribute]) -> Result<(Vec<String>, Vec<Ident>)> {
263 let mut docs = Vec::new();
264 let mut flags = Vec::new();
265 for attr in attrs {
266 if !attr.path().is_ident("doc") {
267 let syn::Meta::Path(path) = &attr.meta else {
268 return Err(syn::Error::new_spanned(
269 attr,
270 "an attribute with arguments is not a production yet",
271 ));
272 };
273 let Some(name) = path.get_ident() else {
274 return Err(syn::Error::new_spanned(attr, "expected a bare attribute"));
275 };
276 if !FLAGS.contains(&name.to_string().as_str()) {
277 return Err(syn::Error::new_spanned(
278 attr,
279 format!("`{name}` is not a flag a declaration may carry"),
280 ));
281 }
282 flags.push(name.clone());
283 continue;
284 }
285 let syn::Meta::NameValue(value) = &attr.meta else {
286 return Err(syn::Error::new_spanned(attr, "expected a doc comment"));
287 };
288 let syn::Expr::Lit(syn::ExprLit {
289 lit: syn::Lit::Str(text),
290 ..
291 }) = &value.value
292 else {
293 return Err(syn::Error::new_spanned(attr, "expected a doc comment"));
294 };
295 docs.push(text.value());
296 }
297 Ok((docs, flags))
298 }
299
300 impl Parse for Param {
301 fn parse(input: ParseStream) -> Result<Self> {
302 let name: Ident = input.parse()?;
303 input.parse::<Token![:]>()?;
304 let ty: Type = input.parse()?;
305 Ok(Self { name, ty })
306 }
307 }
308
309 impl Parse for Item {
310 fn parse(input: ParseStream) -> Result<Self> {
311 if input.peek(Token![let]) {
312 input.parse::<Token![let]>()?;
313 let name: Ident = input.parse()?;
314 input.parse::<Token![=]>()?;
315 let source: Source = input.parse()?;
316 input.parse::<Token![;]>()?;
317 return Ok(Self::Bind { name, source });
318 }
319 if input.peek(Token![for]) {
320 input.parse::<Token![for]>()?;
321 let dereferenced = input.peek(Token![&]);
322 if dereferenced {
323 input.parse::<Token![&]>()?;
324 }
325 let binder: Ident = input.parse()?;
326 input.parse::<Token![in]>()?;
327 let iterable: Hole = input.parse()?;
328 return Ok(Self::For {
329 dereferenced,
330 binder,
331 iterable,
332 body: block(input)?,
333 });
334 }
335 let member = input.fork().parse::<Ident>()?;
336 let spelling = member.to_string();
337 if MEMBERS.contains(&spelling.as_str()) || NODE_MEMBERS.contains(&spelling.as_str()) {
338 return Ok(Self::Emit(input.parse()?));
339 }
340 let name: Ident = input.parse()?;
341 let mut args = Vec::new();
342 while !input.peek(Token![;]) && guard_ahead(input)?.is_none() {
343 args.push(input.parse()?);
344 }
345 let guard = guard(input)?;
346 input.parse::<Token![;]>()?;
347 Ok(Self::Attribute { name, args, guard })
348 }
349 }
350
351 impl Parse for Source {
352 fn parse(input: ParseStream) -> Result<Self> {
353 if input.peek(LitStr) {
354 let lit: LitStr = input.parse()?;
355 return Ok(Self::Str(interpolate(&lit)?));
356 }
357 if input.peek(Ident) && input.fork().parse::<Ident>()? == "given" {
358 return parse_choose(input);
359 }
360 Ok(Self::Hole(input.parse()?))
361 }
362 }
363
364 /// `given <hole> { <pattern> -> <source>, otherwise -> <source> }`.
365 ///
366 /// The arms are sources, so a value dispatch produces a value by construction.
367 /// `otherwise` is required rather than optional: a binding has to have a value
368 /// on every path, and rustc's exhaustiveness cannot be borrowed here without
369 /// admitting a pattern that binds.
370 fn parse_choose(input: ParseStream) -> Result<Source> {
371 let keyword: Ident = input.parse()?;
372 let scrutinee: Hole = input.parse()?;
373
374 let body;
375 braced!(body in input);
376
377 let mut arms = Vec::new();
378 let mut otherwise = None;
379 while !body.is_empty() {
380 if body.peek(Ident) && body.fork().parse::<Ident>()? == "otherwise" {
381 body.parse::<Ident>()?;
382 body.parse::<Token![->]>()?;
383 otherwise = Some(Box::new(body.parse()?));
384 } else {
385 let pattern: Pattern = body.parse()?;
386 body.parse::<Token![->]>()?;
387 arms.push((pattern, body.parse()?));
388 }
389 if body.peek(Token![,]) {
390 body.parse::<Token![,]>()?;
391 }
392 }
393
394 let Some(otherwise) = otherwise else {
395 return Err(syn::Error::new(
396 keyword.span(),
397 "a value dispatch needs an `otherwise` arm: a binding has a value on every path",
398 ));
399 };
400
401 Ok(Source::Choose {
402 scrutinee,
403 arms,
404 otherwise,
405 })
406 }
407
408 impl Parse for RegionKind {
409 fn parse(input: ParseStream) -> Result<Self> {
410 // R1's rule, applied to a kind: a bare uppercase ident is the variant,
411 // and anything else is a hole that answers with one.
412 if input.peek(Ident) && !input.peek2(Token![::]) && !input.peek2(token::Paren) {
413 let name: Ident = input.fork().parse()?;
414 let text = name.to_string();
415 if text.starts_with(|letter: char| letter.is_uppercase()) {
416 input.parse::<Ident>()?;
417 return Ok(Self::Variant(name));
418 }
419 }
420 Ok(Self::Supplied(input.parse()?))
421 }
422 }
423
424 impl Parse for Pattern {
425 fn parse(input: ParseStream) -> Result<Self> {
426 if input.peek(LitInt) {
427 let lit: LitInt = input.parse()?;
428 return Ok(Self::Int(lit.base10_parse()?));
429 }
430 if input.peek(LitStr) {
431 let lit: LitStr = input.parse()?;
432 return Ok(Self::Str(lit.value()));
433 }
434 if input.peek(LitBool) {
435 let lit: LitBool = input.parse()?;
436 return Ok(Self::Bool(lit.value()));
437 }
438 Ok(Self::Path(input.parse()?))
439 }
440 }
441
442 impl Parse for Hole {
443 fn parse(input: ParseStream) -> Result<Self> {
444 let path: syn::Path = input.parse()?;
445
446 let root = if input.peek(token::Paren) {
447 HoleRoot::Call {
448 path,
449 args: call_args(input)?,
450 }
451 } else if let Some(name) = binding_ident(&path) {
452 HoleRoot::Binding(name)
453 } else {
454 HoleRoot::Path(path)
455 };
456
457 let mut steps = Vec::new();
458 while input.peek(Token![.]) {
459 input.parse::<Token![.]>()?;
460 let name: Ident = input.parse()?;
461 if input.peek(token::Paren) {
462 steps.push(Step::Method {
463 name,
464 args: call_args(input)?,
465 });
466 } else {
467 steps.push(Step::Field(name));
468 }
469 }
470
471 Ok(Self { root, steps })
472 }
473 }
474
475 /// Rule R1: a bare lowercase-initial ident is a binding; anything qualified or
476 /// uppercase-initial is a Rust path.
477 fn binding_ident(path: &syn::Path) -> Option<Ident> {
478 if path.leading_colon.is_some() || path.segments.len() != 1 {
479 return None;
480 }
481 let segment = path.segments.first()?;
482 if !segment.arguments.is_none() {
483 return None;
484 }
485 let text = segment.ident.to_string();
486 let first = text.chars().next()?;
487 (first.is_lowercase()).then(|| segment.ident.clone())
488 }
489
490 fn call_args(input: ParseStream) -> Result<Vec<Arg>> {
491 let content;
492 parenthesized!(content in input);
493 let args = content.parse_terminated(Arg::parse, Token![,])?;
494 Ok(args.into_iter().collect())
495 }
496
497 impl Parse for Arg {
498 fn parse(input: ParseStream) -> Result<Self> {
499 if input.peek(Token![&]) {
500 input.parse::<Token![&]>()?;
501 return Ok(Self::Borrow(Box::new(input.parse()?)));
502 }
503 if input.peek(token::Bracket) {
504 let items;
505 syn::bracketed!(items in input);
506 let items = items.parse_terminated(Self::parse, Token![,])?;
507 return Ok(Self::List(items.into_iter().collect()));
508 }
509 if input.peek(LitStr) {
510 let lit: LitStr = input.parse()?;
511 return Ok(Self::Str(interpolate(&lit)?));
512 }
513 if input.peek(LitInt) {
514 let lit: LitInt = input.parse()?;
515 return Ok(Self::Int(lit.base10_parse()?));
516 }
517 if input.peek(LitBool) {
518 let lit: LitBool = input.parse()?;
519 return Ok(Self::Bool(lit.value()));
520 }
521 Ok(Self::Hole(input.parse()?))
522 }
523 }
524
525 impl Parse for Emission {
526 fn parse(input: ParseStream) -> Result<Self> {
527 let member: Ident = input.parse()?;
528 match member.to_string().as_str() {
529 "framed" => {
530 let label: Arg = input.parse()?;
531 let inner: Self = input.parse()?;
532 // A guard on a placed member guards the placing, exactly as it
533 // does for `beside`.
534 Ok(match inner {
535 Self::Guarded { guard, inner } => Self::Guarded {
536 guard,
537 inner: Box::new(Self::Framed { label, inner }),
538 },
539 inner => Self::Framed {
540 label,
541 inner: Box::new(inner),
542 },
543 })
544 }
545 "beside" => {
546 let priority: Arg = input.parse()?;
547 // A second argument before the member is the width. Told apart
548 // by what a member is: the words in `MEMBERS` and
549 // `NODE_MEMBERS` and nothing else, so an ident that is not one
550 // of them cannot be the emission and must be the width. Same
551 // rule the parser already uses to tell a setting from a member,
552 // applied one position along.
553 let width = if member_ahead(input)? {
554 None
555 } else {
556 Some(input.parse()?)
557 };
558 let inner: Self = input.parse()?;
559 // A guard on a placed member guards the placing. The run is
560 // what may hold nothing and `beside` is how a member reaches
561 // it, so the guard belongs outside: read the other way round
562 // it asks a run to hold an absence, which nothing can do.
563 Ok(match inner {
564 Self::Guarded { guard, inner } => Self::Guarded {
565 guard,
566 inner: Box::new(Self::Beside {
567 priority,
568 width,
569 inner,
570 }),
571 },
572 inner => Self::Beside {
573 priority,
574 width,
575 inner: Box::new(inner),
576 },
577 })
578 }
579 "at" => {
580 let at: Arg = input.parse()?;
581 let inner: Self = input.parse()?;
582 // A guard on a placed entry guards the placing, exactly as
583 // `beside`'s does: the timeline is what may hold nothing.
584 Ok(match inner {
585 Self::Guarded { guard, inner } => Self::Guarded {
586 guard,
587 inner: Box::new(Self::At { at, inner }),
588 },
589 inner => Self::At {
590 at,
591 inner: Box::new(inner),
592 },
593 })
594 }
595 "region" => {
596 let name: Arg = input.parse()?;
597 input.parse::<Token![as]>()?;
598 let kind: RegionKind = input.parse()?;
599 // The guard sits between the kind and the body, where `act`
600 // puts its own: a region that is sometimes not there is the
601 // same fact as a control that is sometimes not offered.
602 let guard = guard(input)?;
603 Ok(guarded(
604 guard,
605 Self::Region {
606 name,
607 kind,
608 body: block(input)?,
609 },
610 ))
611 }
612 "across" => {
613 let fallback: Ident = input.parse()?;
614 Ok(Self::Across {
615 fallback,
616 body: block(input)?,
617 })
618 }
619 "act" | "offers" => {
620 let held_back = member == "offers";
621 let label: Arg = input.parse()?;
622 preposition(input, "to")?;
623 let action: Action = input.parse()?;
624 let guard = guard(input)?;
625 let body = if input.peek(token::Brace) {
626 block(input)?
627 } else {
628 input.parse::<Token![;]>()?;
629 Vec::new()
630 };
631 Ok(guarded(
632 guard,
633 if held_back {
634 Self::Offers {
635 label,
636 action,
637 body,
638 }
639 } else {
640 Self::Act {
641 label,
642 action,
643 body,
644 }
645 },
646 ))
647 }
648 "removes" => {
649 let label: Arg = input.parse()?;
650 preposition(input, "to")?;
651 let action: Action = input.parse()?;
652 let guard = guard(input)?;
653 let body = if input.peek(token::Brace) {
654 block(input)?
655 } else {
656 input.parse::<Token![;]>()?;
657 Vec::new()
658 };
659 Ok(guarded(
660 guard,
661 Self::Removes {
662 label,
663 action,
664 body,
665 },
666 ))
667 }
668 "repeats" => {
669 let one: Arg = input.parse()?;
670 preposition(input, "adds")?;
671 let label: Arg = input.parse()?;
672 preposition(input, "to")?;
673 let action: Action = input.parse()?;
674 let guard = guard(input)?;
675 let body = if input.peek(token::Brace) {
676 block(input)?
677 } else {
678 input.parse::<Token![;]>()?;
679 Vec::new()
680 };
681 Ok(guarded(
682 guard,
683 Self::Repeats {
684 one,
685 label,
686 action,
687 body,
688 },
689 ))
690 }
691 "include" => {
692 // `include each <shape>` splices a shape that answers many
693 // members. `each` cannot be a shape name here: a hole starts
694 // with a path or a binding and the next token would be `(`.
695 let every = input.peek(Ident)
696 && input.fork().parse::<Ident>()? == "each"
697 && !input.fork().parse::<Hole>().is_ok_and(|_| false);
698 let every = every && {
699 let fork = input.fork();
700 fork.parse::<Ident>()?;
701 fork.peek(Ident) || fork.peek(Token![:])
702 };
703 if every {
704 input.parse::<Ident>()?;
705 }
706 let supplier: Hole = input.parse()?;
707 let guard = guard(input)?;
708 input.parse::<Token![;]>()?;
709 Ok(guarded(
710 guard,
711 if every {
712 Self::IncludeEach(supplier)
713 } else {
714 Self::Include(supplier)
715 },
716 ))
717 }
718 "given" => {
719 let scrutinee: Hole = input.parse()?;
720 let body;
721 braced!(body in input);
722 let mut arms = Vec::new();
723 let mut otherwise = None;
724 while !body.is_empty() {
725 if body.peek(Ident) && body.fork().parse::<Ident>()? == "otherwise" {
726 body.parse::<Ident>()?;
727 body.parse::<Token![->]>()?;
728 otherwise = Some(Box::new(body.parse()?));
729 } else {
730 let pattern: Pattern = body.parse()?;
731 body.parse::<Token![->]>()?;
732 arms.push((pattern, Box::new(body.parse()?)));
733 }
734 }
735 Ok(Self::Given {
736 scrutinee,
737 arms,
738 otherwise,
739 })
740 }
741 "screen" => {
742 let arrangement: Ident = input.parse()?;
743 if !ARRANGEMENTS.contains(&arrangement.to_string().as_str()) {
744 return Err(syn::Error::new(
745 arrangement.span(),
746 "a screen is laid out `single`, `list_detail` or `sidebar_content`",
747 ));
748 }
749 // Every arrangement takes a title, and `list_detail` takes
750 // whether it is tabbed as well. Read to the body rather than
751 // taking exactly one, so the grammar says what the constructor
752 // says. `pricing` is the site: the first converted screen that
753 // is not `single`.
754 let mut args = Vec::new();
755 while !input.peek(token::Brace) {
756 args.push(input.parse()?);
757 }
758 Ok(Self::Screen {
759 arrangement,
760 args,
761 body: block(input)?,
762 })
763 }
764 "row" => {
765 let primary: Arg = input.parse()?;
766 // Before the body, where `column`, `cell`, `act`, `offering`
767 // and `form` already put theirs. `pricing`'s footer is the
768 // site: Changelog is linked only while a published changelog
769 // project exists, and the route 404s otherwise.
770 let guard = guard(input)?;
771 let body = if input.peek(token::Brace) {
772 block(input)?
773 } else {
774 input.parse::<Token![;]>()?;
775 Vec::new()
776 };
777 Ok(guarded(guard, Self::Row { primary, body }))
778 }
779 // The guard goes after the body here rather than before it, because
780 // the body is what tells this `list` from the node member of the
781 // same name. `user_projects` draws its list only when there is
782 // something in it.
783 "list" if input.peek(token::Brace) => {
784 let rows = block(input)?;
785 let guard = guard(input)?;
786 if guard.is_some() {
787 input.parse::<Token![;]>()?;
788 }
789 Ok(guarded(guard, Self::List(rows)))
790 }
791 // A guard after the body, which is where `list` takes one and for
792 // its reason: a table is a member like any other, and a member of
793 // an accumulating container may be absent. audiofiles' file list is
794 // the site -- an empty vault says so with a stand-in instead, and a
795 // table drawn anyway would be a header row over nothing.
796 "table" => {
797 let body = block(input)?;
798 let guard = guard(input)?;
799 if guard.is_some() {
800 input.parse::<Token![;]>()?;
801 }
802 Ok(guarded(guard, Self::Table(body)))
803 }
804 "column" => {
805 let name: Arg = input.parse()?;
806 // Before the body, where every other guarded member puts it.
807 // `git_repos` shows a visibility column to the owner and to
808 // nobody else, and the same guard decides the cell below, so
809 // the two cannot fall out of step.
810 let guard = guard(input)?;
811 let body = if input.peek(token::Brace) {
812 block(input)?
813 } else {
814 input.parse::<Token![;]>()?;
815 Vec::new()
816 };
817 Ok(guarded(guard, Self::Column { name, body }))
818 }
819 "cells" => Ok(Self::Cells(block(input)?)),
820 "cell" => {
821 let column = if input.peek(Ident) && input.fork().parse::<Ident>()? == "at" {
822 input.parse::<Ident>()?;
823 Some(input.parse()?)
824 } else {
825 None
826 };
827 let value: Arg = input.parse()?;
828 let guard = guard(input)?;
829 let body = if input.peek(token::Brace) {
830 block(input)?
831 } else {
832 input.parse::<Token![;]>()?;
833 Vec::new()
834 };
835 Ok(guarded(
836 guard,
837 Self::Cell {
838 column,
839 value,
840 body,
841 },
842 ))
843 }
844 "offering" => {
845 let label: Arg = input.parse()?;
846 preposition(input, "to")?;
847 let action: Action = input.parse()?;
848 let guard = guard(input)?;
849 let body = if input.peek(token::Brace) {
850 block(input)?
851 } else {
852 input.parse::<Token![;]>()?;
853 Vec::new()
854 };
855 Ok(guarded(
856 guard,
857 Self::Offering {
858 label,
859 action,
860 body,
861 },
862 ))
863 }
864 "activate" => {
865 preposition(input, "to")?;
866 let action: Action = input.parse()?;
867 let guard = guard(input)?;
868 input.parse::<Token![;]>()?;
869 Ok(guarded(guard, Self::Activate(action)))
870 }
871 "form" => {
872 let action: Action = input.parse()?;
873 // Before the body, where `act`, `offering`, `column` and `cell`
874 // already put theirs. `auth_pages` is the site twice over: the
875 // login form is drawn only where there is a local password to
876 // type, and the reset form only where the signed link still
877 // resolves. Both are one screen with two shapes rather than two
878 // screens, because everything around them is the same.
879 let guard = guard(input)?;
880 Ok(guarded(
881 guard,
882 Self::Form {
883 action,
884 body: block(input)?,
885 },
886 ))
887 }
888 // A chip is a tag that goes somewhere, so it takes an action, and
889 // an action is spelled `to <verb>` rather than as an argument. That
890 // is why it is not in NODE_MEMBERS with `badge`.
891 "chip" | "removable" => {
892 let removable = member == "removable";
893 let value: Arg = input.parse()?;
894 preposition(input, "to")?;
895 let action: Action = input.parse()?;
896 let guard = guard(input)?;
897 let body = if input.peek(token::Brace) {
898 block(input)?
899 } else {
900 input.parse::<Token![;]>()?;
901 Vec::new()
902 };
903 Ok(guarded(
904 guard,
905 Self::Chip {
906 value,
907 action,
908 removable,
909 body,
910 },
911 ))
912 }
913 "field" => {
914 let kind: Ident = input.parse()?;
915 let name: Arg = input.parse()?;
916 let label: Arg = input.parse()?;
917 // Before the body, where every other guarded member puts it.
918 // `pricing` is the site: the founder-or-list question is asked
919 // only while there are two rates to choose between, and with
920 // the window shut the page is what it was before the question
921 // existed.
922 let guard = guard(input)?;
923 let body = if input.peek(token::Brace) {
924 block(input)?
925 } else {
926 input.parse::<Token![;]>()?;
927 Vec::new()
928 };
929 Ok(guarded(
930 guard,
931 Self::Field {
932 kind,
933 name,
934 label,
935 body,
936 },
937 ))
938 }
939 name if NODE_MEMBERS.contains(&name) => {
940 let mut args = Vec::new();
941 while !input.peek(Token![;])
942 && !input.peek(token::Brace)
943 && guard_ahead(input)?.is_none()
944 {
945 args.push(input.parse()?);
946 }
947 let guard = guard(input)?;
948 // Told something afterwards, or nothing. `act` reads the same
949 // way and for the same reason: a member with nothing to say
950 // about itself should not have to open a block to say so.
951 let body = if input.peek(token::Brace) {
952 block(input)?
953 } else {
954 input.parse::<Token![;]>()?;
955 Vec::new()
956 };
957 Ok(guarded(
958 guard,
959 Self::Simple {
960 member: member.clone(),
961 args,
962 body,
963 },
964 ))
965 }
966 "link" => {
967 let text: Arg = input.parse()?;
968 preposition(input, "to")?;
969 let action: Action = input.parse()?;
970 let guard = guard(input)?;
971 input.parse::<Token![;]>()?;
972 Ok(guarded(guard, Self::Link { text, action }))
973 }
974 other => Err(syn::Error::new(
975 member.span(),
976 format!(
977 "`{other}` is not a member this form can say yet. \
978 Add the production, and name the screen that demanded it in the commit."
979 ),
980 )),
981 }
982 }
983 }
984
985 /// Whether a guard starts here, without consuming it.
986 fn guard_ahead(input: ParseStream) -> Result<Option<bool>> {
987 if !input.peek(Ident) {
988 return Ok(None);
989 }
990 Ok(match input.fork().parse::<Ident>()?.to_string().as_str() {
991 "when" => Some(false),
992 "unless" => Some(true),
993 _ => None,
994 })
995 }
996
997 /// `when <predicate>` or `unless <predicate>`, if one is written here.
998 fn guard(input: ParseStream) -> Result<Option<Guard>> {
999 let Some(negated) = guard_ahead(input)? else {
1000 return Ok(None);
1001 };
1002 let word: Ident = input.parse()?;
1003 Ok(Some(Guard {
1004 negated,
1005 predicate: predicate(input)?,
1006 span: word.span(),
1007 }))
1008 }
1009
1010 /// One predicate: clauses joined by one connective, and never two.
1011 fn predicate(input: ParseStream) -> Result<Predicate> {
1012 let first = clause(input)?;
1013 let Some(connective) = connective_ahead(input)? else {
1014 return Ok(first);
1015 };
1016 let mut clauses = vec![first];
1017 while let Some(next) = connective_ahead(input)? {
1018 if next != connective {
1019 return Err(syn::Error::new(
1020 input.span(),
1021 "one connective per predicate: parenthesise to mix `and` with `or`",
1022 ));
1023 }
1024 input.parse::<Ident>()?;
1025 clauses.push(clause(input)?);
1026 }
1027 Ok(Predicate::Joined {
1028 connective,
1029 clauses,
1030 })
1031 }
1032
1033 /// Whether `and` or `or` continues the predicate here.
1034 fn connective_ahead(input: ParseStream) -> Result<Option<Ident>> {
1035 if !input.peek(Ident) {
1036 return Ok(None);
1037 }
1038 let word = input.fork().parse::<Ident>()?;
1039 Ok(match word.to_string().as_str() {
1040 "and" | "or" => Some(word),
1041 _ => None,
1042 })
1043 }
1044
1045 fn clause(input: ParseStream) -> Result<Predicate> {
1046 if input.peek(Ident) && input.fork().parse::<Ident>()? == "not" {
1047 input.parse::<Ident>()?;
1048 return Ok(Predicate::Not(Box::new(clause(input)?)));
1049 }
1050 if input.peek(token::Paren) {
1051 let inner;
1052 parenthesized!(inner in input);
1053 return predicate(&inner);
1054 }
1055
1056 let left: Hole = input.parse()?;
1057 if !input.peek(Ident) {
1058 return Ok(Predicate::Truth(left));
1059 }
1060 let word = input.fork().parse::<Ident>()?;
1061 let spelling = word.to_string();
1062 if !COMPARISONS.iter().any(|(known, _)| *known == spelling) {
1063 return Ok(Predicate::Truth(left));
1064 }
1065 let compare: Ident = input.parse()?;
1066 Ok(Predicate::Comparison {
1067 left,
1068 compare,
1069 right: input.parse()?,
1070 })
1071 }
1072
1073 fn guarded(guard: Option<Guard>, inner: Emission) -> Emission {
1074 match guard {
1075 Some(guard) => Emission::Guarded {
1076 guard,
1077 inner: Box::new(inner),
1078 },
1079 None => inner,
1080 }
1081 }
1082
1083 /// One of the prepositions, which carry no meaning beyond reading as English.
1084 fn preposition(input: ParseStream, expected: &str) -> Result<()> {
1085 let word: Ident = input.parse()?;
1086 if word != expected {
1087 return Err(syn::Error::new(
1088 word.span(),
1089 format!("expected `{expected}` here"),
1090 ));
1091 }
1092 Ok(())
1093 }
1094
1095 /// Whether an emission starts here, which is how `beside` finds its width.
1096 fn member_ahead(input: ParseStream) -> Result<bool> {
1097 if !input.peek(Ident) {
1098 return Ok(false);
1099 }
1100 let spelling = input.fork().parse::<Ident>()?.to_string();
1101 Ok(MEMBERS.contains(&spelling.as_str()) || NODE_MEMBERS.contains(&spelling.as_str()))
1102 }
1103
1104 fn block(input: ParseStream) -> Result<Vec<Item>> {
1105 let body;
1106 braced!(body in input);
1107 items(&body)
1108 }
1109
1110 /// Every item until the input runs out.
1111 ///
1112 /// One place rather than two, because a copy loop is not one item: it is the
1113 /// items its file has entries for, written out here so nothing downstream can
1114 /// tell it was ever a loop. `pub(crate)` because a copy loop's own body is
1115 /// parsed here too, once per entry, and a body that went through anything else
1116 /// would be the one place a nested copy loop stopped working.
1117 pub fn items(input: ParseStream) -> Result<Vec<Item>> {
1118 let mut items = Vec::new();
1119 while !input.is_empty() {
1120 if copy::ahead(input) {
1121 items.extend(copy::expand(input)?);
1122 continue;
1123 }
1124 items.push(input.parse()?);
1125 }
1126 Ok(items)
1127 }
1128
1129 impl Parse for Action {
1130 fn parse(input: ParseStream) -> Result<Self> {
1131 let verb: Ident = input.parse()?;
1132 let name = verb.to_string();
1133 let addresses = if name == "doing" {
1134 true
1135 } else {
1136 let Some((_, addresses)) = VERBS.iter().find(|(known, _)| *known == name) else {
1137 return Err(syn::Error::new(
1138 verb.span(),
1139 format!("`{name}` is not one of the verbs"),
1140 ));
1141 };
1142 *addresses
1143 };
1144
1145 let target = if addresses {
1146 Some(input.parse()?)
1147 } else {
1148 None
1149 };
1150
1151 let mut modifiers = Vec::new();
1152 // A guard ends the action rather than being read as a modifier of it.
1153 // `offering "Browse Communities" to external base unless base.is_empty()`
1154 // is the site: without this the guard word is the next word after a
1155 // target and the action swallows it.
1156 while input.peek(Ident) && guard_ahead(input)?.is_none() {
1157 let name: Ident = input.parse()?;
1158 let spelling = name.to_string();
1159 let Some((_, arity)) = MODIFIERS.iter().find(|(known, _)| *known == spelling) else {
1160 return Err(syn::Error::new(
1161 name.span(),
1162 format!("`{spelling}` is not a modifier this form can say yet"),
1163 ));
1164 };
1165 let mut args = Vec::new();
1166 for _ in 0..*arity {
1167 args.push(input.parse()?);
1168 }
1169 modifiers.push(Modifier { name, args });
1170 }
1171
1172 Ok(Self {
1173 verb,
1174 target,
1175 modifiers,
1176 })
1177 }
1178 }
1179
1180 /// Split a string literal into its literal runs and its `{hole}` holes.
1181 ///
1182 /// `{{` and `}}` are the escapes, as they are in `format!`, because a hint that
1183 /// wants a literal brace is a real site rather than a hypothetical one.
1184 fn interpolate(lit: &LitStr) -> Result<Interpolated> {
1185 let span = lit.span();
1186 let text = lit.value();
1187 let mut parts = Vec::new();
1188 let mut literal = String::new();
1189 let mut rest = text.as_str();
1190
1191 while let Some(at) = rest.find(['{', '}']) {
1192 let (before, tail) = rest.split_at(at);
1193 literal.push_str(before);
1194 let mut chars = tail.chars();
1195 let opener = chars.next().expect("find reported a brace");
1196 let tail = chars.as_str();
1197
1198 if tail.starts_with(opener) {
1199 literal.push(opener);
1200 rest = &tail[opener.len_utf8()..];
1201 continue;
1202 }
1203 if opener == '}' {
1204 return Err(syn::Error::new(
1205 span,
1206 "a lone `}` in a string: write `}}` for a literal brace",
1207 ));
1208 }
1209
1210 let Some(end) = tail.find('}') else {
1211 return Err(syn::Error::new(span, "an unclosed `{` in a string"));
1212 };
1213 let inner = &tail[..end];
1214 if let Some(colon) = spec_separator(inner) {
1215 return Err(syn::Error::new(
1216 span,
1217 format!(
1218 "the format spec `{}` is not a production yet",
1219 &inner[colon + 1..]
1220 ),
1221 ));
1222 }
1223
1224 if !literal.is_empty() {
1225 parts.push(StrPart::Lit(std::mem::take(&mut literal)));
1226 }
1227 let hole: Hole = syn::parse_str(inner).map_err(|error| {
1228 syn::Error::new(span, format!("`{{{inner}}}` is not a hole: {error}"))
1229 })?;
1230 parts.push(StrPart::Hole(hole));
1231 rest = &tail[end + 1..];
1232 }
1233
1234 literal.push_str(rest);
1235 if !literal.is_empty() {
1236 parts.push(StrPart::Lit(literal));
1237 }
1238
1239 Ok(Interpolated { parts, span })
1240 }
1241
1242 /// Where a format spec starts inside a hole, skipping the `::` of a path.
1243 fn spec_separator(inner: &str) -> Option<usize> {
1244 let bytes = inner.as_bytes();
1245 let mut at = 0;
1246 while at < bytes.len() {
1247 if bytes[at] == b':' {
1248 if bytes.get(at + 1) == Some(&b':') {
1249 at += 2;
1250 continue;
1251 }
1252 return Some(at);
1253 }
1254 at += 1;
1255 }
1256 None
1257 }
1258