hw_regmap 0.2.2

hw_regmap: A register map generation tool.
Documentation
use std::collections::HashMap;

use hw_regmap::generator;
use hw_regmap::regmap;

use regex::Regex;
use tera::Tera;

/// Define CLI arguments
use clap::Parser;
#[derive(clap::Parser, Debug, Clone)]
#[clap(long_about = "Generate RTL register map")]
pub struct Args {
    // Regmap configuration ----------------------------------------------------
    #[clap(long, value_parser)]
    toml_file: Vec<String>,

    // Output configuration ----------------------------------------------------
    // Output folder path
    #[clap(long, value_parser, default_value = "output")]
    output_path: String,

    // Basename of the generated file
    #[clap(long, value_parser, default_value = "regmap")]
    basename: String,

    // Debug options ----------------------------------------------------------
    /// Enable verbosity
    #[clap(long, value_parser)]
    verbose: bool,
}

/// Simple post-process of String generated by templating engine
/// Templating introduce a lot of consecutive newlines due to filtering
/// -> This function remove consecutive newline to have something more readable in the end
fn post_process(raw: &str) -> String {
    // Remove extra new-line generated by tera templating
    let regex = Regex::new(r"\s*\n\s*\n+").unwrap();
    let post_rendered = regex.replace_all(raw, "\n");

    post_rendered.to_string()
}

/// Simple function used to render integer in hexadecimal format with tera
/// Syntax in tera file is: as_hex()
pub fn as_hex(args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
    // Extract width if specified
    let width = if let Some(width) = args.get("width") {
        if let tera::Value::Number(num) = width {
            Ok(num.as_u64().unwrap() as usize)
        } else {
            Err(tera::Error::msg("Width is not an integer"))
        }
    } else {
        Ok(0)
    }?;

    if let Some(value) = args.get("val") {
        if let tera::Value::Number(num) = value {
            let hex_str = format!("0x{:0>width$x}", num.as_u64().unwrap(), width = width);
            Ok(tera::Value::String(hex_str))
        } else {
            Err(tera::Error::msg("Value is not an integer"))
        }
    } else {
        Err(tera::Error::msg(
            "Function `as_hex` didn't receive a `val` argument",
        ))
    }
}

/// Simple function used to render integer in hexadecimal SystemVerilog format with tera
/// Syntax in tera file is: as_sv_hex()
pub fn as_sv_hex(args: &HashMap<String, tera::Value>) -> tera::Result<tera::Value> {
    // Extract width if specified
    let width = if let Some(width) = args.get("width") {
        if let tera::Value::Number(num) = width {
            Ok(Some(num.as_u64().unwrap() as usize))
        } else {
            Err(tera::Error::msg("Width is not an integer"))
        }
    } else {
        Ok(None)
    }?;

    if let Some(value) = args.get("val") {
        if let tera::Value::Number(num) = value {
            let hex_str = format!(
                "{}'h{:x}",
                if let Some(w) = width {
                    format!("{w}")
                } else {
                    "".to_string()
                },
                num.as_u64().unwrap(),
            );
            Ok(tera::Value::String(hex_str))
        } else {
            Err(tera::Error::msg("Value is not an integer"))
        }
    } else {
        Err(tera::Error::msg(
            "Function `as_sv_hex` didn't receive a `val` argument",
        ))
    }
}

fn generate_sv(regmap: &regmap::Regmap, output_path: &str, engine: &Tera) {
    // Generate module body  ==================================================
    let rtl_module = format!("{}/{}.sv", output_path, regmap.module_name());

    // Convert regmap in rtl snippets based on Tera
    let mut regs_sv = Vec::new();
    let mut used_params = Vec::new();
    regmap.section().iter().for_each(|sec| {
        sec.register().iter().for_each(|reg| {
            regs_sv.push(generator::SvRegister::from_register(
                sec.name(),
                reg,
                &mut used_params,
                engine,
            ));
        })
    });

    // Expand to rtl module and store in targeted file
    let mut context = tera::Context::new();
    // Extract version from env
    let git_version = option_env!("GIT_VERSION").unwrap_or("unknown");
    context.insert("tool_version", git_version);
    context.insert("module_name", &regmap.module_name());
    context.insert("word_size_b", &regmap.word_size_b());
    context.insert("offset", &regmap.offset());
    context.insert("ext_pkg", &regmap.ext_pkg());
    context.insert("range", &regmap.range());
    context.insert("regs_sv", &regs_sv);
    let module_rendered = engine.render("module.sv", &context).unwrap();
    let module_post_rendered = post_process(&module_rendered);
    std::fs::write(&rtl_module, module_post_rendered)
        .unwrap_or_else(|_| panic!("Unable to write file {rtl_module}"));

    // Generate addr/field pkg ================================================
    let rtl_pkg = format!("{}/{}_pkg.sv", output_path, regmap.module_name());

    // Convert regmap in pkg snippets based on Tera
    let mut regs_pkg_sv = Vec::new();
    regmap.section().iter().for_each(|sec| {
        sec.register().iter().for_each(|reg| {
            regs_pkg_sv.push(generator::SvRegisterPkg::from_register(
                sec.name(),
                regmap.word_size_b(),
                reg,
                engine,
            ));
        })
    });

    // Expand to rtl module and store in targeted file
    let mut context = tera::Context::new();
    // Extract version from env
    let git_version = option_env!("GIT_VERSION").unwrap_or("unknown");
    context.insert("tool_version", git_version);
    context.insert("module_name", &regmap.module_name());
    context.insert("word_size_b", &regmap.word_size_b());
    context.insert("regs_pkg_sv", &regs_pkg_sv);
    let pkg_rendered = engine.render("pkg.sv", &context).unwrap();
    let pkg_post_rendered = post_process(&pkg_rendered);

    std::fs::write(&rtl_pkg, pkg_post_rendered)
        .unwrap_or_else(|_| panic!("Unable to write file {rtl_pkg}"));
}

/// Generate Markdown and Json documentation in output_path folder
fn generate_doc(regmap: &regmap::Regmap, output_path: &str, engine: &Tera) -> std::io::Result<()> {
    // Generate documentation ====================================================================
    // JSON
    // Serialize as json for full access all fields
    let doc_json = format!("{}/{}_doc.json", output_path, regmap.module_name());
    let doc_json_f = std::fs::File::create(&doc_json)?;
    serde_json::to_writer_pretty(doc_json_f, &regmap)?;

    // Markdown
    // Generate a structure document targeting online documentation
    let doc_md = format!("{}/{}_doc.md", output_path, regmap.module_name());
    // Expand to docs and store in targeted file
    let mut context = tera::Context::new();
    // Extract version from env
    let git_version = option_env!("GIT_VERSION").unwrap_or("unknown");
    context.insert("tool_version", git_version);
    context.insert("regmap", &regmap);
    let md_rendered = engine.render("docs/fmt_as.md", &context).unwrap();
    std::fs::write(&doc_md, md_rendered)
        .unwrap_or_else(|_| panic!("Unable to write file {doc_md}"));
    Ok(())
}

/// Parse user ClI
/// Generate is done in two-fold:
/// 1. Aggregate all the toml in a fused registermap.
/// > This regmap is generated with the basename as module name.
/// > Documetations is generated (i.e. Markdown and json)
/// 2. Each toml regmap are generated individually
fn main() -> std::io::Result<()> {
    let args = Args::parse();
    println!("User Options: {args:?}");

    // Create a new Tera instances
    // Analyse all available SystemVerilog template
    let mut tera_sv = Tera::new("templates/**/*.sv").unwrap();
    tera_sv.register_function("as_sv_hex", as_sv_hex);
    // Analyse all available doc template
    let mut tera_doc = Tera::new("templates/**/fmt_as.*").unwrap();
    tera_doc.register_function("as_hex", as_hex);

    // Ensure that output folder exist
    std::fs::create_dir_all(&args.output_path).unwrap();

    // 1. Generate the fused regmap ================================================================
    // Expand regmap => Check properties and expand optional fields
    // Parse toml files
    let mut regmap_list = args
        .toml_file
        .iter()
        .map(|toml| regmap::parser::RegmapOpt::read_from(toml))
        .collect::<Vec<_>>();

    let mut fused_regmap = regmap::Regmap::from_opt(&mut regmap_list).unwrap();
    if args.verbose {
        println!("{fused_regmap}");
    }
    // Override module_name with basename for the fused version
    // Fused version only used to check that there is no overlapping between file and for documentation
    // -> No SystemVerilog generated
    *fused_regmap.module_name_mut() = args.basename.clone();
    generate_doc(&fused_regmap, &args.output_path, &tera_doc)?;

    // 2. Generate individual regmap ===============================================================
    args.toml_file.iter().for_each(|toml| {
        let regmap_opt = regmap::parser::RegmapOpt::read_from(toml);
        let regmap = regmap::Regmap::from_opt(&mut [regmap_opt]).unwrap();
        generate_sv(&regmap, &args.output_path, &tera_sv);
    });

    Ok(())
}