1use proc_macro::TokenStream;
4use quote::quote;
5use syn::{
6 Attribute, Data, DeriveInput, Fields, GenericArgument, ItemFn, PathArguments, Type,
7 parse_macro_input,
8};
9
10fn is_functional_state_attr(attr: &Attribute) -> bool {
12 attr.path()
13 .segments
14 .last()
15 .is_some_and(|s| s.ident == "functional_state")
16}
17
18fn strip_functional_state_field_attrs(input: &mut DeriveInput) {
20 if let Data::Struct(data) = &mut input.data {
21 for field in data.fields.iter_mut() {
22 field.attrs.retain(|a| !is_functional_state_attr(a));
23 }
24 }
25}
26
27#[proc_macro_derive(Bundle, attributes(bundle))]
40pub fn derive_bundle(input: TokenStream) -> TokenStream {
41 let input = parse_macro_input!(input as DeriveInput);
42 match expand_derive_bundle(&input) {
43 Ok(ts) => ts,
44 Err(e) => e.to_compile_error().into(),
45 }
46}
47
48fn expand_derive_bundle(input: &DeriveInput) -> Result<TokenStream, syn::Error> {
49 if !input.generics.params.is_empty() {
50 return Err(syn::Error::new_spanned(
51 &input.generics,
52 "rhdl::E0180: #[derive(Bundle)] does not support generic parameters",
53 ));
54 }
55
56 let Data::Struct(data) = &input.data else {
57 return Err(syn::Error::new_spanned(
58 input,
59 "rhdl::E0180: #[derive(Bundle)] only supports structs with named fields",
60 ));
61 };
62
63 let Fields::Named(fields) = &data.fields else {
64 return Err(syn::Error::new_spanned(
65 &data.fields,
66 "rhdl::E0180: #[derive(Bundle)] requires named fields (tuple/unit structs unsupported)",
67 ));
68 };
69
70 let name = &input.ident;
71 let mut leaf_entries = Vec::new();
72 let mut nested_entries = Vec::new();
73
74 for field in &fields.named {
75 let Some(ident) = &field.ident else {
76 return Err(syn::Error::new_spanned(
77 field,
78 "rhdl::E0180: #[derive(Bundle)] requires named fields",
79 ));
80 };
81 let field_name = ident.to_string();
82 match classify_bundle_field(&field.ty)? {
83 BundleFieldKind::Ground { ground_expr } => {
84 leaf_entries.push(quote! {
85 (#field_name, #ground_expr)
86 });
87 }
88 BundleFieldKind::Nested { ty } => {
89 nested_entries.push(quote! {
90 (#field_name, <#ty as ::bitloom_prelude::Bundle>::leaves)
91 });
92 }
93 }
94 }
95
96 Ok(TokenStream::from(quote! {
97 impl ::bitloom_prelude::Bundle for #name {
98 fn leaves() -> &'static [(&'static str, ::bitloom_prelude::GroundType)] {
99 &[#(#leaf_entries),*]
100 }
101
102 fn nested_bundles() -> &'static [(
103 &'static str,
104 fn() -> &'static [(&'static str, ::bitloom_prelude::GroundType)],
105 )] {
106 &[#(#nested_entries),*]
107 }
108 }
109 }))
110}
111
112enum BundleFieldKind {
113 Ground {
114 ground_expr: proc_macro2::TokenStream,
115 },
116 Nested {
117 ty: Type,
118 },
119}
120
121fn classify_bundle_field(ty: &Type) -> Result<BundleFieldKind, syn::Error> {
122 let Type::Path(type_path) = ty else {
123 return Err(syn::Error::new_spanned(
124 ty,
125 "rhdl::E0180: #[derive(Bundle)] field types must be simple paths (ground or nested Bundle)",
126 ));
127 };
128
129 if type_path.qself.is_some() {
130 return Err(syn::Error::new_spanned(
131 ty,
132 "rhdl::E0180: #[derive(Bundle)] does not support qualified self types",
133 ));
134 }
135
136 let last =
137 type_path.path.segments.last().ok_or_else(|| {
138 syn::Error::new_spanned(ty, "rhdl::E0180: empty path in Bundle field")
139 })?;
140 let ident = last.ident.to_string();
141
142 match ident.as_str() {
143 "HwVec" => {
144 return Err(syn::Error::new_spanned(
145 ty,
146 "rhdl::E0180: #[derive(Bundle)] does not support HwVec fields; HwVec<Bundle,_> remains OUT OF SCOPE",
147 ));
148 }
149 "Input" | "Output" => {
150 return Err(syn::Error::new_spanned(
151 ty,
152 "rhdl::E0180: #[derive(Bundle)] fields must be bare ground or nested Bundle types, not Input/Output",
153 ));
154 }
155 "Bool" => {
156 require_no_args(ty, &last.arguments)?;
157 return Ok(BundleFieldKind::Ground {
158 ground_expr: quote! { ::bitloom_prelude::GroundType::Bool },
159 });
160 }
161 "Clock" => {
162 require_no_args(ty, &last.arguments)?;
163 return Ok(BundleFieldKind::Ground {
164 ground_expr: quote! { ::bitloom_prelude::GroundType::Clock },
165 });
166 }
167 "Reset" => {
168 require_no_args(ty, &last.arguments)?;
169 return Ok(BundleFieldKind::Ground {
170 ground_expr: quote! { ::bitloom_prelude::GroundType::Reset },
171 });
172 }
173 "UInt" | "Bits" => {
174 let width = const_generic_u32(ty, &last.arguments)?;
175 return Ok(BundleFieldKind::Ground {
176 ground_expr: quote! { ::bitloom_prelude::GroundType::UInt { width: #width } },
177 });
178 }
179 "SInt" => {
180 let width = const_generic_u32(ty, &last.arguments)?;
181 return Ok(BundleFieldKind::Ground {
182 ground_expr: quote! { ::bitloom_prelude::GroundType::SInt { width: #width } },
183 });
184 }
185 _ => {}
186 }
187
188 if !matches!(last.arguments, PathArguments::None) {
190 return Err(syn::Error::new_spanned(
191 ty,
192 "rhdl::E0180: #[derive(Bundle)] nested Bundle fields must be bare type paths without generics",
193 ));
194 }
195
196 Ok(BundleFieldKind::Nested { ty: ty.clone() })
197}
198
199fn require_no_args(ty: &Type, args: &PathArguments) -> Result<(), syn::Error> {
200 if matches!(args, PathArguments::None) {
201 Ok(())
202 } else {
203 Err(syn::Error::new_spanned(
204 ty,
205 "rhdl::E0180: unexpected type arguments on ground Bundle field",
206 ))
207 }
208}
209
210fn const_generic_u32(ty: &Type, args: &PathArguments) -> Result<u32, syn::Error> {
211 let PathArguments::AngleBracketed(ab) = args else {
212 return Err(syn::Error::new_spanned(
213 ty,
214 "rhdl::E0180: width-parameterized ground types require a const generic (e.g. UInt<8>)",
215 ));
216 };
217 let mut width: Option<u32> = None;
218 for arg in &ab.args {
219 match arg {
220 GenericArgument::Const(syn::Expr::Lit(syn::ExprLit {
221 lit: syn::Lit::Int(lit),
222 ..
223 })) => {
224 if width.is_some() {
225 return Err(syn::Error::new_spanned(
226 ty,
227 "rhdl::E0180: expected exactly one const width parameter",
228 ));
229 }
230 width = Some(lit.base10_parse()?);
231 }
232 _ => {
233 return Err(syn::Error::new_spanned(
234 arg,
235 "rhdl::E0180: ground width must be an integer literal const generic",
236 ));
237 }
238 }
239 }
240 width.ok_or_else(|| {
241 syn::Error::new_spanned(
242 ty,
243 "rhdl::E0180: width-parameterized ground types require a const generic (e.g. UInt<8>)",
244 )
245 })
246}
247
248#[proc_macro_attribute]
255pub fn module(_attr: TokenStream, item: TokenStream) -> TokenStream {
256 let input = parse_macro_input!(item as DeriveInput);
257 let name = &input.ident;
258 let vis = &input.vis;
259
260 let syn::Data::Struct(data) = &input.data else {
261 return syn::Error::new_spanned(&input, "rhdl::module only supports structs")
262 .to_compile_error()
263 .into();
264 };
265
266 let mod_name = name.to_string();
267 let field_defs = data.fields.iter().map(|f| {
268 let id = f.ident.as_ref().unwrap();
269 let ty = &f.ty;
270 let fvis = &f.vis;
271 let keep_attrs: Vec<_> = f
273 .attrs
274 .iter()
275 .filter(|a| !is_functional_state_attr(a))
276 .collect();
277 quote! { #(#keep_attrs)* #fvis #id: #ty }
278 });
279
280 let port_stmts = data.fields.iter().filter_map(|field| {
281 if field.attrs.iter().any(is_functional_state_attr) {
282 return None;
284 }
285 let Some(ident) = &field.ident else {
286 return Some(quote! {
287 compile_error!("tuple structs are not supported by rhdl::module");
288 });
289 };
290 let ty = &field.ty;
291 let name_str = ident.to_string();
292 Some(quote! {
293 {
294 type __PortTy = #ty;
295 for (__leaf, __dir, __gt) in
296 <__PortTy as ::bitloom_prelude::PortField>::flatten(#name_str)
297 {
298 match __dir {
299 ::bitloom_prelude::PortDir::Input => {
300 __session.add_input(
301 __leaf,
302 __gt,
303 ::bitloom_prelude::Span::default(),
304 );
305 }
306 ::bitloom_prelude::PortDir::Output => {
307 __session.add_output(
308 __leaf,
309 __gt,
310 ::bitloom_prelude::Span::default(),
311 );
312 }
313 }
314 }
315 }
316 })
317 });
318
319 TokenStream::from(quote! {
320 #vis struct #name {
321 #(#field_defs),*
322 }
323
324 impl ::bitloom_prelude::Elaboratable for #name {
325 fn elaborate() -> ::core::result::Result<
326 ::bitloom_prelude::FrozenHir,
327 ::bitloom_prelude::Diagnostics,
328 > {
329 let mut __session = ::bitloom_prelude::ElaborateSession::new(#mod_name);
330 __session.begin_module(#mod_name, ::bitloom_prelude::Span::default());
331 #(#port_stmts)*
332 __session.end_module();
333 __session.finish()
334 }
335 }
336 })
337}
338
339#[proc_macro_attribute]
342pub fn combinational(_attr: TokenStream, item: TokenStream) -> TokenStream {
343 let input = parse_macro_input!(item as ItemFn);
344 let vis = &input.vis;
345 let sig = &input.sig;
346 let block = &input.block;
347 let attrs = &input.attrs;
348 TokenStream::from(quote! {
349 #(#attrs)*
350 #vis #sig {
351 const _: () = ();
353 let __rhdl_process_kind = ::bitloom_prelude::ProcessKindMark::Combinational;
354 let _ = __rhdl_process_kind;
355 #block
356 }
357 })
358}
359
360#[proc_macro_attribute]
362pub fn sequential(_attr: TokenStream, item: TokenStream) -> TokenStream {
363 let input = parse_macro_input!(item as ItemFn);
364 let vis = &input.vis;
365 let sig = &input.sig;
366 let block = &input.block;
367 let attrs = &input.attrs;
368 TokenStream::from(quote! {
369 #(#attrs)*
370 #vis #sig {
371 const _: () = ();
372 let __rhdl_process_kind = ::bitloom_prelude::ProcessKindMark::Sequential;
373 let _ = __rhdl_process_kind;
374 #block
375 }
376 })
377}
378
379#[proc_macro_attribute]
381pub fn process(_attr: TokenStream, item: TokenStream) -> TokenStream {
382 let input = parse_macro_input!(item as ItemFn);
383 syn::Error::new_spanned(
384 input.sig.ident,
385 "hardware processes must use #[combinational] or #[sequential]; bare #[process] is forbidden",
386 )
387 .to_compile_error()
388 .into()
389}
390
391#[proc_macro_attribute]
393pub fn functional_model(_attr: TokenStream, item: TokenStream) -> TokenStream {
394 host_only_view(item, "FunctionalModel")
395}
396
397#[proc_macro_attribute]
400pub fn bridge(_attr: TokenStream, item: TokenStream) -> TokenStream {
401 host_only_view(item, "Bridge")
402}
403
404#[proc_macro_attribute]
406pub fn abstraction(_attr: TokenStream, item: TokenStream) -> TokenStream {
407 host_only_view(item, "Abstraction")
408}
409
410#[proc_macro_attribute]
412pub fn both(_attr: TokenStream, item: TokenStream) -> TokenStream {
413 host_only_view(item, "Both")
414}
415
416#[proc_macro_attribute]
419pub fn hls(_attr: TokenStream, item: TokenStream) -> TokenStream {
420 let input = parse_macro_input!(item as ItemFn);
421 let vis = &input.vis;
422 let sig = &input.sig;
423 let block = &input.block;
424 let attrs = &input.attrs;
425 TokenStream::from(quote! {
426 #(#attrs)*
427 #vis #sig {
428 const _: () = ();
429 let __rhdl_hls = ::bitloom_prelude::HlsMark;
430 let _ = __rhdl_hls;
431 #block
432 }
433 })
434}
435
436fn host_only_view(item: TokenStream, kind: &str) -> TokenStream {
437 let mut input = parse_macro_input!(item as DeriveInput);
438 strip_functional_state_field_attrs(&mut input);
440 let name = &input.ident;
441 let kind_ident = syn::Ident::new(kind, name.span());
442 TokenStream::from(quote! {
443 #input
444
445 impl ::bitloom_prelude::HostView for #name {
446 const KIND: ::bitloom_prelude::ViewKind = ::bitloom_prelude::ViewKind::#kind_ident;
447 }
448 })
450}
451
452#[proc_macro_attribute]
454pub fn top(_attr: TokenStream, item: TokenStream) -> TokenStream {
455 let input = parse_macro_input!(item as DeriveInput);
456 let name = &input.ident;
457 TokenStream::from(quote! {
458 #input
459
460 impl #name {
461 pub const RHDL_TOP: bool = true;
463 }
464 })
465}
466
467#[proc_macro_attribute]
475pub fn fsm(attr: TokenStream, item: TokenStream) -> TokenStream {
476 let args = parse_macro_input!(attr as FsmAttrArgs);
477 let input = parse_macro_input!(item as DeriveInput);
478 match expand_fsm(&args, &input) {
479 Ok(ts) => ts,
480 Err(e) => e.to_compile_error().into(),
481 }
482}
483
484struct FsmAttrArgs {
485 name: Option<syn::LitStr>,
486}
487
488impl syn::parse::Parse for FsmAttrArgs {
489 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
490 if input.is_empty() {
491 return Ok(Self { name: None });
492 }
493 let ident: syn::Ident = input.parse()?;
494 if ident != "name" {
495 return Err(syn::Error::new(ident.span(), "expected `name = \"...\"`"));
496 }
497 input.parse::<syn::Token![=]>()?;
498 let name: syn::LitStr = input.parse()?;
499 Ok(Self { name: Some(name) })
500 }
501}
502
503fn expand_fsm(args: &FsmAttrArgs, input: &DeriveInput) -> Result<TokenStream, syn::Error> {
504 let Data::Enum(data) = &input.data else {
505 return Err(syn::Error::new_spanned(
506 &input.ident,
507 "#[bitloom::fsm] / #[rhdl::fsm] may only be applied to enums (FR157)",
508 ));
509 };
510 if data.variants.is_empty() {
511 return Err(syn::Error::new_spanned(
512 &input.ident,
513 "#[bitloom::fsm] enum must have at least one variant (FR157)",
514 ));
515 }
516 let mut label_lits = Vec::new();
517 for v in &data.variants {
518 if !matches!(v.fields, Fields::Unit) {
519 return Err(syn::Error::new_spanned(
520 &v.ident,
521 "#[bitloom::fsm] supports only unit variants (FR157 MVP)",
522 ));
523 }
524 let lit = v.ident.to_string();
525 label_lits.push(syn::LitStr::new(&lit, v.ident.span()));
526 }
527 let ty = &input.ident;
528 let fsm_id = if let Some(n) = &args.name {
529 n.value()
530 } else {
531 ty.to_string()
532 };
533 let fsm_id_lit = syn::LitStr::new(&fsm_id, ty.span());
534 Ok(TokenStream::from(quote! {
535 #input
536
537 impl ::bitloom_prelude::FsmLabels for #ty {
538 const FSM_ID: &'static str = #fsm_id_lit;
539 fn state_labels() -> &'static [&'static str] {
540 &[#(#label_lits),*]
541 }
542 }
543 }))
544}