use proc_macro::TokenStream;
use quote::quote;
use syn::{
Attribute, Data, DeriveInput, Fields, GenericArgument, ItemFn, PathArguments, Type,
parse_macro_input,
};
fn is_functional_state_attr(attr: &Attribute) -> bool {
attr.path()
.segments
.last()
.is_some_and(|s| s.ident == "functional_state")
}
fn strip_functional_state_field_attrs(input: &mut DeriveInput) {
if let Data::Struct(data) = &mut input.data {
for field in data.fields.iter_mut() {
field.attrs.retain(|a| !is_functional_state_attr(a));
}
}
}
#[proc_macro_derive(Bundle, attributes(bundle))]
pub fn derive_bundle(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match expand_derive_bundle(&input) {
Ok(ts) => ts,
Err(e) => e.to_compile_error().into(),
}
}
fn expand_derive_bundle(input: &DeriveInput) -> Result<TokenStream, syn::Error> {
if !input.generics.params.is_empty() {
return Err(syn::Error::new_spanned(
&input.generics,
"rhdl::E0180: #[derive(Bundle)] does not support generic parameters",
));
}
let Data::Struct(data) = &input.data else {
return Err(syn::Error::new_spanned(
input,
"rhdl::E0180: #[derive(Bundle)] only supports structs with named fields",
));
};
let Fields::Named(fields) = &data.fields else {
return Err(syn::Error::new_spanned(
&data.fields,
"rhdl::E0180: #[derive(Bundle)] requires named fields (tuple/unit structs unsupported)",
));
};
let name = &input.ident;
let mut leaf_entries = Vec::new();
let mut nested_entries = Vec::new();
for field in &fields.named {
let Some(ident) = &field.ident else {
return Err(syn::Error::new_spanned(
field,
"rhdl::E0180: #[derive(Bundle)] requires named fields",
));
};
let field_name = ident.to_string();
match classify_bundle_field(&field.ty)? {
BundleFieldKind::Ground { ground_expr } => {
leaf_entries.push(quote! {
(#field_name, #ground_expr)
});
}
BundleFieldKind::Nested { ty } => {
nested_entries.push(quote! {
(#field_name, <#ty as ::bitloom_prelude::Bundle>::leaves)
});
}
}
}
Ok(TokenStream::from(quote! {
impl ::bitloom_prelude::Bundle for #name {
fn leaves() -> &'static [(&'static str, ::bitloom_prelude::GroundType)] {
&[#(#leaf_entries),*]
}
fn nested_bundles() -> &'static [(
&'static str,
fn() -> &'static [(&'static str, ::bitloom_prelude::GroundType)],
)] {
&[#(#nested_entries),*]
}
}
}))
}
enum BundleFieldKind {
Ground {
ground_expr: proc_macro2::TokenStream,
},
Nested {
ty: Type,
},
}
fn classify_bundle_field(ty: &Type) -> Result<BundleFieldKind, syn::Error> {
let Type::Path(type_path) = ty else {
return Err(syn::Error::new_spanned(
ty,
"rhdl::E0180: #[derive(Bundle)] field types must be simple paths (ground or nested Bundle)",
));
};
if type_path.qself.is_some() {
return Err(syn::Error::new_spanned(
ty,
"rhdl::E0180: #[derive(Bundle)] does not support qualified self types",
));
}
let last =
type_path.path.segments.last().ok_or_else(|| {
syn::Error::new_spanned(ty, "rhdl::E0180: empty path in Bundle field")
})?;
let ident = last.ident.to_string();
match ident.as_str() {
"HwVec" => {
return Err(syn::Error::new_spanned(
ty,
"rhdl::E0180: #[derive(Bundle)] does not support HwVec fields; HwVec<Bundle,_> remains OUT OF SCOPE",
));
}
"Input" | "Output" => {
return Err(syn::Error::new_spanned(
ty,
"rhdl::E0180: #[derive(Bundle)] fields must be bare ground or nested Bundle types, not Input/Output",
));
}
"Bool" => {
require_no_args(ty, &last.arguments)?;
return Ok(BundleFieldKind::Ground {
ground_expr: quote! { ::bitloom_prelude::GroundType::Bool },
});
}
"Clock" => {
require_no_args(ty, &last.arguments)?;
return Ok(BundleFieldKind::Ground {
ground_expr: quote! { ::bitloom_prelude::GroundType::Clock },
});
}
"Reset" => {
require_no_args(ty, &last.arguments)?;
return Ok(BundleFieldKind::Ground {
ground_expr: quote! { ::bitloom_prelude::GroundType::Reset },
});
}
"UInt" | "Bits" => {
let width = const_generic_u32(ty, &last.arguments)?;
return Ok(BundleFieldKind::Ground {
ground_expr: quote! { ::bitloom_prelude::GroundType::UInt { width: #width } },
});
}
"SInt" => {
let width = const_generic_u32(ty, &last.arguments)?;
return Ok(BundleFieldKind::Ground {
ground_expr: quote! { ::bitloom_prelude::GroundType::SInt { width: #width } },
});
}
_ => {}
}
if !matches!(last.arguments, PathArguments::None) {
return Err(syn::Error::new_spanned(
ty,
"rhdl::E0180: #[derive(Bundle)] nested Bundle fields must be bare type paths without generics",
));
}
Ok(BundleFieldKind::Nested { ty: ty.clone() })
}
fn require_no_args(ty: &Type, args: &PathArguments) -> Result<(), syn::Error> {
if matches!(args, PathArguments::None) {
Ok(())
} else {
Err(syn::Error::new_spanned(
ty,
"rhdl::E0180: unexpected type arguments on ground Bundle field",
))
}
}
fn const_generic_u32(ty: &Type, args: &PathArguments) -> Result<u32, syn::Error> {
let PathArguments::AngleBracketed(ab) = args else {
return Err(syn::Error::new_spanned(
ty,
"rhdl::E0180: width-parameterized ground types require a const generic (e.g. UInt<8>)",
));
};
let mut width: Option<u32> = None;
for arg in &ab.args {
match arg {
GenericArgument::Const(syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(lit),
..
})) => {
if width.is_some() {
return Err(syn::Error::new_spanned(
ty,
"rhdl::E0180: expected exactly one const width parameter",
));
}
width = Some(lit.base10_parse()?);
}
_ => {
return Err(syn::Error::new_spanned(
arg,
"rhdl::E0180: ground width must be an integer literal const generic",
));
}
}
}
width.ok_or_else(|| {
syn::Error::new_spanned(
ty,
"rhdl::E0180: width-parameterized ground types require a const generic (e.g. UInt<8>)",
)
})
}
#[proc_macro_attribute]
pub fn module(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let name = &input.ident;
let vis = &input.vis;
let syn::Data::Struct(data) = &input.data else {
return syn::Error::new_spanned(&input, "rhdl::module only supports structs")
.to_compile_error()
.into();
};
let mod_name = name.to_string();
let field_defs = data.fields.iter().map(|f| {
let id = f.ident.as_ref().unwrap();
let ty = &f.ty;
let fvis = &f.vis;
let keep_attrs: Vec<_> = f
.attrs
.iter()
.filter(|a| !is_functional_state_attr(a))
.collect();
quote! { #(#keep_attrs)* #fvis #id: #ty }
});
let port_stmts = data.fields.iter().filter_map(|field| {
if field.attrs.iter().any(is_functional_state_attr) {
return None;
}
let Some(ident) = &field.ident else {
return Some(quote! {
compile_error!("tuple structs are not supported by rhdl::module");
});
};
let ty = &field.ty;
let name_str = ident.to_string();
Some(quote! {
{
type __PortTy = #ty;
for (__leaf, __dir, __gt) in
<__PortTy as ::bitloom_prelude::PortField>::flatten(#name_str)
{
match __dir {
::bitloom_prelude::PortDir::Input => {
__session.add_input(
__leaf,
__gt,
::bitloom_prelude::Span::default(),
);
}
::bitloom_prelude::PortDir::Output => {
__session.add_output(
__leaf,
__gt,
::bitloom_prelude::Span::default(),
);
}
}
}
}
})
});
TokenStream::from(quote! {
#vis struct #name {
#(#field_defs),*
}
impl ::bitloom_prelude::Elaboratable for #name {
fn elaborate() -> ::core::result::Result<
::bitloom_prelude::FrozenHir,
::bitloom_prelude::Diagnostics,
> {
let mut __session = ::bitloom_prelude::ElaborateSession::new(#mod_name);
__session.begin_module(#mod_name, ::bitloom_prelude::Span::default());
#(#port_stmts)*
__session.end_module();
__session.finish()
}
}
})
}
#[proc_macro_attribute]
pub fn combinational(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let vis = &input.vis;
let sig = &input.sig;
let block = &input.block;
let attrs = &input.attrs;
TokenStream::from(quote! {
#(#attrs)*
#vis #sig {
const _: () = ();
let __rhdl_process_kind = ::bitloom_prelude::ProcessKindMark::Combinational;
let _ = __rhdl_process_kind;
#block
}
})
}
#[proc_macro_attribute]
pub fn sequential(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let vis = &input.vis;
let sig = &input.sig;
let block = &input.block;
let attrs = &input.attrs;
TokenStream::from(quote! {
#(#attrs)*
#vis #sig {
const _: () = ();
let __rhdl_process_kind = ::bitloom_prelude::ProcessKindMark::Sequential;
let _ = __rhdl_process_kind;
#block
}
})
}
#[proc_macro_attribute]
pub fn process(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
syn::Error::new_spanned(
input.sig.ident,
"hardware processes must use #[combinational] or #[sequential]; bare #[process] is forbidden",
)
.to_compile_error()
.into()
}
#[proc_macro_attribute]
pub fn functional_model(_attr: TokenStream, item: TokenStream) -> TokenStream {
host_only_view(item, "FunctionalModel")
}
#[proc_macro_attribute]
pub fn bridge(_attr: TokenStream, item: TokenStream) -> TokenStream {
host_only_view(item, "Bridge")
}
#[proc_macro_attribute]
pub fn abstraction(_attr: TokenStream, item: TokenStream) -> TokenStream {
host_only_view(item, "Abstraction")
}
#[proc_macro_attribute]
pub fn both(_attr: TokenStream, item: TokenStream) -> TokenStream {
host_only_view(item, "Both")
}
#[proc_macro_attribute]
pub fn hls(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let vis = &input.vis;
let sig = &input.sig;
let block = &input.block;
let attrs = &input.attrs;
TokenStream::from(quote! {
#(#attrs)*
#vis #sig {
const _: () = ();
let __rhdl_hls = ::bitloom_prelude::HlsMark;
let _ = __rhdl_hls;
#block
}
})
}
fn host_only_view(item: TokenStream, kind: &str) -> TokenStream {
let mut input = parse_macro_input!(item as DeriveInput);
strip_functional_state_field_attrs(&mut input);
let name = &input.ident;
let kind_ident = syn::Ident::new(kind, name.span());
TokenStream::from(quote! {
#input
impl ::bitloom_prelude::HostView for #name {
const KIND: ::bitloom_prelude::ViewKind = ::bitloom_prelude::ViewKind::#kind_ident;
}
})
}
#[proc_macro_attribute]
pub fn top(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput);
let name = &input.ident;
TokenStream::from(quote! {
#input
impl #name {
pub const RHDL_TOP: bool = true;
}
})
}
#[proc_macro_attribute]
pub fn fsm(attr: TokenStream, item: TokenStream) -> TokenStream {
let args = parse_macro_input!(attr as FsmAttrArgs);
let input = parse_macro_input!(item as DeriveInput);
match expand_fsm(&args, &input) {
Ok(ts) => ts,
Err(e) => e.to_compile_error().into(),
}
}
struct FsmAttrArgs {
name: Option<syn::LitStr>,
}
impl syn::parse::Parse for FsmAttrArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.is_empty() {
return Ok(Self { name: None });
}
let ident: syn::Ident = input.parse()?;
if ident != "name" {
return Err(syn::Error::new(ident.span(), "expected `name = \"...\"`"));
}
input.parse::<syn::Token![=]>()?;
let name: syn::LitStr = input.parse()?;
Ok(Self { name: Some(name) })
}
}
fn expand_fsm(args: &FsmAttrArgs, input: &DeriveInput) -> Result<TokenStream, syn::Error> {
let Data::Enum(data) = &input.data else {
return Err(syn::Error::new_spanned(
&input.ident,
"#[bitloom::fsm] / #[rhdl::fsm] may only be applied to enums (FR157)",
));
};
if data.variants.is_empty() {
return Err(syn::Error::new_spanned(
&input.ident,
"#[bitloom::fsm] enum must have at least one variant (FR157)",
));
}
let mut label_lits = Vec::new();
for v in &data.variants {
if !matches!(v.fields, Fields::Unit) {
return Err(syn::Error::new_spanned(
&v.ident,
"#[bitloom::fsm] supports only unit variants (FR157 MVP)",
));
}
let lit = v.ident.to_string();
label_lits.push(syn::LitStr::new(&lit, v.ident.span()));
}
let ty = &input.ident;
let fsm_id = if let Some(n) = &args.name {
n.value()
} else {
ty.to_string()
};
let fsm_id_lit = syn::LitStr::new(&fsm_id, ty.span());
Ok(TokenStream::from(quote! {
#input
impl ::bitloom_prelude::FsmLabels for #ty {
const FSM_ID: &'static str = #fsm_id_lit;
fn state_labels() -> &'static [&'static str] {
&[#(#label_lits),*]
}
}
}))
}