fayalite 0.2.0

Hardware Description Language embedded in Rust, using FIRRTL's semantics
Documentation
// SPDX-License-Identifier: LGPL-3.0-or-later
// See Notices.txt for copyright information
use crate::{
    cli::{FormalArgs, FormalMode, FormalOutput, RunPhase},
    firrtl::ExportOptions,
};
use clap::Parser;
use hashbrown::HashMap;
use serde::Deserialize;
use std::{
    fmt::Write,
    path::{Path, PathBuf},
    process::Command,
    sync::{Mutex, OnceLock},
};

fn assert_formal_helper() -> FormalArgs {
    static FORMAL_ARGS: OnceLock<FormalArgs> = OnceLock::new();
    // ensure we only run parsing once, so errors from env vars don't produce overlapping output if we're called on multiple threads
    FORMAL_ARGS
        .get_or_init(|| FormalArgs::parse_from(["fayalite::testing::assert_formal"]))
        .clone()
}

#[derive(Deserialize)]
struct CargoMetadata {
    target_directory: String,
}

fn get_cargo_target_dir() -> &'static Path {
    static RETVAL: OnceLock<PathBuf> = OnceLock::new();
    RETVAL.get_or_init(|| {
        let output = Command::new(
            std::env::var_os("CARGO")
                .as_deref()
                .unwrap_or("cargo".as_ref()),
        )
        .arg("metadata")
        .output()
        .expect("can't run `cargo metadata`");
        if !output.status.success() {
            panic!(
                "can't run `cargo metadata`:\n{}\nexited with status: {}",
                String::from_utf8_lossy(&output.stderr),
                output.status
            );
        }
        let CargoMetadata { target_directory } =
            serde_json::from_slice(&output.stdout).expect("can't parse output of `cargo metadata`");
        PathBuf::from(target_directory)
    })
}

#[track_caller]
fn get_assert_formal_target_path(test_name: &dyn std::fmt::Display) -> PathBuf {
    static DIRS: Mutex<Option<HashMap<String, u64>>> = Mutex::new(None);
    let test_name = test_name.to_string();
    // don't use line/column numbers since that constantly changes as you edit tests
    let file = std::panic::Location::caller().file();
    // simple reproducible hash
    let simple_hash = file.bytes().chain(test_name.bytes()).fold(
        ((file.len() as u32) << 16).wrapping_add(test_name.len() as u32),
        |mut h, b| {
            h = h.wrapping_mul(0xaa0d184b);
            h ^= h.rotate_right(5);
            h ^= h.rotate_right(13);
            h.wrapping_add(b as u32)
        },
    );
    let mut dir = String::with_capacity(64);
    for ch in Path::new(file)
        .file_stem()
        .unwrap_or_default()
        .to_str()
        .unwrap()
        .chars()
        .chain(['-'])
        .chain(test_name.chars())
    {
        dir.push(match ch {
            ch if ch.is_alphanumeric() => ch,
            '_' | '-' | '+' | '.' | ',' | ' ' => ch,
            _ => '_',
        });
    }
    write!(dir, "-{simple_hash:08x}").unwrap();
    let index = *DIRS
        .lock()
        .unwrap()
        .get_or_insert_with(HashMap::new)
        .entry_ref(&dir)
        .and_modify(|v| *v += 1)
        .or_insert(0);
    write!(dir, "-{index}").unwrap();
    get_cargo_target_dir()
        .join("fayalite_assert_formal")
        .join(dir)
}

#[track_caller]
pub fn assert_formal<M>(
    test_name: impl std::fmt::Display,
    module: M,
    mode: FormalMode,
    depth: u64,
    solver: Option<&str>,
    export_options: ExportOptions,
) where
    FormalArgs: RunPhase<M, Output = FormalOutput>,
{
    let mut args = assert_formal_helper();
    args.verilog.firrtl.base.redirect_output_for_rust_test = true;
    args.verilog.firrtl.base.output = Some(get_assert_formal_target_path(&test_name));
    args.verilog.firrtl.export_options = export_options;
    args.verilog.debug = true;
    args.mode = mode;
    args.depth = depth;
    if let Some(solver) = solver {
        args.solver = solver.into();
    }
    args.run(module).expect("testing::assert_formal() failed");
}