Skip to content

Commit a46fb66

Browse files
committed
yeast: Add internal AST for tree templates
Adds a structured representation of tree templates, with a lowering operation for turning them into Rust code. Thus, instead of generating the code directly inside the rule parser, we now create the internal AST, and then lower it into Rust code. This makes the boundary between parsing and code generation cleaner and easier to test.
1 parent 6046d76 commit a46fb66

5 files changed

Lines changed: 375 additions & 214 deletions

File tree

‎shared/yeast-macros/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ proc-macro = true
99
[dependencies]
1010
proc-macro2 = "1.0"
1111
quote = "1.0"
12-
syn = "3.0"
12+
syn = { version = "3.0", features = ["full"] }

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use proc_macro2::{Ident, Literal};
2+
3+
pub(crate) enum Template {
4+
/// A parenthesized node template such as `(call method: {name})`.
5+
Node(Node),
6+
/// An embedded Rust block such as `{args}`.
7+
Splice(syn::Block),
8+
}
9+
10+
pub(crate) struct Node {
11+
/// The node kind at the start of the template, such as `call` in `(call)`.
12+
pub(crate) kind: Ident,
13+
/// Optional leaf content such as `"foo"` or `#{name}` in
14+
/// `(identifier "foo")` or `(identifier #{name})`.
15+
pub(crate) content: Option<Content>,
16+
/// Named fields such as `method: {name}` in `(call method: {name})`.
17+
pub(crate) fields: Vec<Field>,
18+
}
19+
20+
pub(crate) enum Content {
21+
/// Static leaf content such as `"foo"` in `(identifier "foo")`.
22+
Static(Literal),
23+
/// Computed leaf content such as `#{name}` in `(identifier #{name})`.
24+
Computed(syn::Block),
25+
}
26+
27+
pub(crate) enum Field {
28+
/// A named field such as `argument: {args}` or
29+
/// `label: (identifier #{name})?`.
30+
Named {
31+
/// The field name before `:`, such as `argument`.
32+
name: Ident,
33+
/// The node or Rust splice after `:`, such as `(identifier "x")` or
34+
/// `{args}`.
35+
value: Template,
36+
/// Whether the field value has a trailing `?`.
37+
optional: bool,
38+
},
39+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use proc_macro::TokenStream;
22
use proc_macro2::TokenStream as TokenStream2;
33

4+
mod ast;
5+
mod lower;
46
mod parse;
57

68
/// Proc macro for constructing a `QueryNode` from a tree-sitter-inspired pattern.

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

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
use proc_macro2::{Ident, Span, TokenStream};
2+
use quote::quote;
3+
use std::sync::atomic::{AtomicUsize, Ordering};
4+
use syn::Lifetime;
5+
6+
use crate::ast::{Content, Field, Node, Template};
7+
8+
struct LoweringContext<'a> {
9+
/// Enclosing optional-field label to exit when a computed value is absent.
10+
fallible_scope: Option<&'a Lifetime>,
11+
}
12+
13+
fn fresh_fallible_label() -> Lifetime {
14+
static COUNTER: AtomicUsize = AtomicUsize::new(0);
15+
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
16+
Lifetime::new(&format!("'__yeast_field_{n}"), Span::call_site())
17+
}
18+
19+
impl Template {
20+
pub(crate) fn lower_root(&self) -> TokenStream {
21+
self.lower(&LoweringContext {
22+
fallible_scope: None,
23+
})
24+
}
25+
26+
pub(crate) fn lower_list(templates: &[Self]) -> TokenStream {
27+
let context = LoweringContext {
28+
fallible_scope: None,
29+
};
30+
let items = templates
31+
.iter()
32+
.map(|template| match template {
33+
Template::Node(node) => {
34+
let node = node.lower(&context);
35+
quote! { __nodes.push(#node); }
36+
}
37+
Template::Splice(block) => {
38+
let block = rust_block(block);
39+
quote! {
40+
yeast::IntoFieldIds::extend_into(#block, &mut __nodes);
41+
}
42+
}
43+
})
44+
.collect::<Vec<_>>();
45+
46+
quote! {
47+
{
48+
let mut __nodes: Vec<yeast::Id> = Vec::new();
49+
#(#items)*
50+
__nodes
51+
}
52+
}
53+
}
54+
55+
fn lower(&self, context: &LoweringContext<'_>) -> TokenStream {
56+
match self {
57+
Template::Node(node) => node.lower(context),
58+
Template::Splice(block) => {
59+
let block = rust_block(block);
60+
quote! { ::std::convert::Into::<yeast::Id>::into(#block) }
61+
}
62+
}
63+
}
64+
}
65+
66+
impl Node {
67+
fn lower(&self, context: &LoweringContext<'_>) -> TokenStream {
68+
let ctx = Ident::new("ctx", Span::call_site());
69+
let kind_str = self.kind.to_string();
70+
71+
if let Some(content) = &self.content {
72+
return match content {
73+
Content::Static(lit) => quote! { #ctx.literal(#kind_str, #lit) },
74+
Content::Computed(block) => {
75+
let block = rust_block(block);
76+
if let Some(label) = context.fallible_scope {
77+
quote! {
78+
{
79+
let __expr = #block;
80+
let ::std::option::Option::Some(__value_ref) =
81+
yeast::MaybeYeastValue::maybe_yeast_value(&__expr)
82+
else {
83+
break #label ::std::option::Option::None;
84+
};
85+
let __value =
86+
yeast::YeastDisplay::yeast_to_string(__value_ref, &*#ctx.ast);
87+
let __source_range =
88+
yeast::YeastSourceRange::yeast_source_range(__value_ref, &*#ctx.ast);
89+
#ctx.literal_with_source_range(
90+
#kind_str,
91+
&__value,
92+
__source_range,
93+
)
94+
}
95+
}
96+
} else {
97+
quote! {
98+
{
99+
let __expr = #block;
100+
let __value =
101+
yeast::YeastDisplay::yeast_to_string(&__expr, &*#ctx.ast);
102+
let __source_range =
103+
yeast::YeastSourceRange::yeast_source_range(&__expr, &*#ctx.ast);
104+
#ctx.literal_with_source_range(
105+
#kind_str,
106+
&__value,
107+
__source_range,
108+
)
109+
}
110+
}
111+
}
112+
}
113+
};
114+
}
115+
116+
let mut stmts = Vec::new();
117+
let mut field_args = Vec::new();
118+
119+
for (field_counter, field) in self.fields.iter().enumerate() {
120+
let Field::Named {
121+
name,
122+
value,
123+
optional,
124+
} = field;
125+
let field_name = name.to_string();
126+
let field_str = field_name
127+
.strip_prefix("r#")
128+
.unwrap_or(&field_name)
129+
.to_string();
130+
let temp = Ident::new(
131+
&format!("__field_{field_str}_{field_counter}"),
132+
Span::call_site(),
133+
);
134+
135+
match value {
136+
Template::Splice(block) => {
137+
debug_assert!(!optional);
138+
let block = rust_block(block);
139+
stmts.push(quote! {
140+
let mut #temp: Vec<yeast::Id> = Vec::new();
141+
yeast::IntoFieldIds::extend_into(#block, &mut #temp);
142+
});
143+
field_args.push(quote! {
144+
if !#temp.is_empty() {
145+
__fields.push((#field_str, #temp));
146+
}
147+
});
148+
}
149+
Template::Node(child) if *optional => {
150+
let label = fresh_fallible_label();
151+
let value = child.lower(&LoweringContext {
152+
fallible_scope: Some(&label),
153+
});
154+
stmts.push(quote! {
155+
#[allow(unused_labels)]
156+
let #temp: ::std::option::Option<yeast::Id> = #label: {
157+
::std::option::Option::Some(#value)
158+
};
159+
});
160+
field_args.push(quote! {
161+
if let ::std::option::Option::Some(__id) = #temp {
162+
__fields.push((#field_str, vec![__id]));
163+
}
164+
});
165+
}
166+
Template::Node(child) => {
167+
let value = child.lower(context);
168+
stmts.push(quote! { let #temp: yeast::Id = #value; });
169+
field_args.push(quote! {
170+
__fields.push((#field_str, vec![#temp]));
171+
});
172+
}
173+
}
174+
}
175+
176+
quote! {
177+
{
178+
#(#stmts)*
179+
let mut __fields: Vec<(&str, Vec<yeast::Id>)> = Vec::new();
180+
#(#field_args)*
181+
#ctx.node(#kind_str, __fields)
182+
}
183+
}
184+
}
185+
}
186+
187+
fn rust_block(block: &syn::Block) -> TokenStream {
188+
let statements = &block.stmts;
189+
quote!({ #(#statements)* })
190+
}

0 commit comments

Comments
 (0)