Skip to main content

max / quasi

Grow the form for the three act shapes and the follow control Productions, each named by the screen that demanded it. version_delete_act: `must_use` as a flag, a `-> Act` shape whose single `act` member is the result, an attribute as a builder call, and `tone Danger` resolving through rule R1(4). export_act: `include`, and modifiers that carry an argument, which is what `saving` needed. follow: a dispatch in member position, a guard with a comparison, and `-> Option<Node>` as the grammar's `single "?"`. Two things the follow control forced that are rules rather than productions. A member is evaluated into its own binding before the container is built, because a container's own name is often the same value a member borrows: the region is named and then the press is aimed at it, and built inline the container would move the name before the member read it. And a guard on the whole result is refused unless the shape may omit it, so R10's "emits nothing yields None" is the only way a guard can drop something. `From<Act> for Node` goes in beside `From<Table>`: a shape that returns a control has to be placeable in a body without the caller naming the variant.
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:21 UTC
Signed with PGP, not checked
Commit: b99519f2d5645db8bcad06be29491b6f8e29a3f6
Parent: 4857546
5 files changed, +532 insertions, -76 deletions
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-router"
3 - version = "0.101.1"
3 + version = "0.101.2"
4 4 description = "Host-agnostic router: a request in, a renderer-agnostic description out"
5 5 edition.workspace = true
6 6 rust-version.workspace = true
@@ -17,6 +17,10 @@
17 17 /// One declared shape: a header and the items its body emits.
18 18 pub struct Declaration {
19 19 pub docs: Vec<String>,
20 + /// The bare attributes the generated function carries, `must_use` and
21 + /// `inline`. Nothing that takes an argument: an attribute with a body is a
22 + /// second grammar, and none of the converted screens has wanted one.
23 + pub flags: Vec<Ident>,
20 24 pub vis: Option<syn::Visibility>,
21 25 pub name: Ident,
22 26 pub params: Vec<Param>,
@@ -32,6 +36,13 @@
32 36 pub enum Item {
33 37 /// `let name = <source>;`
34 38 Bind { name: Ident, source: Source },
39 + /// `<builder> <arg>*;` -- one setting on the enclosing container.
40 + ///
41 + /// The ident is the **builder method's** name and not the struct field's,
42 + /// which is what ATTRIBUTE NAMING settles: `Slot::named` writes `name`, and
43 + /// a form that spelled the field would be naming something no caller can
44 + /// reach.
45 + Attribute { name: Ident, args: Vec<Arg> },
35 46 /// Anything that puts something into the enclosing container.
36 47 Emit(Emission),
37 48 }
@@ -115,6 +126,14 @@
115 126 pub enum Emission {
116 127 /// `text <arg>;`
117 128 Text(Arg),
129 + /// `act <arg> to <action> ( { .. } | ; )`
130 + Act {
131 + label: Arg,
132 + action: Action,
133 + body: Vec<Item>,
134 + },
135 + /// `include <hole>;` -- whatever another shape built.
136 + Include(Hole),
118 137 /// `link <arg> to <action>;`
119 138 Link { text: Arg, action: Action },
120 139 /// `region <arg> as <kind> { .. }`
@@ -130,10 +149,51 @@
130 149 priority: Ident,
131 150 inner: Box<Emission>,
132 151 },
152 + /// `<emission> when <predicate>` / `unless <predicate>`.
153 + ///
154 + /// A guard suppresses the emission and nothing else. R9: the holes inside
155 + /// it are still evaluated, because a hole is one eager evaluation and a
156 + /// guard is not a scope.
157 + Guarded { guard: Guard, inner: Box<Emission> },
158 + /// `given <hole> { <pattern> -> <emission> .. }` in member position.
159 + ///
160 + /// Every arm emits exactly one member, which is what makes the dispatch a
161 + /// member itself rather than a statement. R7: this is a real `match`, so
162 + /// exhaustiveness and arm order are rustc's.
163 + Given {
164 + scrutinee: Hole,
165 + arms: Vec<(Pattern, Box<Emission>)>,
166 + otherwise: Option<Box<Emission>>,
167 + },
168 + }
169 +
170 + /// `when <predicate>`, or `unless <predicate>`, which is its negation.
171 + pub struct Guard {
172 + pub negated: bool,
173 + pub predicate: Predicate,
174 + pub span: Span,
175 + }
176 +
177 + pub enum Predicate {
178 + /// A hole that is already a `bool`.
179 + Truth(Hole),
180 + /// `<hole> <compare> <arg>`. The comparison is a word rather than an
181 + /// operator, so nothing in the form is an expression.
182 + Comparison {
183 + left: Hole,
184 + compare: Ident,
185 + right: Arg,
186 + },
133 187 }
134 188
135 189 pub struct Action {
136 190 pub verb: Ident,
137 191 pub target: Option<Arg>,
138 - pub modifiers: Vec<Ident>,
192 + pub modifiers: Vec<Modifier>,
193 + }
194 +
195 + /// One word after the verb, and whatever it needs to say it.
196 + pub struct Modifier {
197 + pub name: Ident,
198 + pub args: Vec<Arg>,
139 199 }
@@ -15,22 +15,26 @@
15 15 use syn::{LitStr, Result};
16 16
17 17 use crate::ast::{
18 - Action, Arg, Declaration, Emission, Hole, HoleRoot, Interpolated, Item, Pattern, Source, Step,
19 - StrPart,
18 + Action, Arg, Declaration, Emission, Guard, Hole, HoleRoot, Interpolated, Item, Pattern,
19 + Predicate, Source, Step, StrPart,
20 20 };
21 + use crate::parse::{COMPARISONS, VOCABULARY};
21 22
22 23 /// What the items of a body accrete onto.
23 - #[derive(Clone, Copy)]
24 + #[derive(Clone, Copy, PartialEq, Eq)]
24 25 enum Container {
25 26 /// A region: emissions accrete onto a `Slot`.
26 27 Slot,
27 28 /// A row: every emission is `beside` and carries a priority.
28 29 Run,
30 + /// One control: its body sets, and emits nothing.
31 + Act,
29 32 }
30 33
31 34 pub fn declaration(declaration: &Declaration) -> Result<TokenStream> {
32 35 let Declaration {
33 36 docs,
37 + flags,
34 38 vis,
35 39 name,
36 40 params,
@@ -38,56 +42,71 @@
38 42 items,
39 43 } = declaration;
40 44
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 - }
45 + let (shaped, optional) = shaped_name(returns)?;
51 46
52 47 let docs = docs.iter().map(|line| {
53 48 let line = LitStr::new(line, Span::call_site());
54 49 quote!(#[doc = #line])
55 50 });
51 + let flags = flags.iter().map(|flag| quote!(#[#flag]));
56 52 let params = params.iter().map(|param| {
57 53 let name = &param.name;
58 54 let ty = &param.ty;
59 55 quote!(#name: #ty)
60 56 });
61 - let body = value_body(items, name.span())?;
57 + let body = value_body(items, &shaped, optional, name.span())?;
62 58
63 59 Ok(quote! {
64 60 #(#docs)*
61 + #(#flags)*
65 62 #vis fn #name(#(#params),*) -> #returns #body
66 63 })
67 64 }
68 65
69 - /// The last segment of the return type, which is what R2 dispatches on.
70 - fn shaped_name(returns: &syn::Type) -> Result<String> {
66 + /// The vocabulary type a shape returns, and whether omission is allowed.
67 + ///
68 + /// `Option<T>` is the grammar's `single "?"`, and rule R10 is what makes it
69 + /// usable: a body whose emission is guarded away yields `None`.
70 + fn shaped_name(returns: &syn::Type) -> Result<(String, bool)> {
71 71 let syn::Type::Path(path) = returns else {
72 72 return Err(syn::Error::new_spanned(
73 73 returns,
74 74 "a shape returns one of the vocabulary types",
75 75 ));
76 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"))
77 + let Some(last) = path.path.segments.last() else {
78 + return Err(syn::Error::new_spanned(returns, "an empty return type"));
79 + };
80 + if last.ident == "Option" {
81 + let syn::PathArguments::AngleBracketed(arguments) = &last.arguments else {
82 + return Err(syn::Error::new_spanned(returns, "`Option` of what?"));
83 + };
84 + let Some(syn::GenericArgument::Type(inner)) = arguments.args.first() else {
85 + return Err(syn::Error::new_spanned(returns, "`Option` of what?"));
86 + };
87 + let (shaped, nested) = shaped_name(inner)?;
88 + if nested {
89 + return Err(syn::Error::new_spanned(
90 + returns,
91 + "a shape omits its result or does not; there is no second omission",
92 + ));
93 + }
94 + return Ok((shaped, true));
95 + }
96 + Ok((last.ident.to_string(), false))
82 97 }
83 98
84 - /// A `-> Node` body: its bindings, then the single emission that is the value.
99 + /// A shape's body: its bindings, then the single emission that is the value.
100 + ///
101 + /// Rule R2 for the two shapes converted so far. A `-> Node` shape's single
102 + /// emission IS the result and a `-> Act` shape's single `act` member is, so
103 + /// neither fabricates a container: what a caller gets is what the body said.
85 104 ///
86 105 /// Bindings are hoisted above the emissions of their own body. Nothing is
87 106 /// reordered by that: a binding can only name bindings written before it, and
88 107 /// an emission produces a value rather than an effect, so the two are
89 108 /// independent within one block.
90 - fn value_body(items: &[Item], span: Span) -> Result<TokenStream> {
109 + fn value_body(items: &[Item], shaped: &str, optional: bool, span: Span) -> Result<TokenStream> {
91 110 let mut bindings = Vec::new();
92 111 let mut emissions = Vec::new();
93 112 for item in items {
@@ -96,6 +115,12 @@
96 115 let value = source_value(source)?;
97 116 bindings.push(quote!(let #name = #value;));
98 117 }
118 + Item::Attribute { name, .. } => {
119 + return Err(syn::Error::new(
120 + name.span(),
121 + "a setting needs something to set: put it in the member's body",
122 + ));
123 + }
99 124 Item::Emit(emission) => emissions.push(emission),
100 125 }
101 126 }
@@ -104,12 +129,54 @@
104 129 return Err(syn::Error::new(
105 130 span,
106 131 format!(
107 - "a `-> Node` shape is its single emission, and this one has {}",
132 + "a `-> {shaped}` shape is its single emission, and this one has {}",
108 133 emissions.len()
109 134 ),
110 135 ));
111 136 };
112 - let built = node(only)?;
137 +
138 + let (guard, only) = match only {
139 + Emission::Guarded { guard, inner } => (Some(guard), &**inner),
140 + other => (None, *other),
141 + };
142 + if guard.is_some() && !optional {
143 + return Err(syn::Error::new(
144 + span,
145 + "a guard on the whole result needs a shape that may omit it: `-> Option<_>`",
146 + ));
147 + }
148 +
149 + let built = match shaped {
150 + "Node" => node(only)?,
151 + "Act" => match only {
152 + Emission::Act { .. } => act(only)?,
153 + other => {
154 + return Err(syn::Error::new(
155 + emission_span(other),
156 + "a `-> Act` shape is its single `act` member",
157 + ));
158 + }
159 + },
160 + other => {
161 + return Err(syn::Error::new(
162 + span,
163 + format!(
164 + "`-> {other}` is not a shape this form can build yet. \
165 + Add the R2 case, and name the screen that demanded it in the commit."
166 + ),
167 + ));
168 + }
169 + };
170 +
171 + let built = if optional {
172 + let test = match guard {
173 + Some(guard) => predicate(guard)?,
174 + None => quote!(true),
175 + };
176 + quote!(if #test { ::std::option::Option::Some(#built) } else { ::std::option::Option::None })
177 + } else {
178 + built
179 + };
113 180
114 181 Ok(quote!({ #(#bindings)* #built }))
115 182 }
@@ -129,14 +196,68 @@
129 196 let value = source_value(source)?;
130 197 bindings.push(quote!(let #name = #value;));
131 198 }
132 - Item::Emit(emission) => steps.push(step(emission, container)?),
199 + Item::Attribute { name, args } => steps.push(attribute(name, args)?),
200 + Item::Emit(emission) => {
201 + // A member is evaluated into its own binding before the
202 + // container is built, because a container's own name is often
203 + // the same value one of its members borrows: follow.rs names
204 + // the region and then aims the press at it. Built inline, the
205 + // container would move the name before the member read it.
206 + let held = format_ident!("member_{}", bindings.len(), span = Span::call_site());
207 + let (value, call) = step(emission, container, &held)?;
208 + bindings.push(quote!(let #held = #value;));
209 + steps.push(call);
210 + }
133 211 }
134 212 }
135 213 Ok(quote!({ #(#bindings)* #base #(#steps)* }))
136 214 }
137 215
138 - /// One emission, as a step onto the container it lands in.
139 - fn step(emission: &Emission, container: Container) -> Result<TokenStream> {
216 + /// One setting, as the builder call ATTRIBUTE NAMING says it is.
217 + fn attribute(name: &syn::Ident, args: &[Arg]) -> Result<TokenStream> {
218 + let slot = name.to_string();
219 + let enumeration = VOCABULARY
220 + .iter()
221 + .find(|(attribute, _)| *attribute == slot)
222 + .map(|(_, enumeration)| *enumeration);
223 + let args = args
224 + .iter()
225 + .map(|value| match enumeration {
226 + Some(enumeration) => variant(value, enumeration),
227 + None => arg(value),
228 + })
229 + .collect::<Result<Vec<_>>>()?;
230 + Ok(quote!(.#name(#(#args),*)))
231 + }
232 +
233 + /// Rule R1(4): a bare uppercase ident in a vocabulary slot is that enum's
234 + /// variant. A `SCREAMING_CASE` ident never is, because 19 of 19 real uses of
235 + /// the `measured` slot pass a module const rather than a variant.
236 + fn variant(value: &Arg, enumeration: &str) -> Result<TokenStream> {
237 + let Arg::Hole(hole) = value else {
238 + return arg(value);
239 + };
240 + let HoleRoot::Path(path) = &hole.root else {
241 + return arg(value);
242 + };
243 + if !hole.steps.is_empty() || path.leading_colon.is_some() || path.segments.len() != 1 {
244 + return arg(value);
245 + }
246 + let name = path.segments[0].ident.to_string();
247 + if name.chars().all(|letter| !letter.is_lowercase()) {
248 + return arg(value);
249 + }
250 + let enumeration = format_ident!("{}", enumeration);
251 + let name = &path.segments[0].ident;
252 + Ok(quote!(::quasi_router::layout::#enumeration::#name))
253 + }
254 +
255 + /// One emission, as the value it builds and the call that places it.
256 + fn step(
257 + emission: &Emission,
258 + container: Container,
259 + held: &proc_macro2::Ident,
260 + ) -> Result<(TokenStream, TokenStream)> {
140 261 match (container, emission) {
141 262 (Container::Slot, Emission::Across { fallback, body }) => {
142 263 let run = accrete(
@@ -144,23 +265,54 @@
144 265 Container::Run,
145 266 &quote!(::quasi_router::Run::new(::quasi_router::layout::Fallback::#fallback)),
146 267 )?;
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)))
268 + Ok((run, quote!(.across(#held))))
156 269 }
270 + (Container::Slot, other) => Ok((node(other)?, quote!(.with(#held)))),
271 + (Container::Run, Emission::Beside { priority, inner }) => Ok((
272 + node(inner)?,
273 + quote!(.beside(#held, ::quasi_router::layout::Priority::#priority)),
274 + )),
157 275 (Container::Run, other) => Err(syn::Error::new(
158 276 emission_span(other),
159 277 "a row ranks what it holds: write `beside <priority> <emission>`",
160 278 )),
279 + (Container::Act, other) => Err(syn::Error::new(
280 + emission_span(other),
281 + "a control holds no members: its body says what it is like",
282 + )),
161 283 }
162 284 }
163 285
286 + /// One guard, as the `bool` it tests.
287 + fn predicate(guard: &Guard) -> Result<TokenStream> {
288 + let test = match &guard.predicate {
289 + Predicate::Truth(hole) => self::hole(hole)?,
290 + Predicate::Comparison {
291 + left,
292 + compare,
293 + right,
294 + } => {
295 + let spelling = compare.to_string();
296 + let operator = COMPARISONS
297 + .iter()
298 + .find(|(known, _)| *known == spelling)
299 + .map(|(_, operator)| *operator)
300 + .ok_or_else(|| {
301 + syn::Error::new(compare.span(), format!("`{spelling}` is not a comparison"))
302 + })?;
303 + let operator: TokenStream = operator.parse().expect("a comparison operator");
304 + let left = self::hole(left)?;
305 + let right = arg(right)?;
306 + quote!(#left #operator #right)
307 + }
308 + };
309 + Ok(if guard.negated {
310 + quote!(!(#test))
311 + } else {
312 + test
313 + })
314 + }
315 +
164 316 /// One emission as a `Node` value.
165 317 fn node(emission: &Emission) -> Result<TokenStream> {
166 318 match emission {
@@ -178,6 +330,14 @@
178 330 }
179 331 })
180 332 }
333 + Emission::Act { .. } => {
334 + let control = act(emission)?;
335 + Ok(quote!(::quasi_router::Node::Act(#control)))
336 + }
337 + Emission::Include(supplier) => {
338 + let supplier = hole(supplier)?;
339 + Ok(quote!(::std::convert::Into::into(#supplier)))
340 + }
181 341 Emission::Region { name, kind, body } => {
182 342 let name = arg(name)?;
183 343 let slot = accrete(
@@ -187,6 +347,34 @@
187 347 )?;
188 348 Ok(quote!(::quasi_router::Node::Region(#slot)))
189 349 }
350 + Emission::Given {
351 + scrutinee,
352 + arms,
353 + otherwise,
354 + } => {
355 + let scrutinee = hole(scrutinee)?;
356 + let arms = arms
357 + .iter()
358 + .map(|(pattern, arm)| {
359 + let pattern = self::pattern(pattern);
360 + let arm = node(arm)?;
361 + Ok(quote!(#pattern => #arm,))
362 + })
363 + .collect::<Result<Vec<_>>>()?;
364 + let otherwise = match otherwise {
365 + Some(otherwise) => {
366 + let otherwise = node(otherwise)?;
367 + quote!(_ => #otherwise,)
368 + }
369 + None => quote!(),
370 + };
371 + Ok(quote!(match #scrutinee { #(#arms)* #otherwise }))
372 + }
373 + Emission::Guarded { guard, .. } => Err(syn::Error::new(
374 + guard.span,
375 + "a guarded member needs a container that may hold nothing; \
376 + only a `-> Option<_>` shape can drop its whole result so far",
377 + )),
190 378 Emission::Across { fallback, .. } => Err(syn::Error::new(
191 379 fallback.span(),
192 380 "a row is not a node: `across` belongs in a region",
@@ -198,6 +386,28 @@
198 386 }
199 387 }
200 388
389 + /// One control: what it is called, what it does, and what it is like.
390 + fn act(emission: &Emission) -> Result<TokenStream> {
391 + let Emission::Act {
392 + label,
393 + action,
394 + body,
395 + } = emission
396 + else {
397 + return Err(syn::Error::new(
398 + emission_span(emission),
399 + "expected a control here",
400 + ));
401 + };
402 + let label = arg(label)?;
403 + let called = self::action(action)?;
404 + accrete(
405 + body,
406 + Container::Act,
407 + &quote!(::quasi_router::Act::new(#label, #called)),
408 + )
409 + }
410 +
201 411 fn action(action: &Action) -> Result<TokenStream> {
202 412 let Action {
203 413 verb,
@@ -212,7 +422,14 @@
212 422 }
213 423 None => quote!(),
214 424 };
215 - let modifiers = modifiers.iter().map(|modifier| quote!(.#modifier()));
425 + let modifiers = modifiers
426 + .iter()
427 + .map(|modifier| {
428 + let name = &modifier.name;
429 + let args = modifier.args.iter().map(arg).collect::<Result<Vec<_>>>()?;
430 + Ok(quote!(.#name(#(#args),*)))
431 + })
432 + .collect::<Result<Vec<_>>>()?;
216 433 Ok(quote!(::quasi_router::Action::#verb(#target) #(#modifiers)*))
217 434 }
218 435
@@ -369,7 +586,10 @@
369 586
370 587 fn emission_span(emission: &Emission) -> Span {
371 588 match emission {
372 - Emission::Text(_) | Emission::Link { .. } => Span::call_site(),
589 + Emission::Text(_) | Emission::Link { .. } | Emission::Include(_) => Span::call_site(),
590 + Emission::Act { action, .. } => action.verb.span(),
591 + Emission::Guarded { guard, .. } => guard.span,
592 + Emission::Given { .. } => Span::call_site(),
373 593 Emission::Region { kind, .. } => kind.span(),
374 594 Emission::Across { fallback, .. } => fallback.span(),
375 595 Emission::Beside { priority, .. } => priority.span(),
@@ -16,20 +16,52 @@
16 16 use syn::{braced, parenthesized, token};
17 17
18 18 use crate::ast::{
19 - Action, Arg, Declaration, Emission, Hole, HoleRoot, Interpolated, Item, Param, Pattern, Source,
20 - Step, StrPart,
19 + Action, Arg, Declaration, Emission, Guard, Hole, HoleRoot, Interpolated, Item, Modifier, Param,
20 + Pattern, Predicate, Source, Step, StrPart,
21 21 };
22 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",
23 + /// The modifiers, and how many arguments each takes.
24 + const MODIFIERS: &[(&str, usize)] = &[
25 + ("navigating", 0),
26 + ("awaiting", 0),
27 + ("by_host", 0),
28 + ("elsewhere", 0),
29 + ("invalidating", 0),
30 + ("replacing_enclosing", 0),
31 + ("saving", 1),
32 + ("replacing", 1),
33 + ("copying", 1),
34 + ("filling", 2),
35 + ("carrying", 2),
31 36 ];
32 37
38 + /// The members: everything that emits rather than sets.
39 + ///
40 + /// An ident inside a body is a member if it is here and an attribute if it is
41 + /// not, which is the whole of how the two are told apart. A member added here
42 + /// without a production is a parse error naming it, which is the failure worth
43 + /// having.
44 + const MEMBERS: &[&str] = &[
45 + "act", "across", "beside", "given", "include", "link", "region", "text",
46 + ];
47 +
48 + /// The comparisons, and the Rust operator each is.
49 + pub const COMPARISONS: &[(&str, &str)] = &[
50 + ("is", "=="),
51 + ("is_not", "!="),
52 + ("over", ">"),
53 + ("under", "<"),
54 + ("at_least", ">="),
55 + ("at_most", "<="),
56 + ];
57 +
58 + /// The attributes whose argument is a variant of a vocabulary enum, and the
59 + /// enum it belongs to. Rule R1(4).
60 + pub const VOCABULARY: &[(&str, &str)] = &[("tone", "Tone")];
61 +
62 + /// The attributes the generated function may carry.
63 + const FLAGS: &[&str] = &["must_use", "inline"];
64 +
33 65 /// The verbs, and whether each addresses something.
34 66 const VERBS: &[(&str, bool)] = &[
35 67 ("get", true),
@@ -45,7 +77,7 @@
45 77 impl Parse for Declaration {
46 78 fn parse(input: ParseStream) -> Result<Self> {
47 79 let attrs = Attribute::parse_outer(input)?;
48 - let docs = doc_lines(&attrs)?;
80 + let (docs, flags) = docs_and_flags(&attrs)?;
49 81
50 82 let vis: Visibility = input.parse()?;
51 83 let vis = match vis {
@@ -77,6 +109,7 @@
77 109
78 110 Ok(Self {
79 111 docs,
112 + flags,
80 113 vis,
81 114 name,
82 115 params: params.into_iter().collect(),
@@ -86,16 +119,29 @@
86 119 }
87 120 }
88 121
89 - /// The `///` lines, in order, with the leading space rustdoc adds removed.
90 - fn doc_lines(attrs: &[Attribute]) -> Result<Vec<String>> {
122 + /// The `///` lines and the bare flags, in order.
123 + fn docs_and_flags(attrs: &[Attribute]) -> Result<(Vec<String>, Vec<Ident>)> {
91 124 let mut docs = Vec::new();
125 + let mut flags = Vec::new();
92 126 for attr in attrs {
93 127 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 - ));
128 + let syn::Meta::Path(path) = &attr.meta else {
129 + return Err(syn::Error::new_spanned(
130 + attr,
131 + "an attribute with arguments is not a production yet",
132 + ));
133 + };
134 + let Some(name) = path.get_ident() else {
135 + return Err(syn::Error::new_spanned(attr, "expected a bare attribute"));
136 + };
137 + if !FLAGS.contains(&name.to_string().as_str()) {
138 + return Err(syn::Error::new_spanned(
139 + attr,
140 + format!("`{name}` is not a flag a declaration may carry"),
141 + ));
142 + }
143 + flags.push(name.clone());
144 + continue;
99 145 }
100 146 let syn::Meta::NameValue(value) = &attr.meta else {
101 147 return Err(syn::Error::new_spanned(attr, "expected a doc comment"));
@@ -109,7 +155,7 @@
109 155 };
110 156 docs.push(text.value());
111 157 }
112 - Ok(docs)
158 + Ok((docs, flags))
113 159 }
114 160
115 161 impl Parse for Param {
@@ -131,7 +177,17 @@
131 177 input.parse::<Token![;]>()?;
132 178 return Ok(Self::Bind { name, source });
133 179 }
134 - Ok(Self::Emit(input.parse()?))
180 + let member = input.fork().parse::<Ident>()?;
181 + if MEMBERS.contains(&member.to_string().as_str()) {
182 + return Ok(Self::Emit(input.parse()?));
183 + }
184 + let name: Ident = input.parse()?;
185 + let mut args = Vec::new();
186 + while !input.peek(Token![;]) {
187 + args.push(input.parse()?);
188 + }
189 + input.parse::<Token![;]>()?;
190 + Ok(Self::Attribute { name, args })
135 191 }
136 192 }
137 193
@@ -315,23 +371,68 @@
315 371 body: block(input)?,
316 372 })
317 373 }
374 + "act" => {
375 + let label: Arg = input.parse()?;
376 + preposition(input, "to")?;
377 + let action: Action = input.parse()?;
378 + let guard = guard(input)?;
379 + let body = if input.peek(token::Brace) {
380 + block(input)?
381 + } else {
382 + input.parse::<Token![;]>()?;
383 + Vec::new()
384 + };
385 + Ok(guarded(
386 + guard,
387 + Self::Act {
388 + label,
389 + action,
390 + body,
391 + },
392 + ))
393 + }
394 + "include" => {
395 + let supplier: Hole = input.parse()?;
396 + let guard = guard(input)?;
397 + input.parse::<Token![;]>()?;
398 + Ok(guarded(guard, Self::Include(supplier)))
399 + }
400 + "given" => {
401 + let scrutinee: Hole = input.parse()?;
402 + let body;
403 + braced!(body in input);
404 + let mut arms = Vec::new();
405 + let mut otherwise = None;
406 + while !body.is_empty() {
407 + if body.peek(Ident) && body.fork().parse::<Ident>()? == "otherwise" {
408 + body.parse::<Ident>()?;
409 + body.parse::<Token![->]>()?;
410 + otherwise = Some(Box::new(body.parse()?));
411 + } else {
412 + let pattern: Pattern = body.parse()?;
413 + body.parse::<Token![->]>()?;
414 + arms.push((pattern, Box::new(body.parse()?)));
415 + }
416 + }
417 + Ok(Self::Given {
418 + scrutinee,
419 + arms,
420 + otherwise,
421 + })
422 + }
318 423 "text" => {
319 424 let text: Arg = input.parse()?;
425 + let guard = guard(input)?;
320 426 input.parse::<Token![;]>()?;
321 - Ok(Self::Text(text))
427 + Ok(guarded(guard, Self::Text(text)))
322 428 }
323 429 "link" => {
324 430 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 - }
431 + preposition(input, "to")?;
332 432 let action: Action = input.parse()?;
433 + let guard = guard(input)?;
333 434 input.parse::<Token![;]>()?;
334 - Ok(Self::Link { text, action })
435 + Ok(guarded(guard, Self::Link { text, action }))
335 436 }
336 437 other => Err(syn::Error::new(
337 438 member.span(),
@@ -344,6 +445,67 @@
344 445 }
345 446 }
346 447
448 + /// `when <predicate>` or `unless <predicate>`, if one is written here.
449 + fn guard(input: ParseStream) -> Result<Option<Guard>> {
450 + if !input.peek(Ident) {
451 + return Ok(None);
452 + }
453 + let word = input.fork().parse::<Ident>()?;
454 + let negated = match word.to_string().as_str() {
455 + "when" => false,
456 + "unless" => true,
457 + _ => return Ok(None),
458 + };
459 + input.parse::<Ident>()?;
460 +
461 + let left: Hole = input.parse()?;
462 + let predicate = if input.peek(Ident) {
463 + let compare: Ident = input.parse()?;
464 + let spelling = compare.to_string();
465 + if !COMPARISONS.iter().any(|(known, _)| *known == spelling) {
466 + return Err(syn::Error::new(
467 + compare.span(),
468 + format!("`{spelling}` is not one of the comparisons"),
469 + ));
470 + }
471 + Predicate::Comparison {
472 + left,
473 + compare,
474 + right: input.parse()?,
475 + }
476 + } else {
477 + Predicate::Truth(left)
478 + };
479 +
480 + Ok(Some(Guard {
481 + negated,
482 + predicate,
483 + span: word.span(),
484 + }))
485 + }
486 +
487 + fn guarded(guard: Option<Guard>, inner: Emission) -> Emission {
488 + match guard {
489 + Some(guard) => Emission::Guarded {
490 + guard,
491 + inner: Box::new(inner),
492 + },
493 + None => inner,
494 + }
495 + }
496 +
497 + /// One of the prepositions, which carry no meaning beyond reading as English.
498 + fn preposition(input: ParseStream, expected: &str) -> Result<()> {
499 + let word: Ident = input.parse()?;
500 + if word != expected {
501 + return Err(syn::Error::new(
502 + word.span(),
503 + format!("expected `{expected}` here"),
504 + ));
505 + }
506 + Ok(())
507 + }
508 +
347 509 fn block(input: ParseStream) -> Result<Vec<Item>> {
348 510 let body;
349 511 braced!(body in input);
@@ -373,15 +535,19 @@
373 535
374 536 let mut modifiers = Vec::new();
375 537 while input.peek(Ident) {
376 - let modifier: Ident = input.parse()?;
377 - let name = modifier.to_string();
378 - if !BARE_MODIFIERS.contains(&name.as_str()) {
538 + let name: Ident = input.parse()?;
539 + let spelling = name.to_string();
540 + let Some((_, arity)) = MODIFIERS.iter().find(|(known, _)| *known == spelling) else {
379 541 return Err(syn::Error::new(
380 - modifier.span(),
381 - format!("`{name}` is not a modifier this form can say yet"),
542 + name.span(),
543 + format!("`{spelling}` is not a modifier this form can say yet"),
382 544 ));
545 + };
546 + let mut args = Vec::new();
547 + for _ in 0..*arity {
548 + args.push(input.parse()?);
383 549 }
384 - modifiers.push(modifier);
550 + modifiers.push(Modifier { name, args });
385 551 }
386 552
387 553 Ok(Self {
@@ -7581,6 +7581,16 @@
7581 7581 }
7582 7582 }
7583 7583
7584 + impl From<Act> for Node {
7585 + /// The wrapping every caller of a control-returning function writes by
7586 + /// hand. `From<Table>` below is the same courtesy for the same reason, and
7587 + /// a shape that returns a control has to be placeable in a body without
7588 + /// the caller naming the variant.
7589 + fn from(act: Act) -> Self {
7590 + Self::Act(act)
7591 + }
7592 + }
7593 +
7584 7594 impl From<Table> for Node {
7585 7595 fn from(table: Table) -> Self {
7586 7596 Self::Table {