1use crate::ast::*;
5use crate::expr::{FullExpr, TrBody};
6use proc_macro2::{Span, TokenStream};
7use syn::spanned::Spanned;
8use syn::{Attribute, Ident, Item, ItemFn, ItemMod, Meta};
9
10#[derive(Clone, Debug)]
12pub struct ProtocolArgs {
13 pub namespace: String,
14 pub version: u32,
15 pub span: Span,
16}
17
18#[derive(Clone, Debug)]
19pub struct FieldItem {
20 pub name: Ident,
21 pub ty: TypeRef,
22 pub syn_ty: syn::Type,
23 pub doc: String,
24 pub initial: Option<Expr>,
25 pub content: bool,
27 pub span: Span,
28}
29
30#[derive(Clone, Debug)]
31pub struct VariantItem {
32 pub name: Ident,
33 pub fields: Vec<FieldItem>,
34 pub label: Option<Tr>,
35 pub doc: String,
36 pub span: Span,
37}
38
39#[derive(Clone, Debug)]
40pub enum ValueKind {
41 Enum(Vec<VariantItem>),
42 Record(Vec<FieldItem>),
43}
44
45#[derive(Clone, Debug)]
47pub struct ValueItem {
48 pub name: Ident,
49 pub kind: ValueKind,
50 pub doc: String,
51 pub pages: Option<String>,
54 pub span: Span,
55}
56
57#[derive(Clone, Debug)]
58pub struct StateItem {
59 pub name: Ident,
60 pub fields: Vec<FieldItem>,
61 pub span: Span,
62}
63
64#[derive(Clone, Debug)]
65pub struct CommandParam {
66 pub name: Ident,
67 pub ty: TypeRef,
68 pub syn_ty: syn::Type,
69}
70
71#[derive(Clone, Debug)]
72pub struct CommandItem {
73 pub name: Ident,
74 pub params: Vec<CommandParam>,
75 pub result: TypeRef,
76 pub result_ty: syn::Type,
77 pub doc: String,
78 pub errors: Vec<String>,
79 pub idempotent: bool,
80 pub on: Option<(Ident, Ident)>,
83 pub span: Span,
84}
85
86#[derive(Clone, Debug)]
88pub struct EventItem {
89 pub name: Ident,
90 pub params: Vec<CommandParam>,
91 pub doc: String,
92 pub span: Span,
93}
94
95#[derive(Clone, Debug)]
96pub struct EventsItem {
97 pub name: Ident,
98 pub methods: Vec<EventItem>,
99 pub span: Span,
100}
101
102#[derive(Clone, Debug)]
103pub struct CommandsItem {
104 pub name: Ident,
105 pub methods: Vec<CommandItem>,
106 pub span: Span,
107}
108
109#[derive(Clone, Debug)]
111pub struct QueryItem {
112 pub name: Ident,
113 pub params: Vec<CommandParam>,
114 pub result: TypeRef,
116 pub result_ty: syn::Type,
117 pub doc: String,
118 pub errors: Vec<String>,
119 pub reads: Vec<Ident>,
121 pub span: Span,
122}
123
124#[derive(Clone, Debug)]
125pub struct QueriesItem {
126 pub name: Ident,
127 pub methods: Vec<QueryItem>,
128 pub span: Span,
129}
130
131#[derive(Clone, Debug)]
134pub struct ProtocolItem {
135 pub args: ProtocolArgs,
136 pub module: ItemMod,
137 pub doc: String,
138 pub values: Vec<ValueItem>,
139 pub state: StateItem,
140 pub commands: CommandsItem,
141 pub queries: Option<QueriesItem>,
142 pub events: Option<EventsItem>,
143 pub uses: Vec<syn::ItemUse>,
144}
145
146#[derive(Clone, Debug)]
148pub struct SessionItem {
149 pub name: Ident,
150 pub fields: Vec<FieldItem>,
151 pub doc: String,
152 pub span: Span,
153}
154
155#[derive(Clone, Debug)]
157pub struct PlayerField {
158 pub name: Ident,
159 pub doc: String,
160 pub span: Span,
161}
162
163#[derive(Clone, Debug)]
166pub struct PlayersItem {
167 pub name: Ident,
168 pub players: Vec<PlayerField>,
169 pub doc: String,
170 pub span: Span,
171}
172
173#[derive(Clone, Copy, Debug, PartialEq, Eq)]
174pub enum ParamKind {
175 Prop,
177 Session,
179}
180
181#[derive(Clone, Debug)]
182pub struct Param {
183 pub name: Ident,
184 pub ty: TypeRef,
185 pub syn_ty: syn::Type,
186 pub kind: ParamKind,
187 pub content: bool,
189 pub span: Span,
190}
191
192#[derive(Clone, Debug)]
194pub struct ComponentItem {
195 pub func: ItemFn,
196 pub name: Ident,
197 pub params: Vec<Param>,
198 pub body: UiBody,
199 pub features: Option<syn::Path>,
201 pub doc: String,
202 pub span: Span,
203}
204
205#[derive(Clone, Debug)]
208pub struct CommandRef {
209 pub name: String,
210 pub span: Span,
211}
212
213#[derive(Clone, Debug)]
215pub struct FeatureItem {
216 pub name: Ident,
217 pub doc: String,
218 pub commands: Vec<CommandRef>,
219 pub layouts: Vec<Ident>,
221 pub guaranteed: Option<syn::LitStr>,
222 pub exemptions: Vec<(Ident, syn::LitStr)>,
224 pub span: Span,
225}
226
227#[derive(Clone, Debug)]
229pub struct FeaturesItem {
230 pub name: Ident,
231 pub features: Vec<FeatureItem>,
232 pub doc: String,
233 pub span: Span,
234}
235
236#[derive(Clone, Debug, Default)]
239pub struct SourceItems {
240 pub protocols: Vec<(Vec<String>, ProtocolItem)>,
241 pub values: Vec<(Vec<String>, ValueItem)>,
242 pub sessions: Vec<(Vec<String>, SessionItem)>,
243 pub players: Vec<(Vec<String>, PlayersItem)>,
244 pub components: Vec<(Vec<String>, ComponentItem)>,
245 pub features: Vec<(Vec<String>, FeaturesItem)>,
246 pub styles: Vec<(Vec<String>, crate::StyleItem)>,
247}
248
249fn error(span: Span, message: impl std::fmt::Display) -> syn::Error {
250 syn::Error::new(span, message)
251}
252
253pub fn is_bern_attr(attr: &Attribute, name: &str) -> bool {
255 let path = attr.path();
256 let segments: Vec<_> = path.segments.iter().map(|s| s.ident.to_string()).collect();
257 match segments.as_slice() {
258 [only] => only == name,
259 [first, second] => first == "bern" && second == name,
260 _ => false,
261 }
262}
263
264fn find_attr<'a>(attrs: &'a [Attribute], name: &str) -> Option<&'a Attribute> {
265 attrs.iter().find(|attr| is_bern_attr(attr, name))
266}
267
268pub fn doc_comment(attrs: &[Attribute]) -> String {
269 let mut lines = Vec::new();
270 for attr in attrs {
271 if attr.path().is_ident("doc")
272 && let Meta::NameValue(value) = &attr.meta
273 && let syn::Expr::Lit(syn::ExprLit {
274 lit: syn::Lit::Str(text),
275 ..
276 }) = &value.value
277 {
278 lines.push(text.value().trim().to_string());
279 }
280 }
281 lines.join("\n").trim().to_string()
282}
283
284fn reject_cfg(attrs: &[Attribute], what: &str) -> syn::Result<()> {
285 for attr in attrs {
286 if attr.path().is_ident("cfg") || attr.path().is_ident("cfg_attr") {
287 return Err(error(
288 attr.span(),
289 format!(
290 "conditional compilation on {what} is not supported in this version; \
291 the collector cannot match `cfg` conditions reliably"
292 ),
293 ));
294 }
295 }
296 Ok(())
297}
298
299pub fn type_ref(ty: &syn::Type) -> syn::Result<TypeRef> {
301 match ty {
302 syn::Type::Tuple(tuple) if tuple.elems.is_empty() => Ok(TypeRef::Unit),
303 syn::Type::Paren(inner) => type_ref(&inner.elem),
304 syn::Type::Path(path) if path.qself.is_none() => {
305 let last = path.path.segments.last().unwrap();
306 let name = last.ident.to_string();
307 let single = path.path.segments.len() == 1;
308 let generic = |index: usize| -> syn::Result<&syn::Type> {
309 if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
310 let types: Vec<_> = args
311 .args
312 .iter()
313 .filter_map(|arg| match arg {
314 syn::GenericArgument::Type(ty) => Some(ty),
315 _ => None,
316 })
317 .collect();
318 if let Some(ty) = types.get(index) {
319 return Ok(ty);
320 }
321 }
322 Err(error(
323 last.span(),
324 format!("`{name}` needs a type argument"),
325 ))
326 };
327 match name.as_str() {
328 "bool" if single => Ok(TypeRef::Bool),
329 "i64" if single => Ok(TypeRef::Int),
330 "f64" if single => Ok(TypeRef::Float),
331 "String" => Ok(TypeRef::Text),
332 "Option" => Ok(TypeRef::Optional(Box::new(type_ref(generic(0)?)?))),
333 "Vec" => Ok(TypeRef::List(Box::new(type_ref(generic(0)?)?))),
334 "i8" | "i16" | "i32" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64" | "u128"
335 | "usize" => Err(error(
336 last.span(),
337 format!("`{name}` cannot cross a Bern boundary; use `i64`"),
338 )),
339 "f32" => Err(error(
340 last.span(),
341 "`f32` cannot cross a Bern boundary; use `f64`",
342 )),
343 "char" | "str" => Err(error(last.span(), "use `String` for text")),
344 "Box" | "Rc" | "Arc" | "HashMap" | "BTreeMap" | "HashSet" => Err(error(
345 last.span(),
346 format!(
347 "`{name}` cannot cross a Bern boundary; the first version supports \
348 bool, i64, f64, String, Option, Vec, enums and records"
349 ),
350 )),
351 _ => {
352 if !matches!(last.arguments, syn::PathArguments::None) {
353 return Err(error(
354 last.span(),
355 "generic types cannot cross a Bern boundary",
356 ));
357 }
358 Ok(TypeRef::Named(
359 path.path
360 .segments
361 .iter()
362 .map(|segment| segment.ident.to_string())
363 .collect(),
364 ))
365 }
366 }
367 }
368 syn::Type::Reference(_) => Err(error(
369 ty.span(),
370 "references cannot cross a Bern boundary; use owned values",
371 )),
372 _ => Err(error(
373 ty.span(),
374 "this type cannot cross a Bern boundary; the first version supports bool, i64, \
375 f64, String, Option, Vec, enums and records",
376 )),
377 }
378}
379
380fn parse_initial(attrs: &[Attribute]) -> syn::Result<Option<Expr>> {
381 match find_attr(attrs, "initial") {
382 Some(attr) => {
383 let tokens = attr.meta.require_list()?.tokens.clone();
384 Ok(Some(syn::parse2::<FullExpr>(tokens)?.0))
385 }
386 None => Ok(None),
387 }
388}
389
390fn parse_label(attrs: &[Attribute]) -> syn::Result<Option<Tr>> {
391 match find_attr(attrs, "label") {
392 Some(attr) => {
393 let tokens = attr.meta.require_list()?.tokens.clone();
394 let expr = syn::parse2::<FullExpr>(tokens.clone());
395 match expr {
396 Ok(FullExpr(Expr::Tr(tr))) => Ok(Some(tr)),
397 _ => {
398 let tr = syn::parse2::<TrBody>(tokens).map_err(|_| {
400 error(attr.span(), "expected `#[label(tr! { \"en\" => \"…\" })]`")
401 })?;
402 Ok(Some(tr.0))
403 }
404 }
405 }
406 None => Ok(None),
407 }
408}
409
410fn check_known_attrs(attrs: &[Attribute], allowed: &[&str], what: &str) -> syn::Result<()> {
411 for attr in attrs {
412 let path = attr.path();
413 if path.is_ident("doc") || path.is_ident("allow") || path.is_ident("expect") {
414 continue;
415 }
416 if allowed.iter().any(|name| is_bern_attr(attr, name)) {
417 continue;
418 }
419 if path.is_ident("derive") {
420 return Err(error(
421 attr.span(),
422 format!("{what} derive their traits from Bern; remove `#[derive]`"),
423 ));
424 }
425 if path.is_ident("cfg") || path.is_ident("cfg_attr") {
426 reject_cfg(std::slice::from_ref(attr), what)?;
427 }
428 return Err(error(
429 attr.span(),
430 format!("attribute is not supported on {what}"),
431 ));
432 }
433 Ok(())
434}
435
436fn parse_named_fields(
437 fields: &syn::Fields,
438 owner: &Ident,
439 allow_initial: bool,
440 require_pub: bool,
441) -> syn::Result<Vec<FieldItem>> {
442 let named = match fields {
443 syn::Fields::Named(named) => named,
444 syn::Fields::Unit => return Ok(Vec::new()),
445 syn::Fields::Unnamed(_) => {
446 return Err(error(
447 owner.span(),
448 format!("`{owner}` must use named fields"),
449 ));
450 }
451 };
452 let mut result = Vec::new();
453 for field in &named.named {
454 let name = field.ident.clone().unwrap();
455 let allowed: &[&str] = if allow_initial {
458 &["initial"]
459 } else {
460 &["content"]
461 };
462 check_known_attrs(&field.attrs, allowed, "fields")?;
463 if require_pub && !matches!(field.vis, syn::Visibility::Public(_)) {
464 return Err(error(
465 name.span(),
466 format!("field `{name}` must be `pub` so every entry can read it"),
467 ));
468 }
469 result.push(FieldItem {
470 ty: type_ref(&field.ty)?,
471 syn_ty: field.ty.clone(),
472 doc: doc_comment(&field.attrs),
473 initial: if allow_initial {
474 parse_initial(&field.attrs)?
475 } else {
476 None
477 },
478 content: !allow_initial && find_attr(&field.attrs, "content").is_some(),
479 span: name.span(),
480 name,
481 });
482 }
483 Ok(result)
484}
485
486fn reject_generics(generics: &syn::Generics, name: &Ident) -> syn::Result<()> {
487 if !generics.params.is_empty() || generics.where_clause.is_some() {
488 return Err(error(
489 name.span(),
490 format!("`{name}` cannot be generic; Bern types have one schema"),
491 ));
492 }
493 Ok(())
494}
495
496pub fn parse_value_args(tokens: proc_macro2::TokenStream) -> syn::Result<Option<String>> {
498 if tokens.is_empty() {
499 return Ok(None);
500 }
501 let meta: syn::MetaNameValue = syn::parse2(tokens)?;
502 if !meta.path.is_ident("pages") {
503 return Err(error(
504 meta.path.span(),
505 "`value` takes only `pages = \"slot\"`",
506 ));
507 }
508 match &meta.value {
509 syn::Expr::Lit(syn::ExprLit {
510 lit: syn::Lit::Str(slot),
511 ..
512 }) if crate::valid_id(&slot.value()) => Ok(Some(slot.value())),
513 other => Err(error(
514 other.span(),
515 "`pages` names a slot in lower snake case, e.g. `pages = \"main\"`",
516 )),
517 }
518}
519
520fn value_pages(attrs: &[Attribute]) -> syn::Result<Option<String>> {
523 for attr in attrs {
524 if is_bern_attr(attr, "value")
525 && let syn::Meta::List(list) = &attr.meta
526 {
527 return parse_value_args(list.tokens.clone());
528 }
529 }
530 Ok(None)
531}
532
533pub fn parse_value(item: &Item) -> syn::Result<ValueItem> {
536 match item {
537 Item::Enum(item) => {
538 reject_generics(&item.generics, &item.ident)?;
539 check_known_attrs(&item.attrs, &["value"], "value types")?;
540 let mut variants = Vec::new();
541 for variant in &item.variants {
542 check_known_attrs(&variant.attrs, &["label"], "enum variants")?;
543 if variant.discriminant.is_some() {
544 return Err(error(
545 variant.ident.span(),
546 "explicit discriminants are not supported",
547 ));
548 }
549 if matches!(variant.fields, syn::Fields::Unnamed(_)) {
550 return Err(error(
551 variant.ident.span(),
552 "use named fields `Variant { field: Type }` instead of tuple variants",
553 ));
554 }
555 variants.push(VariantItem {
556 name: variant.ident.clone(),
557 fields: parse_named_fields(&variant.fields, &variant.ident, false, false)?,
558 label: parse_label(&variant.attrs)?,
559 doc: doc_comment(&variant.attrs),
560 span: variant.ident.span(),
561 });
562 }
563 if variants.is_empty() {
564 return Err(error(
565 item.ident.span(),
566 "an enum needs at least one variant",
567 ));
568 }
569 Ok(ValueItem {
570 name: item.ident.clone(),
571 kind: ValueKind::Enum(variants),
572 doc: doc_comment(&item.attrs),
573 pages: value_pages(&item.attrs)?,
574 span: item.ident.span(),
575 })
576 }
577 Item::Struct(item) => {
578 reject_generics(&item.generics, &item.ident)?;
579 check_known_attrs(&item.attrs, &["value"], "value types")?;
580 if value_pages(&item.attrs)?.is_some() {
581 return Err(error(
582 item.ident.span(),
583 "`pages` marks the enum selecting a slot's pages; a struct cannot",
584 ));
585 }
586 Ok(ValueItem {
587 name: item.ident.clone(),
588 pages: None,
589 kind: ValueKind::Record(parse_named_fields(
590 &item.fields,
591 &item.ident,
592 false,
593 true,
594 )?),
595 doc: doc_comment(&item.attrs),
596 span: item.ident.span(),
597 })
598 }
599 other => Err(error(
600 other.span(),
601 "`#[bern::value]` applies to enums and structs",
602 )),
603 }
604}
605
606pub fn parse_session(item: &Item) -> syn::Result<SessionItem> {
608 let Item::Struct(item) = item else {
609 return Err(error(item.span(), "`#[bern::session]` applies to structs"));
610 };
611 reject_generics(&item.generics, &item.ident)?;
612 check_known_attrs(&item.attrs, &["session"], "session state")?;
613 let fields = parse_named_fields(&item.fields, &item.ident, true, true)?;
614 if fields.is_empty() {
615 return Err(error(
616 item.ident.span(),
617 "session state needs at least one field",
618 ));
619 }
620 Ok(SessionItem {
621 name: item.ident.clone(),
622 fields,
623 doc: doc_comment(&item.attrs),
624 span: item.ident.span(),
625 })
626}
627
628pub fn parse_players(item: &Item) -> syn::Result<PlayersItem> {
631 let Item::Struct(item) = item else {
632 return Err(error(item.span(), "`#[bern::players]` applies to structs"));
633 };
634 reject_generics(&item.generics, &item.ident)?;
635 check_known_attrs(&item.attrs, &["players"], "player catalogs")?;
636 let syn::Fields::Named(fields) = &item.fields else {
637 return Err(error(
638 item.ident.span(),
639 "declare players as named fields: `pub preview: Player`",
640 ));
641 };
642 let mut players = Vec::new();
643 for field in &fields.named {
644 let name = field.ident.clone().unwrap();
645 check_known_attrs(&field.attrs, &[], "players")?;
646 if !matches!(field.vis, syn::Visibility::Public(_)) {
647 return Err(error(name.span(), format!("player `{name}` must be `pub`")));
648 }
649 let is_player = matches!(&field.ty, syn::Type::Path(path)
650 if path.qself.is_none()
651 && path.path.segments.last().is_some_and(|segment| {
652 segment.ident == "Player" && segment.arguments.is_none()
653 }));
654 if !is_player {
655 return Err(error(
656 field.ty.span(),
657 "every field of a player catalog has the type `Player`",
658 ));
659 }
660 if !crate::valid_id(&name.to_string()) {
661 return Err(error(
662 name.span(),
663 "player names are lower snake case; they become stable player ids",
664 ));
665 }
666 players.push(PlayerField {
667 doc: doc_comment(&field.attrs),
668 span: name.span(),
669 name,
670 });
671 }
672 if players.is_empty() {
673 return Err(error(item.ident.span(), "declare at least one player"));
674 }
675 Ok(PlayersItem {
676 name: item.ident.clone(),
677 players,
678 doc: doc_comment(&item.attrs),
679 span: item.ident.span(),
680 })
681}
682
683pub fn parse_protocol_args(args: TokenStream, span: Span) -> syn::Result<ProtocolArgs> {
685 let mut namespace = None;
686 let mut version = None;
687 let parser = syn::meta::parser(|meta| {
688 if meta.path.is_ident("namespace") {
689 let value: syn::LitStr = meta.value()?.parse()?;
690 namespace = Some(value.value());
691 Ok(())
692 } else if meta.path.is_ident("version") {
693 let value: syn::LitInt = meta.value()?.parse()?;
694 version = Some(value.base10_parse()?);
695 Ok(())
696 } else {
697 Err(meta.error("expected `namespace = \"…\"` or `version = N`"))
698 }
699 });
700 syn::parse::Parser::parse2(parser, args)?;
701 Ok(ProtocolArgs {
702 namespace: namespace
703 .ok_or_else(|| error(span, "the protocol needs `namespace = \"…\"`"))?,
704 version: version.ok_or_else(|| error(span, "the protocol needs `version = N`"))?,
705 span,
706 })
707}
708
709fn parse_command(method: &syn::TraitItemFn) -> syn::Result<CommandItem> {
710 let sig = &method.sig;
711 let name = sig.ident.clone();
712 check_known_attrs(&method.attrs, &["errors", "idempotent", "on"], "commands")?;
713 if method.default.is_some() {
714 return Err(error(
715 name.span(),
716 "commands are implemented by the engine, not in the protocol",
717 ));
718 }
719 if sig.asyncness.is_some() || !sig.generics.params.is_empty() {
720 return Err(error(
721 name.span(),
722 "commands cannot be async or generic in this version",
723 ));
724 }
725 let mut inputs = sig.inputs.iter();
726 match inputs.next() {
727 Some(syn::FnArg::Receiver(receiver))
728 if receiver.reference.is_some() && receiver.mutability.is_some() => {}
729 _ => {
730 return Err(error(
731 name.span(),
732 "a command takes `&mut self` as its first parameter",
733 ));
734 }
735 }
736 let mut params = Vec::new();
737 for input in inputs {
738 let syn::FnArg::Typed(typed) = input else {
739 unreachable!()
740 };
741 let syn::Pat::Ident(pat) = &*typed.pat else {
742 return Err(error(typed.pat.span(), "parameters must be simple names"));
743 };
744 params.push(CommandParam {
745 name: pat.ident.clone(),
746 ty: type_ref(&typed.ty)?,
747 syn_ty: (*typed.ty).clone(),
748 });
749 }
750 let result_ty = match &sig.output {
751 syn::ReturnType::Type(_, ty) => match &**ty {
752 syn::Type::Path(path)
753 if path.path.segments.last().map(|s| s.ident == "Result") == Some(true) =>
754 {
755 let last = path.path.segments.last().unwrap();
756 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
757 return Err(error(ty.span(), "expected `Result<T, CommandError>`"));
758 };
759 let types: Vec<_> = args
760 .args
761 .iter()
762 .filter_map(|arg| match arg {
763 syn::GenericArgument::Type(ty) => Some(ty.clone()),
764 _ => None,
765 })
766 .collect();
767 match types.as_slice() {
768 [ok, syn::Type::Path(err)]
769 if err.path.segments.last().map(|s| s.ident == "CommandError")
770 == Some(true) =>
771 {
772 ok.clone()
773 }
774 _ => return Err(error(ty.span(), "expected `Result<T, CommandError>`")),
775 }
776 }
777 _ => return Err(error(ty.span(), "expected `Result<T, CommandError>`")),
778 },
779 syn::ReturnType::Default => {
780 return Err(error(
781 name.span(),
782 "commands return `Result<T, CommandError>` so failures reach every entry",
783 ));
784 }
785 };
786 let mut errors = Vec::new();
787 if let Some(attr) = find_attr(&method.attrs, "errors") {
788 let list = attr.parse_args_with(
789 syn::punctuated::Punctuated::<syn::LitStr, syn::Token![,]>::parse_terminated,
790 )?;
791 errors = list.iter().map(syn::LitStr::value).collect();
792 }
793 let on = match find_attr(&method.attrs, "on") {
794 Some(attr) => {
795 let path: syn::Path = attr.parse_args()?;
796 match path.segments.iter().collect::<Vec<_>>().as_slice() {
797 [module, event] => Some((module.ident.clone(), event.ident.clone())),
798 _ => {
799 return Err(error(
800 path.span(),
801 "name the event as `#[on(module::event)]`",
802 ));
803 }
804 }
805 }
806 None => None,
807 };
808 Ok(CommandItem {
809 result: type_ref(&result_ty)?,
810 result_ty,
811 params,
812 doc: doc_comment(&method.attrs),
813 errors,
814 idempotent: find_attr(&method.attrs, "idempotent").is_some(),
815 on,
816 span: name.span(),
817 name,
818 })
819}
820
821fn answer_type(ty: &syn::Type) -> Option<&syn::Type> {
823 let syn::Type::Path(path) = ty else {
824 return None;
825 };
826 let last = path.path.segments.last()?;
827 if last.ident != "Answer" {
828 return None;
829 }
830 let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
831 return None;
832 };
833 match args.args.iter().collect::<Vec<_>>().as_slice() {
834 [syn::GenericArgument::Type(inner)] => Some(inner),
835 _ => None,
836 }
837}
838
839fn parse_query(method: &syn::TraitItemFn) -> syn::Result<QueryItem> {
840 let sig = &method.sig;
841 let name = sig.ident.clone();
842 check_known_attrs(&method.attrs, &["errors", "reads"], "queries")?;
843 if method.default.is_some() {
844 return Err(error(
845 name.span(),
846 "queries are answered by the engine, not in the protocol",
847 ));
848 }
849 if sig.asyncness.is_some() || !sig.generics.params.is_empty() {
850 return Err(error(
851 name.span(),
852 "queries are not async or generic; answer later through `answer: Answer<T>`",
853 ));
854 }
855 let mut inputs = sig.inputs.iter();
856 match inputs.next() {
857 Some(syn::FnArg::Receiver(receiver))
858 if receiver.reference.is_some() && receiver.mutability.is_none() => {}
859 Some(syn::FnArg::Receiver(receiver)) if receiver.mutability.is_some() => {
860 return Err(error(
861 receiver.span(),
862 "a query takes `&self`: queries only read engine state, commands change it",
863 ));
864 }
865 _ => {
866 return Err(error(
867 name.span(),
868 "a query takes `&self` as its first parameter",
869 ));
870 }
871 }
872 let typed: Vec<&syn::PatType> = inputs
873 .map(|input| match input {
874 syn::FnArg::Typed(typed) => typed,
875 syn::FnArg::Receiver(_) => unreachable!(),
876 })
877 .collect();
878 let Some((answer, rest)) = typed.split_last() else {
879 return Err(error(
880 name.span(),
881 "a query answers through its last parameter, `answer: Answer<T>`",
882 ));
883 };
884 let Some(result_ty) = answer_type(&answer.ty) else {
885 return Err(error(
886 answer.ty.span(),
887 "a query answers through its last parameter, `answer: Answer<T>`",
888 ));
889 };
890 let mut params = Vec::new();
891 for typed in rest {
892 let syn::Pat::Ident(pat) = &*typed.pat else {
893 return Err(error(typed.pat.span(), "parameters must be simple names"));
894 };
895 if answer_type(&typed.ty).is_some() {
896 return Err(error(
897 typed.ty.span(),
898 "a query has one `Answer<T>`, as its last parameter",
899 ));
900 }
901 params.push(CommandParam {
902 name: pat.ident.clone(),
903 ty: type_ref(&typed.ty)?,
904 syn_ty: (*typed.ty).clone(),
905 });
906 }
907 if let syn::ReturnType::Type(_, ty) = &sig.output {
908 return Err(error(
909 ty.span(),
910 "a query returns nothing; it answers through `answer`, now or later",
911 ));
912 }
913 let mut errors = Vec::new();
914 if let Some(attr) = find_attr(&method.attrs, "errors") {
915 let list = attr.parse_args_with(
916 syn::punctuated::Punctuated::<syn::LitStr, syn::Token![,]>::parse_terminated,
917 )?;
918 errors = list.iter().map(syn::LitStr::value).collect();
919 }
920 let mut reads = Vec::new();
921 if let Some(attr) = find_attr(&method.attrs, "reads") {
922 let list = attr.parse_args_with(
923 syn::punctuated::Punctuated::<Ident, syn::Token![,]>::parse_terminated,
924 )?;
925 reads = list.into_iter().collect();
926 }
927 Ok(QueryItem {
928 result: type_ref(result_ty)?,
929 result_ty: result_ty.clone(),
930 params,
931 doc: doc_comment(&method.attrs),
932 errors,
933 reads,
934 span: name.span(),
935 name,
936 })
937}
938
939fn strip_marker(item: &Item, marker: &str) -> Item {
940 let mut item = item.clone();
941 let attrs = match &mut item {
942 Item::Enum(item) => &mut item.attrs,
943 Item::Struct(item) => &mut item.attrs,
944 Item::Trait(item) => &mut item.attrs,
945 _ => return item,
946 };
947 attrs.retain(|attr| !is_bern_attr(attr, marker));
948 item
949}
950
951pub fn parse_protocol(args: ProtocolArgs, module: ItemMod) -> syn::Result<ProtocolItem> {
953 let Some((_, items)) = &module.content else {
954 return Err(error(
955 module.ident.span(),
956 "the protocol module must be declared inline: `mod name { … }`",
957 ));
958 };
959 reject_cfg(&module.attrs, "protocol modules")?;
960 let mut values = Vec::new();
961 let mut state = None;
962 let mut commands = None;
963 let mut queries = None;
964 let mut events = None;
965 let mut uses = Vec::new();
966 for item in items {
967 match item {
968 Item::Use(item_use) => uses.push(item_use.clone()),
969 Item::Enum(syn::ItemEnum { attrs, .. })
970 | Item::Struct(syn::ItemStruct { attrs, .. })
971 if find_attr(attrs, "value").is_some() =>
972 {
973 values.push(parse_value(&strip_marker(item, "value"))?);
974 }
975 Item::Struct(item_struct) if find_attr(&item_struct.attrs, "state").is_some() => {
976 if state.is_some() {
977 return Err(error(
978 item_struct.ident.span(),
979 "a protocol has exactly one `#[state]` struct",
980 ));
981 }
982 let stripped = strip_marker(item, "state");
983 let Item::Struct(stripped) = stripped else {
984 unreachable!()
985 };
986 reject_generics(&stripped.generics, &stripped.ident)?;
987 check_known_attrs(&stripped.attrs, &[], "state")?;
988 state = Some(StateItem {
989 name: stripped.ident.clone(),
990 fields: parse_named_fields(&stripped.fields, &stripped.ident, false, true)?,
991 span: stripped.ident.span(),
992 });
993 }
994 Item::Trait(item_trait) if find_attr(&item_trait.attrs, "commands").is_some() => {
995 if commands.is_some() {
996 return Err(error(
997 item_trait.ident.span(),
998 "a protocol has exactly one `#[commands]` trait",
999 ));
1000 }
1001 reject_generics(&item_trait.generics, &item_trait.ident)?;
1002 let mut methods = Vec::new();
1003 for trait_item in &item_trait.items {
1004 match trait_item {
1005 syn::TraitItem::Fn(method) => methods.push(parse_command(method)?),
1006 other => {
1007 return Err(error(
1008 other.span(),
1009 "the command trait only contains command methods",
1010 ));
1011 }
1012 }
1013 }
1014 commands = Some(CommandsItem {
1015 name: item_trait.ident.clone(),
1016 methods,
1017 span: item_trait.ident.span(),
1018 });
1019 }
1020 Item::Trait(item_trait) if find_attr(&item_trait.attrs, "queries").is_some() => {
1021 if queries.is_some() {
1022 return Err(error(
1023 item_trait.ident.span(),
1024 "a protocol has at most one `#[queries]` trait",
1025 ));
1026 }
1027 reject_generics(&item_trait.generics, &item_trait.ident)?;
1028 let mut methods = Vec::new();
1029 for trait_item in &item_trait.items {
1030 match trait_item {
1031 syn::TraitItem::Fn(method) => methods.push(parse_query(method)?),
1032 other => {
1033 return Err(error(
1034 other.span(),
1035 "the query trait only contains query methods",
1036 ));
1037 }
1038 }
1039 }
1040 queries = Some(QueriesItem {
1041 name: item_trait.ident.clone(),
1042 methods,
1043 span: item_trait.ident.span(),
1044 });
1045 }
1046 Item::Trait(item_trait) if find_attr(&item_trait.attrs, "events").is_some() => {
1047 if events.is_some() {
1048 return Err(error(
1049 item_trait.ident.span(),
1050 "a protocol has at most one `#[events]` trait",
1051 ));
1052 }
1053 reject_generics(&item_trait.generics, &item_trait.ident)?;
1054 let mut methods = Vec::new();
1055 for trait_item in &item_trait.items {
1056 match trait_item {
1057 syn::TraitItem::Fn(method) => methods.push(parse_event(method)?),
1058 other => {
1059 return Err(error(
1060 other.span(),
1061 "the events trait only contains event methods",
1062 ));
1063 }
1064 }
1065 }
1066 events = Some(EventsItem {
1067 name: item_trait.ident.clone(),
1068 methods,
1069 span: item_trait.ident.span(),
1070 });
1071 }
1072 other => {
1073 return Err(error(
1074 other.span(),
1075 "a protocol module contains `use` items, `#[value]` types, one `#[state]` \
1076 struct, one `#[commands]` trait, an optional `#[queries]` trait and an \
1077 optional `#[events]` trait",
1078 ));
1079 }
1080 }
1081 }
1082 let state = state.ok_or_else(|| {
1083 error(
1084 module.ident.span(),
1085 "the protocol needs a `#[state] pub struct State { … }`",
1086 )
1087 })?;
1088 let commands = commands.ok_or_else(|| {
1089 error(
1090 module.ident.span(),
1091 "the protocol needs a `#[commands] pub trait Commands { … }`",
1092 )
1093 })?;
1094 Ok(ProtocolItem {
1095 args,
1096 doc: doc_comment(&module.attrs),
1097 module,
1098 values,
1099 state,
1100 commands,
1101 queries,
1102 events,
1103 uses,
1104 })
1105}
1106
1107fn parse_event(method: &syn::TraitItemFn) -> syn::Result<EventItem> {
1109 let sig = &method.sig;
1110 let name = sig.ident.clone();
1111 check_known_attrs(&method.attrs, &[], "events")?;
1112 if method.default.is_some() || !matches!(sig.output, syn::ReturnType::Default) {
1113 return Err(error(
1114 name.span(),
1115 "an event is declared as `fn name(&self, …);` without a body or result",
1116 ));
1117 }
1118 let mut inputs = sig.inputs.iter();
1119 match inputs.next() {
1120 Some(syn::FnArg::Receiver(receiver))
1121 if receiver.reference.is_some() && receiver.mutability.is_none() => {}
1122 _ => {
1123 return Err(error(
1124 name.span(),
1125 "an event takes `&self` as its first parameter",
1126 ));
1127 }
1128 }
1129 let mut params = Vec::new();
1130 for input in inputs {
1131 let syn::FnArg::Typed(typed) = input else {
1132 unreachable!()
1133 };
1134 let syn::Pat::Ident(pat) = &*typed.pat else {
1135 return Err(error(typed.pat.span(), "parameters must be simple names"));
1136 };
1137 params.push(CommandParam {
1138 name: pat.ident.clone(),
1139 ty: type_ref(&typed.ty)?,
1140 syn_ty: (*typed.ty).clone(),
1141 });
1142 }
1143 Ok(EventItem {
1144 span: name.span(),
1145 doc: doc_comment(&method.attrs),
1146 params,
1147 name,
1148 })
1149}
1150
1151pub fn parse_component_args(args: TokenStream) -> syn::Result<Option<syn::Path>> {
1154 if args.is_empty() {
1155 return Ok(None);
1156 }
1157 let parser = |input: syn::parse::ParseStream| -> syn::Result<syn::Path> {
1158 let key: Ident = input.parse()?;
1159 if key != "features" {
1160 return Err(error(key.span(), "expected `features = FeaturesEnum`"));
1161 }
1162 input.parse::<syn::Token![=]>()?;
1163 let path: syn::Path = input.parse()?;
1164 if !input.is_empty() {
1165 return Err(input.error("expected only `features = FeaturesEnum`"));
1166 }
1167 Ok(path)
1168 };
1169 syn::parse::Parser::parse2(parser, args).map(Some)
1170}
1171
1172pub fn parse_component(func: ItemFn) -> syn::Result<ComponentItem> {
1173 parse_component_with(func, None)
1174}
1175
1176pub fn parse_component_with(
1178 func: ItemFn,
1179 features: Option<syn::Path>,
1180) -> syn::Result<ComponentItem> {
1181 let name = func.sig.ident.clone();
1182 reject_cfg(&func.attrs, "components")?;
1183 check_known_attrs(&func.attrs, &["component"], "components")?;
1184 if !func.sig.generics.params.is_empty() || func.sig.asyncness.is_some() {
1185 return Err(error(name.span(), "components cannot be generic or async"));
1186 }
1187 let returns_view = match &func.sig.output {
1188 syn::ReturnType::Type(_, ty) => match &**ty {
1189 syn::Type::ImplTrait(bounds) => bounds.bounds.iter().any(|bound| {
1190 matches!(bound, syn::TypeParamBound::Trait(t)
1191 if t.path.segments.last().map(|s| s.ident == "View") == Some(true))
1192 }),
1193 syn::Type::Path(path) => {
1194 path.path.segments.last().map(|s| s.ident == "View") == Some(true)
1195 }
1196 _ => false,
1197 },
1198 syn::ReturnType::Default => false,
1199 };
1200 if !returns_view {
1201 return Err(error(name.span(), "a component returns `impl View`"));
1202 }
1203 let mut params = Vec::new();
1204 for input in &func.sig.inputs {
1205 let syn::FnArg::Typed(typed) = input else {
1206 return Err(error(input.span(), "components do not take `self`"));
1207 };
1208 let syn::Pat::Ident(pat) = &*typed.pat else {
1209 return Err(error(typed.pat.span(), "parameters must be simple names"));
1210 };
1211 let mut kind = ParamKind::Prop;
1212 let mut content = false;
1213 for attr in &typed.attrs {
1214 if is_bern_attr(attr, "session") {
1215 kind = ParamKind::Session;
1216 } else if is_bern_attr(attr, "content") {
1217 content = true;
1218 } else {
1219 return Err(error(
1220 attr.span(),
1221 "only `#[session]` and `#[content]` are allowed on component parameters",
1222 ));
1223 }
1224 }
1225 if content && kind == ParamKind::Session {
1226 return Err(error(
1227 pat.ident.span(),
1228 "`#[content]` marks a text parameter; session state is not one",
1229 ));
1230 }
1231 params.push(Param {
1232 name: pat.ident.clone(),
1233 ty: type_ref(&typed.ty)?,
1234 syn_ty: (*typed.ty).clone(),
1235 kind,
1236 content,
1237 span: pat.ident.span(),
1238 });
1239 }
1240 let stmts = &func.block.stmts;
1241 let tokens = match stmts.as_slice() {
1242 [syn::Stmt::Macro(stmt)] if stmt.mac.path.is_ident("ui") => stmt.mac.tokens.clone(),
1243 [syn::Stmt::Expr(syn::Expr::Macro(expr), None)] if expr.mac.path.is_ident("ui") => {
1244 expr.mac.tokens.clone()
1245 }
1246 _ => {
1247 return Err(error(
1248 func.block.span(),
1249 "a component body is exactly one `ui! { … }` declaration",
1250 ));
1251 }
1252 };
1253 let body: UiBody = syn::parse2(tokens)?;
1254 Ok(ComponentItem {
1255 doc: doc_comment(&func.attrs),
1256 span: name.span(),
1257 name,
1258 params,
1259 body,
1260 features,
1261 func,
1262 })
1263}
1264
1265fn parse_command_refs(attr: &Attribute) -> syn::Result<Vec<CommandRef>> {
1266 attr.parse_args_with(|input: syn::parse::ParseStream| {
1267 let mut refs = Vec::new();
1268 while !input.is_empty() {
1269 let first: Ident = input.parse()?;
1270 let mut name = first.to_string();
1271 if input.peek(syn::Token![.]) {
1272 input.parse::<syn::Token![.]>()?;
1273 let second: Ident = input.parse()?;
1274 name = format!("{name}.{second}");
1275 }
1276 refs.push(CommandRef {
1277 name,
1278 span: first.span(),
1279 });
1280 if input.is_empty() {
1281 break;
1282 }
1283 input.parse::<syn::Token![,]>()?;
1284 }
1285 Ok(refs)
1286 })
1287}
1288
1289pub fn parse_features(item: &Item) -> syn::Result<FeaturesItem> {
1291 let Item::Enum(item) = item else {
1292 return Err(error(
1293 item.span(),
1294 "`#[bern::features]` declares an enum whose variants are the features",
1295 ));
1296 };
1297 check_known_attrs(&item.attrs, &["features"], "feature contracts")?;
1298 if !item.generics.params.is_empty() {
1299 return Err(error(
1300 item.ident.span(),
1301 "feature contracts cannot be generic",
1302 ));
1303 }
1304 let mut features = Vec::new();
1305 for variant in &item.variants {
1306 if !matches!(variant.fields, syn::Fields::Unit) {
1307 return Err(error(
1308 variant.ident.span(),
1309 "features are plain variants without fields",
1310 ));
1311 }
1312 check_known_attrs(
1313 &variant.attrs,
1314 &["commands", "layouts", "guaranteed", "exempt"],
1315 "features",
1316 )?;
1317 let mut feature = FeatureItem {
1318 name: variant.ident.clone(),
1319 doc: doc_comment(&variant.attrs),
1320 commands: Vec::new(),
1321 layouts: Vec::new(),
1322 guaranteed: None,
1323 exemptions: Vec::new(),
1324 span: variant.ident.span(),
1325 };
1326 for attr in &variant.attrs {
1327 if is_bern_attr(attr, "commands") {
1328 feature.commands.extend(parse_command_refs(attr)?);
1329 } else if is_bern_attr(attr, "layouts") {
1330 let sizes = attr.parse_args_with(
1331 syn::punctuated::Punctuated::<Ident, syn::Token![,]>::parse_terminated,
1332 )?;
1333 feature.layouts.extend(sizes);
1334 } else if is_bern_attr(attr, "guaranteed") {
1335 feature.guaranteed = Some(attr.parse_args::<syn::LitStr>()?);
1336 } else if is_bern_attr(attr, "exempt") {
1337 let (platform, reason) =
1338 attr.parse_args_with(|input: syn::parse::ParseStream| {
1339 let platform: Ident = input.parse()?;
1340 input.parse::<syn::Token![,]>()?;
1341 let reason: syn::LitStr = input.parse()?;
1342 Ok((platform, reason))
1343 })?;
1344 feature.exemptions.push((platform, reason));
1345 }
1346 }
1347 if feature.commands.is_empty() {
1348 return Err(error(
1349 variant.ident.span(),
1350 "a feature names the commands that implement it: `#[commands(…)]`",
1351 ));
1352 }
1353 features.push(feature);
1354 }
1355 if features.is_empty() {
1356 return Err(error(
1357 item.ident.span(),
1358 "a feature contract declares at least one feature",
1359 ));
1360 }
1361 Ok(FeaturesItem {
1362 name: item.ident.clone(),
1363 features,
1364 doc: doc_comment(&item.attrs),
1365 span: item.ident.span(),
1366 })
1367}
1368
1369fn strip_fn_marker(func: &ItemFn) -> ItemFn {
1370 let mut func = func.clone();
1371 func.attrs.retain(|attr| !is_bern_attr(attr, "component"));
1372 func
1373}
1374
1375fn collect(items: &[Item], path: &mut Vec<String>, out: &mut SourceItems) -> syn::Result<()> {
1376 for item in items {
1377 match item {
1378 Item::Mod(module) => {
1379 if let Some(attr) = find_attr(&module.attrs, "protocol") {
1380 let args = match &attr.meta {
1381 Meta::List(list) => list.tokens.clone(),
1382 _ => TokenStream::new(),
1383 };
1384 let args = parse_protocol_args(args, attr.span())?;
1385 let mut stripped = module.clone();
1386 stripped
1387 .attrs
1388 .retain(|attr| !is_bern_attr(attr, "protocol"));
1389 out.protocols
1390 .push((path.clone(), parse_protocol(args, stripped)?));
1391 } else if let Some((_, content)) = &module.content {
1392 path.push(module.ident.to_string());
1393 collect(content, path, out)?;
1394 path.pop();
1395 }
1396 }
1397 Item::Enum(syn::ItemEnum { attrs, .. })
1398 | Item::Struct(syn::ItemStruct { attrs, .. })
1399 if find_attr(attrs, "value").is_some() =>
1400 {
1401 reject_cfg(attrs, "value types")?;
1402 out.values.push((path.clone(), parse_value(item)?));
1404 }
1405 Item::Struct(syn::ItemStruct { attrs, .. })
1406 if find_attr(attrs, "session").is_some() =>
1407 {
1408 reject_cfg(attrs, "session state")?;
1409 out.sessions
1410 .push((path.clone(), parse_session(&strip_marker(item, "session"))?));
1411 }
1412 Item::Struct(syn::ItemStruct { attrs, .. })
1413 if find_attr(attrs, "players").is_some() =>
1414 {
1415 reject_cfg(attrs, "player catalogs")?;
1416 out.players
1417 .push((path.clone(), parse_players(&strip_marker(item, "players"))?));
1418 }
1419 Item::Enum(syn::ItemEnum { attrs, .. }) if find_attr(attrs, "features").is_some() => {
1420 reject_cfg(attrs, "feature contracts")?;
1421 out.features.push((
1422 path.clone(),
1423 parse_features(&strip_marker(item, "features"))?,
1424 ));
1425 }
1426 Item::Macro(item)
1427 if item
1428 .mac
1429 .path
1430 .segments
1431 .last()
1432 .is_some_and(|segment| segment.ident == "style") =>
1433 {
1434 out.styles
1435 .push((path.clone(), crate::parse_style(item.mac.tokens.clone())?));
1436 }
1437 Item::Fn(func) if find_attr(&func.attrs, "component").is_some() => {
1438 let args = match &find_attr(&func.attrs, "component").map(|attr| &attr.meta) {
1439 Some(Meta::List(list)) => list.tokens.clone(),
1440 _ => TokenStream::new(),
1441 };
1442 let features = parse_component_args(args)?;
1443 out.components.push((
1444 path.clone(),
1445 parse_component_with(strip_fn_marker(func), features)?,
1446 ));
1447 }
1448 _ => {}
1449 }
1450 }
1451 Ok(())
1452}
1453
1454pub fn parse_source_file(source: &str) -> syn::Result<SourceItems> {
1456 parse_source_file_with_modules(source, &crate::build::Modules::default())
1457}
1458
1459pub fn parse_source_file_with_modules(
1460 source: &str,
1461 modules: &crate::build::Modules,
1462) -> syn::Result<SourceItems> {
1463 let mut file = syn::parse_file(source)?;
1464 modules.items(&mut file.items)?;
1465 let mut out = SourceItems::default();
1466 collect(&file.items, &mut Vec::new(), &mut out)?;
1467 for (_, component) in &mut out.components {
1468 modules.component(component)?;
1469 }
1470 Ok(out)
1471}