use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprLit, Fields, Lit, LitStr, Type};
#[derive(Clone, Copy, PartialEq, Eq)]
enum Role {
Key,
SpanId,
ParentSpanId,
}
impl Role {
fn tokens(self) -> TokenStream2 {
match self {
Role::Key => quote! { ::backbeat::schema::FieldRole::Key },
Role::SpanId => quote! { ::backbeat::schema::FieldRole::SpanId },
Role::ParentSpanId => quote! { ::backbeat::schema::FieldRole::ParentSpanId },
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Phase {
None,
Enter,
Exit,
}
impl Phase {
fn tokens(self) -> TokenStream2 {
match self {
Phase::None => quote! { ::backbeat::schema::Phase::None },
Phase::Enter => quote! { ::backbeat::schema::Phase::Enter },
Phase::Exit => quote! { ::backbeat::schema::Phase::Exit },
}
}
}
#[proc_macro_derive(Event, attributes(event))]
pub fn derive_event(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match expand(input) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
#[proc_macro_derive(EventEnum)]
pub fn derive_event_enum(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match expand_event_enum(input) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
fn expand_event_enum(input: DeriveInput) -> syn::Result<TokenStream2> {
let name = &input.ident;
let data = match &input.data {
Data::Enum(e) => e,
_ => {
return Err(syn::Error::new_spanned(
&input,
"`#[derive(EventEnum)]` can only be applied to an enum",
))
}
};
let repr = enum_repr_width(&input)?;
let mut labels = Vec::with_capacity(data.variants.len());
for variant in &data.variants {
if !matches!(variant.fields, Fields::Unit) {
return Err(syn::Error::new_spanned(
variant,
"`#[derive(EventEnum)]` requires fieldless variants",
));
}
let label = variant.ident.to_string();
let value =
match &variant.discriminant {
Some((_, expr)) => expr.clone(),
None => return Err(syn::Error::new_spanned(
variant,
"`#[derive(EventEnum)]` requires an explicit discriminant, e.g. `Variant = 0` \
(the value is part of the event's on-disk identity)",
)),
};
labels.push(quote! {
::backbeat::schema::EnumLabel { value: (#value) as u64, label: #label }
});
}
Ok(quote! {
impl ::backbeat::EventEnum for #name {
const REPR: u8 = #repr;
const LABELS: &'static [::backbeat::schema::EnumLabel] = &[ #(#labels),* ];
}
})
}
fn enum_repr_width(input: &DeriveInput) -> syn::Result<u8> {
for attr in &input.attrs {
if !attr.path().is_ident("repr") {
continue;
}
let mut width = None;
attr.parse_nested_meta(|meta| {
width = meta
.path
.get_ident()
.and_then(|i| match i.to_string().as_str() {
"u8" => Some(1),
"u16" => Some(2),
"u32" => Some(4),
"u64" => Some(8),
_ => None,
});
Ok(())
})?;
if let Some(w) = width {
return Ok(w);
}
}
Err(syn::Error::new_spanned(
input,
"`#[derive(EventEnum)]` requires an explicit `#[repr(u8|u16|u32|u64)]`",
))
}
fn expand(input: DeriveInput) -> syn::Result<TokenStream2> {
let name = &input.ident;
let container = ContainerAttrs::parse(&input)?;
let fields =
match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(named) => named.named.iter().collect::<Vec<_>>(),
Fields::Unit => Vec::new(),
Fields::Unnamed(_) => return Err(syn::Error::new_spanned(
&s.fields,
"`#[derive(Event)]` requires named fields (tuple structs have no field names \
to use as column names)",
)),
},
_ => {
return Err(syn::Error::new_spanned(
&input,
"`#[derive(Event)]` can only be applied to a struct",
))
}
};
let description = doc_of(&input.attrs);
let mut field_defs = Vec::with_capacity(fields.len());
let mut span_id_fields = 0usize;
let mut parent_span_fields = 0usize;
for field in &fields {
let ident = field.ident.as_ref().expect("named field");
let fname = ident.to_string();
let fdesc = doc_of(&field.attrs);
let attrs = FieldAttrs::parse(&field.attrs)?;
if matches!(attrs.role, Some(Role::SpanId | Role::ParentSpanId)) {
require_u64(&field.ty, attrs.role.unwrap())?;
}
if attrs.sentinel.is_some() {
if attrs.interned.is_some() {
return Err(syn::Error::new_spanned(
field,
"`#[event(sentinel = …)]` is not valid on an interned field (it marks an \
absent *integer*, compared against the raw value)",
));
}
if is_bool(&field.ty) || is_byte_array(&field.ty) {
return Err(syn::Error::new_spanned(
&field.ty,
"`#[event(sentinel = …)]` is only valid on an integer field (the sentinel is \
an in-band absent marker compared against the raw integer value)",
));
}
}
match attrs.role {
Some(Role::SpanId) => span_id_fields += 1,
Some(Role::ParentSpanId) => parent_span_fields += 1,
_ => {}
}
let fty = &field.ty;
let (ty_expr, labels_expr) = if let Some(dynamic) = attrs.interned {
(
quote! { ::backbeat::schema::FieldType::Interned { dynamic: #dynamic } },
quote! { &[] },
)
} else {
(
quote! { <#fty as ::backbeat::FieldTy>::FIELD_TYPE },
quote! { <#fty as ::backbeat::FieldTy>::LABELS },
)
};
let desc_expr = opt_str(fdesc.as_deref());
let unit_expr = opt_str(attrs.unit.as_deref());
let role_expr = match attrs.role {
Some(r) => r.tokens(),
None => quote! { ::backbeat::schema::FieldRole::None },
};
let sentinel_expr = match &attrs.sentinel {
Some(expr) => quote! {
::core::option::Option::Some({
let w = ::core::mem::size_of::<#fty>();
let mask = if w >= 8 { u64::MAX } else { (1u64 << (8 * w)) - 1 };
(((#expr) as #fty) as u64) & mask
})
},
None => quote! { ::core::option::Option::None },
};
field_defs.push(quote! {
::backbeat::schema::FieldSchema {
name: #fname,
description: #desc_expr,
ty: #ty_expr,
offset: ::backbeat::schema::layout_u16(::core::mem::offset_of!(#name, #ident)),
width: ::backbeat::schema::layout_u16(::core::mem::size_of::<#fty>()),
role: #role_expr,
unit: #unit_expr,
sentinel: #sentinel_expr,
enum_labels: #labels_expr,
}
});
}
if span_id_fields > 1 {
return Err(syn::Error::new_spanned(
&input,
"an event may declare at most one `#[event(span_id)]` field",
));
}
if parent_span_fields > 1 {
return Err(syn::Error::new_spanned(
&input,
"an event may declare at most one `#[event(parent_span_id)]` field",
));
}
match container.phase {
Phase::Enter | Phase::Exit if span_id_fields == 0 => {
return Err(syn::Error::new_spanned(
&input,
"`#[event(span = enter|exit)]` requires exactly one `#[event(span_id)]` field",
));
}
Phase::None if span_id_fields > 0 => {
return Err(syn::Error::new_spanned(
&input,
"`#[event(span_id)]` requires the event to be a span \
(`#[event(span = enter)]` or `#[event(span = exit)]`)",
));
}
_ => {}
}
Ok(emit(name, &container, description, field_defs))
}
fn emit(
name: &syn::Ident,
container: &ContainerAttrs,
description: Option<String>,
field_defs: Vec<TokenStream2>,
) -> TokenStream2 {
let qualified = format!("{}::{name}", container.namespace);
let desc_expr = opt_str(description.as_deref());
let phase_expr = container.phase.tokens();
quote! {
impl #name {
pub const QUALIFIED_NAME: &'static str = #qualified;
const FIELDS: &'static [::backbeat::schema::FieldSchema] = &[ #(#field_defs),* ];
pub const ID: ::backbeat::id::EventId =
::backbeat::schema::EventSchema::compute_id(
Self::QUALIFIED_NAME,
#phase_expr,
Self::FIELDS,
);
pub const SCHEMA: ::backbeat::schema::EventSchema =
::backbeat::schema::EventSchema {
id: Self::ID,
qualified_name: Self::QUALIFIED_NAME,
description: #desc_expr,
record_size: ::backbeat::schema::layout_u16(::core::mem::size_of::<#name>()),
phase: #phase_expr,
fields: Self::FIELDS,
};
}
impl ::backbeat::Event for #name {
const SCHEMA: ::backbeat::schema::EventSchema = Self::SCHEMA;
const ID: ::backbeat::id::EventId = Self::ID;
const QUALIFIED_NAME: &'static str = Self::QUALIFIED_NAME;
}
::backbeat::register_event!(#name);
}
}
#[derive(Default)]
struct FieldAttrs {
role: Option<Role>,
unit: Option<String>,
interned: Option<bool>,
sentinel: Option<Expr>,
}
impl FieldAttrs {
fn parse(attrs: &[syn::Attribute]) -> syn::Result<Self> {
let mut out = FieldAttrs::default();
for attr in attrs {
if !attr.path().is_ident("event") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("key") {
out.set_role(Role::Key, &meta)?;
Ok(())
} else if meta.path.is_ident("span_id") {
out.set_role(Role::SpanId, &meta)?;
Ok(())
} else if meta.path.is_ident("parent_span_id") {
out.set_role(Role::ParentSpanId, &meta)?;
Ok(())
} else if meta.path.is_ident("unit") {
let lit: LitStr = meta.value()?.parse()?;
out.unit = Some(lit.value());
Ok(())
} else if meta.path.is_ident("sentinel") {
let expr: Expr = meta.value()?.parse()?;
out.sentinel = Some(expr);
Ok(())
} else if meta.path.is_ident("interned") {
let mut dynamic = false;
if meta.input.peek(syn::token::Paren) {
meta.parse_nested_meta(|inner| {
if inner.path.is_ident("dynamic") {
dynamic = true;
Ok(())
} else {
Err(inner.error("expected `dynamic`"))
}
})?;
}
out.interned = Some(dynamic);
Ok(())
} else {
Err(meta.error("unknown `#[event(...)]` field attribute"))
}
})?;
}
Ok(out)
}
fn set_role(&mut self, role: Role, meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<()> {
if self.role.is_some() {
return Err(meta.error(
"a field may have at most one role \
(`key` / `span_id` / `parent_span_id` are mutually exclusive)",
));
}
self.role = Some(role);
Ok(())
}
}
struct ContainerAttrs {
namespace: String,
phase: Phase,
}
impl ContainerAttrs {
fn parse(input: &DeriveInput) -> syn::Result<Self> {
let mut namespace = None;
let mut phase = Phase::None;
for attr in &input.attrs {
if !attr.path().is_ident("event") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("namespace") {
let lit: LitStr = meta.value()?.parse()?;
namespace = Some(lit.value());
Ok(())
} else if meta.path.is_ident("span") {
let ident: syn::Ident = meta.value()?.parse()?;
phase = match ident.to_string().as_str() {
"enter" => Phase::Enter,
"exit" => Phase::Exit,
_ => {
return Err(meta.error("`span` must be `enter` or `exit`"));
}
};
Ok(())
} else {
Err(meta.error("unknown container-level `#[event(...)]` attribute"))
}
})?;
}
let namespace = namespace.ok_or_else(|| {
syn::Error::new_spanned(
input,
"`#[derive(Event)]` requires `#[event(namespace = \"...\")]`",
)
})?;
Ok(Self { namespace, phase })
}
}
fn is_bool(ty: &Type) -> bool {
matches!(ty, Type::Path(p) if p.path.is_ident("bool"))
}
fn is_byte_array(ty: &Type) -> bool {
matches!(ty, Type::Array(_))
}
fn require_u64(ty: &Type, role: Role) -> syn::Result<()> {
let is_u64 = matches!(ty, Type::Path(p) if p.path.is_ident("u64"));
if is_u64 {
Ok(())
} else {
let attr = match role {
Role::SpanId => "span_id",
Role::ParentSpanId => "parent_span_id",
Role::Key => unreachable!("require_u64 is only called for span roles"),
};
Err(syn::Error::new_spanned(
ty,
format!(
"`#[event({attr})]` fields must be `u64` (span ids are compared for equality \
across the enter/exit events, so they need a uniform width)"
),
))
}
}
fn doc_of(attrs: &[syn::Attribute]) -> Option<String> {
let mut lines = Vec::new();
for attr in attrs {
if !attr.path().is_ident("doc") {
continue;
}
if let syn::Meta::NameValue(nv) = &attr.meta {
if let Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) = &nv.value
{
lines.push(s.value().trim().to_string());
}
}
}
if lines.is_empty() {
None
} else {
Some(lines.join("\n"))
}
}
fn opt_str(s: Option<&str>) -> TokenStream2 {
match s {
Some(s) => quote! { ::core::option::Option::Some(#s) },
None => quote! { ::core::option::Option::None },
}
}