//! Reading the declared form into [`crate::ast`]. //! //! The parser admits exactly what the syntax tree can hold, which is what a //! converted screen has demanded. Anything else is a compile error naming the //! construct, never a silent skip: a declaration that parsed but described less //! than it said would be worse than one that refused. //! //! Two things this deliberately cannot do. It never parses a Rust expression, //! so there is no path by which an operator, a closure or a block reaches the //! tree. And it resolves nothing: rule R1 decides binding-versus-path by the //! shape of the ident alone, so the parser never needs to know what is in //! scope. use syn::parse::{Parse, ParseStream}; use syn::{Attribute, Ident, LitBool, LitInt, LitStr, Result, Token, Type, Visibility}; use syn::{braced, parenthesized, token}; use crate::ast::{ Action, Arg, Declaration, Emission, Guard, Hole, HoleRoot, Interpolated, Item, Modifier, Param, Pattern, Predicate, RegionKind, Source, Step, StrPart, }; use crate::copy; /// The modifiers, and how many arguments each takes. /// /// **Two words were here that are settings on the control rather than modifiers /// of the action, and both were caught by a screen rather than by reading.** /// `filling` is `Act::filling` and made `media_picker`'s tiles fail against /// `Action`; `copying` is `Act::copying`, which sets the destination to /// `Action::local` itself, and `collections`' Copy link is the site. A word /// belongs here only if `Action` has the method. /// /// **`with` was refused in wave 3 and the refusal was too wide.** What was /// refused is a *form* saying "and this value too, sometimes" -- a conditional /// payload, which the form still cannot say and which `doing` is still the /// remedy for. An unconditional one is `Action::with(name, value)` and nothing /// else, so it is a modifier by this table's own rule. Four suppliers existed /// only to write it: `tip::checkout`, `auth_pages::reset_action`, /// `git_commit::note_delete` and goingson's board move. const MODIFIERS: &[(&str, usize)] = &[ ("navigating", 0), ("awaiting", 0), ("by_host", 0), ("elsewhere", 0), ("invalidating", 0), ("replacing_enclosing", 0), ("saving", 1), ("replacing", 1), ("carrying", 2), ("with", 2), ]; /// The members: everything that emits rather than sets. /// /// An ident inside a body is a member if it is here and an attribute if it is /// not, which is the whole of how the two are told apart. A member added here /// without a production is a parse error naming it, which is the failure worth /// having. const MEMBERS: &[&str] = &[ "act", "across", "activate", "at", "beside", "framed", "cell", "cells", "chip", "column", "field", "form", "given", "include", "link", "offering", "offers", "region", "removable", "removes", "repeats", "row", "screen", "table", ]; /// The members that are one of `Node`'s own constructors, called by its name. /// Growing this list is the whole of adding one. /// /// `picture` is the one exception and it is the last entry for that reason: /// [`Node::Image`] carries a builder type of its own, so the member's body sets /// on the `Image` and the emitter wraps the result rather than calling a /// constructor on `Node`. Everything else here is `Node::`. /// /// **A name that is a setting on any container cannot go here.** An ident inside /// a body is a member if it is in one of these two lists and a setting if it is /// not, and that is the whole of how the two are told apart, so one name cannot /// mean both. `token` is the measured case: `Node::Token` looks like a member, /// and `Row::token` and `Cell::token` are settings at four converted sites. A /// screen wanting a tag standing on its own reaches `Node::token` through a /// supplier, which is the remedy the deferred table already names, and /// `project_analytics`'s `chip` is that supplier. /// /// **Where a variant holds a builder type, the member is named for the /// constructor rather than for the variant.** That is the rule the exceptions /// below share, and it is what `badge` and `chip` settled: `Node::Token` holds a /// `Tag`, `token` is a setting on `Row` and on `Cell` so it cannot be a member /// at all, and `Tag`'s two constructors are what a screen actually reaches for. /// So there is no `token` member and there never will be. quasicoherent /// `e2030032` is the decision, measured over 33 sites in the three trees. /// /// `canvas` is the second exception and is the last entry for that reason: /// `Canvas` is a builder type of its own, the way `Image` is, so the member's /// body accretes onto the canvas and the emitter wraps the variant afterwards. /// `custom_page` is the site -- a creator's opaque markup under the scope its /// stylesheet was rewritten for, with the platform's own blocks drawn inside /// it. /// /// `badge` is the third and `chip` the fourth, and `chip` is not on this list /// because it carries an action: it has a parse arm of its own. Both accrete /// onto a `Tag` under `Container::Tag` and the emitter wraps `Node::token`. /// /// `timeline` is the fifth, and the only one whose body holds members rather /// than settings: an axis is told what sits on it and where, which is the `at` /// member under `Container::Timeline`. It is a `Node` either way, so nothing is /// wrapped afterwards. /// /// `proportion` is the seventh, and the second renamed rather than reshaped. It /// is `Node::Meter`, which holds a `Meter` builder the way `badge` holds a /// `Tag`, so the body accretes onto the meter and the emitter wraps the /// variant. It cannot be spelled `meter` because `Row::meter` and `Cell::meter` /// are settings; `Row` puts one in `RowPart::Proportion`, which is where the /// name comes from. goingson's task drawer is the site, twice -- subtask /// completion and time against estimate -- and MNW's pricing panel is the /// third, where the reader's volume is drawn against the crossover. /// /// `underway` is the sixth, and the only one renamed rather than reshaped. It /// is `Node::pending`, the third of the three `StandIn` states beside `empty` /// and `failed`, and it cannot be spelled `pending` because `Slot::pending` is /// a setting and one name cannot mean both. Renaming is what the rule leaves /// open where `token` had to go to a supplier: `token` collided with a setting /// that meant the same thing on a container that could hold one, so a second /// spelling would have been two ways to say one thing, while `Slot::pending` /// says a region is waiting and this says what to draw in it. goingson's /// backups region is the site: it is `Slot::live`, so a swapping host has /// nowhere to put an attribute and needs the node. const NODE_MEMBERS: &[&str] = &[ "text", "toned", "page", "section", "subsection", "list", "stats", "banner", "toast", "failed", "empty", "rich", "literal", "code", "since", "until", "picture", "canvas", "badge", "timeline", "underway", "proportion", "tally", "chart", ]; /// The arrangements a screen may be laid out in. const ARRANGEMENTS: &[&str] = &["single", "list_detail", "sidebar_content"]; /// The comparisons, and the Rust operator each is. pub const COMPARISONS: &[(&str, &str)] = &[ ("is", "=="), ("is_not", "!="), ("over", ">"), ("under", "<"), ("at_least", ">="), ("at_most", "<="), ]; /// The attributes whose argument is a variant of a vocabulary enum, and the /// enum it belongs to. Rule R1(4). pub const VOCABULARY: &[(&str, &str)] = &[ ("tone", "Tone"), ("width", "Width"), ("priority", "Priority"), ("fit", "Fit"), ]; /// The attributes the generated function may carry. // `staged` is not an attribute the generated function carries: it asks for // a second function beside it. See `symbolic`. const FLAGS: &[&str] = &["must_use", "inline", "staged", "constant"]; /// The verbs, and whether each addresses something. /// /// **`leaving` said it addressed nothing and it addresses a url.** Same class /// of mistake as `filling` and `copying` above, and it survived for the same /// reason: no declared screen had used the word, so the table was wrong with /// nothing looking. `custom_page` is the site -- every link off a custom page /// replaces it rather than opening a tab, which is what `Action::leaving(url)` /// says. Read the signature, not the neighbours: `local` and `back` take /// nothing and `leaving` sits between them. const VERBS: &[(&str, bool)] = &[ ("get", true), ("post", true), ("put", true), ("delete", true), ("external", true), ("local", false), ("leaving", true), ("back", false), ]; impl Parse for Declaration { fn parse(input: ParseStream) -> Result { let attrs = Attribute::parse_outer(input)?; let (docs, flags) = docs_and_flags(&attrs)?; let vis: Visibility = input.parse()?; let vis = match vis { Visibility::Inherited => None, other => Some(other), }; let keyword: Ident = input.parse()?; if keyword != "shape" { return Err(syn::Error::new( keyword.span(), "a declaration begins with `shape`", )); } let name: Ident = input.parse()?; let signature; parenthesized!(signature in input); let params = signature.parse_terminated(Param::parse, Token![,])?; input.parse::]>()?; let returns: Type = input.parse()?; input.parse::()?; let items = items(input)?; Ok(Self { docs, flags, vis, name, params: params.into_iter().collect(), returns, items, }) } } /// The `///` lines and the bare flags, in order. fn docs_and_flags(attrs: &[Attribute]) -> Result<(Vec, Vec)> { let mut docs = Vec::new(); let mut flags = Vec::new(); for attr in attrs { if !attr.path().is_ident("doc") { let syn::Meta::Path(path) = &attr.meta else { return Err(syn::Error::new_spanned( attr, "an attribute with arguments is not a production yet", )); }; let Some(name) = path.get_ident() else { return Err(syn::Error::new_spanned(attr, "expected a bare attribute")); }; if !FLAGS.contains(&name.to_string().as_str()) { return Err(syn::Error::new_spanned( attr, format!("`{name}` is not a flag a declaration may carry"), )); } flags.push(name.clone()); continue; } let syn::Meta::NameValue(value) = &attr.meta else { return Err(syn::Error::new_spanned(attr, "expected a doc comment")); }; let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(text), .. }) = &value.value else { return Err(syn::Error::new_spanned(attr, "expected a doc comment")); }; docs.push(text.value()); } Ok((docs, flags)) } impl Parse for Param { fn parse(input: ParseStream) -> Result { let name: Ident = input.parse()?; input.parse::()?; let ty: Type = input.parse()?; Ok(Self { name, ty }) } } impl Parse for Item { fn parse(input: ParseStream) -> Result { if input.peek(Token![let]) { input.parse::()?; let name: Ident = input.parse()?; input.parse::()?; let source: Source = input.parse()?; input.parse::()?; return Ok(Self::Bind { name, source }); } if input.peek(Token![for]) { input.parse::()?; let dereferenced = input.peek(Token![&]); if dereferenced { input.parse::()?; } let binder: Ident = input.parse()?; input.parse::()?; let iterable: Hole = input.parse()?; return Ok(Self::For { dereferenced, binder, iterable, body: block(input)?, }); } let member = input.fork().parse::()?; let spelling = member.to_string(); if MEMBERS.contains(&spelling.as_str()) || NODE_MEMBERS.contains(&spelling.as_str()) { return Ok(Self::Emit(input.parse()?)); } let name: Ident = input.parse()?; let mut args = Vec::new(); while !input.peek(Token![;]) && !input.peek(syn::token::Brace) && guard_ahead(input)?.is_none() { args.push(input.parse()?); } // A braced body is settings made on the argument, and it ends the // statement the way a loop's body does: no semicolon, for the same // reason -- a brace already says where the item stops. if input.peek(syn::token::Brace) { let body = block(input)?; // A guard after the body guards the whole setting, and the body's // own guards guard one of its settings each. Unambiguous because // the brace has already closed: a `when` here can only belong to // the setting the braces settled. let guard = guard(input)?; if input.peek(Token![;]) { input.parse::()?; } return Ok(Self::Attribute { name, args, guard, body, }); } let guard = guard(input)?; input.parse::()?; Ok(Self::Attribute { name, args, guard, body: Vec::new(), }) } } impl Parse for Source { fn parse(input: ParseStream) -> Result { if input.peek(LitStr) { let lit: LitStr = input.parse()?; return Ok(Self::Str(interpolate(&lit)?)); } if input.peek(Ident) && input.fork().parse::()? == "given" { return parse_choose(input); } Ok(Self::Hole(input.parse()?)) } } /// `given { -> , otherwise -> }`. /// /// The arms are sources, so a value dispatch produces a value by construction. /// `otherwise` is required rather than optional: a binding has to have a value /// on every path, and rustc's exhaustiveness cannot be borrowed here without /// admitting a pattern that binds. fn parse_choose(input: ParseStream) -> Result { let keyword: Ident = input.parse()?; let scrutinee: Hole = input.parse()?; let body; braced!(body in input); let mut arms = Vec::new(); let mut otherwise = None; while !body.is_empty() { if body.peek(Ident) && body.fork().parse::()? == "otherwise" { body.parse::()?; body.parse::]>()?; otherwise = Some(Box::new(body.parse()?)); } else { let pattern: Pattern = body.parse()?; body.parse::]>()?; arms.push((pattern, body.parse()?)); } if body.peek(Token![,]) { body.parse::()?; } } let Some(otherwise) = otherwise else { return Err(syn::Error::new( keyword.span(), "a value dispatch needs an `otherwise` arm: a binding has a value on every path", )); }; Ok(Source::Choose { scrutinee, arms, otherwise, }) } impl Parse for RegionKind { fn parse(input: ParseStream) -> Result { // R1's rule, applied to a kind: a bare uppercase ident is the variant, // and anything else is a hole that answers with one. if input.peek(Ident) && !input.peek2(Token![::]) && !input.peek2(token::Paren) { let name: Ident = input.fork().parse()?; let text = name.to_string(); if text.starts_with(|letter: char| letter.is_uppercase()) { input.parse::()?; return Ok(Self::Variant(name)); } } Ok(Self::Supplied(input.parse()?)) } } impl Parse for Pattern { fn parse(input: ParseStream) -> Result { if input.peek(LitInt) { let lit: LitInt = input.parse()?; return Ok(Self::Int(lit.base10_parse()?)); } if input.peek(LitStr) { let lit: LitStr = input.parse()?; return Ok(Self::Str(lit.value())); } if input.peek(LitBool) { let lit: LitBool = input.parse()?; return Ok(Self::Bool(lit.value())); } Ok(Self::Path(input.parse()?)) } } impl Parse for Hole { fn parse(input: ParseStream) -> Result { let path: syn::Path = input.parse()?; let root = if input.peek(token::Paren) { HoleRoot::Call { path, args: call_args(input)?, } } else if let Some(name) = binding_ident(&path) { HoleRoot::Binding(name) } else { HoleRoot::Path(path) }; let mut steps = Vec::new(); while input.peek(Token![.]) { input.parse::()?; let name: Ident = input.parse()?; if input.peek(token::Paren) { steps.push(Step::Method { name, args: call_args(input)?, }); } else { steps.push(Step::Field(name)); } } Ok(Self { root, steps }) } } /// Rule R1: a bare lowercase-initial ident is a binding; anything qualified or /// uppercase-initial is a Rust path. fn binding_ident(path: &syn::Path) -> Option { if path.leading_colon.is_some() || path.segments.len() != 1 { return None; } let segment = path.segments.first()?; if !segment.arguments.is_none() { return None; } let text = segment.ident.to_string(); let first = text.chars().next()?; (first.is_lowercase()).then(|| segment.ident.clone()) } fn call_args(input: ParseStream) -> Result> { let content; parenthesized!(content in input); let args = content.parse_terminated(Arg::parse, Token![,])?; Ok(args.into_iter().collect()) } impl Parse for Arg { fn parse(input: ParseStream) -> Result { if input.peek(Token![&]) { input.parse::()?; return Ok(Self::Borrow(Box::new(input.parse()?))); } if input.peek(token::Bracket) { let items; syn::bracketed!(items in input); let items = items.parse_terminated(Self::parse, Token![,])?; return Ok(Self::List(items.into_iter().collect())); } if input.peek(LitStr) { let lit: LitStr = input.parse()?; return Ok(Self::Str(interpolate(&lit)?)); } if input.peek(LitInt) { let lit: LitInt = input.parse()?; return Ok(Self::Int(lit.base10_parse()?)); } if input.peek(LitBool) { let lit: LitBool = input.parse()?; return Ok(Self::Bool(lit.value())); } Ok(Self::Hole(input.parse()?)) } } impl Parse for Emission { fn parse(input: ParseStream) -> Result { let member: Ident = input.parse()?; match member.to_string().as_str() { "framed" => { let label: Arg = input.parse()?; let inner: Self = input.parse()?; // A guard on a placed member guards the placing, exactly as it // does for `beside`. Ok(match inner { Self::Guarded { guard, inner } => Self::Guarded { guard, inner: Box::new(Self::Framed { label, inner }), }, inner => Self::Framed { label, inner: Box::new(inner), }, }) } "beside" => { let priority: Arg = input.parse()?; // A second argument before the member is the width. Told apart // by what a member is: the words in `MEMBERS` and // `NODE_MEMBERS` and nothing else, so an ident that is not one // of them cannot be the emission and must be the width. Same // rule the parser already uses to tell a setting from a member, // applied one position along. let width = if member_ahead(input)? { None } else { Some(input.parse()?) }; let inner: Self = input.parse()?; // A guard on a placed member guards the placing. The run is // what may hold nothing and `beside` is how a member reaches // it, so the guard belongs outside: read the other way round // it asks a run to hold an absence, which nothing can do. Ok(match inner { Self::Guarded { guard, inner } => Self::Guarded { guard, inner: Box::new(Self::Beside { priority, width, inner, }), }, inner => Self::Beside { priority, width, inner: Box::new(inner), }, }) } "at" => { let at: Arg = input.parse()?; let inner: Self = input.parse()?; // A guard on a placed entry guards the placing, exactly as // `beside`'s does: the timeline is what may hold nothing. Ok(match inner { Self::Guarded { guard, inner } => Self::Guarded { guard, inner: Box::new(Self::At { at, inner }), }, inner => Self::At { at, inner: Box::new(inner), }, }) } "region" => { let name: Arg = input.parse()?; input.parse::()?; let kind: RegionKind = input.parse()?; // The guard sits between the kind and the body, where `act` // puts its own: a region that is sometimes not there is the // same fact as a control that is sometimes not offered. let guard = guard(input)?; Ok(guarded( guard, Self::Region { name, kind, body: block(input)?, }, )) } "across" => { let fallback: Ident = input.parse()?; Ok(Self::Across { fallback, body: block(input)?, }) } "act" | "offers" => { let held_back = member == "offers"; let label: Arg = input.parse()?; preposition(input, "to")?; let action: Action = input.parse()?; let guard = guard(input)?; let body = if input.peek(token::Brace) { block(input)? } else { input.parse::()?; Vec::new() }; Ok(guarded( guard, if held_back { Self::Offers { label, action, body, } } else { Self::Act { label, action, body, } }, )) } "removes" => { let label: Arg = input.parse()?; preposition(input, "to")?; let action: Action = input.parse()?; let guard = guard(input)?; let body = if input.peek(token::Brace) { block(input)? } else { input.parse::()?; Vec::new() }; Ok(guarded( guard, Self::Removes { label, action, body, }, )) } "repeats" => { let one: Arg = input.parse()?; preposition(input, "adds")?; let label: Arg = input.parse()?; preposition(input, "to")?; let action: Action = input.parse()?; let guard = guard(input)?; let body = if input.peek(token::Brace) { block(input)? } else { input.parse::()?; Vec::new() }; Ok(guarded( guard, Self::Repeats { one, label, action, body, }, )) } "include" => { // `include each ` splices a shape that answers many // members. `each` cannot be a shape name here: a hole starts // with a path or a binding and the next token would be `(`. let every = input.peek(Ident) && input.fork().parse::()? == "each" && !input.fork().parse::().is_ok_and(|_| false); let every = every && { let fork = input.fork(); fork.parse::()?; fork.peek(Ident) || fork.peek(Token![:]) }; if every { input.parse::()?; } let supplier: Hole = input.parse()?; let guard = guard(input)?; input.parse::()?; Ok(guarded( guard, if every { Self::IncludeEach(supplier) } else { Self::Include(supplier) }, )) } "given" => { let scrutinee: Hole = input.parse()?; let body; braced!(body in input); let mut arms = Vec::new(); let mut otherwise = None; while !body.is_empty() { if body.peek(Ident) && body.fork().parse::()? == "otherwise" { body.parse::()?; body.parse::]>()?; otherwise = Some(Box::new(body.parse()?)); } else { let pattern: Pattern = body.parse()?; body.parse::]>()?; arms.push((pattern, Box::new(body.parse()?))); } } Ok(Self::Given { scrutinee, arms, otherwise, }) } "screen" => { let arrangement: Ident = input.parse()?; if !ARRANGEMENTS.contains(&arrangement.to_string().as_str()) { return Err(syn::Error::new( arrangement.span(), "a screen is laid out `single`, `list_detail` or `sidebar_content`", )); } // Every arrangement takes a title, and `list_detail` takes // whether it is tabbed as well. Read to the body rather than // taking exactly one, so the grammar says what the constructor // says. `pricing` is the site: the first converted screen that // is not `single`. let mut args = Vec::new(); while !input.peek(token::Brace) { args.push(input.parse()?); } Ok(Self::Screen { arrangement, args, body: block(input)?, }) } "row" => { let primary: Arg = input.parse()?; // Before the body, where `column`, `cell`, `act`, `offering` // and `form` already put theirs. `pricing`'s footer is the // site: Changelog is linked only while a published changelog // project exists, and the route 404s otherwise. let guard = guard(input)?; let body = if input.peek(token::Brace) { block(input)? } else { input.parse::()?; Vec::new() }; Ok(guarded(guard, Self::Row { primary, body })) } // The guard goes after the body here rather than before it, because // the body is what tells this `list` from the node member of the // same name. `user_projects` draws its list only when there is // something in it. "list" if input.peek(token::Brace) => { let rows = block(input)?; let guard = guard(input)?; if guard.is_some() { input.parse::()?; } Ok(guarded(guard, Self::List(rows))) } // A guard after the body, which is where `list` takes one and for // its reason: a table is a member like any other, and a member of // an accumulating container may be absent. audiofiles' file list is // the site -- an empty vault says so with a stand-in instead, and a // table drawn anyway would be a header row over nothing. "table" => { let body = block(input)?; let guard = guard(input)?; if guard.is_some() { input.parse::()?; } Ok(guarded(guard, Self::Table(body))) } "column" => { let name: Arg = input.parse()?; // Before the body, where every other guarded member puts it. // `git_repos` shows a visibility column to the owner and to // nobody else, and the same guard decides the cell below, so // the two cannot fall out of step. let guard = guard(input)?; let body = if input.peek(token::Brace) { block(input)? } else { input.parse::()?; Vec::new() }; Ok(guarded(guard, Self::Column { name, body })) } "cells" => Ok(Self::Cells(block(input)?)), "cell" => { let column = if input.peek(Ident) && input.fork().parse::()? == "at" { input.parse::()?; Some(input.parse()?) } else { None }; let value: Arg = input.parse()?; let guard = guard(input)?; let body = if input.peek(token::Brace) { block(input)? } else { input.parse::()?; Vec::new() }; Ok(guarded( guard, Self::Cell { column, value, body, }, )) } "offering" => { let label: Arg = input.parse()?; preposition(input, "to")?; let action: Action = input.parse()?; let guard = guard(input)?; let body = if input.peek(token::Brace) { block(input)? } else { input.parse::()?; Vec::new() }; Ok(guarded( guard, Self::Offering { label, action, body, }, )) } "activate" => { preposition(input, "to")?; let action: Action = input.parse()?; let guard = guard(input)?; input.parse::()?; Ok(guarded(guard, Self::Activate(action))) } "form" => { let action: Action = input.parse()?; // Before the body, where `act`, `offering`, `column` and `cell` // already put theirs. `auth_pages` is the site twice over: the // login form is drawn only where there is a local password to // type, and the reset form only where the signed link still // resolves. Both are one screen with two shapes rather than two // screens, because everything around them is the same. let guard = guard(input)?; Ok(guarded( guard, Self::Form { action, body: block(input)?, }, )) } // A chip is a tag that goes somewhere, so it takes an action, and // an action is spelled `to ` rather than as an argument. That // is why it is not in NODE_MEMBERS with `badge`. "chip" | "removable" => { let removable = member == "removable"; let value: Arg = input.parse()?; preposition(input, "to")?; let action: Action = input.parse()?; let guard = guard(input)?; let body = if input.peek(token::Brace) { block(input)? } else { input.parse::()?; Vec::new() }; Ok(guarded( guard, Self::Chip { value, action, removable, body, }, )) } "field" => { let kind: Ident = input.parse()?; let name: Arg = input.parse()?; let label: Arg = input.parse()?; // Before the body, where every other guarded member puts it. // `pricing` is the site: the founder-or-list question is asked // only while there are two rates to choose between, and with // the window shut the page is what it was before the question // existed. let guard = guard(input)?; let body = if input.peek(token::Brace) { block(input)? } else { input.parse::()?; Vec::new() }; Ok(guarded( guard, Self::Field { kind, name, label, body, }, )) } name if NODE_MEMBERS.contains(&name) => { let mut args = Vec::new(); while !input.peek(Token![;]) && !input.peek(token::Brace) && guard_ahead(input)?.is_none() { args.push(input.parse()?); } let guard = guard(input)?; // Told something afterwards, or nothing. `act` reads the same // way and for the same reason: a member with nothing to say // about itself should not have to open a block to say so. let body = if input.peek(token::Brace) { block(input)? } else { input.parse::()?; Vec::new() }; Ok(guarded( guard, Self::Simple { member: member.clone(), args, body, }, )) } "link" => { let text: Arg = input.parse()?; preposition(input, "to")?; let action: Action = input.parse()?; let guard = guard(input)?; input.parse::()?; Ok(guarded(guard, Self::Link { text, action })) } other => Err(syn::Error::new( member.span(), format!( "`{other}` is not a member this form can say yet. \ Add the production, and name the screen that demanded it in the commit." ), )), } } } /// Whether a guard starts here, without consuming it. fn guard_ahead(input: ParseStream) -> Result> { if !input.peek(Ident) { return Ok(None); } Ok(match input.fork().parse::()?.to_string().as_str() { "when" => Some(false), "unless" => Some(true), _ => None, }) } /// `when ` or `unless `, if one is written here. fn guard(input: ParseStream) -> Result> { let Some(negated) = guard_ahead(input)? else { return Ok(None); }; let word: Ident = input.parse()?; Ok(Some(Guard { negated, predicate: predicate(input)?, span: word.span(), })) } /// One predicate: clauses joined by one connective, and never two. fn predicate(input: ParseStream) -> Result { let first = clause(input)?; let Some(connective) = connective_ahead(input)? else { return Ok(first); }; let mut clauses = vec![first]; while let Some(next) = connective_ahead(input)? { if next != connective { return Err(syn::Error::new( input.span(), "one connective per predicate: parenthesise to mix `and` with `or`", )); } input.parse::()?; clauses.push(clause(input)?); } Ok(Predicate::Joined { connective, clauses, }) } /// Whether `and` or `or` continues the predicate here. fn connective_ahead(input: ParseStream) -> Result> { if !input.peek(Ident) { return Ok(None); } let word = input.fork().parse::()?; Ok(match word.to_string().as_str() { "and" | "or" => Some(word), _ => None, }) } fn clause(input: ParseStream) -> Result { if input.peek(Ident) && input.fork().parse::()? == "not" { input.parse::()?; return Ok(Predicate::Not(Box::new(clause(input)?))); } if input.peek(token::Paren) { let inner; parenthesized!(inner in input); return predicate(&inner); } let left: Hole = input.parse()?; if !input.peek(Ident) { return Ok(Predicate::Truth(left)); } let word = input.fork().parse::()?; let spelling = word.to_string(); if !COMPARISONS.iter().any(|(known, _)| *known == spelling) { return Ok(Predicate::Truth(left)); } let compare: Ident = input.parse()?; Ok(Predicate::Comparison { left, compare, right: input.parse()?, }) } fn guarded(guard: Option, inner: Emission) -> Emission { match guard { Some(guard) => Emission::Guarded { guard, inner: Box::new(inner), }, None => inner, } } /// One of the prepositions, which carry no meaning beyond reading as English. fn preposition(input: ParseStream, expected: &str) -> Result<()> { let word: Ident = input.parse()?; if word != expected { return Err(syn::Error::new( word.span(), format!("expected `{expected}` here"), )); } Ok(()) } /// Whether an emission starts here, which is how `beside` finds its width. fn member_ahead(input: ParseStream) -> Result { if !input.peek(Ident) { return Ok(false); } let spelling = input.fork().parse::()?.to_string(); Ok(MEMBERS.contains(&spelling.as_str()) || NODE_MEMBERS.contains(&spelling.as_str())) } fn block(input: ParseStream) -> Result> { let body; braced!(body in input); items(&body) } /// Every item until the input runs out. /// /// One place rather than two, because a copy loop is not one item: it is the /// items its file has entries for, written out here so nothing downstream can /// tell it was ever a loop. `pub(crate)` because a copy loop's own body is /// parsed here too, once per entry, and a body that went through anything else /// would be the one place a nested copy loop stopped working. pub fn items(input: ParseStream) -> Result> { let mut items = Vec::new(); while !input.is_empty() { if copy::ahead(input) { items.extend(copy::expand(input)?); continue; } items.push(input.parse()?); } Ok(items) } impl Parse for Action { fn parse(input: ParseStream) -> Result { let verb: Ident = input.parse()?; let name = verb.to_string(); let addresses = if name == "doing" { true } else { let Some((_, addresses)) = VERBS.iter().find(|(known, _)| *known == name) else { return Err(syn::Error::new( verb.span(), format!("`{name}` is not one of the verbs"), )); }; *addresses }; let target = if addresses { Some(input.parse()?) } else { None }; let mut modifiers = Vec::new(); // A guard ends the action rather than being read as a modifier of it. // `offering "Browse Communities" to external base unless base.is_empty()` // is the site: without this the guard word is the next word after a // target and the action swallows it. while input.peek(Ident) && guard_ahead(input)?.is_none() { let name: Ident = input.parse()?; let spelling = name.to_string(); let Some((_, arity)) = MODIFIERS.iter().find(|(known, _)| *known == spelling) else { return Err(syn::Error::new( name.span(), format!("`{spelling}` is not a modifier this form can say yet"), )); }; let mut args = Vec::new(); for _ in 0..*arity { args.push(input.parse()?); } modifiers.push(Modifier { name, args }); } Ok(Self { verb, target, modifiers, }) } } /// Split a string literal into its literal runs and its `{hole}` holes. /// /// `{{` and `}}` are the escapes, as they are in `format!`, because a hint that /// wants a literal brace is a real site rather than a hypothetical one. fn interpolate(lit: &LitStr) -> Result { let span = lit.span(); let text = lit.value(); let mut parts = Vec::new(); let mut literal = String::new(); let mut rest = text.as_str(); while let Some(at) = rest.find(['{', '}']) { let (before, tail) = rest.split_at(at); literal.push_str(before); let mut chars = tail.chars(); let opener = chars.next().expect("find reported a brace"); let tail = chars.as_str(); if tail.starts_with(opener) { literal.push(opener); rest = &tail[opener.len_utf8()..]; continue; } if opener == '}' { return Err(syn::Error::new( span, "a lone `}` in a string: write `}}` for a literal brace", )); } let Some(end) = tail.find('}') else { return Err(syn::Error::new(span, "an unclosed `{` in a string")); }; let inner = &tail[..end]; if let Some(colon) = spec_separator(inner) { return Err(syn::Error::new( span, format!( "the format spec `{}` is not a production yet", &inner[colon + 1..] ), )); } if !literal.is_empty() { parts.push(StrPart::Lit(std::mem::take(&mut literal))); } let hole: Hole = syn::parse_str(inner).map_err(|error| { syn::Error::new(span, format!("`{{{inner}}}` is not a hole: {error}")) })?; parts.push(StrPart::Hole(hole)); rest = &tail[end + 1..]; } literal.push_str(rest); if !literal.is_empty() { parts.push(StrPart::Lit(literal)); } Ok(Interpolated { parts, span }) } /// Where a format spec starts inside a hole, skipping the `::` of a path. fn spec_separator(inner: &str) -> Option { let bytes = inner.as_bytes(); let mut at = 0; while at < bytes.len() { if bytes[at] == b':' { if bytes.get(at + 1) == Some(&b':') { at += 2; continue; } return Some(at); } at += 1; } None }