Skip to main content

max / quasi

Land the parser and the emitter, for MNW git_blame::header The first screen that could not otherwise be written. Every production here was demanded by it: a shape header, a string with holes, a value dispatch, a region as a Group, a run with a fallback, three ranked members, a text and two links with a verb and a modifier. The value dispatch settles what two probe rounds argued about. Its arms are sources rather than emissions, so it produces a value by construction and cannot admit a block, and the `&format!`-versus-literal split needs no `&` or `owned` spelling: the arms of one dispatch have to agree on a type, so the dispatch decides the ownership for all of them. The dead-code allow comes off ast.rs in the same commit, as it said it would. Two nodes went with it: `Hole::span`, which no error reads, and a `Value` container, which is the absence of a container rather than one.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-03 17:11 UTC
Signed with PGP, not checked
Commit: 4857546379cc7b5f68bc4876bffd8d289e55736f
Parent: b9199a0
4 files changed, +856 insertions, -17 deletions
@@ -11,11 +11,6 @@
11 11 //! argument from reopening into a sub-grammar. A shape that needs one of them
12 12 //! calls a supplier function beside the declaration instead.
13 13
14 - // The parser and the emitter land with the first converted screen, so every
15 - // node here is defined before it is read. The allow comes off in that same
16 - // commit; a node still unread after it is a node no screen asked for.
17 - #![allow(dead_code)]
18 -
19 14 use proc_macro2::Span;
20 15 use syn::{Ident, Type};
21 16
@@ -90,7 +85,6 @@
90 85 pub struct Hole {
91 86 pub root: HoleRoot,
92 87 pub steps: Vec<Step>,
93 - pub span: Span,
94 88 }
95 89
96 90 pub enum HoleRoot {
@@ -19,8 +19,11 @@
19 19 //! literal, because those four are what keep `Node: Eq` intact.
20 20
21 21 mod ast;
22 + mod emit;
23 + mod parse;
22 24
23 25 use proc_macro::TokenStream;
26 + use syn::parse_macro_input;
24 27
25 28 /// Declare one shape.
26 29 ///
@@ -40,16 +43,9 @@
40 43 /// ```
41 44 #[proc_macro]
42 45 pub fn declare(input: TokenStream) -> TokenStream {
43 - let _ = input;
44 - // The parser and the emitter land with the first converted screen, MNW
45 - // `git_blame::header`. Until then this is a loud failure rather than a
46 - // quiet no-op: a macro that expands to nothing would let a caller believe
47 - // a screen had been converted when it had not.
48 - quote::quote! {
49 - compile_error!(
50 - "quasi-declare: the parser has not landed yet. \
51 - Track it on GoingsOn, and do not convert a screen against this."
52 - );
46 + let declaration = parse_macro_input!(input as ast::Declaration);
47 + match emit::declaration(&declaration) {
48 + Ok(expansion) => expansion.into(),
49 + Err(error) => error.into_compile_error().into(),
53 50 }
54 - .into()
55 51 }
@@ -1,0 +1,377 @@
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::{LitStr, Result};
16 +
17 + use crate::ast::{
18 + Action, Arg, Declaration, Emission, Hole, HoleRoot, Interpolated, Item, Pattern, Source, Step,
19 + StrPart,
20 + };
21 +
22 + /// What the items of a body accrete onto.
23 + #[derive(Clone, Copy)]
24 + enum Container {
25 + /// A region: emissions accrete onto a `Slot`.
26 + Slot,
27 + /// A row: every emission is `beside` and carries a priority.
28 + Run,
29 + }
30 +
31 + pub fn declaration(declaration: &Declaration) -> Result<TokenStream> {
32 + let Declaration {
33 + docs,
34 + vis,
35 + name,
36 + params,
37 + returns,
38 + items,
39 + } = declaration;
40 +
41 + let shaped = shaped_name(returns)?;
42 + if shaped != "Node" {
43 + return Err(syn::Error::new_spanned(
44 + returns,
45 + format!(
46 + "`-> {shaped}` is not a shape this form can build yet. \
47 + Add the R2 case, and name the screen that demanded it in the commit."
48 + ),
49 + ));
50 + }
51 +
52 + let docs = docs.iter().map(|line| {
53 + let line = LitStr::new(line, Span::call_site());
54 + quote!(#[doc = #line])
55 + });
56 + let params = params.iter().map(|param| {
57 + let name = &param.name;
58 + let ty = &param.ty;
59 + quote!(#name: #ty)
60 + });
61 + let body = value_body(items, name.span())?;
62 +
63 + Ok(quote! {
64 + #(#docs)*
65 + #vis fn #name(#(#params),*) -> #returns #body
66 + })
67 + }
68 +
69 + /// The last segment of the return type, which is what R2 dispatches on.
70 + fn shaped_name(returns: &syn::Type) -> Result<String> {
71 + let syn::Type::Path(path) = returns else {
72 + return Err(syn::Error::new_spanned(
73 + returns,
74 + "a shape returns one of the vocabulary types",
75 + ));
76 + };
77 + path.path
78 + .segments
79 + .last()
80 + .map(|segment| segment.ident.to_string())
81 + .ok_or_else(|| syn::Error::new_spanned(returns, "an empty return type"))
82 + }
83 +
84 + /// A `-> Node` body: its bindings, then the single emission that is the value.
85 + ///
86 + /// Bindings are hoisted above the emissions of their own body. Nothing is
87 + /// reordered by that: a binding can only name bindings written before it, and
88 + /// an emission produces a value rather than an effect, so the two are
89 + /// independent within one block.
90 + fn value_body(items: &[Item], span: Span) -> Result<TokenStream> {
91 + let mut bindings = Vec::new();
92 + let mut emissions = Vec::new();
93 + for item in items {
94 + match item {
95 + Item::Bind { name, source } => {
96 + let value = source_value(source)?;
97 + bindings.push(quote!(let #name = #value;));
98 + }
99 + Item::Emit(emission) => emissions.push(emission),
100 + }
101 + }
102 +
103 + let [only] = emissions.as_slice() else {
104 + return Err(syn::Error::new(
105 + span,
106 + format!(
107 + "a `-> Node` shape is its single emission, and this one has {}",
108 + emissions.len()
109 + ),
110 + ));
111 + };
112 + let built = node(only)?;
113 +
114 + Ok(quote!({ #(#bindings)* #built }))
115 + }
116 +
117 + /// A binding's value, with the dispatch's ownership already decided.
118 + fn source_value(source: &Source) -> Result<TokenStream> {
119 + self::source(source, owned_arms(source))
120 + }
121 +
122 + /// A body that accretes onto something already built.
123 + fn accrete(items: &[Item], container: Container, base: &TokenStream) -> Result<TokenStream> {
124 + let mut bindings = Vec::new();
125 + let mut steps = Vec::new();
126 + for item in items {
127 + match item {
128 + Item::Bind { name, source } => {
129 + let value = source_value(source)?;
130 + bindings.push(quote!(let #name = #value;));
131 + }
132 + Item::Emit(emission) => steps.push(step(emission, container)?),
133 + }
134 + }
135 + Ok(quote!({ #(#bindings)* #base #(#steps)* }))
136 + }
137 +
138 + /// One emission, as a step onto the container it lands in.
139 + fn step(emission: &Emission, container: Container) -> Result<TokenStream> {
140 + match (container, emission) {
141 + (Container::Slot, Emission::Across { fallback, body }) => {
142 + let run = accrete(
143 + body,
144 + Container::Run,
145 + &quote!(::quasi_router::Run::new(::quasi_router::layout::Fallback::#fallback)),
146 + )?;
147 + Ok(quote!(.across(#run)))
148 + }
149 + (Container::Slot, other) => {
150 + let node = node(other)?;
151 + Ok(quote!(.with(#node)))
152 + }
153 + (Container::Run, Emission::Beside { priority, inner }) => {
154 + let node = node(inner)?;
155 + Ok(quote!(.beside(#node, ::quasi_router::layout::Priority::#priority)))
156 + }
157 + (Container::Run, other) => Err(syn::Error::new(
158 + emission_span(other),
159 + "a row ranks what it holds: write `beside <priority> <emission>`",
160 + )),
161 + }
162 + }
163 +
164 + /// One emission as a `Node` value.
165 + fn node(emission: &Emission) -> Result<TokenStream> {
166 + match emission {
167 + Emission::Text(text) => {
168 + let text = arg(text)?;
169 + Ok(quote!(::quasi_router::Node::text(#text)))
170 + }
171 + Emission::Link { text, action } => {
172 + let text = arg(text)?;
173 + let action = self::action(action)?;
174 + Ok(quote! {
175 + ::quasi_router::Node::Link {
176 + text: ::std::convert::Into::into(#text),
177 + action: #action,
178 + }
179 + })
180 + }
181 + Emission::Region { name, kind, body } => {
182 + let name = arg(name)?;
183 + let slot = accrete(
184 + body,
185 + Container::Slot,
186 + &quote!(::quasi_router::Slot::new(#name, ::quasi_router::RegionKind::#kind)),
187 + )?;
188 + Ok(quote!(::quasi_router::Node::Region(#slot)))
189 + }
190 + Emission::Across { fallback, .. } => Err(syn::Error::new(
191 + fallback.span(),
192 + "a row is not a node: `across` belongs in a region",
193 + )),
194 + Emission::Beside { priority, .. } => Err(syn::Error::new(
195 + priority.span(),
196 + "`beside` needs a run in scope, which is rule R3",
197 + )),
198 + }
199 + }
200 +
201 + fn action(action: &Action) -> Result<TokenStream> {
202 + let Action {
203 + verb,
204 + target,
205 + modifiers,
206 + } = action;
207 + let verb = format_ident!("{}", verb);
208 + let target = match target {
209 + Some(target) => {
210 + let target = arg(target)?;
211 + quote!(#target)
212 + }
213 + None => quote!(),
214 + };
215 + let modifiers = modifiers.iter().map(|modifier| quote!(.#modifier()));
216 + Ok(quote!(::quasi_router::Action::#verb(#target) #(#modifiers)*))
217 + }
218 +
219 + fn source(source: &Source, owned: bool) -> Result<TokenStream> {
220 + match source {
221 + Source::Str(text) => Ok(interpolated(text, owned)),
222 + Source::Hole(hole) => self::hole(hole),
223 + Source::Choose {
224 + scrutinee,
225 + arms,
226 + otherwise,
227 + } => {
228 + let scrutinee = self::hole(scrutinee)?;
229 + let arms = arms
230 + .iter()
231 + .map(|(pattern, value)| {
232 + let pattern = self::pattern(pattern);
233 + let value = self::source(value, owned)?;
234 + Ok(quote!(#pattern => #value,))
235 + })
236 + .collect::<Result<Vec<_>>>()?;
237 + let otherwise = self::source(otherwise, owned)?;
238 + Ok(quote! {
239 + match #scrutinee {
240 + #(#arms)*
241 + _ => #otherwise,
242 + }
243 + })
244 + }
245 + }
246 + }
247 +
248 + /// Whether a source's arms have to agree on `String`.
249 + ///
250 + /// A dispatch whose arms are all strings and at least one of which interpolates
251 + /// mixes a `&'static str` with a `String`, which is the split the record's
252 + /// amendment 10 reached for with a `&`/`owned` spelling. It does not need one:
253 + /// the arms of one dispatch have to agree on a type, so the dispatch decides
254 + /// the ownership for all of them, and nothing outside a dispatch has an
255 + /// ambiguity to settle.
256 + fn owned_arms(source: &Source) -> bool {
257 + let Source::Choose {
258 + arms, otherwise, ..
259 + } = source
260 + else {
261 + return false;
262 + };
263 + let every = arms
264 + .iter()
265 + .map(|(_, value)| value)
266 + .chain(std::iter::once(&**otherwise));
267 + let mut all_strings = true;
268 + let mut any_interpolates = false;
269 + for value in every {
270 + match value {
271 + Source::Str(text) => {
272 + any_interpolates |= text
273 + .parts
274 + .iter()
275 + .any(|part| matches!(part, StrPart::Hole(_)));
276 + }
277 + _ => all_strings = false,
278 + }
279 + }
280 + all_strings && any_interpolates
281 + }
282 +
283 + fn pattern(pattern: &Pattern) -> TokenStream {
284 + match pattern {
285 + Pattern::Int(value) => {
286 + let value = proc_macro2::Literal::i64_unsuffixed(*value);
287 + quote!(#value)
288 + }
289 + Pattern::Str(value) => {
290 + let value = LitStr::new(value, Span::call_site());
291 + quote!(#value)
292 + }
293 + Pattern::Bool(value) => quote!(#value),
294 + Pattern::Path(path) => quote!(#path),
295 + }
296 + }
297 +
298 + fn interpolated(text: &Interpolated, owned: bool) -> TokenStream {
299 + let mut format = String::new();
300 + let mut holes = Vec::new();
301 + for part in &text.parts {
302 + match part {
303 + StrPart::Lit(literal) => {
304 + for character in literal.chars() {
305 + if character == '{' || character == '}' {
306 + format.push(character);
307 + }
308 + format.push(character);
309 + }
310 + }
311 + StrPart::Hole(hole) => {
312 + format.push_str("{}");
313 + holes.push(hole);
314 + }
315 + }
316 + }
317 + let literal = LitStr::new(&format, text.span);
318 +
319 + if holes.is_empty() {
320 + return if owned {
321 + quote!(::std::string::String::from(#literal))
322 + } else {
323 + quote!(#literal)
324 + };
325 + }
326 +
327 + let holes = holes
328 + .iter()
329 + .map(|hole| self::hole(hole).unwrap_or_else(syn::Error::into_compile_error));
330 + quote!(::std::format!(#literal, #(#holes),*))
331 + }
332 +
333 + fn hole(hole: &Hole) -> Result<TokenStream> {
334 + let mut built = match &hole.root {
335 + HoleRoot::Binding(name) => quote!(#name),
336 + HoleRoot::Path(path) => quote!(#path),
337 + HoleRoot::Call { path, args } => {
338 + let args = args.iter().map(arg).collect::<Result<Vec<_>>>()?;
339 + quote!(#path(#(#args),*))
340 + }
341 + };
342 + for step in &hole.steps {
343 + built = match step {
344 + Step::Field(name) => quote!(#built.#name),
345 + Step::Method { name, args } => {
346 + let args = args.iter().map(arg).collect::<Result<Vec<_>>>()?;
347 + quote!(#built.#name(#(#args),*))
348 + }
349 + };
350 + }
351 + Ok(built)
352 + }
353 +
354 + fn arg(arg: &Arg) -> Result<TokenStream> {
355 + match arg {
356 + Arg::Str(text) => Ok(interpolated(text, false)),
357 + Arg::Hole(hole) => self::hole(hole),
358 + Arg::Int(value) => {
359 + let value = proc_macro2::Literal::i64_unsuffixed(*value);
360 + Ok(quote!(#value))
361 + }
362 + Arg::Bool(value) => Ok(quote!(#value)),
363 + Arg::Borrow(inner) => {
364 + let inner = self::arg(inner)?;
365 + Ok(quote!(&#inner))
366 + }
367 + }
368 + }
369 +
370 + fn emission_span(emission: &Emission) -> Span {
371 + match emission {
372 + Emission::Text(_) | Emission::Link { .. } => Span::call_site(),
373 + Emission::Region { kind, .. } => kind.span(),
374 + Emission::Across { fallback, .. } => fallback.span(),
375 + Emission::Beside { priority, .. } => priority.span(),
376 + }
377 + }
@@ -1,0 +1,472 @@
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, Hole, HoleRoot, Interpolated, Item, Param, Pattern, Source,
20 + Step, StrPart,
21 + };
22 +
23 + /// The modifiers a verb may carry with no argument of their own.
24 + const BARE_MODIFIERS: &[&str] = &[
25 + "navigating",
26 + "awaiting",
27 + "by_host",
28 + "elsewhere",
29 + "invalidating",
30 + "saving",
31 + ];
32 +
33 + /// The verbs, and whether each addresses something.
34 + const VERBS: &[(&str, bool)] = &[
35 + ("get", true),
36 + ("post", true),
37 + ("put", true),
38 + ("delete", true),
39 + ("external", true),
40 + ("local", false),
41 + ("leaving", false),
42 + ("back", false),
43 + ];
44 +
45 + impl Parse for Declaration {
46 + fn parse(input: ParseStream) -> Result<Self> {
47 + let attrs = Attribute::parse_outer(input)?;
48 + let docs = doc_lines(&attrs)?;
49 +
50 + let vis: Visibility = input.parse()?;
51 + let vis = match vis {
52 + Visibility::Inherited => None,
53 + other => Some(other),
54 + };
55 +
56 + let keyword: Ident = input.parse()?;
57 + if keyword != "shape" {
58 + return Err(syn::Error::new(
59 + keyword.span(),
60 + "a declaration begins with `shape`",
61 + ));
62 + }
63 + let name: Ident = input.parse()?;
64 +
65 + let signature;
66 + parenthesized!(signature in input);
67 + let params = signature.parse_terminated(Param::parse, Token![,])?;
68 +
69 + input.parse::<Token![->]>()?;
70 + let returns: Type = input.parse()?;
71 + input.parse::<Token![;]>()?;
72 +
73 + let mut items = Vec::new();
74 + while !input.is_empty() {
75 + items.push(input.parse()?);
76 + }
77 +
78 + Ok(Self {
79 + docs,
80 + vis,
81 + name,
82 + params: params.into_iter().collect(),
83 + returns,
84 + items,
85 + })
86 + }
87 + }
88 +
89 + /// The `///` lines, in order, with the leading space rustdoc adds removed.
90 + fn doc_lines(attrs: &[Attribute]) -> Result<Vec<String>> {
91 + let mut docs = Vec::new();
92 + for attr in attrs {
93 + if !attr.path().is_ident("doc") {
94 + return Err(syn::Error::new_spanned(
95 + attr,
96 + "a declaration carries doc comments and nothing else; \
97 + an attribute on the generated function is not a production yet",
98 + ));
99 + }
100 + let syn::Meta::NameValue(value) = &attr.meta else {
101 + return Err(syn::Error::new_spanned(attr, "expected a doc comment"));
102 + };
103 + let syn::Expr::Lit(syn::ExprLit {
104 + lit: syn::Lit::Str(text),
105 + ..
106 + }) = &value.value
107 + else {
108 + return Err(syn::Error::new_spanned(attr, "expected a doc comment"));
109 + };
110 + docs.push(text.value());
111 + }
112 + Ok(docs)
113 + }
114 +
115 + impl Parse for Param {
116 + fn parse(input: ParseStream) -> Result<Self> {
117 + let name: Ident = input.parse()?;
118 + input.parse::<Token![:]>()?;
119 + let ty: Type = input.parse()?;
120 + Ok(Self { name, ty })
121 + }
122 + }
123 +
124 + impl Parse for Item {
125 + fn parse(input: ParseStream) -> Result<Self> {
126 + if input.peek(Token![let]) {
127 + input.parse::<Token![let]>()?;
128 + let name: Ident = input.parse()?;
129 + input.parse::<Token![=]>()?;
130 + let source: Source = input.parse()?;
131 + input.parse::<Token![;]>()?;
132 + return Ok(Self::Bind { name, source });
133 + }
134 + Ok(Self::Emit(input.parse()?))
135 + }
136 + }
137 +
138 + impl Parse for Source {
139 + fn parse(input: ParseStream) -> Result<Self> {
140 + if input.peek(LitStr) {
141 + let lit: LitStr = input.parse()?;
142 + return Ok(Self::Str(interpolate(&lit)?));
143 + }
144 + if input.peek(Ident) && input.fork().parse::<Ident>()? == "given" {
145 + return parse_choose(input);
146 + }
147 + Ok(Self::Hole(input.parse()?))
148 + }
149 + }
150 +
151 + /// `given <hole> { <pattern> -> <source>, otherwise -> <source> }`.
152 + ///
153 + /// The arms are sources, so a value dispatch produces a value by construction.
154 + /// `otherwise` is required rather than optional: a binding has to have a value
155 + /// on every path, and rustc's exhaustiveness cannot be borrowed here without
156 + /// admitting a pattern that binds.
157 + fn parse_choose(input: ParseStream) -> Result<Source> {
158 + let keyword: Ident = input.parse()?;
159 + let scrutinee: Hole = input.parse()?;
160 +
161 + let body;
162 + braced!(body in input);
163 +
164 + let mut arms = Vec::new();
165 + let mut otherwise = None;
166 + while !body.is_empty() {
167 + if body.peek(Ident) && body.fork().parse::<Ident>()? == "otherwise" {
168 + body.parse::<Ident>()?;
169 + body.parse::<Token![->]>()?;
170 + otherwise = Some(Box::new(body.parse()?));
171 + } else {
172 + let pattern: Pattern = body.parse()?;
173 + body.parse::<Token![->]>()?;
174 + arms.push((pattern, body.parse()?));
175 + }
176 + if body.peek(Token![,]) {
177 + body.parse::<Token![,]>()?;
178 + }
179 + }
180 +
181 + let Some(otherwise) = otherwise else {
182 + return Err(syn::Error::new(
183 + keyword.span(),
184 + "a value dispatch needs an `otherwise` arm: a binding has a value on every path",
185 + ));
186 + };
187 +
188 + Ok(Source::Choose {
189 + scrutinee,
190 + arms,
191 + otherwise,
192 + })
193 + }
194 +
195 + impl Parse for Pattern {
196 + fn parse(input: ParseStream) -> Result<Self> {
197 + if input.peek(LitInt) {
198 + let lit: LitInt = input.parse()?;
199 + return Ok(Self::Int(lit.base10_parse()?));
200 + }
201 + if input.peek(LitStr) {
202 + let lit: LitStr = input.parse()?;
203 + return Ok(Self::Str(lit.value()));
204 + }
205 + if input.peek(LitBool) {
206 + let lit: LitBool = input.parse()?;
207 + return Ok(Self::Bool(lit.value()));
208 + }
209 + Ok(Self::Path(input.parse()?))
210 + }
211 + }
212 +
213 + impl Parse for Hole {
214 + fn parse(input: ParseStream) -> Result<Self> {
215 + let path: syn::Path = input.parse()?;
216 +
217 + let root = if input.peek(token::Paren) {
218 + HoleRoot::Call {
219 + path,
220 + args: call_args(input)?,
221 + }
222 + } else if let Some(name) = binding_ident(&path) {
223 + HoleRoot::Binding(name)
224 + } else {
225 + HoleRoot::Path(path)
226 + };
227 +
228 + let mut steps = Vec::new();
229 + while input.peek(Token![.]) {
230 + input.parse::<Token![.]>()?;
231 + let name: Ident = input.parse()?;
232 + if input.peek(token::Paren) {
233 + steps.push(Step::Method {
234 + name,
235 + args: call_args(input)?,
236 + });
237 + } else {
238 + steps.push(Step::Field(name));
239 + }
240 + }
241 +
242 + Ok(Self { root, steps })
243 + }
244 + }
245 +
246 + /// Rule R1: a bare lowercase-initial ident is a binding; anything qualified or
247 + /// uppercase-initial is a Rust path.
248 + fn binding_ident(path: &syn::Path) -> Option<Ident> {
249 + if path.leading_colon.is_some() || path.segments.len() != 1 {
250 + return None;
251 + }
252 + let segment = path.segments.first()?;
253 + if !segment.arguments.is_none() {
254 + return None;
255 + }
256 + let text = segment.ident.to_string();
257 + let first = text.chars().next()?;
258 + (first.is_lowercase()).then(|| segment.ident.clone())
259 + }
260 +
261 + fn call_args(input: ParseStream) -> Result<Vec<Arg>> {
262 + let content;
263 + parenthesized!(content in input);
264 + let args = content.parse_terminated(Arg::parse, Token![,])?;
265 + Ok(args.into_iter().collect())
266 + }
267 +
268 + impl Parse for Arg {
269 + fn parse(input: ParseStream) -> Result<Self> {
270 + if input.peek(Token![&]) {
271 + input.parse::<Token![&]>()?;
272 + return Ok(Self::Borrow(Box::new(input.parse()?)));
273 + }
274 + if input.peek(LitStr) {
275 + let lit: LitStr = input.parse()?;
276 + return Ok(Self::Str(interpolate(&lit)?));
277 + }
278 + if input.peek(LitInt) {
279 + let lit: LitInt = input.parse()?;
280 + return Ok(Self::Int(lit.base10_parse()?));
281 + }
282 + if input.peek(LitBool) {
283 + let lit: LitBool = input.parse()?;
284 + return Ok(Self::Bool(lit.value()));
285 + }
286 + Ok(Self::Hole(input.parse()?))
287 + }
288 + }
289 +
290 + impl Parse for Emission {
291 + fn parse(input: ParseStream) -> Result<Self> {
292 + let member: Ident = input.parse()?;
293 + match member.to_string().as_str() {
294 + "beside" => {
295 + let priority: Ident = input.parse()?;
296 + Ok(Self::Beside {
297 + priority,
298 + inner: Box::new(input.parse()?),
299 + })
300 + }
301 + "region" => {
302 + let name: Arg = input.parse()?;
303 + input.parse::<Token![as]>()?;
304 + let kind: Ident = input.parse()?;
305 + Ok(Self::Region {
306 + name,
307 + kind,
308 + body: block(input)?,
309 + })
310 + }
311 + "across" => {
312 + let fallback: Ident = input.parse()?;
313 + Ok(Self::Across {
314 + fallback,
315 + body: block(input)?,
316 + })
317 + }
318 + "text" => {
319 + let text: Arg = input.parse()?;
320 + input.parse::<Token![;]>()?;
321 + Ok(Self::Text(text))
322 + }
323 + "link" => {
324 + let text: Arg = input.parse()?;
325 + let to: Ident = input.parse()?;
326 + if to != "to" {
327 + return Err(syn::Error::new(
328 + to.span(),
329 + "a link says where it goes: `link <text> to <action>;`",
330 + ));
331 + }
332 + let action: Action = input.parse()?;
333 + input.parse::<Token![;]>()?;
334 + Ok(Self::Link { text, action })
335 + }
336 + other => Err(syn::Error::new(
337 + member.span(),
338 + format!(
339 + "`{other}` is not a member this form can say yet. \
340 + Add the production, and name the screen that demanded it in the commit."
341 + ),
342 + )),
343 + }
344 + }
345 + }
346 +
347 + fn block(input: ParseStream) -> Result<Vec<Item>> {
348 + let body;
349 + braced!(body in input);
350 + let mut items = Vec::new();
351 + while !body.is_empty() {
352 + items.push(body.parse()?);
353 + }
354 + Ok(items)
355 + }
356 +
357 + impl Parse for Action {
358 + fn parse(input: ParseStream) -> Result<Self> {
359 + let verb: Ident = input.parse()?;
360 + let name = verb.to_string();
361 + let Some((_, addresses)) = VERBS.iter().find(|(known, _)| *known == name) else {
362 + return Err(syn::Error::new(
363 + verb.span(),
364 + format!("`{name}` is not one of the verbs"),
365 + ));
366 + };
367 +
368 + let target = if *addresses {
369 + Some(input.parse()?)
370 + } else {
371 + None
372 + };
373 +
374 + let mut modifiers = Vec::new();
375 + while input.peek(Ident) {
376 + let modifier: Ident = input.parse()?;
377 + let name = modifier.to_string();
378 + if !BARE_MODIFIERS.contains(&name.as_str()) {
379 + return Err(syn::Error::new(
380 + modifier.span(),
381 + format!("`{name}` is not a modifier this form can say yet"),
382 + ));
383 + }
384 + modifiers.push(modifier);
385 + }
386 +
387 + Ok(Self {
388 + verb,
389 + target,
390 + modifiers,
391 + })
392 + }
393 + }
394 +
395 + /// Split a string literal into its literal runs and its `{hole}` holes.
396 + ///
397 + /// `{{` and `}}` are the escapes, as they are in `format!`, because a hint that
398 + /// wants a literal brace is a real site rather than a hypothetical one.
399 + fn interpolate(lit: &LitStr) -> Result<Interpolated> {
400 + let span = lit.span();
401 + let text = lit.value();
402 + let mut parts = Vec::new();
403 + let mut literal = String::new();
404 + let mut rest = text.as_str();
405 +
406 + while let Some(at) = rest.find(['{', '}']) {
407 + let (before, tail) = rest.split_at(at);
408 + literal.push_str(before);
409 + let mut chars = tail.chars();
410 + let opener = chars.next().expect("find reported a brace");
411 + let tail = chars.as_str();
412 +
413 + if tail.starts_with(opener) {
414 + literal.push(opener);
415 + rest = &tail[opener.len_utf8()..];
416 + continue;
417 + }
418 + if opener == '}' {
419 + return Err(syn::Error::new(
420 + span,
421 + "a lone `}` in a string: write `}}` for a literal brace",
422 + ));
423 + }
424 +
425 + let Some(end) = tail.find('}') else {
426 + return Err(syn::Error::new(span, "an unclosed `{` in a string"));
427 + };
428 + let inner = &tail[..end];
429 + if let Some(colon) = spec_separator(inner) {
430 + return Err(syn::Error::new(
431 + span,
432 + format!(
433 + "the format spec `{}` is not a production yet",
434 + &inner[colon + 1..]
435 + ),
436 + ));
437 + }
438 +
439 + if !literal.is_empty() {
440 + parts.push(StrPart::Lit(std::mem::take(&mut literal)));
441 + }
442 + let hole: Hole = syn::parse_str(inner).map_err(|error| {
443 + syn::Error::new(span, format!("`{{{inner}}}` is not a hole: {error}"))
444 + })?;
445 + parts.push(StrPart::Hole(hole));
446 + rest = &tail[end + 1..];
447 + }
448 +
449 + literal.push_str(rest);
450 + if !literal.is_empty() {
451 + parts.push(StrPart::Lit(literal));
452 + }
453 +
454 + Ok(Interpolated { parts, span })
455 + }
456 +
457 + /// Where a format spec starts inside a hole, skipping the `::` of a path.
458 + fn spec_separator(inner: &str) -> Option<usize> {
459 + let bytes = inner.as_bytes();
460 + let mut at = 0;
461 + while at < bytes.len() {
462 + if bytes[at] == b':' {
463 + if bytes.get(at + 1) == Some(&b':') {
464 + at += 2;
465 + continue;
466 + }
467 + return Some(at);
468 + }
469 + at += 1;
470 + }
471 + None
472 + }