Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_scene/macros/src/bsn/codegen.rs
30638 views
1
use crate::bsn::types::{
2
Bsn, BsnConstructor, BsnEntry, BsnFields, BsnListRoot, BsnRelatedSceneList, BsnRoot, BsnScene,
3
BsnSceneFn, BsnSceneFnArg, BsnSceneFnArgs, BsnSceneListItem, BsnSceneListItems, BsnType,
4
BsnValue,
5
};
6
use bevy_macro_utils::{fq_std::FQDefault, path_to_string};
7
use proc_macro2::TokenStream;
8
use quote::{format_ident, quote, ToTokens};
9
use std::collections::{hash_map::Entry, HashMap, HashSet};
10
use syn::{parse::Parse, punctuated::Punctuated, ExprTuple, Ident, Index, Lit, Member, Path};
11
12
/// Tracks named entity references and assigns them unique, sequential indices
13
/// during the code generation process.
14
#[derive(Default)]
15
pub(crate) struct EntityRefs {
16
refs: HashMap<String, usize>,
17
next: usize,
18
}
19
20
impl EntityRefs {
21
/// Retrieves the index for a given entity name.
22
/// Creates a new one if it hasn't been seen yet.
23
fn get(&mut self, name: String) -> usize {
24
match self.refs.entry(name) {
25
Entry::Occupied(entry) => *entry.get(),
26
Entry::Vacant(entry) => {
27
let index = self.next;
28
entry.insert(index);
29
self.next += 1;
30
index
31
}
32
}
33
}
34
}
35
36
#[derive(Default)]
37
pub(crate) struct HoistedExpressions {
38
expressions: Vec<TokenStream>,
39
index: usize,
40
}
41
42
impl HoistedExpressions {
43
fn next_ident(&mut self) -> Ident {
44
let index = self.index;
45
let ident = format_ident!("_expr{index}");
46
self.index += 1;
47
ident
48
}
49
50
pub fn hoist(&mut self, value: &BsnValue) -> Ident {
51
let ident = self.next_ident();
52
self.expressions.push(quote! {let #ident = #value;});
53
ident
54
}
55
}
56
57
/// Context used in the [`Bsn`] code generation pipeline.
58
/// Used to accumulate validation errors without short-circuiting.
59
pub(crate) struct BsnCodegenCtx<'a> {
60
pub bevy_scene: &'a Path,
61
pub bevy_ecs: &'a Path,
62
pub invocation_index: ExprTuple,
63
pub entity_refs: &'a mut EntityRefs,
64
pub hoisted_expressions: &'a mut HoistedExpressions,
65
/// Accumulated parsing and validation errors.
66
pub errors: Vec<syn::Error>,
67
}
68
69
/// Represents the target path and whether it is a reference, e.g.,
70
/// when applying a template patch.
71
struct PatchTarget<'a> {
72
/// The path to the field being patched.
73
pub path: &'a [Member],
74
/// Whether the target is a reference.
75
/// - `true`: Requires dereferencing (`*`) to assign a value to the target.
76
/// - `false`: Requires a mutable borrow (`&mut`) to create a temporary
77
/// reference.
78
pub is_ref: bool,
79
}
80
81
pub trait BsnTokenStream: Parse {
82
fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream;
83
}
84
85
impl BsnTokenStream for BsnRoot {
86
fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream {
87
let tokens = self.0.to_tokens(ctx);
88
let errors = ctx.errors.iter().map(|e| e.to_compile_error());
89
let bevy_scene = ctx.bevy_scene;
90
let hoisted_exprs = ctx.hoisted_expressions.expressions.drain(..);
91
92
// NOTE: Assigning the result to a variable first so that the LSP's
93
// type inference can see assignments before it encounters
94
// any compile errors. This keeps autocomplete working in broken states,
95
// e.g. when typing the name of a field but no value yet.
96
quote! {
97
#bevy_scene::SceneScope({
98
#(#hoisted_exprs)*
99
let _res = #tokens;
100
#(#errors)*
101
_res
102
})
103
}
104
}
105
}
106
107
impl BsnTokenStream for BsnListRoot {
108
fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream {
109
let tokens = self.0.to_tokens(ctx);
110
let errors = ctx.errors.iter().map(|e| e.to_compile_error());
111
let bevy_scene = ctx.bevy_scene;
112
let hoisted_exprs = ctx.hoisted_expressions.expressions.drain(..);
113
114
// NOTE: Assigning the result to a variable first so that the LSP's
115
// type inference can see assignments before it encounters
116
// any compile errors. This keeps autocomplete working in broken states,
117
// e.g. when typing the name of a field but no value yet.
118
quote! {
119
{
120
#(#hoisted_exprs)*
121
let _res = #bevy_scene::SceneListScope(#tokens);
122
#(#errors)*
123
_res
124
}
125
}
126
}
127
}
128
129
impl<const ALLOW_FLAT: bool> Bsn<ALLOW_FLAT> {
130
/// Converts to tokens and performs validation checks.
131
/// Accumulates errors in [`BsnCodegenCtx`].
132
pub fn try_to_tokens(&self, ctx: &mut BsnCodegenCtx) -> syn::Result<TokenStream> {
133
let bevy_scene = ctx.bevy_scene;
134
let mut combined_patches = Vec::new();
135
let mut scene_impls = Vec::new();
136
for entry in &self.entries {
137
match entry.try_to_tokens(ctx) {
138
Ok(EntryResult::CombinedSceneFunction(patch)) => combined_patches.push(patch),
139
Ok(EntryResult::NewSceneImpl(scene_impl)) => {
140
if !combined_patches.is_empty() {
141
let patches = combined_patches.drain(..);
142
scene_impls.push(quote! {
143
#bevy_scene::SceneFunction(move |_context, _scene| {
144
#(#patches)*
145
})
146
});
147
}
148
scene_impls.push(scene_impl)
149
}
150
Err(err) => scene_impls.push(err.to_compile_error()),
151
}
152
}
153
if !combined_patches.is_empty() {
154
let patches = combined_patches.drain(..);
155
scene_impls.push(quote! {
156
#bevy_scene::SceneFunction(move |_context, _scene| {
157
#(#patches)*
158
})
159
});
160
}
161
Ok(quote! { #bevy_scene::auto_nest_tuple!(#(#scene_impls),*) })
162
}
163
164
pub fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream {
165
self.try_to_tokens(ctx)
166
.unwrap_or_else(|e| e.to_compile_error())
167
}
168
}
169
170
enum EntryResult {
171
CombinedSceneFunction(TokenStream),
172
NewSceneImpl(TokenStream),
173
}
174
175
impl BsnEntry {
176
fn try_to_tokens(&self, ctx: &mut BsnCodegenCtx) -> syn::Result<EntryResult> {
177
let (bevy_scene, bevy_ecs) = (ctx.bevy_scene, ctx.bevy_ecs);
178
179
Ok(match self {
180
BsnEntry::TemplatePatch(ty) => {
181
let mut assigns = Vec::new();
182
let target = PatchTarget {
183
path: &[Member::Named(Ident::new(
184
"value",
185
proc_macro2::Span::call_site(),
186
))],
187
is_ref: true,
188
};
189
ty.to_patch_tokens(ctx, &mut assigns, true, false, true, target)?;
190
let path = &ty.path;
191
EntryResult::CombinedSceneFunction(if assigns.is_empty() {
192
quote! {
193
let _ = _scene.get_or_insert_template::<#path>(_context);
194
}
195
} else {
196
quote! {
197
let value = _scene.get_or_insert_template::<#path>(_context);
198
#(#assigns)*
199
}
200
})
201
}
202
BsnEntry::FromTemplatePatch(ty) => {
203
let mut assigns = Vec::new();
204
let target = PatchTarget {
205
path: &[Member::Named(Ident::new(
206
"value",
207
proc_macro2::Span::call_site(),
208
))],
209
is_ref: true,
210
};
211
ty.to_patch_tokens(ctx, &mut assigns, true, false, false, target)?;
212
let path = &ty.path;
213
EntryResult::CombinedSceneFunction(if assigns.is_empty() {
214
quote! {
215
let _ = _scene.get_or_insert_template::<<#path as #bevy_ecs::template::FromTemplate>::Template>(_context);
216
}
217
} else {
218
quote! {
219
let value = _scene.get_or_insert_template::<<#path as #bevy_ecs::template::FromTemplate>::Template>(_context);
220
#(#assigns)*
221
}
222
})
223
}
224
BsnEntry::TemplateConst {
225
type_path,
226
const_ident,
227
} => EntryResult::CombinedSceneFunction(quote! {
228
let value = _scene.get_or_insert_template::<#type_path>(_context);
229
*value = #type_path::#const_ident;
230
}),
231
BsnEntry::TemplateConstructor(BsnConstructor {
232
type_path,
233
function,
234
args,
235
}) => EntryResult::CombinedSceneFunction({
236
let args = args.to_tokens(ctx);
237
quote! {
238
let value = _scene.get_or_insert_template::<#type_path>(_context);
239
*value = #type_path::#function(#args);
240
}
241
}),
242
BsnEntry::FromTemplateConstructor(BsnConstructor {
243
type_path,
244
function,
245
args,
246
}) => EntryResult::CombinedSceneFunction({
247
let args = args.to_tokens(ctx);
248
quote! {
249
let value = _scene.get_or_insert_template::<<#type_path as #bevy_ecs::template::FromTemplate>::Template>(_context);
250
*value = <#type_path as #bevy_ecs::template::FromTemplate>::Template::#function(#args);
251
}
252
}),
253
BsnEntry::RelatedSceneList(BsnRelatedSceneList {
254
scene_list,
255
relationship_path,
256
}) => {
257
let scenes = scene_list.0.to_tokens(ctx);
258
EntryResult::NewSceneImpl(quote! {
259
#bevy_scene::RelatedScenes::<<#relationship_path as #bevy_ecs::relationship::RelationshipTarget>
260
::Relationship, _>::new(#scenes)
261
})
262
}
263
BsnEntry::UncachedScene(s) => EntryResult::NewSceneImpl(s.to_tokens(ctx)?),
264
BsnEntry::CachedScene(s) => EntryResult::NewSceneImpl(s.to_tokens(ctx)?),
265
BsnEntry::Name(ident) => {
266
let (name, index) = (ident.to_string(), ctx.entity_refs.get(ident.to_string()));
267
let invocation = ctx.invocation_index.clone();
268
EntryResult::CombinedSceneFunction(quote! {
269
#bevy_scene::NameEntityReference { name: #bevy_ecs::name::Name(#name.into()), reference: #bevy_ecs::template::SceneEntityReference::new(#invocation, #index) }.resolve_inline(_context, _scene);
270
})
271
}
272
BsnEntry::NameExpression(expr) => EntryResult::CombinedSceneFunction(quote! {
273
let value = _scene.get_or_insert_template::<<#bevy_ecs::name::Name as #bevy_ecs::template::FromTemplate>::Template>(_context);
274
*value = #bevy_ecs::name::Name({#expr}.into());
275
}),
276
})
277
}
278
}
279
280
impl BsnScene {
281
fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> syn::Result<TokenStream> {
282
let bevy_scene = ctx.bevy_scene;
283
match self {
284
BsnScene::Asset(lit) => Ok(quote! {
285
#bevy_scene::CachedSceneAsset::from(#lit)
286
}),
287
BsnScene::Fn(func) => Ok(func.to_tokens(ctx)),
288
BsnScene::SceneComponent(bsn_type) => {
289
// TODO: this can and should use a simpler codegen path than BsnType::to_patch_tokens,
290
// which imposes constraints like requiring the type to impl FromTemplate, and requiring
291
// enums to have VariantDefault.
292
let mut assignments = Vec::new();
293
let props = format_ident!("props");
294
let props_ref = format_ident!("props_ref");
295
let target = PatchTarget {
296
path: &[Member::Named(props_ref.clone())],
297
is_ref: true,
298
};
299
bsn_type.to_patch_tokens(ctx, &mut assignments, false, true, true, target)?;
300
let mut assigns = Vec::new();
301
let target = PatchTarget {
302
path: &[Member::Named(Ident::new(
303
"value",
304
proc_macro2::Span::call_site(),
305
))],
306
is_ref: true,
307
};
308
bsn_type.to_patch_tokens(ctx, &mut assigns, true, false, true, target)?;
309
let path = &bsn_type.path;
310
let bevy_scene = ctx.bevy_scene;
311
let from_template_patch = quote! {
312
<#path as #bevy_scene::PatchFromTemplate>::patch(move |value, _context| {
313
#(#assigns)*
314
})
315
};
316
Ok(quote! {{
317
let mut #props = <<#path as #bevy_scene::SceneComponent>::Props as #FQDefault>::default();
318
let #props_ref = &mut #props;
319
#(#assignments)*
320
(<#path as #bevy_scene::SceneComponent>::scene(#props), #from_template_patch)
321
}})
322
}
323
BsnScene::Expression(tokens) => Ok(quote! {
324
#tokens
325
}),
326
}
327
}
328
}
329
330
impl BsnType {
331
/// Recursively generates token streams.
332
fn to_patch_tokens(
333
&self,
334
ctx: &mut BsnCodegenCtx,
335
assignments: &mut Vec<TokenStream>,
336
is_root: bool,
337
is_props: bool,
338
is_scene_component: bool,
339
target: PatchTarget,
340
) -> syn::Result<()> {
341
if !is_root {
342
let (path, bevy_scene) = (&self.path, ctx.bevy_scene);
343
assignments.push(quote! {#bevy_scene::macro_utils::touch_type::<#path>();});
344
}
345
346
if let Some(variant) = &self.enum_variant {
347
if is_props {
348
self.push_struct_patch(ctx, assignments, true, is_scene_component, target)?;
349
} else {
350
self.push_enum_patch(ctx, variant, assignments, target)?;
351
}
352
} else {
353
self.push_struct_patch(ctx, assignments, is_props, is_scene_component, target)?;
354
}
355
356
Ok(())
357
}
358
359
fn push_enum_patch(
360
&self,
361
ctx: &mut BsnCodegenCtx,
362
variant: &Ident,
363
assignments: &mut Vec<TokenStream>,
364
target: PatchTarget,
365
) -> syn::Result<()> {
366
let (bevy_scene, bevy_ecs, path) = (ctx.bevy_scene, ctx.bevy_ecs, &self.path);
367
let variant_default = format_ident!("default_{}", variant.to_string().to_lowercase());
368
let template_path = quote! { #bevy_scene::macro_utils::PathResolveHelper::<<#path as #bevy_ecs::template::FromTemplate>::Template> };
369
370
let maybe_deref = target.is_ref.then(|| quote! {*});
371
let maybe_borrow_mut = (!target.is_ref).then(|| quote! {&mut});
372
let field_path = target.path;
373
374
let (check_pattern, binding_pattern, field_updates) = match &self.fields {
375
BsnFields::Named(fields) => {
376
let mut seen = HashSet::with_capacity(fields.len());
377
let mut names = Vec::new();
378
let mut assigns = Vec::new();
379
380
for field in fields {
381
let field_name = &field.name;
382
if !seen.insert(field_name.to_string()) {
383
ctx.errors.push(syn::Error::new_spanned(
384
field_name,
385
format!("Duplicate field `{}` found in BSN enum variant", field_name),
386
));
387
continue;
388
}
389
390
names.push(field_name);
391
392
assigns.push(self.process_enum_field(ctx, field_name, field.value.as_ref())?);
393
}
394
395
(
396
quote! { #variant { .. } },
397
quote! { #variant { #(#names,)* .. } },
398
assigns,
399
)
400
}
401
BsnFields::Tuple(fields) if fields.is_empty() => {
402
(quote! { #variant }, quote! { #variant }, vec![])
403
}
404
BsnFields::Tuple(fields) => {
405
let names: Vec<_> = (0..fields.len()).map(|i| format_ident!("t{}", i)).collect();
406
let assigns = fields
407
.iter()
408
.enumerate()
409
.map(|(i, f)| self.process_enum_field(ctx, &names[i], Some(&f.value)))
410
.collect::<syn::Result<Vec<_>>>()?;
411
412
(
413
quote! { #variant(..) },
414
quote! { #variant(#(#names,)* ..) },
415
assigns,
416
)
417
}
418
};
419
420
assignments.push(quote! {
421
{
422
let _node = #maybe_borrow_mut #(#field_path).*;
423
if !::core::matches!(_node, #template_path::#check_pattern) {
424
#maybe_deref _node = #template_path::#variant_default();
425
}
426
if let #template_path::#binding_pattern = _node {
427
#(#field_updates)*
428
}
429
}
430
});
431
Ok(())
432
}
433
434
fn push_struct_patch(
435
&self,
436
ctx: &mut BsnCodegenCtx,
437
assignments: &mut Vec<TokenStream>,
438
is_props: bool,
439
is_scene_component: bool,
440
target: PatchTarget,
441
) -> syn::Result<()> {
442
match &self.fields {
443
BsnFields::Named(fields) => {
444
let mut seen = HashSet::with_capacity(fields.len());
445
446
for field in fields {
447
let field_name = &field.name;
448
if is_props != field.is_prop {
449
if !is_scene_component && field.is_prop {
450
let type_path = &self.path;
451
ctx.errors.push(syn::Error::new_spanned(
452
field_name,
453
format!(
454
"Scene prop fields are not supported in normal component patches\
455
. If you would like to set a component scene's prop field, it \
456
should be set using \"scene component\" syntax: \
457
bsn! {{ @{} {{ @{field_name}: VALUE }} }}",
458
path_to_string(type_path)
459
),
460
));
461
}
462
continue;
463
}
464
if !seen.insert(field_name.to_string()) {
465
ctx.errors.push(syn::Error::new_spanned(
466
field_name,
467
format!("Duplicate field `{}` found in BSN struct", field_name),
468
));
469
continue;
470
}
471
472
if field.value.is_none() {
473
ctx.errors.push(syn::Error::new_spanned(
474
field_name,
475
format!("Field `{}` is missing a value.", field_name),
476
));
477
}
478
479
let path = if field.is_prop {
480
&[Member::Named(format_ident!("props"))]
481
} else {
482
target.path
483
};
484
485
self.process_field(
486
ctx,
487
assignments,
488
path,
489
Member::Named(field_name.clone()),
490
field.value.as_ref(),
491
)?;
492
}
493
}
494
BsnFields::Tuple(fields) => {
495
// Tuple fields can't be props
496
if is_props {
497
return Ok(());
498
}
499
for (i, field) in fields.iter().enumerate() {
500
if let Err(err) = self.process_field(
501
ctx,
502
assignments,
503
target.path,
504
Member::Unnamed(Index::from(i)),
505
Some(&field.value),
506
) {
507
ctx.errors.push(err);
508
}
509
}
510
}
511
}
512
Ok(())
513
}
514
515
fn process_field(
516
&self,
517
ctx: &mut BsnCodegenCtx,
518
assignments: &mut Vec<TokenStream>,
519
base_path: &[Member],
520
member: Member,
521
value: Option<&BsnValue>,
522
) -> syn::Result<()> {
523
match value {
524
// Enables field autocomplete in Rust Analyzer
525
Some(
526
BsnValue::Ident(_) | BsnValue::Expr(_) | BsnValue::Closure(_) | BsnValue::Tuple(_),
527
)
528
| None => {
529
// NOTE: It is very important to still produce outputs for None field values. This is what
530
// enables field autocomplete in Rust Analyzer
531
assignments.push(
532
value
533
.map(|v| {
534
let ident = ctx.hoisted_expressions.hoist(v);
535
quote! { #(#base_path.)*#member = #ident; }
536
})
537
.unwrap_or(quote! {
538
#(#base_path.)*#member;
539
}),
540
);
541
}
542
Some(BsnValue::Lit(_)) => {
543
// value is Some
544
let value = value.unwrap();
545
assignments.push(quote! { #(#base_path.)*#member = #value; });
546
}
547
Some(BsnValue::Name(ident)) => {
548
let index = ctx.entity_refs.get(ident.to_string());
549
let bevy_ecs = ctx.bevy_ecs;
550
let invocation = ctx.invocation_index.clone();
551
assignments.push(quote! {
552
#(#base_path.)*#member = #bevy_ecs::template::EntityTemplate::SceneEntityReference(#bevy_ecs::template::SceneEntityReference::new(#invocation, #index));
553
});
554
}
555
Some(BsnValue::NameExpression(tokens)) => {
556
assignments.push(quote! {
557
#(#base_path.)*#member = #tokens.into();
558
});
559
}
560
Some(BsnValue::Type(ty)) if ty.enum_variant.is_some() => {
561
assignments.push(quote! {#(#base_path.)*#member = #ty;});
562
}
563
Some(BsnValue::Type(ty)) => {
564
let mut new_path = base_path.to_vec();
565
new_path.push(member);
566
ty.to_patch_tokens(
567
ctx,
568
assignments,
569
false,
570
false,
571
false,
572
PatchTarget {
573
path: &new_path,
574
is_ref: false,
575
},
576
)?;
577
}
578
}
579
Ok(())
580
}
581
582
fn process_enum_field(
583
&self,
584
ctx: &mut BsnCodegenCtx,
585
bind_name: &Ident,
586
value: Option<&BsnValue>,
587
) -> syn::Result<TokenStream> {
588
if value.is_none() {
589
ctx.errors.push(syn::Error::new_spanned(
590
bind_name,
591
format!("Enum field `{}` is missing a value", bind_name),
592
));
593
}
594
595
if let Some(BsnValue::Type(ty)) = value
596
&& ty.enum_variant.is_none()
597
{
598
let mut type_assigns = Vec::new();
599
ty.to_patch_tokens(
600
ctx,
601
&mut type_assigns,
602
false,
603
false,
604
false,
605
PatchTarget {
606
path: &[Member::Named(bind_name.clone())],
607
is_ref: true,
608
},
609
)?;
610
return Ok(quote! {#(#type_assigns)*});
611
}
612
613
// NOTE: It is very important to still produce outputs for None field values. This is what
614
// enables field autocomplete in Rust Analyzer
615
value
616
.map(|v| Ok(quote! { *#bind_name = #v; }))
617
.unwrap_or(Ok(quote! { #bind_name; }))
618
}
619
}
620
621
impl BsnTokenStream for BsnSceneListItems {
622
fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream {
623
let bevy_scene = ctx.bevy_scene;
624
let scenes = self.0.iter().map(|s| match s {
625
BsnSceneListItem::Scene(bsn) => {
626
let tokens = bsn.to_tokens(ctx);
627
quote! {#bevy_scene::EntityScene(#tokens)}
628
}
629
BsnSceneListItem::Expression(stmts) => quote! {#(#stmts)*},
630
});
631
632
quote! { #bevy_scene::auto_nest_tuple!(#(#scenes),*) }
633
}
634
}
635
636
impl BsnTokenStream for BsnSceneFnArg {
637
fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream {
638
let bevy_ecs = ctx.bevy_ecs;
639
match self {
640
BsnSceneFnArg::Expr(expr) => quote! {#expr},
641
BsnSceneFnArg::NameExpression(tokens) => {
642
quote! {#bevy_ecs::template::EntityTemplate::Entity(#tokens)}
643
}
644
BsnSceneFnArg::Name(ident) => {
645
let index = ctx.entity_refs.get(ident.to_string());
646
let invocation = ctx.invocation_index.clone();
647
quote! {
648
#bevy_ecs::template::EntityTemplate::SceneEntityReference(
649
#bevy_ecs::template::SceneEntityReference::new(#invocation, #index)
650
)
651
}
652
}
653
}
654
}
655
}
656
657
impl BsnTokenStream for BsnSceneFnArgs {
658
fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream {
659
let mut args: Punctuated<_, syn::Token![,]> = Punctuated::new();
660
// rebuilding the punctuated is required because ctx needs to be passed
661
for arg in self.0.iter().flatten() {
662
args.push(arg.to_tokens(ctx));
663
}
664
quote! {#args}
665
}
666
}
667
impl BsnSceneFn {
668
fn to_tokens(&self, ctx: &mut BsnCodegenCtx) -> TokenStream {
669
let bevy_scene = ctx.bevy_scene;
670
let args = self.args.to_tokens(ctx);
671
let path = self.path.clone();
672
quote! {#bevy_scene::SceneScope(#path(#args))}
673
}
674
}
675
676
impl ToTokens for BsnType {
677
fn to_tokens(&self, tokens: &mut TokenStream) {
678
let (path, variant) = (
679
&self.path,
680
self.enum_variant.as_ref().map(|v| quote! {::#v}),
681
);
682
match &self.fields {
683
BsnFields::Named(fields) => {
684
let assigns = fields.iter().map(|f| {
685
let (name, value) = (&f.name, &f.value);
686
quote! {#name: #value}
687
});
688
quote! { #path #variant { #(#assigns,)* } }
689
}
690
BsnFields::Tuple(fields) => {
691
let assigns = fields.iter().map(|f| &f.value);
692
quote! { #path #variant ( #(#assigns,)* ) }
693
}
694
}
695
.to_tokens(tokens);
696
}
697
}
698
699
impl ToTokens for BsnValue {
700
fn to_tokens(&self, tokens: &mut TokenStream) {
701
match self {
702
BsnValue::Expr(e) => quote! {{#e}.into()}.to_tokens(tokens),
703
BsnValue::Closure(c) => quote! {(#c).into()}.to_tokens(tokens),
704
BsnValue::Ident(i) => quote! {(#i).into()}.to_tokens(tokens),
705
BsnValue::Lit(Lit::Str(s)) => quote! {#s.into()}.to_tokens(tokens),
706
BsnValue::Lit(l) => {
707
if l.suffix().is_empty() {
708
l.to_tokens(tokens)
709
} else {
710
quote! {(#l).into()}.to_tokens(tokens)
711
}
712
}
713
BsnValue::Tuple(t) => {
714
let inner = t.0.iter();
715
quote! {(#(#inner),*)}.to_tokens(tokens);
716
}
717
BsnValue::Type(ty) => ty.to_tokens(tokens),
718
BsnValue::Name(_) | BsnValue::NameExpression(_) => {
719
// Name requires additional context to convert to tokens
720
unreachable!()
721
}
722
}
723
}
724
}
725
726
#[cfg(test)]
727
mod tests {
728
use super::*;
729
use crate::bsn::types::*;
730
use syn::parse_quote;
731
732
struct TestPaths {
733
bevy_scene: Path,
734
bevy_ecs: Path,
735
}
736
737
impl TestPaths {
738
fn new() -> Self {
739
Self {
740
bevy_scene: parse_quote!(bevy_scene),
741
bevy_ecs: parse_quote!(bevy_ecs),
742
}
743
}
744
745
fn ctx<'a>(
746
&'a self,
747
refs: &'a mut EntityRefs,
748
hoisted_expressions: &'a mut HoistedExpressions,
749
) -> BsnCodegenCtx<'a> {
750
BsnCodegenCtx {
751
bevy_scene: &self.bevy_scene,
752
bevy_ecs: &self.bevy_ecs,
753
entity_refs: refs,
754
invocation_index: parse_quote!(("", 0, 0)),
755
hoisted_expressions,
756
errors: Vec::new(),
757
}
758
}
759
}
760
761
#[test]
762
fn duplicate_field() {
763
let mut refs = EntityRefs::default();
764
let paths = TestPaths::new();
765
let mut exprs = HoistedExpressions::default();
766
let mut ctx = paths.ctx(&mut refs, &mut exprs);
767
let mut assignments = vec![];
768
let duplicate = BsnType {
769
path: parse_quote!(Transform),
770
enum_variant: None,
771
fields: BsnFields::Named(vec![
772
BsnNamedField {
773
name: parse_quote!(x),
774
value: Some(BsnValue::Expr(quote!({}))),
775
is_prop: false,
776
},
777
BsnNamedField {
778
name: parse_quote!(x),
779
value: Some(BsnValue::Expr(quote!({}))),
780
is_prop: false,
781
},
782
]),
783
};
784
785
let res = duplicate.push_struct_patch(
786
&mut ctx,
787
&mut assignments,
788
false,
789
false,
790
PatchTarget {
791
path: &[],
792
is_ref: false,
793
},
794
);
795
796
assert!(res.is_ok());
797
assert_eq!(ctx.errors.len(), 1);
798
assert!(ctx.errors[0]
799
.to_string()
800
.contains("Duplicate field `x` found in BSN struct"));
801
}
802
803
#[test]
804
fn recursive_duplicate_field() {
805
let mut refs = EntityRefs::default();
806
let paths = TestPaths::new();
807
let mut exprs = HoistedExpressions::default();
808
let mut ctx = paths.ctx(&mut refs, &mut exprs);
809
let mut assignments = vec![];
810
let nested_duplicate = BsnType {
811
path: parse_quote!(Parent),
812
enum_variant: None,
813
fields: BsnFields::Named(vec![BsnNamedField {
814
is_prop: false,
815
name: parse_quote!(child_field),
816
value: Some(BsnValue::Type(BsnType {
817
path: parse_quote!(Child),
818
enum_variant: None,
819
fields: BsnFields::Named(vec![
820
BsnNamedField {
821
name: parse_quote!(x),
822
value: Some(BsnValue::Expr(quote!({}))),
823
is_prop: false,
824
},
825
BsnNamedField {
826
name: parse_quote!(x),
827
value: Some(BsnValue::Expr(quote!({}))),
828
is_prop: false,
829
},
830
]),
831
})),
832
}]),
833
};
834
835
let res = nested_duplicate.to_patch_tokens(
836
&mut ctx,
837
&mut assignments,
838
true,
839
false,
840
false,
841
PatchTarget {
842
path: &[],
843
is_ref: false,
844
},
845
);
846
847
assert!(res.is_ok());
848
assert_eq!(ctx.errors.len(), 1);
849
assert!(ctx.errors[0]
850
.to_string()
851
.contains("Duplicate field `x` found in BSN struct"));
852
}
853
854
#[test]
855
fn missing_struct_field() {
856
let mut refs = EntityRefs::default();
857
let paths = TestPaths::new();
858
let mut exprs = HoistedExpressions::default();
859
let mut ctx = paths.ctx(&mut refs, &mut exprs);
860
let mut assignments = Vec::new();
861
let missing = BsnType {
862
path: parse_quote!(Transform),
863
enum_variant: None,
864
fields: BsnFields::Named(vec![BsnNamedField {
865
is_prop: false,
866
name: parse_quote!(x),
867
value: None,
868
}]),
869
};
870
871
let res = missing.push_struct_patch(
872
&mut ctx,
873
&mut assignments,
874
false,
875
false,
876
PatchTarget {
877
path: &[Member::Named(parse_quote!(value))],
878
is_ref: false,
879
},
880
);
881
882
assert!(res.is_ok());
883
assert_eq!(ctx.errors.len(), 1);
884
assert!(ctx.errors[0]
885
.to_string()
886
.contains("Field `x` is missing a value"));
887
}
888
889
#[test]
890
fn enum_duplicate_field() {
891
// Arrange
892
let mut refs = EntityRefs::default();
893
let paths = TestPaths::new();
894
let mut exprs = HoistedExpressions::default();
895
let mut ctx = paths.ctx(&mut refs, &mut exprs);
896
let mut assignments = vec![];
897
let duplicate = BsnType {
898
path: parse_quote!(MyEnum),
899
enum_variant: Some(parse_quote!(Variant)),
900
fields: BsnFields::Named(vec![
901
BsnNamedField {
902
is_prop: false,
903
name: parse_quote!(x),
904
value: Some(BsnValue::Expr(quote!(1))),
905
},
906
BsnNamedField {
907
is_prop: false,
908
name: parse_quote!(x),
909
value: Some(BsnValue::Expr(quote!(2))),
910
},
911
]),
912
};
913
914
// Act
915
let res = duplicate.push_enum_patch(
916
&mut ctx,
917
&parse_quote!(Variant),
918
&mut assignments,
919
PatchTarget {
920
path: &[],
921
is_ref: false,
922
},
923
);
924
925
// Assert
926
assert!(res.is_ok());
927
assert_eq!(ctx.errors.len(), 1);
928
assert!(ctx.errors[0]
929
.to_string()
930
.contains("Duplicate field `x` found in BSN enum variant"));
931
}
932
933
#[test]
934
fn bsn_root_preserves_inference_on_error() {
935
// Arrange
936
let expected = "bevy_scene :: SceneScope ({ let _res = bevy_scene :: auto_nest_tuple \
937
! () ; :: core :: compile_error ! { \"Test Error\" } _res })";
938
939
let mut refs = EntityRefs::default();
940
let paths = TestPaths::new();
941
let mut exprs = HoistedExpressions::default();
942
let mut ctx = paths.ctx(&mut refs, &mut exprs);
943
ctx.errors.push(syn::Error::new(
944
proc_macro2::Span::call_site(),
945
"Test Error",
946
));
947
let root = BsnRoot(Bsn::<true> { entries: vec![] });
948
949
// Act
950
let res = root.to_tokens(&mut ctx).to_string();
951
952
// Assert
953
assert_eq!(res, expected,);
954
}
955
956
#[test]
957
fn bsn_list_root_preserves_inference_on_error() {
958
// Arrange
959
let expected =
960
"{ let _res = bevy_scene :: SceneListScope (bevy_scene :: auto_nest_tuple ! ()) ;"
961
.to_string()
962
+ " :: core :: compile_error ! { \"Test Error\" }"
963
+ " _res }";
964
965
let mut refs = EntityRefs::default();
966
let paths = TestPaths::new();
967
let mut exprs = HoistedExpressions::default();
968
let mut ctx = paths.ctx(&mut refs, &mut exprs);
969
ctx.errors.push(syn::Error::new(
970
proc_macro2::Span::call_site(),
971
"Test Error",
972
));
973
let root = BsnListRoot(BsnSceneListItems(vec![]));
974
975
// Act
976
let res = root.to_tokens(&mut ctx).to_string();
977
978
// Assert
979
assert_eq!(res, expected,);
980
}
981
}
982
983