lvbitfile2rust 0.1.1

Generate Rust register maps (`struct`s) from lvbitx files - in the spirit of svd2rust
Documentation
use quote::quote;
use quote::ToTokens;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum CodegenError {
    #[error("Failed to read bitfile {0}: {1}")]
    BitfileRead(String, std::io::Error),
    #[error("Failed to parse bitfile {0}: {1}")]
    BitfileParse(String, roxmltree::Error),
    #[error("<{0}> node has no <{1}> child")]
    MissingChild(String, String),
    #[error("<{0}> node has no children")]
    NoChildren(String),
    #[error("<{0}> node has no text")]
    NoText(String),
    #[error("Unknown bitfile type: {0}")]
    UnknownBitfileType(String),
}

struct HashableTokenStream(proc_macro2::TokenStream);

impl std::cmp::PartialEq for HashableTokenStream {
    fn eq(&self, other: &Self) -> bool {
        self.0.to_string().eq(&other.0.to_string())
    }
}

impl std::cmp::Eq for HashableTokenStream {}

impl std::cmp::PartialOrd for HashableTokenStream {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl std::cmp::Ord for HashableTokenStream {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.0.to_string().cmp(&other.0.to_string())
    }
}

impl std::hash::Hash for HashableTokenStream {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.0.to_string().hash(state);
    }
}

impl ToTokens for HashableTokenStream {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        self.0.to_tokens(tokens)
    }
}

fn sanitize_ident(ident: &str) -> String {
    ident.replace(".", "_").replace(" ", "_")
}

fn first_child_by_tag<'a, 'b>(
    node: &'a roxmltree::Node<'b, 'b>,
    child_tag: &'a str,
) -> Result<roxmltree::Node<'b, 'b>, CodegenError> {
    Ok(node
        .children()
        .find(|child| child.tag_name().name() == child_tag)
        .ok_or_else(|| {
            CodegenError::MissingChild(node.tag_name().name().to_owned(), child_tag.to_owned())
        })?)
}

fn node_text<'a>(node: &'a roxmltree::Node) -> Result<&'a str, CodegenError> {
    node.text()
        .ok_or_else(|| CodegenError::NoText(node.tag_name().name().to_owned()))
}

fn type_node_to_rust_typedecl(
    type_node: &roxmltree::Node,
) -> Result<proc_macro2::TokenStream, CodegenError> {
    match type_node.tag_name().name() {
        "Boolean" => Ok(quote! {bool}),
        "U8" => Ok(quote! {u8}),
        "U16" => Ok(quote! {u16}),
        "U32" => Ok(quote! {u32}),
        "U64" => Ok(quote! {u64}),
        "I8" => Ok(quote! {i8}),
        "I16" => Ok(quote! {i16}),
        "I32" => Ok(quote! {i32}),
        "I64" => Ok(quote! {i64}),
        "SGL" => Ok(quote! {f32}),
        "DBL" => Ok(quote! {f64}),
        "Array" => {
            let size = syn::LitInt::new(
                node_text(&first_child_by_tag(type_node, "Size")?)?,
                proc_macro2::Span::call_site(),
            );
            let inner_type_node = first_child_by_tag(type_node, "Type")?
                .first_element_child()
                .ok_or_else(|| CodegenError::NoChildren("Type".to_owned()))?;
            let inner_typedecl = type_node_to_rust_typedecl(&inner_type_node)?;
            Ok(quote! {
                [#inner_typedecl; #size]
            })
        }
        "Cluster" | "EnumU8" | "EnumU16" | "EnumU32" | "EnumU64" => {
            let ident = syn::Ident::new(
                &sanitize_ident(node_text(&first_child_by_tag(type_node, "Name")?)?),
                proc_macro2::Span::call_site(),
            );
            Ok(quote! {
                types::#ident
            })
        }
        "FXP" => {
            let signed = syn::LitBool {
                value: node_text(&first_child_by_tag(type_node, "Signed")?)? == "true",
                span: proc_macro2::Span::call_site(),
            };
            let word_length = syn::LitInt::new(
                node_text(&first_child_by_tag(type_node, "WordLength")?)?,
                proc_macro2::Span::call_site(),
            );
            let integer_word_length = syn::LitInt::new(
                node_text(&first_child_by_tag(type_node, "IntegerWordLength")?)?,
                proc_macro2::Span::call_site(),
            );
            Ok(quote! {
                ni_fpga::fxp::FXP<#word_length, #integer_word_length, #signed>
            })
        }
        _ => Err(CodegenError::UnknownBitfileType(
            type_node.tag_name().name().to_owned(),
        )),
    }
}

pub fn codegen(bitfile_path: String) -> Result<proc_macro2::TokenStream, CodegenError> {
    let bitfile_contents = match std::fs::read_to_string(&bitfile_path) {
        Ok(contents) => contents,
        Err(e) => return Err(CodegenError::BitfileRead(bitfile_path, e)),
    };
    let bitfile = match roxmltree::Document::parse(&bitfile_contents) {
        Ok(doc) => doc,
        Err(e) => return Err(CodegenError::BitfileParse(bitfile_path, e)),
    };

    let mut typedefs = std::collections::HashSet::<HashableTokenStream>::new();
    let mut register_defs = Vec::<proc_macro2::TokenStream>::new();
    let mut register_fields = Vec::<proc_macro2::TokenStream>::new();
    let mut register_inits = Vec::<proc_macro2::TokenStream>::new();
    let mut vi_signature = String::new();

    for node in bitfile.root().descendants() {
        match node.tag_name().name() {
            "Cluster" => {
                let ident = syn::Ident::new(
                    &sanitize_ident(node_text(&first_child_by_tag(&node, "Name")?)?),
                    proc_macro2::Span::call_site(),
                );
                let mut fields = Vec::<proc_macro2::TokenStream>::new();
                let type_list_node = first_child_by_tag(&node, "TypeList")?;
                for field_node in type_list_node.children().filter(|child| child.is_element()) {
                    let field_name = syn::Ident::new(
                        node_text(&first_child_by_tag(&field_node, "Name")?)?,
                        proc_macro2::Span::call_site(),
                    );
                    let field_typedecl = type_node_to_rust_typedecl(&field_node)?;
                    fields.push(quote! {
                        pub #field_name: #field_typedecl
                    });
                }
                typedefs.insert(HashableTokenStream(quote! {
                    #[derive(Cluster, Debug)]
                    pub struct #ident {
                        #(#fields),*
                    }
                }));
            }
            _name if _name.starts_with("Enum") => {
                let ident = syn::Ident::new(
                    &sanitize_ident(node_text(&first_child_by_tag(&node, "Name")?)?),
                    proc_macro2::Span::call_site(),
                );
                let mut variants = Vec::<proc_macro2::TokenStream>::new();
                let string_list_node = first_child_by_tag(&node, "StringList")?;
                for variant_node in string_list_node
                    .children()
                    .filter(|child| child.is_element())
                {
                    let variant_ident = syn::Ident::new(
                        &sanitize_ident(node_text(&variant_node)?),
                        proc_macro2::Span::call_site(),
                    );
                    variants.push(quote! {
                        #variant_ident
                    });
                }
                typedefs.insert(HashableTokenStream(quote! {
                    #[derive(Debug, Enum)]
                    pub enum #ident {
                        #(#variants),*
                    }
                }));
            }
            "Register" => {
                if node_text(&first_child_by_tag(&node, "Hidden")?)? == "false" {
                    let ident = syn::Ident::new(
                        &sanitize_ident(node_text(&first_child_by_tag(&node, "Name")?)?),
                        proc_macro2::Span::call_site(),
                    );
                    let offset = syn::LitInt::new(
                        &sanitize_ident(node_text(&first_child_by_tag(&node, "Offset")?)?),
                        proc_macro2::Span::call_site(),
                    );
                    let type_node = first_child_by_tag(&node, "Datatype")?
                        .first_element_child()
                        .ok_or_else(|| CodegenError::NoChildren("Datatype".to_owned()))?;
                    let typedecl = type_node_to_rust_typedecl(&type_node)?;
                    let write_fn = match node_text(&first_child_by_tag(&node, "Indicator")?)? {
                        "false" => quote! {
                            pub fn write(&self, value: &#typedecl) -> Result<(), ni_fpga::Error> {
                                unsafe { SESSION.as_ref() }.unwrap().write(#offset, value)
                            }
                        },
                        _ => quote! {},
                    };
                    register_defs.push(quote! {
                        pub struct #ident {
                            _marker: PhantomData<*const ()>,
                        }

                        impl #ident {
                            pub fn read(&self) -> Result<#typedecl, ni_fpga::Error> {
                                unsafe { SESSION.as_ref() }.unwrap().read(#offset)
                            }
                            #write_fn
                        }
                    });
                    register_fields.push(quote! {
                        pub #ident: #ident
                    });
                    register_inits.push(quote! {
                        #ident: #ident{ _marker: PhantomData }
                    });
                }
            }
            "SignatureRegister" => {
                vi_signature = node_text(&node)?.to_owned();
            }
            _ => {}
        }
    }

    let mut typedefs_sorted = typedefs.iter().collect::<Vec<_>>();
    typedefs_sorted.sort();

    Ok(quote! {
        use std::marker::PhantomData;

        #[no_mangle]
        static mut SESSION: *const ni_fpga::Session = std::ptr::null();

        mod types {
            use ni_fpga_macros::{Cluster, Enum};
            use super::types;
            #(#typedefs_sorted)*
        }

        #(#register_defs)*

        pub struct Peripherals {
            _marker: PhantomData<*const ()>,
            #(#register_fields),*
        }

        impl Peripherals {
            pub fn take(resource: &str) -> Result<Self, Box<dyn std::error::Error>> {
                if unsafe{ !SESSION.is_null() } {
                    Err("Peripherals already in use")?
                } else {
                    let session = ni_fpga::Session::open(
                        #bitfile_path,
                        #vi_signature,
                        resource,
                    )?;
                    unsafe { SESSION = &session as *const ni_fpga::Session; }
                    std::mem::forget(session);
                    Ok(
                        Self{
                            _marker: PhantomData,
                            #(#register_inits),*
                        },
                    )
                }
            }
        }

        impl Drop for Peripherals {
            fn drop(&mut self) {
                std::mem::drop(unsafe { SESSION.as_ref() }.unwrap())
            }
        }
    })
}