Skip to content

Commit bb7eb03

Browse files
committed
yeast: Add support for output template choices
Adds a new syntactic form ``` [tree1 if {cond1} tree2 tree3 if {cond3} ] ``` to the language, the intended semantics being that we output the first tree that is usable. Two things can prevent a tree from being used: - the guard condition (a Rust expression evaluating to a boolean) may fail; - a string interpolation (e.g. `#{name}`) may fail to have a value. If all of the alternatives fail, then the result is `None`. This makes the syntax a generalisation of the `?` postfix operator that was removed in the previous commit. `(foo)?` can be replaced with a single-element alternative `[(foo)]`. As the changes to swift.rs show, this can clean up a bunch of constructions where we currently rely on helper functions or somewhat complicated Rust blocks. In terms of the implementation, we go through the alternatives in turn, first evaluating the guard condition. If this succeeds, we evaluate (and temporarily store) all of the `#{...}` interpolations, and if those all return `Some(...)` values, we instantiate the template itself. A benefit of this implementation compared to the one for `?` (which used named labels and `break`s) is that if either requirement fails, no AST nodes are constructed. (The `?` implementation would eagerly construct the tree, and then abort if any of the interpolations failed. This could leave orphaned nodes in the AST arena.) A choice cannot contain another choice as an immediate child (as that choice could simply have its branches inlined in the parent), but nesting further down is allow. In that case, the inner choice takes care of handling its internal "failures", and these do not bubble up to the outer choice.
1 parent c0a00a9 commit bb7eb03

8 files changed

Lines changed: 716 additions & 66 deletions

File tree

‎shared/yeast-macros/src/ast.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ pub(crate) enum Template {
100100
Node(Node),
101101
/// An embedded Rust block such as `{args}`.
102102
Splice(syn::Block),
103+
/// Ordered alternatives such as `[(identifier #{name}) (missing)]`.
104+
Choice(Vec<Alternative>),
105+
}
106+
107+
pub(crate) struct Alternative {
108+
/// Template to construct when the guard and dependencies succeed.
109+
pub(crate) template: Template,
110+
/// Optional postfix guard such as `if { name.is_some() }`.
111+
pub(crate) guard: Option<syn::Block>,
103112
}
104113

105114
pub(crate) struct Node {

‎shared/yeast-macros/src/lib.rs‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ pub fn query(input: TokenStream) -> TokenStream {
5252

5353
/// Build a single AST node from a template, returning its `OutputId`.
5454
///
55+
/// A top-level choice returns `Option<OutputId>`.
56+
///
5557
/// # Template syntax
5658
///
5759
/// ```text
@@ -65,8 +67,42 @@ pub fn query(input: TokenStream) -> TokenStream {
6567
/// field: {expr} - extend a named field with `{expr}`'s ids
6668
/// location: {expr} - override this node's source range with the
6769
/// span of one or more nodes; may be repeated
70+
/// field: [(kind ...)] - set the field only if the alternative can
71+
/// be constructed (see below)
72+
/// field: [(a) (b)] - use the first constructible alternative
73+
/// field: [(a) if {guard} (b)] - skip an alternative when its guard is false
74+
/// ```
75+
///
76+
/// # Template choices
77+
///
78+
/// Brackets contain one or more ordered alternatives. For each alternative,
79+
/// its optional `if {guard}` is evaluated first, followed by every `#{expr}`
80+
/// dependency beneath it. An absent dependency — an `Option` that is `None` —
81+
/// skips the alternative without constructing any of its template nodes. The
82+
/// first alternative whose guard and dependencies succeed is constructed:
83+
///
84+
/// ```text
85+
/// (break_expr label: {lbl.map(|l| tree!((identifier #{l})))}) // before
86+
/// (break_expr label: [(identifier #{lbl})]) // after
6887
/// ```
6988
///
89+
/// Dependencies may be nested arbitrarily deeply. A nested choice handles its
90+
/// own dependencies, so an inner absent value need not discard an outer node:
91+
///
92+
/// ```text
93+
/// (parameter pattern: [
94+
/// (name_pattern identifier: [(identifier #{name})])
95+
/// ] type: {ty})
96+
/// ```
97+
///
98+
/// Only `#{expr}` is treated as a dependency. A `{expr}` splice is unaffected,
99+
/// since yielding no ids already leaves its field unset. Outside a choice,
100+
/// interpolating an `Option` with `#{expr}` remains a compile error.
101+
///
102+
/// A choice used as a field or list item contributes zero or one node. A
103+
/// top-level choice passed to `tree!` returns `Option<OutputId>`; ordinary
104+
/// templates continue to return `OutputId`.
105+
///
70106
/// Uses the `BuildCtx` binding named `ctx` from the surrounding scope:
71107
///
72108
/// ```text

‎shared/yeast-macros/src/lower.rs‎

Lines changed: 173 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,33 @@
11
use proc_macro2::{Ident, Span, TokenStream};
22
use quote::quote;
3+
use std::cell::RefCell;
4+
use std::sync::atomic::{AtomicUsize, Ordering};
35

4-
use crate::ast::{Cardinality, Content, Field, Node, Pattern, Replacement, Rule, Template};
6+
use crate::ast::{
7+
Alternative, Cardinality, Content, Field, Node, Pattern, Replacement, Rule, Template,
8+
};
9+
10+
#[derive(Clone, Copy, Default)]
11+
struct LoweringContext<'a> {
12+
dependency_scope: Option<DependencyScope<'a>>,
13+
}
14+
15+
#[derive(Clone, Copy)]
16+
struct DependencyScope<'a> {
17+
preparations: &'a RefCell<Vec<DependencyPreparation>>,
18+
}
19+
20+
struct DependencyPreparation {
21+
value: Ident,
22+
source_range: Ident,
23+
expression: TokenStream,
24+
}
25+
26+
fn fresh_prepared_ident(suffix: &str) -> Ident {
27+
static COUNTER: AtomicUsize = AtomicUsize::new(0);
28+
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
29+
Ident::new(&format!("__yeast_prepared_{suffix}_{n}"), Span::call_site())
30+
}
531

632
#[derive(Clone)]
733
struct CaptureBinding {
@@ -273,15 +299,19 @@ fn capture_bindings<'a>(
273299

274300
impl Template {
275301
pub(crate) fn lower_root(&self) -> TokenStream {
276-
self.lower()
302+
match self {
303+
Template::Choice(alternatives) => Self::lower_choice(alternatives),
304+
_ => self.lower_single(LoweringContext::default()),
305+
}
277306
}
278307

279308
pub(crate) fn lower_list(templates: &[Self]) -> TokenStream {
309+
let context = LoweringContext::default();
280310
let items = templates
281311
.iter()
282312
.map(|template| match template {
283313
Template::Node(node) => {
284-
let node = node.lower();
314+
let node = node.lower(context);
285315
quote! { __nodes.push(#node); }
286316
}
287317
Template::Splice(block) => {
@@ -292,6 +322,14 @@ impl Template {
292322
ctx.extend_field_ids(__value, &mut __nodes);
293323
}
294324
}
325+
Template::Choice(alternatives) => {
326+
let pick = Self::lower_choice(alternatives);
327+
quote! {
328+
if let ::std::option::Option::Some(__id) = #pick {
329+
__nodes.push(__id);
330+
}
331+
}
332+
}
295333
})
296334
.collect::<Vec<_>>();
297335

@@ -304,19 +342,92 @@ impl Template {
304342
}
305343
}
306344

307-
fn lower(&self) -> TokenStream {
345+
fn lower_single(&self, context: LoweringContext<'_>) -> TokenStream {
308346
match self {
309-
Template::Node(node) => node.lower(),
347+
Template::Node(node) => node.lower(context),
310348
Template::Splice(block) => {
311349
let block = rust_block(block);
312350
quote! { ::std::convert::Into::<yeast::OutputId>::into(#block) }
313351
}
352+
Template::Choice(_) => {
353+
unreachable!("choices are lowered through Template::lower_choice")
354+
}
314355
}
315356
}
357+
358+
fn lower_choice(alternatives: &[Alternative]) -> TokenStream {
359+
let pick = alternatives.iter().rev().fold(
360+
quote! { ::std::option::Option::None },
361+
|fallback, alternative| {
362+
let candidate = alternative.lower_candidate();
363+
quote! {
364+
{
365+
let __yeast_candidate = #candidate;
366+
if __yeast_candidate.is_some() {
367+
__yeast_candidate
368+
} else {
369+
#fallback
370+
}
371+
}
372+
}
373+
},
374+
);
375+
quote! {{ #pick }}
376+
}
377+
}
378+
379+
impl Alternative {
380+
fn lower_candidate(&self) -> TokenStream {
381+
let preparations = RefCell::new(Vec::new());
382+
let context = LoweringContext {
383+
dependency_scope: Some(DependencyScope {
384+
preparations: &preparations,
385+
}),
386+
};
387+
let value = self.template.lower_single(context);
388+
let candidate = quote! { ::std::option::Option::Some(#value) };
389+
390+
// Nest dependency matches around construction in reverse, so they are
391+
// evaluated in source order and construction runs only after all have
392+
// produced values.
393+
let candidate = preparations.into_inner().into_iter().rev().fold(
394+
candidate,
395+
|candidate, preparation| {
396+
let DependencyPreparation {
397+
value,
398+
source_range,
399+
expression,
400+
} = preparation;
401+
// Do not use `map`/`and_then` here: `candidate` may contain
402+
// embedded user Rust, and putting it in a closure would change
403+
// the target of `return` and the result type seen by `?`.
404+
quote! {
405+
match #expression {
406+
::std::option::Option::Some((#value, #source_range)) => #candidate,
407+
::std::option::Option::None => ::std::option::Option::None,
408+
}
409+
}
410+
},
411+
);
412+
let candidate = if let Some(guard) = &self.guard {
413+
let guard = rust_block(guard);
414+
quote! {
415+
if #guard {
416+
#candidate
417+
} else {
418+
::std::option::Option::None
419+
}
420+
}
421+
} else {
422+
candidate
423+
};
424+
425+
candidate
426+
}
316427
}
317428

318429
impl Node {
319-
fn lower(&self) -> TokenStream {
430+
fn lower(&self, context: LoweringContext<'_>) -> TokenStream {
320431
let ctx = Ident::new("ctx", Span::call_site());
321432
let kind_str = self.kind.to_string();
322433

@@ -325,19 +436,54 @@ impl Node {
325436
Content::Static(lit) => quote! { #ctx.literal(#kind_str, #lit) },
326437
Content::Computed(block) => {
327438
let block = rust_block(block);
328-
quote! {
329-
{
330-
let __expr = #block;
331-
let __value =
332-
yeast::YeastDisplay::yeast_to_string(&__expr, &#ctx);
333-
let __source_range =
334-
yeast::YeastSourceRange::yeast_source_range(&__expr, &#ctx);
439+
if let Some(scope) = context.dependency_scope {
440+
let value = fresh_prepared_ident("value");
441+
let source_range = fresh_prepared_ident("source_range");
442+
scope.preparations.borrow_mut().push(DependencyPreparation {
443+
value: value.clone(),
444+
source_range: source_range.clone(),
445+
expression: quote! {
446+
{
447+
let __expr = #block;
448+
yeast::MaybeYeastValue::maybe_yeast_value(&__expr).map(
449+
|__value_ref| {
450+
(
451+
yeast::YeastDisplay::yeast_to_string(
452+
__value_ref,
453+
&#ctx,
454+
),
455+
yeast::YeastSourceRange::yeast_source_range(
456+
__value_ref,
457+
&#ctx,
458+
),
459+
)
460+
},
461+
)
462+
}
463+
},
464+
});
465+
quote! {
335466
#ctx.literal_with_source_range(
336467
#kind_str,
337-
&__value,
338-
__source_range,
468+
&#value,
469+
#source_range,
339470
)
340471
}
472+
} else {
473+
quote! {
474+
{
475+
let __expr = #block;
476+
let __value =
477+
yeast::YeastDisplay::yeast_to_string(&__expr, &#ctx);
478+
let __source_range =
479+
yeast::YeastSourceRange::yeast_source_range(&__expr, &#ctx);
480+
#ctx.literal_with_source_range(
481+
#kind_str,
482+
&__value,
483+
__source_range,
484+
)
485+
}
486+
}
341487
}
342488
}
343489
};
@@ -384,8 +530,19 @@ impl Node {
384530
}
385531
});
386532
}
533+
Template::Choice(alternatives) => {
534+
let pick = Template::lower_choice(alternatives);
535+
stmts.push(quote! {
536+
let #temp: ::std::option::Option<yeast::OutputId> = #pick;
537+
});
538+
field_args.push(quote! {
539+
if let ::std::option::Option::Some(__id) = #temp {
540+
__fields.push((#field_str, vec![__id]));
541+
}
542+
});
543+
}
387544
Template::Node(child) => {
388-
let value = child.lower();
545+
let value = child.lower(context);
389546
stmts.push(quote! { let #temp: yeast::OutputId = #value; });
390547
field_args.push(quote! {
391548
__fields.push((#field_str, vec![#temp]));

0 commit comments

Comments
 (0)