Skip to content

Commit d9e3204

Browse files
committed
yeast: Clone ctx for each interpolation
This prevents mutations in one output template interpolation from affecting sibling output.
1 parent 1a8dbb2 commit d9e3204

3 files changed

Lines changed: 147 additions & 41 deletions

File tree

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

Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,11 @@ impl Template {
304304
}
305305
Template::Splice(block) => {
306306
let block = rust_block(block);
307-
quote! {
307+
isolated_interpolation(quote! {
308308
let __value = ::std::iter::IntoIterator::into_iter(#block)
309309
.collect::<Vec<_>>();
310310
ctx.extend_field_ids(__value, &mut __nodes);
311-
}
311+
})
312312
}
313313
})
314314
.collect::<Vec<_>>();
@@ -327,7 +327,9 @@ impl Template {
327327
Template::Node(node) => node.lower(context),
328328
Template::Splice(block) => {
329329
let block = rust_block(block);
330-
quote! { ::std::convert::Into::<yeast::OutputId>::into(#block) }
330+
isolated_interpolation(quote! {
331+
::std::convert::Into::<yeast::OutputId>::into(#block)
332+
})
331333
}
332334
}
333335
}
@@ -344,40 +346,36 @@ impl Node {
344346
Content::Computed(block) => {
345347
let block = rust_block(block);
346348
if let Some(label) = context.fallible_scope {
347-
quote! {
348-
{
349-
let __expr = #block;
350-
let ::std::option::Option::Some(__value_ref) =
351-
yeast::MaybeYeastValue::maybe_yeast_value(&__expr)
352-
else {
353-
break #label ::std::option::Option::None;
354-
};
355-
let __value =
356-
yeast::YeastDisplay::yeast_to_string(__value_ref, &#ctx);
357-
let __source_range =
358-
yeast::YeastSourceRange::yeast_source_range(__value_ref, &#ctx);
359-
#ctx.literal_with_source_range(
360-
#kind_str,
361-
&__value,
362-
__source_range,
363-
)
364-
}
365-
}
349+
isolated_interpolation(quote! {
350+
let __expr = #block;
351+
let ::std::option::Option::Some(__value_ref) =
352+
yeast::MaybeYeastValue::maybe_yeast_value(&__expr)
353+
else {
354+
break #label ::std::option::Option::None;
355+
};
356+
let __value =
357+
yeast::YeastDisplay::yeast_to_string(__value_ref, &#ctx);
358+
let __source_range =
359+
yeast::YeastSourceRange::yeast_source_range(__value_ref, &#ctx);
360+
#ctx.literal_with_source_range(
361+
#kind_str,
362+
&__value,
363+
__source_range,
364+
)
365+
})
366366
} else {
367-
quote! {
368-
{
369-
let __expr = #block;
370-
let __value =
371-
yeast::YeastDisplay::yeast_to_string(&__expr, &#ctx);
372-
let __source_range =
373-
yeast::YeastSourceRange::yeast_source_range(&__expr, &#ctx);
374-
#ctx.literal_with_source_range(
375-
#kind_str,
376-
&__value,
377-
__source_range,
378-
)
379-
}
380-
}
367+
isolated_interpolation(quote! {
368+
let __expr = #block;
369+
let __value =
370+
yeast::YeastDisplay::yeast_to_string(&__expr, &#ctx);
371+
let __source_range =
372+
yeast::YeastSourceRange::yeast_source_range(&__expr, &#ctx);
373+
#ctx.literal_with_source_range(
374+
#kind_str,
375+
&__value,
376+
__source_range,
377+
)
378+
})
381379
}
382380
}
383381
};
@@ -393,11 +391,11 @@ impl Node {
393391
match field {
394392
Field::Location { block } => {
395393
let block = rust_block(block);
396-
stmts.push(quote! {
394+
stmts.push(isolated_interpolation(quote! {
397395
let __value = ::std::iter::IntoIterator::into_iter(#block)
398396
.collect::<Vec<_>>();
399397
#ctx.extend_location_ranges(__value, &mut __location_ranges);
400-
});
398+
}));
401399
}
402400
Field::Named {
403401
name,
@@ -418,12 +416,15 @@ impl Node {
418416
Template::Splice(block) => {
419417
debug_assert!(!optional);
420418
let block = rust_block(block);
421-
stmts.push(quote! {
422-
let mut #temp: Vec<yeast::OutputId> = Vec::new();
419+
let interpolation = isolated_interpolation(quote! {
423420
let __value = ::std::iter::IntoIterator::into_iter(#block)
424421
.collect::<Vec<_>>();
425422
#ctx.extend_field_ids(__value, &mut #temp);
426423
});
424+
stmts.push(quote! {
425+
let mut #temp: Vec<yeast::OutputId> = Vec::new();
426+
#interpolation
427+
});
427428
field_args.push(quote! {
428429
if !#temp.is_empty() {
429430
__fields.push((#field_str, #temp));
@@ -486,3 +487,10 @@ fn rust_block(block: &syn::Block) -> TokenStream {
486487
let statements = &block.stmts;
487488
quote!({ #(#statements)* })
488489
}
490+
491+
fn isolated_interpolation(body: TokenStream) -> TokenStream {
492+
quote! {{
493+
let mut ctx = ctx.cloned_user_ctx();
494+
#body
495+
}}
496+
}

‎shared/yeast/src/build.rs‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,39 @@ pub struct BuildCtx<'a, C: 'a = ()> {
5656
created_nodes_without_source_range: BTreeSet<OutputId>,
5757
}
5858

59+
#[doc(hidden)]
60+
/// RAII guard that gives an output-template interpolation its own user context
61+
/// while preserving the surrounding lexical scope for control flow.
62+
///
63+
/// It temporarily swaps a clone into the existing [`BuildCtx`] and restores
64+
/// the original context on drop.
65+
pub struct ClonedUserCtx<'ctx, 'a, C: 'a> {
66+
ctx: &'ctx mut BuildCtx<'a, C>,
67+
original_user_ctx: Option<C>,
68+
}
69+
70+
impl<'a, C> std::ops::Deref for ClonedUserCtx<'_, 'a, C> {
71+
type Target = BuildCtx<'a, C>;
72+
73+
fn deref(&self) -> &Self::Target {
74+
self.ctx
75+
}
76+
}
77+
78+
impl<C> std::ops::DerefMut for ClonedUserCtx<'_, '_, C> {
79+
fn deref_mut(&mut self) -> &mut Self::Target {
80+
self.ctx
81+
}
82+
}
83+
84+
impl<C> Drop for ClonedUserCtx<'_, '_, C> {
85+
fn drop(&mut self) {
86+
if let Some(original_user_ctx) = self.original_user_ctx.take() {
87+
*self.ctx.user_ctx = original_user_ctx;
88+
}
89+
}
90+
}
91+
5992
impl<'a, C> BuildCtx<'a, C> {
6093
pub fn new(ast: &'a Ast, output: &'a mut OutputAst, user_ctx: &'a mut C) -> Self {
6194
Self {
@@ -263,7 +296,17 @@ impl<'a, C> BuildCtx<'a, C> {
263296
}
264297
}
265298

266-
impl<C: Clone> BuildCtx<'_, C> {
299+
impl<'a, C: Clone> BuildCtx<'a, C> {
300+
#[doc(hidden)]
301+
pub fn cloned_user_ctx(&mut self) -> ClonedUserCtx<'_, 'a, C> {
302+
let cloned_user_ctx = (*self.user_ctx).clone();
303+
let original_user_ctx = std::mem::replace(self.user_ctx, cloned_user_ctx);
304+
ClonedUserCtx {
305+
ctx: self,
306+
original_user_ctx: Some(original_user_ctx),
307+
}
308+
}
309+
267310
#[doc(hidden)]
268311
pub fn extend_field_ids<V: IntoFieldIds<C>>(&mut self, value: V, out: &mut Vec<OutputId>) {
269312
value.extend_into(self, out);

‎shared/yeast/tests/test.rs‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,61 @@ fn test_tree_builder() {
713713
);
714714
}
715715

716+
#[derive(Clone, Default)]
717+
struct InterpolationContext {
718+
counter: usize,
719+
}
720+
721+
#[test]
722+
fn test_output_template_interpolations_clone_user_context() {
723+
let input = "x = 1";
724+
let input_ast = parse_input(input);
725+
let mut ast = empty_output_ast(input);
726+
let mut user_ctx = InterpolationContext::default();
727+
let mut ctx = yeast::build::BuildCtx::new(&input_ast, &mut ast, &mut user_ctx);
728+
let source = yeast::tree!((identifier "source"));
729+
730+
let nodes = yeast::trees!(
731+
(call
732+
location: {
733+
ctx.counter += 1;
734+
source
735+
}
736+
method: {
737+
ctx.counter += 1;
738+
let value = ctx.counter.to_string();
739+
ctx.literal("identifier", &value)
740+
}
741+
receiver: (identifier #{
742+
ctx.counter += 1;
743+
ctx.counter
744+
})
745+
)
746+
{
747+
ctx.counter += 1;
748+
let value = ctx.counter.to_string();
749+
[ctx.literal("identifier", &value)]
750+
}
751+
);
752+
753+
assert_eq!(ctx.counter, 0);
754+
assert_eq!(nodes.len(), 2);
755+
assert_dump_eq(
756+
&dump_ast(ctx.output, nodes[0], input),
757+
r#"
758+
call
759+
method: identifier "1"
760+
receiver: identifier "1"
761+
"#,
762+
);
763+
assert_dump_eq(
764+
&dump_ast(ctx.output, nodes[1], input),
765+
r#"
766+
identifier "1"
767+
"#,
768+
);
769+
}
770+
716771
/// Builds `(assignment left: … right: (integer #{value})?)`, where a `None`
717772
/// `value` leaves `right` unset rather than producing an `integer` with no
718773
/// content.

0 commit comments

Comments
 (0)