Skip to main content

bitloom_macro/
lib.rs

1//! Proc macros expand only to `bitloom_builder` / prelude paths (AD-6). Never depend on rhdl-hir.
2
3use proc_macro::TokenStream;
4use quote::quote;
5use syn::{
6    Attribute, Data, DeriveInput, Fields, GenericArgument, ItemFn, PathArguments, Type,
7    parse_macro_input,
8};
9
10/// FR102 / AD-5: `#[functional_state]` / `#[rhdl::functional_state]` (last path segment).
11fn 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
18/// Drop inert `functional_state` field attrs so rustc never sees unknown attributes.
19fn 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/// Derive [`bitloom_prelude::Bundle`] from named struct fields (FR80 / Story 32.3).
28///
29/// **Supported:** named-field structs; ground fields `Bool` / `Clock` / `Reset` /
30/// `UInt<N>` / `SInt<N>` / `Bits<N>`; other simple path types as **one-level** nested
31/// Bundles (`Type::leaves`).
32///
33/// **Rejected (stable `compile_error!`):** enums, unions, tuple structs, unit structs
34/// without named fields, `HwVec<_>`, `Input<_>` / `Output<_>`, references/tuples/arrays,
35/// and non-path field types.
36///
37/// Re-exported from `bitloom-prelude` so design crates never depend on this crate
38/// directly (AD-6).
39#[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    // Nested Bundle: simple path, no type args (Epic 32 one-level contract).
189    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/// Marks a struct as an RHDL module shell for Story 1.1.
249/// Generates `Elaboratable` that records directed ports via the builder session.
250///
251/// Fields marked `#[functional_state]` / `#[rhdl::functional_state]` (FR102) are
252/// host-only soft state: kept on the Rust struct, **skipped** for port registration,
253/// and never enter FrozenHir / `freeze` (AD-5 / AD-18).
254#[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        // Strip functional_state so the expanded struct is plain Rust.
272        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            // FR102 negative gate: soft fields must not register as HIR ports.
283            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/// Marks a hardware process as combinational. Expands to a builder open/close
340/// around the function body (must call session helpers for assigns).
341#[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            // Marker retained so unmarked hardware fns are distinguishable.
352            const _: () = ();
353            let __rhdl_process_kind = ::bitloom_prelude::ProcessKindMark::Combinational;
354            let _ = __rhdl_process_kind;
355            #block
356        }
357    })
358}
359
360/// Marks a hardware process as sequential.
361#[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/// Rejects unmarked hardware process attributes — use `combinational`/`sequential`.
380#[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/// Marks a handwritten functional model (Story 3.3). Does not enter FrozenHir.
392#[proc_macro_attribute]
393pub fn functional_model(_attr: TokenStream, item: TokenStream) -> TokenStream {
394    host_only_view(item, "FunctionalModel")
395}
396
397/// Handwritten TLM↔pin adapter (FR29). Host-only; never enters FrozenHir / freeze.
398/// Does **not** generate TLM from HIR.
399#[proc_macro_attribute]
400pub fn bridge(_attr: TokenStream, item: TokenStream) -> TokenStream {
401    host_only_view(item, "Bridge")
402}
403
404/// Handwritten untimed / transaction abstraction (FR29). Host-only.
405#[proc_macro_attribute]
406pub fn abstraction(_attr: TokenStream, item: TokenStream) -> TokenStream {
407    host_only_view(item, "Abstraction")
408}
409
410/// Mixed `both` simulation: RTL (`tick`) + handwritten view in one fixture (FR29).
411#[proc_macro_attribute]
412pub fn both(_attr: TokenStream, item: TokenStream) -> TokenStream {
413    host_only_view(item, "Both")
414}
415
416/// Marks a function for HLS (FR35 external and/or FR95 in-tree). Does not schedule in the macro;
417/// scheduling is `bitloom::hls::schedule_in_tree` (FR95) or external Bambu (FR35).
418#[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    // FR102: allow `#[functional_state]` on host view fields; strip before emit.
439    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        // Host-only; never participates in freeze/HIR. No HIR→TLM lowering.
449    })
450}
451
452/// Marks the elaboratable top for `cargo rhdl build` (AD-19).
453#[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            /// ABI marker used by the RHDL host/CLI.
462            pub const RHDL_TOP: bool = true;
463        }
464    })
465}
466
467/// FR157: mark an FSM state `enum` so variant names become a label set.
468///
469/// Optional attribute: `name = "demo"` overrides the FSM id (default: type name).
470/// Only unit variants are supported. Empty enums / non-enums → `compile_error!`.
471///
472/// Implements [`bitloom_prelude::FsmLabels`]. Re-exported as `#[rhdl::fsm]` /
473/// `#[bitloom::fsm]` from `bitloom-prelude` (AD-6).
474#[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}