Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions shared/yeast-macros/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ pub(crate) enum Template {
Node(Node),
/// An embedded Rust block such as `{args}`.
Splice(syn::Block),
/// Ordered alternatives such as `[(identifier #{name}) (missing)]`.
Choice(Vec<Alternative>),
}

pub(crate) struct Alternative {
/// Template to construct when the guard and dependencies succeed.
pub(crate) template: Template,
/// Optional postfix guard such as `if { name.is_some() }`.
pub(crate) guard: Option<syn::Block>,
}

pub(crate) struct Node {
Expand All @@ -121,15 +130,13 @@ pub(crate) enum Content {

pub(crate) enum Field {
/// A named field such as `argument: {args}` or
/// `label: (identifier #{name})?`.
/// `label: (identifier #{name})`.
Named {
/// The field name before `:`, such as `argument`.
name: Ident,
/// The node or Rust splice after `:`, such as `(identifier "x")` or
/// `{args}`.
value: Template,
/// Whether the field value has a trailing `?`.
optional: bool,
},
/// Source-location metadata such as `location: {source}`.
Location {
Expand Down
41 changes: 25 additions & 16 deletions shared/yeast-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ pub fn query(input: TokenStream) -> TokenStream {

/// Build a single AST node from a template, returning its `OutputId`.
///
/// A top-level choice returns `Option<OutputId>`.
///
/// # Template syntax
///
/// ```text
Expand All @@ -63,36 +65,43 @@ pub fn query(input: TokenStream) -> TokenStream {
/// `Option<OutputId>`, iterator chains) splice
/// their elements
/// field: {expr} - extend a named field with `{expr}`'s ids
/// field: (kind ...)? - set the field only if every `#{expr}`
/// beneath it has a value (see below)
/// location: {expr} - override this node's source range with the
/// span of one or more nodes; may be repeated
/// field: [(kind ...)] - set the field only if the alternative can
/// be constructed (see below)
/// field: [(a) (b)] - use the first constructible alternative
/// field: [(a) if {guard} (b)] - skip an alternative when its guard is false
/// ```
///
/// # Optional fields
/// # Template choices
///
/// A `?` on a field's value makes that field fallible: if a `#{expr}`
/// anywhere beneath it interpolates an absent value — an `Option` that is
/// `None` — the subtree is abandoned and the field is left unset. This
/// replaces the surrounding `Option::map` that would otherwise be needed:
/// Brackets contain one or more ordered alternatives. For each alternative,
/// its optional `if {guard}` is evaluated first, followed by every `#{expr}`
/// dependency beneath it. An absent dependency — an `Option` that is `None` —
/// skips the alternative without constructing any of its template nodes. The
/// first alternative whose guard and dependencies succeed is constructed:
///
/// ```text
/// (break_expr label: {lbl.map(|l| tree!((identifier #{l})))}) // before
/// (break_expr label: (identifier #{lbl})?) // after
/// (break_expr label: [(identifier #{lbl})]) // after
/// ```
///
/// The value may be nested arbitrarily deeply, and a nested `?` catches
/// first, so an inner absent value need not discard the outer node:
/// Dependencies may be nested arbitrarily deeply. A nested choice handles its
/// own dependencies, so an inner absent value need not discard an outer node:
///
/// ```text
/// (parameter pattern: (name_pattern identifier: (identifier #{name}))? type: {ty})
/// (parameter pattern: [
/// (name_pattern identifier: [(identifier #{name})])
/// ] type: {ty})
/// ```
///
/// Only `#{expr}` propagates absence. A `{expr}` splice is unaffected, since
/// yielding no ids already leaves a field unset, and `?` is rejected on one.
/// Outside a `?`, interpolating an `Option` with `#{expr}` remains a compile
/// error, so the choice between "unset the field" and "unwrap it" stays
/// explicit.
/// Only `#{expr}` is treated as a dependency. A `{expr}` splice is unaffected,
/// since yielding no ids already leaves its field unset. Outside a choice,
/// interpolating an `Option` with `#{expr}` remains a compile error.
///
/// A choice used as a field or list item contributes zero or one node. A
/// top-level choice passed to `tree!` returns `Option<OutputId>`; ordinary
/// templates continue to return `OutputId`.
///
/// Uses the `BuildCtx` binding named `ctx` from the surrounding scope:
///
Expand Down
202 changes: 153 additions & 49 deletions shared/yeast-macros/src/lower.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use std::cell::RefCell;
use std::sync::atomic::{AtomicUsize, Ordering};
use syn::Lifetime;

use crate::ast::{Cardinality, Content, Field, Node, Pattern, Replacement, Rule, Template};
use crate::ast::{
Alternative, Cardinality, Content, Field, Node, Pattern, Replacement, Rule, Template,
};

#[derive(Clone, Copy, Default)]
struct LoweringContext<'a> {
/// Enclosing optional-field label to exit when a computed value is absent.
fallible_scope: Option<&'a Lifetime>,
dependency_scope: Option<DependencyScope<'a>>,
}

fn fresh_fallible_label() -> Lifetime {
#[derive(Clone, Copy)]
struct DependencyScope<'a> {
preparations: &'a RefCell<Vec<DependencyPreparation>>,
}

struct DependencyPreparation {
value: Ident,
source_range: Ident,
expression: TokenStream,
}

fn fresh_prepared_ident(suffix: &str) -> Ident {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
Lifetime::new(&format!("'__yeast_field_{n}"), Span::call_site())
Ident::new(&format!("__yeast_prepared_{suffix}_{n}"), Span::call_site())
}

#[derive(Clone)]
Expand Down Expand Up @@ -286,20 +299,19 @@ fn capture_bindings<'a>(

impl Template {
pub(crate) fn lower_root(&self) -> TokenStream {
self.lower(&LoweringContext {
fallible_scope: None,
})
match self {
Template::Choice(alternatives) => Self::lower_choice(alternatives),
_ => self.lower_single(LoweringContext::default()),
}
}

pub(crate) fn lower_list(templates: &[Self]) -> TokenStream {
let context = LoweringContext {
fallible_scope: None,
};
let context = LoweringContext::default();
let items = templates
.iter()
.map(|template| match template {
Template::Node(node) => {
let node = node.lower(&context);
let node = node.lower(context);
quote! { __nodes.push(#node); }
}
Template::Splice(block) => {
Expand All @@ -310,6 +322,14 @@ impl Template {
ctx.extend_field_ids(__value, &mut __nodes);
}
}
Template::Choice(alternatives) => {
let pick = Self::lower_choice(alternatives);
quote! {
if let ::std::option::Option::Some(__id) = #pick {
__nodes.push(__id);
}
}
}
})
.collect::<Vec<_>>();

Expand All @@ -322,19 +342,102 @@ impl Template {
}
}

fn lower(&self, context: &LoweringContext<'_>) -> TokenStream {
fn lower_single(&self, context: LoweringContext<'_>) -> TokenStream {
match self {
Template::Node(node) => node.lower(context),
Template::Splice(block) => {
let block = rust_block(block);
quote! { ::std::convert::Into::<yeast::OutputId>::into(#block) }
}
Template::Choice(_) => {
unreachable!("choices are lowered through Template::lower_choice")
}
}
}

fn lower_choice(alternatives: &[Alternative]) -> TokenStream {
let pick = alternatives.iter().rev().fold(
quote! { ::std::option::Option::None },
|fallback, alternative| {
alternative.lower_candidate(fallback)
},
);
quote! {{ #pick }}
}
}

impl Alternative {
fn lower_candidate(&self, fallback: TokenStream) -> TokenStream {
let preparations = RefCell::new(Vec::new());
let context = LoweringContext {
dependency_scope: Some(DependencyScope {
preparations: &preparations,
}),
};
let value = self.template.lower_single(context);
let preparations = preparations.into_inner();
let prepared_values = preparations
.iter()
.flat_map(|preparation| {
[
preparation.value.clone(),
preparation.source_range.clone(),
]
})
.collect::<Vec<_>>();
let prepared = quote! {
::std::option::Option::Some((#(#prepared_values),*))
};

// Nest dependency matches around preparation in reverse, so they are
// evaluated in source order. Construction happens only after the
// resulting option has been matched, keeping it in a separate branch
// from the fallback.
let prepared = preparations.into_iter().rev().fold(
prepared,
|candidate, preparation| {
let DependencyPreparation {
value,
source_range,
expression,
} = preparation;
// Do not use `map`/`and_then` here: `candidate` may contain
// embedded user Rust, and putting it in a closure would change
// the target of `return` and the result type seen by `?`.
quote! {
match #expression {
::std::option::Option::Some((#value, #source_range)) => #candidate,
::std::option::Option::None => ::std::option::Option::None,
}
}
},
);
let prepared = if let Some(guard) = &self.guard {
let guard = rust_block(guard);
quote! {
if #guard {
#prepared
} else {
::std::option::Option::None
}
}
} else {
prepared
};

quote! {
match #prepared {
::std::option::Option::Some((#(#prepared_values),*)) => {
::std::option::Option::Some(#value)
}
::std::option::Option::None => #fallback,
}
}
}
}

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

Expand All @@ -343,25 +446,38 @@ impl Node {
Content::Static(lit) => quote! { #ctx.literal(#kind_str, #lit) },
Content::Computed(block) => {
let block = rust_block(block);
if let Some(label) = context.fallible_scope {
if let Some(scope) = context.dependency_scope {
let value = fresh_prepared_ident("value");
let source_range = fresh_prepared_ident("source_range");
scope.preparations.borrow_mut().push(DependencyPreparation {
value: value.clone(),
source_range: source_range.clone(),
expression: quote! {
{
let __expr = #block;
yeast::MaybeYeastValue::maybe_yeast_value(&__expr).map(
|__value_ref| {
(
yeast::YeastDisplay::yeast_to_string(
__value_ref,
&#ctx,
),
yeast::YeastSourceRange::yeast_source_range(
__value_ref,
&#ctx,
),
)
},
)
}
},
});
quote! {
{
let __expr = #block;
let ::std::option::Option::Some(__value_ref) =
yeast::MaybeYeastValue::maybe_yeast_value(&__expr)
else {
break #label ::std::option::Option::None;
};
let __value =
yeast::YeastDisplay::yeast_to_string(__value_ref, &#ctx);
let __source_range =
yeast::YeastSourceRange::yeast_source_range(__value_ref, &#ctx);
#ctx.literal_with_source_range(
#kind_str,
&__value,
__source_range,
)
}
#ctx.literal_with_source_range(
#kind_str,
&#value,
#source_range,
)
}
} else {
quote! {
Expand Down Expand Up @@ -399,11 +515,7 @@ impl Node {
#ctx.extend_location_ranges(__value, &mut __location_ranges);
});
}
Field::Named {
name,
value,
optional,
} => {
Field::Named { name, value } => {
let field_name = name.to_string();
let field_str = field_name
.strip_prefix("r#")
Expand All @@ -413,10 +525,8 @@ impl Node {
&format!("__field_{field_str}_{field_counter}"),
Span::call_site(),
);

match value {
Template::Splice(block) => {
debug_assert!(!optional);
let block = rust_block(block);
stmts.push(quote! {
let mut #temp: Vec<yeast::OutputId> = Vec::new();
Expand All @@ -430,16 +540,10 @@ impl Node {
}
});
}
Template::Node(child) if *optional => {
let label = fresh_fallible_label();
let value = child.lower(&LoweringContext {
fallible_scope: Some(&label),
});
Template::Choice(alternatives) => {
let pick = Template::lower_choice(alternatives);
stmts.push(quote! {
#[allow(unused_labels)]
let #temp: ::std::option::Option<yeast::OutputId> = #label: {
::std::option::Option::Some(#value)
};
let #temp: ::std::option::Option<yeast::OutputId> = #pick;
});
field_args.push(quote! {
if let ::std::option::Option::Some(__id) = #temp {
Expand Down
Loading
Loading