Skip to content
Merged
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
133 changes: 94 additions & 39 deletions shared/yeast-macros/src/lower.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use quote::{quote, ToTokens};
use std::cell::RefCell;
use std::sync::atomic::{AtomicUsize, Ordering};

Expand Down Expand Up @@ -324,12 +324,16 @@ impl Template {
quote! { __nodes.push(#node); }
}
Template::Splice(block) => {
let uses_ctx = block_uses_ctx(block);
let block = rust_block(block);
quote! {
let __value = ::std::iter::IntoIterator::into_iter(#block)
.collect::<Vec<_>>();
ctx.extend_field_ids(__value, &mut __nodes);
}
isolated_interpolation(
uses_ctx,
quote! {
let __value = ::std::iter::IntoIterator::into_iter(#block)
.collect::<Vec<_>>();
ctx.extend_field_ids(__value, &mut __nodes);
},
)
}
Template::Choice(alternatives) => {
let pick = Self::lower_choice(alternatives);
Expand All @@ -355,8 +359,14 @@ impl Template {
match self {
Template::Node(node) => node.lower(context),
Template::Splice(block) => {
let uses_ctx = block_uses_ctx(block);
let block = rust_block(block);
quote! { ::std::convert::Into::<yeast::OutputId>::into(#block) }
isolated_interpolation(
uses_ctx,
quote! {
::std::convert::Into::<yeast::OutputId>::into(#block)
},
)
}
Template::Choice(_) => {
unreachable!("choices are lowered through Template::lower_choice")
Expand Down Expand Up @@ -453,35 +463,38 @@ impl Node {
let node = match content {
Content::Static(lit) => quote! { #ctx.literal(#kind_str, #lit) },
Content::Computed(block) => {
let uses_ctx = block_uses_ctx(block);
let block = rust_block(block);
if let Some(scope) = context.choice_alternative_scope {
let value = fresh_prepared_ident("value");
let source_range = fresh_prepared_ident("source_range");
let expression = isolated_interpolation(
uses_ctx,
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,
),
)
},
)
},
);
scope
.interpolations
.borrow_mut()
.push(GuardingInterpolation {
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,
),
)
},
)
}
},
expression,
});
quote! {
#ctx.literal_with_source_range(
Expand All @@ -491,8 +504,9 @@ impl Node {
)
}
} else {
quote! {
{
isolated_interpolation(
uses_ctx,
quote! {
let __expr = #block;
let __value =
yeast::YeastDisplay::yeast_to_string(&__expr, &#ctx);
Expand All @@ -503,8 +517,8 @@ impl Node {
&__value,
__source_range,
)
}
}
},
)
}
}
Content::Inherited => quote! { #ctx.inherited_literal(#kind_str) },
Expand All @@ -524,12 +538,16 @@ impl Node {
for (field_counter, field) in self.fields.iter().enumerate() {
match field {
Field::Location { block } => {
let uses_ctx = block_uses_ctx(block);
let block = rust_block(block);
stmts.push(quote! {
let __value = ::std::iter::IntoIterator::into_iter(#block)
.collect::<Vec<_>>();
#ctx.extend_location_ranges(__value, &mut __location_ranges);
});
stmts.push(isolated_interpolation(
uses_ctx,
quote! {
let __value = ::std::iter::IntoIterator::into_iter(#block)
.collect::<Vec<_>>();
#ctx.extend_location_ranges(__value, &mut __location_ranges);
},
));
}
Field::LocationNone => {}
Field::Named { name, value } => {
Expand All @@ -544,12 +562,19 @@ impl Node {
);
match value {
Template::Splice(block) => {
let uses_ctx = block_uses_ctx(block);
let block = rust_block(block);
let interpolation = isolated_interpolation(
uses_ctx,
quote! {
let __value = ::std::iter::IntoIterator::into_iter(#block)
.collect::<Vec<_>>();
#ctx.extend_field_ids(__value, &mut #temp);
},
);
stmts.push(quote! {
let mut #temp: Vec<yeast::OutputId> = Vec::new();
let __value = ::std::iter::IntoIterator::into_iter(#block)
.collect::<Vec<_>>();
#ctx.extend_field_ids(__value, &mut #temp);
#interpolation
});
field_args.push(quote! {
if !#temp.is_empty() {
Expand Down Expand Up @@ -612,3 +637,33 @@ fn rust_block(block: &syn::Block) -> TokenStream {
let statements = &block.stmts;
quote!({ #(#statements)* })
}

fn isolated_interpolation(uses_ctx: bool, body: TokenStream) -> TokenStream {
if uses_ctx {
quote! {{
let __saved_user_ctx = (*ctx.user_ctx).clone();
let __result = { #body };
*ctx.user_ctx = __saved_user_ctx;
__result
}}
} else {
quote! {{ #body }}
}
}

/// Returns whether `block` directly mentions `ctx`.
///
/// This lexical check cannot see context access hidden behind a helper or
/// macro. Such an interpolation can opt into isolation by explicitly
/// mentioning `ctx`, for example with `let _ = &ctx;`.
fn block_uses_ctx(block: &syn::Block) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe at a doc comment for this function that also mentions that this is an imperfect check?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e6205e0.

fn uses_ctx(tokens: TokenStream) -> bool {
tokens.into_iter().any(|token| match token {
TokenTree::Ident(ident) => ident == "ctx",
TokenTree::Group(group) => uses_ctx(group.stream()),
Comment thread
tausbn marked this conversation as resolved.
Resolved
TokenTree::Punct(_) | TokenTree::Literal(_) => false,
})
}

uses_ctx(block.to_token_stream())
}
147 changes: 147 additions & 0 deletions shared/yeast/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,153 @@ fn test_choice_fallback_can_reuse_consumed_splice() {
);
}

#[derive(Clone, Default)]
struct InterpolationContext {
counter: u64,
}

#[test]
fn test_output_template_interpolations_clone_user_context() {
let input = "x = 1";
let input_ast = parse_input(input);
let mut ast = empty_output_ast(input);
let mut user_ctx = InterpolationContext::default();
let mut ctx = yeast::build::BuildCtx::new(&input_ast, &mut ast, &mut user_ctx);
let source = yeast::tree!((identifier "source"));

let nodes = yeast::trees!(
(call
location: {
ctx.counter += 1;
source
}
method: {
ctx.counter += 1;
let value = ctx.counter.to_string();
ctx.literal("identifier", &value)
}
receiver: (identifier #{
ctx.counter += 1;
ctx.counter
})
)
{
ctx.counter += 1;
let value = ctx.counter.to_string();
[ctx.literal("identifier", &value)]
}
);

assert_eq!(ctx.counter, 0);
assert_eq!(nodes.len(), 2);
assert_dump_eq(
&dump_ast(ctx.output, nodes[0], input),
r#"
call
method: identifier "1"
receiver: identifier "1"
"#,
);
assert_dump_eq(
&dump_ast(ctx.output, nodes[1], input),
r#"
identifier "1"
"#,
);
}

#[test]
fn test_interpolation_preserves_build_ctx_method_borrowing() {
let input = "x = 1";
let input_ast = parse_input(input);
let mut ast = empty_output_ast(input);
let mut user_ctx = InterpolationContext::default();
let mut ctx = yeast::build::BuildCtx::new(&input_ast, &mut ast, &mut user_ctx);
let source = yeast::tree!((identifier "source"));

let node = yeast::tree!((call
method: {
ctx.literal("identifier", &ctx.source_text(source))
}
));

assert_dump_eq(
&dump_ast(ctx.output, node, input),
r#"
call
method: identifier "source"
"#,
);
}

#[test]
fn test_interpolation_preserves_user_context_assignment() {
let input = "x = 1";
let input_ast = parse_input(input);
let mut ast = empty_output_ast(input);
let mut user_ctx = InterpolationContext::default();
let mut ctx = yeast::build::BuildCtx::new(&input_ast, &mut ast, &mut user_ctx);

let node = yeast::tree!((identifier #{
*ctx = InterpolationContext { counter: 1 };
ctx.counter
}));

assert_eq!(ctx.counter, 0);
assert_dump_eq(
&dump_ast(ctx.output, node, input),
r#"
identifier "1"
"#,
);
}

#[test]
fn test_interpolation_accepts_borrowed_build_ctx() {
fn build_identifier(
ctx: &mut yeast::build::BuildCtx<'_, InterpolationContext>,
) -> yeast::OutputId {
yeast::tree!((identifier #{
ctx.counter += 1;
ctx.counter
}))
}

let input = "x = 1";
let input_ast = parse_input(input);
let mut ast = empty_output_ast(input);
let mut user_ctx = InterpolationContext::default();
let mut ctx = yeast::build::BuildCtx::new(&input_ast, &mut ast, &mut user_ctx);

let node = build_identifier(&mut ctx);
assert_eq!(ctx.counter, 0);
assert_dump_eq(
&dump_ast(ctx.output, node, input),
r#"
identifier "1"
"#,
);
}

#[derive(Clone, Default)]
struct BorrowedInterpolationContext {
locations: Vec<yeast::OutputId>,
}

#[test]
fn test_interpolation_accepts_value_borrowed_from_context() {
let input = "x = 1";
let input_ast = parse_input(input);
let mut ast = empty_output_ast(input);
let mut user_ctx = BorrowedInterpolationContext::default();
let mut ctx = yeast::build::BuildCtx::new(&input_ast, &mut ast, &mut user_ctx);
let location = yeast::tree!((identifier "location"));
ctx.locations.push(location);
let locations = ctx.locations.iter();

yeast::tree!((call location: {locations}));
}

#[test]
fn test_root_choice_returns_an_optional_node() {
let input = "x = 1";
Expand Down
Loading
Loading