From 2218cef727625657de7547bbc1de57ac1b35bd9b Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Fri, 12 Dec 2025 09:52:31 +0000 Subject: [PATCH 01/22] Add recursive search, add in cosim library building --- vw-cli/src/main.rs | 66 +++++++++++----- vw-lib/src/lib.rs | 187 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 219 insertions(+), 34 deletions(-) diff --git a/vw-cli/src/main.rs b/vw-cli/src/main.rs index 510c6c5..171bd90 100644 --- a/vw-cli/src/main.rs +++ b/vw-cli/src/main.rs @@ -7,6 +7,7 @@ use clap::{Parser, Subcommand, ValueEnum}; use colored::*; use std::fmt; use std::process; +use std::collections::HashSet; use vw_lib::{ add_dependency_with_token, clear_cache, extract_hostname_from_repo_url, @@ -96,6 +97,14 @@ enum Commands { std: CliVhdlStandard, #[arg(long, help = "List all available testbenches")] list: bool, + #[arg(long, help = "Enable recursive search when looking for testbenches")] + recurse: bool, + #[arg(long, value_delimiter = ',', help = "Ignore directories matching these names (comma-separated or use multiple times)")] + ignore: Vec, + #[arg(long, value_delimiter = ',', help = "Runtime flags to pass to NVC (comma-separated or use multiple times)", requires = "testbench")] + runtime_flags: Vec, + #[arg(long, help = "Build Rust library for testbench before running", requires = "testbench")] + build_rust: bool, }, } @@ -131,7 +140,6 @@ async fn get_access_credentials_for_workspace( #[tokio::main] async fn main() { let cli = Cli::parse(); - // Get current working directory let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap_or_else(|e| { @@ -318,34 +326,50 @@ async fn main() { testbench, std, list, + recurse, + ignore, + runtime_flags, + build_rust, } => { if list { - match list_testbenches(&cwd) { - Ok(testbenches) => { - if testbenches.is_empty() { - println!("No testbenches found in bench directory"); - } else { - println!("Available testbenches:"); - for tb in testbenches { - println!( - " {} - {}", - tb.name.cyan(), - tb.path - .display() - .to_string() - .bright_black() - ); + let bench_dir = cwd.join("bench"); + if !bench_dir.exists() { + println!("No bench dir found in {:}", bench_dir.as_str()); + } + else { + let mut ignore_set : HashSet = HashSet::new(); + for ignore_pattern in ignore { + ignore_set.insert(ignore_pattern); + } + + match list_testbenches(&bench_dir, &ignore_set, recurse) { + Ok(testbenches) => { + if testbenches.is_empty() { + println!("No testbenches found in bench directory"); + } else { + println!("Available testbenches:"); + for tb in testbenches { + println!( + " {} - {}", + tb.name.cyan(), + tb.path + .display() + .to_string() + .bright_black() + ); + } } } - } - Err(e) => { - eprintln!("{} {e}", "error:".bright_red()); - process::exit(1); + Err(e) => { + eprintln!("{} {e}", "error:".bright_red()); + process::exit(1); + } } } + } else if let Some(testbench_name) = testbench { println!("Running testbench: {}", testbench_name.cyan()); - match run_testbench(&cwd, testbench_name.clone(), std.into()) + match run_testbench(&cwd, testbench_name.clone(), std.into(), recurse, &runtime_flags, build_rust) .await { Ok(()) => { diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index 3315d58..af156d6 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -33,7 +33,7 @@ use std::fmt; use std::fs; use std::path::{Path, PathBuf}; -use camino::Utf8Path; +use camino::{Utf8Path, Utf8PathBuf}; use serde::{Deserialize, Serialize}; // ============================================================================ @@ -197,6 +197,16 @@ pub struct VhdlLsConfig { pub lint: Option>, } +#[derive(Deserialize, Debug)] +struct CargoToml { + package: CargoPackage, +} + +#[derive(Deserialize, Debug)] +struct CargoPackage { + name: String, +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct VhdlLsLibrary { pub files: Vec, @@ -713,13 +723,10 @@ pub fn generate_deps_tcl(workspace_dir: &Utf8Path) -> Result<()> { /// List all available testbenches in the workspace. pub fn list_testbenches( - workspace_dir: &Utf8Path, + bench_dir: &Utf8Path, + ignore_dirs : &HashSet, + recurse : bool ) -> Result> { - let bench_dir = workspace_dir.join("bench"); - if !bench_dir.exists() { - return Ok(Vec::new()); - } - let mut testbenches = Vec::new(); for entry in fs::read_dir(bench_dir).map_err(|e| VwError::FileSystem { @@ -743,6 +750,17 @@ pub fn list_testbenches( } } } + else if recurse { + let dir_path: Utf8PathBuf = path.try_into().map_err(|e| VwError::FileSystem { + message: format!("Failed to get dir path: {e}"), + })?; + if let Some(file_name) = dir_path.file_name() { + if !ignore_dirs.contains(file_name) { + let mut lower_testbenches = list_testbenches(&dir_path, ignore_dirs, recurse)?; + testbenches.append(&mut lower_testbenches); + } + } + } } Ok(testbenches) @@ -759,6 +777,9 @@ pub async fn run_testbench( workspace_dir: &Utf8Path, testbench_name: String, vhdl_std: VhdlStandard, + recurse: bool, + runtime_flags: &[String], + build_rust: bool, ) -> Result<()> { let vhdl_ls_config = load_existing_vhdl_ls_config(workspace_dir)?; @@ -842,7 +863,14 @@ pub async fn run_testbench( }); } - let testbench_file = find_testbench_file(&testbench_name, &bench_dir)?; + let testbench_file = find_testbench_file(&testbench_name, &bench_dir, recurse)?; + + // Build Rust library if requested + let rust_lib_path = if build_rust { + Some(build_rust_library(&testbench_file).await?) + } else { + None + }; // Filter defaultlib files to exclude OTHER testbenches but allow common bench code let bench_dir_abs = workspace_dir.as_std_path().join("bench"); @@ -887,6 +915,12 @@ pub async fn run_testbench( // Run NVC simulation let mut nvc_cmd = tokio::process::Command::new("nvc"); + + // Set GPI_USERS environment variable if we have a Rust library + if let Some(lib_path) = &rust_lib_path { + nvc_cmd.env("GPI_USERS", lib_path.to_string_lossy().as_ref()); + } + nvc_cmd .arg(format!("--std={vhdl_std}")) .arg("-M") @@ -909,7 +943,19 @@ pub async fn run_testbench( .arg("-e") .arg(&testbench_name) .arg("-r") - .arg(&testbench_name) + .arg(&testbench_name); + + // Add user-provided runtime flags + for flag in runtime_flags { + nvc_cmd.arg(flag); + } + + // Add Rust library if built + if let Some(lib_path) = &rust_lib_path { + nvc_cmd.arg(format!("--load={}", lib_path.display())); + } + + nvc_cmd .arg("--dump-arrays") .arg("--format=fst") .arg(format!("--wave={testbench_name}.fst")); @@ -920,7 +966,14 @@ pub async fn run_testbench( if !status.success() { // Build command string for display - let mut cmd_parts = vec!["nvc".to_string()]; + let mut cmd_parts = Vec::new(); + + // Add environment variable if present + if let Some(lib_path) = &rust_lib_path { + cmd_parts.push(format!("GPI_USERS={}", lib_path.display())); + } + + cmd_parts.push("nvc".to_string()); cmd_parts.push(format!("--std={vhdl_std}")); cmd_parts.push("-M".to_string()); cmd_parts.push("256m".to_string()); @@ -937,6 +990,17 @@ pub async fn run_testbench( cmd_parts.push(testbench_name.clone()); cmd_parts.push("-r".to_string()); cmd_parts.push(testbench_name.clone()); + + // Add runtime flags to error message + for flag in runtime_flags { + cmd_parts.push(flag.clone()); + } + + // Add Rust library to error message + if let Some(lib_path) = &rust_lib_path { + cmd_parts.push(format!("--load={}", lib_path.display())); + } + cmd_parts.push("--dump-arrays".to_string()); cmd_parts.push("--format=fst".to_string()); cmd_parts.push(format!("--wave={testbench_name}.fst")); @@ -1218,12 +1282,13 @@ fn find_entities_in_file(file_path: &Path) -> Result> { Ok(entities) } -fn find_testbench_file( +fn find_testbench_file_recurse( testbench_name: &str, bench_dir: &Utf8Path, -) -> Result { + recurse : bool +) -> Result> { let mut found_files = Vec::new(); - + for entry in fs::read_dir(bench_dir).map_err(|e| VwError::FileSystem { message: format!("Failed to read bench directory: {e}"), })? { @@ -1242,7 +1307,24 @@ fn find_testbench_file( } } } + else if recurse { + let dir_path: Utf8PathBuf = path.try_into().map_err(|e| VwError::FileSystem { + message: format!("Failed to get dir path: {e}"), + })?; + let mut lower_testbenches = find_testbench_file_recurse(testbench_name, &dir_path, recurse)?; + found_files.append(&mut lower_testbenches); + } } + Ok(found_files) +} + +fn find_testbench_file( + testbench_name: &str, + bench_dir: &Utf8Path, + recurse : bool +) -> Result { + let found_files = find_testbench_file_recurse(testbench_name, bench_dir, recurse)?; + match found_files.len() { 0 => Err(VwError::Testbench { @@ -1842,3 +1924,82 @@ fn load_existing_vhdl_ls_config( }) } } + +/// Build a Rust library for a testbench. +/// Looks for Cargo.toml in the testbench directory, builds it, and returns the path to the .so file. +async fn build_rust_library(testbench_file: &Path) -> Result { + // Get the testbench directory + let testbench_dir = testbench_file.parent().ok_or_else(|| VwError::Testbench { + message: format!("Testbench file {:?} has no parent directory???", testbench_file), + })?; + + // Look for Cargo.toml in the testbench directory + let cargo_toml_path = testbench_dir.join("Cargo.toml"); + if !cargo_toml_path.exists() { + return Err(VwError::Testbench { + message: format!( + "Cargo.toml not found in testbench directory: {:?}", + testbench_dir + ), + }); + } + + // Parse Cargo.toml to get the package name + let cargo_toml_content = fs::read_to_string(&cargo_toml_path).map_err(|e| { + VwError::FileSystem { + message: format!("Failed to read Cargo.toml: {e}"), + } + })?; + + + let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content)?; + let package_name = cargo_toml.package.name; + + // Run cargo build in the testbench directory + let testbench_dir_owned = testbench_dir.to_path_buf(); + tokio::task::spawn_blocking(move || { + let output = std::process::Command::new("cargo") + .arg("build") + .current_dir(&testbench_dir_owned) + .output() + .map_err(|e| VwError::Testbench { + message: format!("Failed to execute cargo build: {e}"), + })?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(VwError::Testbench { + message: format!("cargo build failed:\n{stderr}"), + }); + } + + Ok::<(), VwError>(()) + }) + .await + .map_err(|e| VwError::Testbench { + message: format!("Failed to execute cargo build task: {e}"), + })??; + + // Find the .so file in the workspace target directory (parent of testbench dir) + let lib_name = format!("lib{}.so", package_name.replace('-', "_")); + let workspace_target = testbench_dir + .parent() + .ok_or_else(|| VwError::Testbench { + message: format!("Testbench directory {:?} has no parent", testbench_dir), + })? + .join("target") + .join("debug"); + + let lib_path = workspace_target.join(&lib_name); + + if !lib_path.exists() { + return Err(VwError::Testbench { + message: format!( + "Built Rust library not found at expected path: {:?}", + lib_path + ), + }); + } + + Ok(lib_path) +} From f47894d3fff2377e4074b50e7d6b6f4b5c2d2c4c Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Thu, 25 Dec 2025 09:25:09 -0800 Subject: [PATCH 02/22] test commit, please ignore --- Cargo.lock | 89 ++++++++--- vw-lib/Cargo.toml | 2 +- vw-lib/src/lib.rs | 145 ++++++++++++++---- vw-lib/src/mapping.rs | 176 ++++++++++++++++++++++ vw-lib/src/visitor.rs | 336 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 698 insertions(+), 50 deletions(-) create mode 100644 vw-lib/src/mapping.rs create mode 100644 vw-lib/src/visitor.rs diff --git a/Cargo.lock b/Cargo.lock index 0fff70a..83587f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -219,7 +219,16 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys", + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", ] [[package]] @@ -230,10 +239,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.6", "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.60.2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -828,7 +849,18 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", ] [[package]] @@ -879,12 +911,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "rustversion" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" - [[package]] name = "ryu" version = "1.0.20" @@ -989,23 +1015,22 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.3" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", "syn", ] @@ -1060,7 +1085,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", ] [[package]] @@ -1074,6 +1108,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -1199,12 +1244,12 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vhdl_lang" -version = "0.83.1" +version = "0.86.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fdc1d1c52bc06f3a4ee0de29a1df1ba44ab36a648d5fd9be7c0725d74d9650" +checksum = "27f3ee86c0a8ca087e0e4eece220e516ce31a00b23de10aa8102b7f98f928977" dependencies = [ "clap", - "dirs", + "dirs 6.0.0", "dunce", "enum-map", "fnv", @@ -1222,9 +1267,9 @@ dependencies = [ [[package]] name = "vhdl_lang_macros" -version = "0.83.1" +version = "0.86.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65b120c4d3b0b90fc7078ac0fd339d59ff9f5090f5df85aa20ce42ec070ad4a0" +checksum = "712baaaca92e0ca66b7a924165c5a17146c91b0b42a675517ac6c364e5969132" dependencies = [ "quote", "syn", @@ -1246,7 +1291,7 @@ name = "vw-lib" version = "0.1.0" dependencies = [ "camino", - "dirs", + "dirs 5.0.1", "git2", "glob", "netrc", @@ -1254,7 +1299,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror", + "thiserror 1.0.69", "tokio", "toml", "url", diff --git a/vw-lib/Cargo.toml b/vw-lib/Cargo.toml index aa898fa..cdb5e30 100644 --- a/vw-lib/Cargo.toml +++ b/vw-lib/Cargo.toml @@ -22,4 +22,4 @@ netrc.workspace = true url.workspace = true glob.workspace = true git2 = "0.18" -vhdl_lang = "0.83" +vhdl_lang = "0.86" diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index af156d6..b1a3575 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -29,12 +29,20 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet, VecDeque}; -use std::fmt; -use std::fs; +use std::hash::Hash; +use std::os::unix::process; +use std::{fmt, fs}; use std::path::{Path, PathBuf}; use camino::{Utf8Path, Utf8PathBuf}; use serde::{Deserialize, Serialize}; +use vhdl_lang::{VHDLParser, VHDLStandard}; + +use crate::mapping::{RecordData, VwSymbol, VwSymbolFinder}; +use crate::visitor::walk_design_file; + +pub mod mapping; +pub mod visitor; // ============================================================================ // Error Types @@ -56,7 +64,6 @@ pub enum VwError { } impl std::error::Error for VwError {} - impl From for VwError { fn from(err: std::io::Error) -> Self { VwError::Io(err) @@ -133,6 +140,15 @@ pub enum VhdlStandard { Vhdl2019, } +impl Into for VhdlStandard { + fn into(self) -> VHDLStandard { + match self { + VhdlStandard::Vhdl2008 => VHDLStandard::VHDL2008, + VhdlStandard::Vhdl2019 => VHDLStandard::VHDL2019, + } + } +} + impl fmt::Display for VhdlStandard { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -772,6 +788,31 @@ pub struct TestbenchInfo { pub path: PathBuf, } +pub struct RecordProcessEntry { + record_data : RecordData, + file_name : PathBuf +} + +pub struct RecordProcessor { + vhdl_std : VhdlStandard, + records : HashMap, + tagged_names : HashSet, + file_to_package : HashMap>, + target_attr : String, +} + +impl RecordProcessor { + pub fn new(std : VhdlStandard) -> Self { + Self { + vhdl_std : std, + records: HashMap::new(), + tagged_names: HashSet::new(), + file_to_package : HashMap::new(), + target_attr : "rust_me".to_string() + } + } +} + /// Run a testbench using NVC simulator. pub async fn run_testbench( workspace_dir: &Utf8Path, @@ -782,7 +823,9 @@ pub async fn run_testbench( build_rust: bool, ) -> Result<()> { let vhdl_ls_config = load_existing_vhdl_ls_config(workspace_dir)?; + let mut processor = RecordProcessor::new(vhdl_std); + println!("Processing external libs"); // First, analyze all non-defaultlib libraries for (lib_name, library) in &vhdl_ls_config.libraries { if lib_name != "defaultlib" { @@ -809,7 +852,7 @@ pub async fn run_testbench( } // Sort files in dependency order (dependencies first) - sort_files_by_dependencies(&mut files)?; + sort_files_by_dependencies(&mut processor, &mut files)?; let file_strings: Vec = files .iter() @@ -863,15 +906,9 @@ pub async fn run_testbench( }); } + println!("Finding testbench files"); let testbench_file = find_testbench_file(&testbench_name, &bench_dir, recurse)?; - // Build Rust library if requested - let rust_lib_path = if build_rust { - Some(build_rust_library(&testbench_file).await?) - } else { - None - }; - // Filter defaultlib files to exclude OTHER testbenches but allow common bench code let bench_dir_abs = workspace_dir.as_std_path().join("bench"); let filtered_defaultlib_files: Vec = defaultlib_files @@ -906,12 +943,21 @@ pub async fn run_testbench( }) .collect(); + println!("Analyzing referenced files"); // Find only the defaultlib files that are actually referenced by this testbench let mut referenced_files = find_referenced_files(&testbench_file, &filtered_defaultlib_files)?; + println!("Putting files in order"); // Sort files in dependency order (dependencies first) - sort_files_by_dependencies(&mut referenced_files)?; + sort_files_by_dependencies(&mut processor, &mut referenced_files)?; + + // Build Rust library if requested + let rust_lib_path = if build_rust { + Some(build_rust_library(&testbench_file).await?) + } else { + None + }; // Run NVC simulation let mut nvc_cmd = tokio::process::Command::new("nvc"); @@ -1023,6 +1069,7 @@ fn find_referenced_files( let mut referenced_files = Vec::new(); let mut processed_files = HashSet::new(); let mut files_to_process = vec![testbench_file.to_path_buf()]; + println!("Finding referenced files"); while let Some(current_file) = files_to_process.pop() { if processed_files.contains(¤t_file) { @@ -1036,25 +1083,43 @@ fn find_referenced_files( referenced_files.push(current_file.clone()); } - // Parse the file to find dependencies + let dependencies = find_file_dependencies(¤t_file)?; // Find corresponding files for each dependency for dep in dependencies { for available_file in available_files { + println!("Does file provide symbol?"); if file_provides_symbol(available_file, &dep)? { + println!("Yes?"); if !processed_files.contains(available_file) { files_to_process.push(available_file.clone()); } break; } + println!("No?"); } } } + println!("Found referenced files"); Ok(referenced_files) } +fn get_package_imports(content : &str) -> Result> { + // Find 'use work.package_name' statements + let use_work_pattern = r"(?i)use\s+work\.(\w+)"; + let use_work_re = regex::Regex::new(use_work_pattern)?; + let mut imports = Vec::new(); + + for captures in use_work_re.captures_iter(&content) { + if let Some(package_name) = captures.get(1) { + imports.push(package_name.as_str().to_string()); + } + } + Ok(imports) +} + fn find_file_dependencies(file_path: &Path) -> Result> { let content = fs::read_to_string(file_path).map_err(|e| VwError::FileSystem { @@ -1063,15 +1128,9 @@ fn find_file_dependencies(file_path: &Path) -> Result> { let mut dependencies = HashSet::new(); - // Find 'use work.package_name' statements - let use_work_pattern = r"(?i)use\s+work\.(\w+)"; - let use_work_re = regex::Regex::new(use_work_pattern)?; + let imports = get_package_imports(&content)?; + dependencies.extend(imports); - for captures in use_work_re.captures_iter(&content) { - if let Some(package_name) = captures.get(1) { - dependencies.insert(package_name.as_str().to_string()); - } - } // Find direct entity instantiations (instance_name: entity work.entity_name) let entity_inst_pattern = r"(?i)\w+\s*:\s*entity\s+work\.(\w+)"; @@ -1136,16 +1195,48 @@ fn file_provides_symbol(file_path: &Path, symbol: &str) -> Result { Ok(false) } -fn sort_files_by_dependencies(files: &mut Vec) -> Result<()> { +fn analyze_file(processor : &mut RecordProcessor, file: &PathBuf) -> Result> { + let parser = VHDLParser::new(processor.vhdl_std.into()); + let mut diagnostics = Vec::new(); + let (_, design_file) = parser.parse_design_file(file, &mut diagnostics)?; + + let mut file_finder = VwSymbolFinder::new(&processor.target_attr); + walk_design_file(&mut file_finder, &design_file); + + for record in file_finder.get_records() { + processor.records.insert(record.get_name().to_string(), + RecordProcessEntry { record_data: record.clone(), file_name: file.clone() }); + } + + for tagged_type in file_finder.get_tagged_types() { + processor.tagged_names.insert(tagged_type.clone()); + } + + Ok(file_finder.get_symbols().clone()) +} + +fn sort_files_by_dependencies( + processor : &mut RecordProcessor, + files: &mut Vec +) -> Result<()> { // Build dependency graph let mut dependencies: HashMap> = HashMap::new(); let mut all_symbols: HashMap = HashMap::new(); // First pass: collect all symbols provided by each file for file in files.iter() { - let symbols = get_file_symbols(file)?; + let symbols = analyze_file(processor, file)?; for symbol in symbols { - all_symbols.insert(symbol, file.clone()); + match symbol { + VwSymbol::Package(name) => { + all_symbols.insert(name.clone(), file.clone()); + processor.file_to_package.entry(file.clone()).or_default().push(name); + } + VwSymbol::Entity(name) => { + all_symbols.insert(name, file.clone()); + } + _ => {} + } } } @@ -1172,7 +1263,7 @@ fn sort_files_by_dependencies(files: &mut Vec) -> Result<()> { Ok(()) } -fn get_file_symbols(file_path: &Path) -> Result> { +fn get_file_symbols(file_path: &Path) -> Result> { let content = fs::read_to_string(file_path).map_err(|e| VwError::FileSystem { message: format!("Failed to read file {file_path:?}: {e}"), @@ -1186,7 +1277,7 @@ fn get_file_symbols(file_path: &Path) -> Result> { for captures in package_re.captures_iter(&content) { if let Some(package_name) = captures.get(1) { - symbols.push(package_name.as_str().to_string()); + symbols.push(VwSymbol::Package(package_name.as_str().to_string())); } } @@ -1196,7 +1287,7 @@ fn get_file_symbols(file_path: &Path) -> Result> { for captures in entity_re.captures_iter(&content) { if let Some(entity_name) = captures.get(1) { - symbols.push(entity_name.as_str().to_string()); + symbols.push(VwSymbol::Entity(entity_name.as_str().to_string())); } } diff --git a/vw-lib/src/mapping.rs b/vw-lib/src/mapping.rs new file mode 100644 index 0000000..b14a9c8 --- /dev/null +++ b/vw-lib/src/mapping.rs @@ -0,0 +1,176 @@ +use vhdl_lang::ast::{ + AttributeSpecification, Designator, DiscreteRange, ElementDeclaration, + EntityClass, EntityDeclaration, EntityName, Name, PackageDeclaration, + Range, RangeConstraint, SubtypeConstraint, TypeDeclaration, + TypeDefinition::Record, +}; + +use crate::visitor::{Visitor, VisitorResult}; + +const RECORD_PARSE_ATTRIBUTE: &str = "rust_me"; + +#[derive(Debug,Clone)] +pub enum VwSymbol { + Package(String), + Entity(String), + Constant(String), + Record(RecordData), +} + +#[derive(Debug, Clone)] +pub struct RecordData { + name: String, + fields: Vec, + tagged: bool, +} +impl RecordData { + pub fn new(name: &str) -> Self { + Self { + name: String::from(name), + fields: Vec::new(), + tagged: false, + } + } + + pub fn get_name(&self) -> &str { + &self.name + } +} + +#[derive(Debug, Clone)] +pub struct FieldData { + pub name: String, + pub subtype_name: String, + pub constraint: Option, +} + +#[derive(Debug)] +pub struct VwSymbolFinder { + symbols: Vec, + records: Vec, + tagged_types: Vec, + target_attr: String, +} + +impl VwSymbolFinder { + pub fn new(target_attr: &str) -> Self { + Self { + symbols: Vec::new(), + records: Vec::new(), + tagged_types: Vec::new(), + target_attr: target_attr.to_string(), + } + } + + pub fn get_symbols(&self) -> &Vec { + &self.symbols + } + + pub fn get_records(&self) -> &Vec { + &self.records + } + + pub fn get_tagged_types(&self) -> &Vec { + &self.tagged_types + } +} + +impl Visitor for VwSymbolFinder { + fn visit_attribute_specification( + &mut self, + spec: &AttributeSpecification, + ) -> VisitorResult { + if spec.ident.item.item.name_utf8() == self.target_attr { + if let EntityClass::Type = spec.entity_class { + if let EntityName::Name(tag) = &spec.entity_name { + if let Designator::Identifier(id) = + &tag.designator.item.item + { + let type_name = id.name_utf8(); + self.tagged_types.push(type_name); + } + } + } + } + VisitorResult::Continue + } + + fn visit_type_declaration( + &mut self, + decl: &TypeDeclaration, + ) -> VisitorResult { + if let Record(elements) = &decl.def { + let name = decl.ident.tree.item.name_utf8(); + let mut record_struct = RecordData::new(&name); + let fields = get_fields(elements); + record_struct.fields = fields; + self.records.push(record_struct); + } + VisitorResult::Continue + } + + fn visit_entity(&mut self, entity: &EntityDeclaration) -> VisitorResult { + let name = entity.ident.tree.item.name_utf8(); + self.symbols.push(VwSymbol::Entity(name)); + VisitorResult::Continue + } + + fn visit_package(&mut self, package: &PackageDeclaration) -> VisitorResult { + let name = package.ident.tree.item.name_utf8(); + self.symbols.push(VwSymbol::Package(name)); + VisitorResult::Continue + } +} + +fn get_fields(elements: &Vec) -> Vec { + let mut fields = Vec::new(); + + for element in elements { + let element_name = element.idents[0].tree.item.name_utf8(); + let element_subtype = if let Name::Designator(designator) = + &element.subtype.type_mark.item + { + if let Designator::Identifier(symbol) = &designator.item { + Some(symbol.name_utf8()) + } else { + None + } + } else { + None + // panic here for now, because i want to see what struct differences there + // might be + } + .unwrap(); + + let element_constraint = + if let Some(constraint) = &element.subtype.constraint { + Some(get_range_constraint(&constraint.item)) + } else { + None + }; + + fields.push(FieldData { + name: element_name, + subtype_name: element_subtype, + constraint: element_constraint, + }); + } + + fields +} + +fn get_range_constraint(constraint: &SubtypeConstraint) -> RangeConstraint { + if let SubtypeConstraint::Array(array_range, _) = constraint { + if let DiscreteRange::Range(discrete_range) = &array_range[0].item { + if let Range::Range(constraint) = discrete_range { + return constraint.clone(); + } else { + panic!("We don't handle other range types") + } + } else { + panic!("We don't handle other DiscreteRange types"); + } + } else { + panic!("We don't handle other constraint types"); + } +} diff --git a/vw-lib/src/visitor.rs b/vw-lib/src/visitor.rs new file mode 100644 index 0000000..51f6a25 --- /dev/null +++ b/vw-lib/src/visitor.rs @@ -0,0 +1,336 @@ +//! Generic AST visitor for vhdl_lang. +//! +//! This module provides a `Visitor` trait that allows arbitrary AST traversal, +//! unlike the built-in `Searcher` trait which only exposes limited node types. +//! +//! # Example +//! +//! ```ignore +//! use vw_lib::visitor::{Visitor, VisitorResult, walk_design_file}; +//! +//! struct MyVisitor { +//! record_count: usize, +//! } +//! +//! impl Visitor for MyVisitor { +//! fn visit_type_declaration(&mut self, decl: &TypeDeclaration) -> VisitorResult { +//! if matches!(&decl.def, TypeDefinition::Record(_)) { +//! self.record_count += 1; +//! } +//! VisitorResult::Continue +//! } +//! } +//! ``` + +use vhdl_lang::ast::{ + AnyDesignUnit, AnyPrimaryUnit, AnySecondaryUnit, + ArchitectureBody, Attribute, AttributeDeclaration, AttributeSpecification, + ComponentDeclaration, ConfigurationDeclaration, ContextDeclaration, + Declaration, DesignFile, EntityDeclaration, + PackageBody, PackageDeclaration, PackageInstantiation, + SubprogramBody, SubprogramDeclaration, SubprogramInstantiation, + TypeDeclaration, +}; + +/// Controls whether AST traversal should continue or stop. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VisitorResult { + /// Continue traversing the AST + Continue, + /// Stop traversal immediately + Stop, +} + +impl VisitorResult { + /// Returns true if traversal should continue + pub fn should_continue(&self) -> bool { + matches!(self, VisitorResult::Continue) + } +} + +/// A trait for visiting nodes in a vhdl_lang AST. +/// +/// All methods have default implementations that return `Continue`, +/// so you only need to override the methods for nodes you care about. +/// +/// Methods are called in a depth-first traversal order. +#[allow(unused_variables)] +pub trait Visitor { + // ======================================================================== + // Design Units + // ======================================================================== + + /// Called for each design file before visiting its contents + fn visit_design_file(&mut self, file: &DesignFile) -> VisitorResult { + VisitorResult::Continue + } + + /// Called for each design unit + fn visit_design_unit(&mut self, unit: &AnyDesignUnit) -> VisitorResult { + VisitorResult::Continue + } + + // ------------------------------------------------------------------------ + // Primary Units + // ------------------------------------------------------------------------ + + /// Called for entity declarations + fn visit_entity(&mut self, entity: &EntityDeclaration) -> VisitorResult { + VisitorResult::Continue + } + + /// Called for package declarations + fn visit_package(&mut self, package: &PackageDeclaration) -> VisitorResult { + VisitorResult::Continue + } + + /// Called for package instantiations + fn visit_package_instance(&mut self, instance: &PackageInstantiation) -> VisitorResult { + VisitorResult::Continue + } + + /// Called for context declarations + fn visit_context(&mut self, context: &ContextDeclaration) -> VisitorResult { + VisitorResult::Continue + } + + /// Called for configuration declarations + fn visit_configuration(&mut self, config: &ConfigurationDeclaration) -> VisitorResult { + VisitorResult::Continue + } + + // ------------------------------------------------------------------------ + // Secondary Units + // ------------------------------------------------------------------------ + + /// Called for architecture bodies + fn visit_architecture(&mut self, arch: &ArchitectureBody) -> VisitorResult { + VisitorResult::Continue + } + + /// Called for package bodies + fn visit_package_body(&mut self, body: &PackageBody) -> VisitorResult { + VisitorResult::Continue + } + + // ======================================================================== + // Declarations + // ======================================================================== + + /// Called for each declaration (before dispatching to specific type) + fn visit_declaration(&mut self, decl: &Declaration) -> VisitorResult { + VisitorResult::Continue + } + + /// Called for type declarations + fn visit_type_declaration(&mut self, decl: &TypeDeclaration) -> VisitorResult { + VisitorResult::Continue + } + + /// Called for component declarations + fn visit_component(&mut self, comp: &ComponentDeclaration) -> VisitorResult { + VisitorResult::Continue + } + + /// Called for subprogram declarations (function/procedure specs) + fn visit_subprogram_declaration(&mut self, decl: &SubprogramDeclaration) -> VisitorResult { + VisitorResult::Continue + } + + /// Called for subprogram bodies (function/procedure implementations) + fn visit_subprogram_body(&mut self, body: &SubprogramBody) -> VisitorResult { + VisitorResult::Continue + } + + /// Called for subprogram instantiations + fn visit_subprogram_instantiation(&mut self, inst: &SubprogramInstantiation) -> VisitorResult { + VisitorResult::Continue + } + + // ------------------------------------------------------------------------ + // Attributes + // ------------------------------------------------------------------------ + + /// Called for attribute declarations (attribute X : type) + fn visit_attribute_declaration(&mut self, decl: &AttributeDeclaration) -> VisitorResult { + VisitorResult::Continue + } + + /// Called for attribute specifications (attribute X of Y : class is value) + fn visit_attribute_specification(&mut self, spec: &AttributeSpecification) -> VisitorResult { + VisitorResult::Continue + } +} + +/// Walk a design file, calling visitor methods for each node. +pub fn walk_design_file(visitor: &mut V, file: &DesignFile) -> VisitorResult { + if !visitor.visit_design_file(file).should_continue() { + return VisitorResult::Stop; + } + + for (_tokens, unit) in &file.design_units { + if !walk_design_unit(visitor, unit).should_continue() { + return VisitorResult::Stop; + } + } + + VisitorResult::Continue +} + +/// Walk a design unit, calling visitor methods for each node. +pub fn walk_design_unit(visitor: &mut V, unit: &AnyDesignUnit) -> VisitorResult { + if !visitor.visit_design_unit(unit).should_continue() { + return VisitorResult::Stop; + } + + match unit { + AnyDesignUnit::Primary(primary) => walk_primary_unit(visitor, primary), + AnyDesignUnit::Secondary(secondary) => walk_secondary_unit(visitor, secondary), + } +} + +/// Walk a primary unit. +fn walk_primary_unit(visitor: &mut V, unit: &AnyPrimaryUnit) -> VisitorResult { + match unit { + AnyPrimaryUnit::Entity(entity) => { + if !visitor.visit_entity(entity).should_continue() { + return VisitorResult::Stop; + } + walk_declarations(visitor, &entity.decl) + } + AnyPrimaryUnit::Package(package) => { + if !visitor.visit_package(package).should_continue() { + return VisitorResult::Stop; + } + walk_declarations(visitor, &package.decl) + } + AnyPrimaryUnit::PackageInstance(instance) => { + visitor.visit_package_instance(instance) + } + AnyPrimaryUnit::Context(context) => { + visitor.visit_context(context) + } + AnyPrimaryUnit::Configuration(config) => { + visitor.visit_configuration(config) + } + } +} + +/// Walk a secondary unit. +fn walk_secondary_unit(visitor: &mut V, unit: &AnySecondaryUnit) -> VisitorResult { + match unit { + AnySecondaryUnit::Architecture(arch) => { + if !visitor.visit_architecture(arch).should_continue() { + return VisitorResult::Stop; + } + walk_declarations(visitor, &arch.decl) + } + AnySecondaryUnit::PackageBody(body) => { + if !visitor.visit_package_body(body).should_continue() { + return VisitorResult::Stop; + } + walk_declarations(visitor, &body.decl) + } + } +} + +/// Walk a list of declarations. +fn walk_declarations( + visitor: &mut V, + decls: &[vhdl_lang::ast::token_range::WithTokenSpan], +) -> VisitorResult { + for decl in decls { + if !walk_declaration(visitor, &decl.item).should_continue() { + return VisitorResult::Stop; + } + } + VisitorResult::Continue +} + +/// Walk a single declaration. +fn walk_declaration(visitor: &mut V, decl: &Declaration) -> VisitorResult { + // First call the generic declaration visitor + if !visitor.visit_declaration(decl).should_continue() { + return VisitorResult::Stop; + } + + // Then dispatch to specific visitors + match decl { + Declaration::Type(type_decl) => { + visitor.visit_type_declaration(type_decl) + } + Declaration::Component(comp) => { + visitor.visit_component(comp) + } + Declaration::Attribute(attr) => { + match attr { + Attribute::Declaration(decl) => { + visitor.visit_attribute_declaration(decl) + } + Attribute::Specification(spec) => { + visitor.visit_attribute_specification(spec) + } + } + } + Declaration::SubprogramDeclaration(decl) => { + visitor.visit_subprogram_declaration(decl) + } + Declaration::SubprogramBody(body) => { + if !visitor.visit_subprogram_body(body).should_continue() { + return VisitorResult::Stop; + } + // Recurse into subprogram body declarations + walk_declarations(visitor, &body.declarations) + } + Declaration::SubprogramInstantiation(inst) => { + visitor.visit_subprogram_instantiation(inst) + } + // For other declaration types, just continue + _ => VisitorResult::Continue, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct CountingVisitor { + entities: usize, + packages: usize, + types: usize, + attr_specs: usize, + } + + impl CountingVisitor { + fn new() -> Self { + Self { + entities: 0, + packages: 0, + types: 0, + attr_specs: 0, + } + } + } + + impl Visitor for CountingVisitor { + fn visit_entity(&mut self, _: &EntityDeclaration) -> VisitorResult { + self.entities += 1; + VisitorResult::Continue + } + + fn visit_package(&mut self, _: &PackageDeclaration) -> VisitorResult { + self.packages += 1; + VisitorResult::Continue + } + + fn visit_type_declaration(&mut self, _: &TypeDeclaration) -> VisitorResult { + self.types += 1; + VisitorResult::Continue + } + + fn visit_attribute_specification(&mut self, _: &AttributeSpecification) -> VisitorResult { + self.attr_specs += 1; + VisitorResult::Continue + } + } +} From 0008b8a8a5d4395acec623e307ef62c5539a2d59 Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Sun, 28 Dec 2025 08:48:34 +0000 Subject: [PATCH 03/22] Break out nvc commands further to help build resolution TB --- vw-lib/src/lib.rs | 207 ++++++++++++-------------------------- vw-lib/src/mapping.rs | 41 ++++++-- vw-lib/src/nvc_helpers.rs | 123 ++++++++++++++++++++++ vw-lib/src/visitor.rs | 62 ++++++------ 4 files changed, 252 insertions(+), 181 deletions(-) create mode 100644 vw-lib/src/nvc_helpers.rs diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index b1a3575..1b17920 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -29,8 +29,6 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet, VecDeque}; -use std::hash::Hash; -use std::os::unix::process; use std::{fmt, fs}; use std::path::{Path, PathBuf}; @@ -39,10 +37,17 @@ use serde::{Deserialize, Serialize}; use vhdl_lang::{VHDLParser, VHDLStandard}; use crate::mapping::{RecordData, VwSymbol, VwSymbolFinder}; +use crate::nvc_helpers::{run_nvc_analysis, run_nvc_elab, run_nvc_sim}; use crate::visitor::walk_design_file; pub mod mapping; pub mod visitor; +pub mod anodizer; +pub mod vhdl_printer; +pub mod nvc_helpers; + +// TODO: make this a flag +const BUILD_DIR : &str = "vw_build"; // ============================================================================ // Error Types @@ -56,7 +61,9 @@ pub enum VwError { FileSystem { message: String }, Testbench { message: String }, NvcSimulation { command: String }, + NvcElab {command: String}, NvcAnalysis { library: String, command: String }, + CodeGen {message: String}, Io(std::io::Error), Serialization(toml::ser::Error), Deserialization(toml::de::Error), @@ -99,12 +106,21 @@ impl fmt::Display for VwError { writeln!(f, "{command}")?; Ok(()) } + VwError::NvcElab { command } => { + writeln!(f, "NVC elaboration failed")?; + writeln!(f, "command:")?; + writeln!(f, "{command}")?; + Ok(()) + } VwError::NvcAnalysis { library, command } => { writeln!(f, "NVC analysis failed for library '{library}'")?; writeln!(f, "command:")?; writeln!(f, "{command}")?; Ok(()) } + VwError::CodeGen { message } => { + write!(f, "Code generation failed: {message}") + } VwError::Config { message } => { write!(f, "Configuration error: {message}") } @@ -788,19 +804,16 @@ pub struct TestbenchInfo { pub path: PathBuf, } -pub struct RecordProcessEntry { - record_data : RecordData, - file_name : PathBuf -} pub struct RecordProcessor { vhdl_std : VhdlStandard, - records : HashMap, + records : HashMap, tagged_names : HashSet, file_to_package : HashMap>, target_attr : String, } +const RECORD_PARSE_ATTRIBUTE: &str = "serialize_rust"; impl RecordProcessor { pub fn new(std : VhdlStandard) -> Self { Self { @@ -808,7 +821,7 @@ impl RecordProcessor { records: HashMap::new(), tagged_names: HashSet::new(), file_to_package : HashMap::new(), - target_attr : "rust_me".to_string() + target_attr : RECORD_PARSE_ATTRIBUTE.to_string() } } } @@ -825,7 +838,8 @@ pub async fn run_testbench( let vhdl_ls_config = load_existing_vhdl_ls_config(workspace_dir)?; let mut processor = RecordProcessor::new(vhdl_std); - println!("Processing external libs"); + fs::create_dir_all(BUILD_DIR)?; + // First, analyze all non-defaultlib libraries for (lib_name, library) in &vhdl_ls_config.libraries { if lib_name != "defaultlib" { @@ -859,35 +873,13 @@ pub async fn run_testbench( .map(|p| p.to_string_lossy().to_string()) .collect(); - let mut nvc_cmd = tokio::process::Command::new("nvc"); - nvc_cmd - .arg(format!("--std={vhdl_std}")) - .arg(format!("--work={nvc_lib_name}")) - .arg("-M") - .arg("256m") - .arg("-a"); - - for file in &file_strings { - nvc_cmd.arg(file); - } - - let status = - nvc_cmd.status().await.map_err(|e| VwError::Testbench { - message: format!("Failed to execute NVC analysis: {e}"), - })?; + run_nvc_analysis( + vhdl_std, + &BUILD_DIR.to_string(), + &nvc_lib_name, + &file_strings, + ).await?; - if !status.success() { - let cmd_str = format!( - "nvc --std={} --work={} -M 256m -a {}", - vhdl_std, - nvc_lib_name, - file_strings.join(" ") - ); - return Err(VwError::NvcAnalysis { - library: lib_name.clone(), - command: cmd_str, - }); - } } } @@ -906,7 +898,6 @@ pub async fn run_testbench( }); } - println!("Finding testbench files"); let testbench_file = find_testbench_file(&testbench_name, &bench_dir, recurse)?; // Filter defaultlib files to exclude OTHER testbenches but allow common bench code @@ -943,121 +934,59 @@ pub async fn run_testbench( }) .collect(); - println!("Analyzing referenced files"); // Find only the defaultlib files that are actually referenced by this testbench let mut referenced_files = find_referenced_files(&testbench_file, &filtered_defaultlib_files)?; - println!("Putting files in order"); // Sort files in dependency order (dependencies first) sort_files_by_dependencies(&mut processor, &mut referenced_files)?; + + let mut files : Vec = referenced_files.iter() + .map(|s| s.to_string_lossy().to_string()) + .collect(); + + files.push(testbench_file.to_string_lossy().to_string()); + + run_nvc_analysis( + vhdl_std, + &BUILD_DIR.to_string(), + &"work".to_string(), + &files + ).await?; + + run_nvc_elab( + vhdl_std, + &BUILD_DIR.to_string(), + &"work".to_string(), + &testbench_name + ).await?; + // Build Rust library if requested let rust_lib_path = if build_rust { - Some(build_rust_library(&testbench_file).await?) + // generate any structs that have to be generated + Some(build_rust_library(&testbench_file).await? + .to_string_lossy() + .to_string()) } else { None }; - + // Run NVC simulation - let mut nvc_cmd = tokio::process::Command::new("nvc"); + run_nvc_sim( + vhdl_std, + &BUILD_DIR.to_string(), + &"work".to_string(), + &testbench_name, + rust_lib_path, + &runtime_flags.to_vec() + ).await?; - // Set GPI_USERS environment variable if we have a Rust library - if let Some(lib_path) = &rust_lib_path { - nvc_cmd.env("GPI_USERS", lib_path.to_string_lossy().as_ref()); - } - - nvc_cmd - .arg(format!("--std={vhdl_std}")) - .arg("-M") - .arg("256m") - .arg("-L") - .arg(".") - .arg("-a") - .arg("--check-synthesis"); - - // Add only the defaultlib files that are referenced by this testbench - for file_path in &referenced_files { - nvc_cmd.arg(file_path.to_string_lossy().as_ref()); - } - - // Add testbench file - nvc_cmd.arg(testbench_file.to_string_lossy().as_ref()); - - // Elaborate and run - nvc_cmd - .arg("-e") - .arg(&testbench_name) - .arg("-r") - .arg(&testbench_name); - - // Add user-provided runtime flags - for flag in runtime_flags { - nvc_cmd.arg(flag); - } - - // Add Rust library if built - if let Some(lib_path) = &rust_lib_path { - nvc_cmd.arg(format!("--load={}", lib_path.display())); - } - - nvc_cmd - .arg("--dump-arrays") - .arg("--format=fst") - .arg(format!("--wave={testbench_name}.fst")); - - let status = nvc_cmd.status().await.map_err(|e| VwError::Testbench { - message: format!("Failed to execute NVC simulation: {e}"), - })?; - - if !status.success() { - // Build command string for display - let mut cmd_parts = Vec::new(); - - // Add environment variable if present - if let Some(lib_path) = &rust_lib_path { - cmd_parts.push(format!("GPI_USERS={}", lib_path.display())); - } - - cmd_parts.push("nvc".to_string()); - cmd_parts.push(format!("--std={vhdl_std}")); - cmd_parts.push("-M".to_string()); - cmd_parts.push("256m".to_string()); - cmd_parts.push("-L".to_string()); - cmd_parts.push(".".to_string()); - cmd_parts.push("-a".to_string()); - cmd_parts.push("--check-synthesis".to_string()); - - for file_path in &referenced_files { - cmd_parts.push(file_path.to_string_lossy().to_string()); - } - cmd_parts.push(testbench_file.to_string_lossy().to_string()); - cmd_parts.push("-e".to_string()); - cmd_parts.push(testbench_name.clone()); - cmd_parts.push("-r".to_string()); - cmd_parts.push(testbench_name.clone()); - - // Add runtime flags to error message - for flag in runtime_flags { - cmd_parts.push(flag.clone()); - } - - // Add Rust library to error message - if let Some(lib_path) = &rust_lib_path { - cmd_parts.push(format!("--load={}", lib_path.display())); - } - - cmd_parts.push("--dump-arrays".to_string()); - cmd_parts.push("--format=fst".to_string()); - cmd_parts.push(format!("--wave={testbench_name}.fst")); - - let cmd_str = cmd_parts.join(" "); - return Err(VwError::NvcSimulation { command: cmd_str }); - } Ok(()) } + // ============================================================================ // Internal Helper Functions // ============================================================================ @@ -1069,7 +998,6 @@ fn find_referenced_files( let mut referenced_files = Vec::new(); let mut processed_files = HashSet::new(); let mut files_to_process = vec![testbench_file.to_path_buf()]; - println!("Finding referenced files"); while let Some(current_file) = files_to_process.pop() { if processed_files.contains(¤t_file) { @@ -1089,20 +1017,16 @@ fn find_referenced_files( // Find corresponding files for each dependency for dep in dependencies { for available_file in available_files { - println!("Does file provide symbol?"); if file_provides_symbol(available_file, &dep)? { - println!("Yes?"); if !processed_files.contains(available_file) { files_to_process.push(available_file.clone()); } break; } - println!("No?"); } } } - println!("Found referenced files"); Ok(referenced_files) } @@ -1204,8 +1128,7 @@ fn analyze_file(processor : &mut RecordProcessor, file: &PathBuf) -> Result, name: String, fields: Vec, - tagged: bool, } impl RecordData { - pub fn new(name: &str) -> Self { + pub fn new(containing_pkg : Option, name: &str) -> Self { Self { + containing_pkg : containing_pkg, name: String::from(name), fields: Vec::new(), - tagged: false, } } + pub fn get_pkg_name(&self) -> Option<&String> { + self.containing_pkg.as_ref() + } + + pub fn get_fields(&self) -> &Vec { + &self.fields + } + pub fn get_name(&self) -> &str { &self.name } @@ -79,10 +83,15 @@ impl Visitor for VwSymbolFinder { fn visit_attribute_specification( &mut self, spec: &AttributeSpecification, + _unit: &AnyDesignUnit, ) -> VisitorResult { + // if we found the attribute with the right name if spec.ident.item.item.name_utf8() == self.target_attr { + // if we tagged a type (like a record) if let EntityClass::Type = spec.entity_class { + // get the entity name if let EntityName::Name(tag) = &spec.entity_name { + // get the identifier if let Designator::Identifier(id) = &tag.designator.item.item { @@ -98,10 +107,24 @@ impl Visitor for VwSymbolFinder { fn visit_type_declaration( &mut self, decl: &TypeDeclaration, + unit: &AnyDesignUnit, ) -> VisitorResult { if let Record(elements) = &decl.def { let name = decl.ident.tree.item.name_utf8(); - let mut record_struct = RecordData::new(&name); + //figure out where this package was defined + let defining_pkg_name = if let AnyDesignUnit::Primary(primary_unit) = unit { + if let AnyPrimaryUnit::Package(package) = primary_unit { + Some(package.ident.tree.item.name_utf8()) + } + else { + None + } + } + else { + None + }; + + let mut record_struct = RecordData::new(defining_pkg_name, &name); let fields = get_fields(elements); record_struct.fields = fields; self.records.push(record_struct); diff --git a/vw-lib/src/nvc_helpers.rs b/vw-lib/src/nvc_helpers.rs new file mode 100644 index 0000000..cbb488f --- /dev/null +++ b/vw-lib/src/nvc_helpers.rs @@ -0,0 +1,123 @@ +use crate::{VhdlStandard, VwError}; + +use tokio::process::Command; + +use std::process::ExitStatus; + +fn get_base_nvc_cmd_args( + std: VhdlStandard, + build_dir: &String, + lib_name: &String, +) -> Vec { + let lib_dir = build_dir.clone() + "/" + lib_name; + let args = vec![ + format!("--std={std}"), + format!("--work={lib_dir}"), + "-M".to_string(), + "256m".to_string(), + "-L".to_string(), + build_dir.clone() + ]; + args +} + +async fn run_cmd( + args : &Vec, + envs : Option<&Vec<(String, String)>> +) -> Result{ + let mut nvc_cmd = Command::new("nvc"); + for arg in args { + nvc_cmd.arg(arg); + } + + if let Some(vars) = envs { + for (env_var, value) in vars { + nvc_cmd.env(env_var, value); + } + } + + nvc_cmd.status().await.map_err(|e| + VwError::Testbench { + message: format!("nvc command failed : {e}") + }) +} + +pub async fn run_nvc_analysis( + std: VhdlStandard, + build_dir: &String, + lib_name: &String, + referenced_files : &Vec +) -> Result<(), VwError> { + let mut args = get_base_nvc_cmd_args(std, build_dir, lib_name); + args.push("-a".to_string()); + + for file in referenced_files { + args.push(file.clone()); + } + + let status = run_cmd(&args, None).await?; + + if !status.success() { + let cmd_str = format!("nvc {}", args.join(" ")); + return Err(VwError::NvcAnalysis { + library: lib_name.clone(), + command: cmd_str + }) + } + Ok(()) +} + +pub async fn run_nvc_elab( + std: VhdlStandard, + build_dir: &String, + lib_name: &String, + testbench_name : &String +) -> Result<(), VwError> { + let mut args = get_base_nvc_cmd_args(std, build_dir, lib_name); + args.push("-e".to_string()); + args.push(testbench_name.clone()); + + let status = run_cmd(&args, None).await?; + + if !status.success() { + let cmd_str = format!("nvc {}", args.join(" ")); + return Err( + VwError::NvcElab { command: cmd_str } + ); + } + + Ok(()) +} + +pub async fn run_nvc_sim( + std: VhdlStandard, + build_dir: &String, + lib_name: &String, + testbench_name : &String, + rust_lib_path : Option, + runtime_flags : &Vec +) -> Result<(), VwError> { + let mut args = get_base_nvc_cmd_args(std, build_dir, lib_name); + args.push("-r".to_string()); + args.push(testbench_name.clone()); + + for flag in runtime_flags { + args.push(flag.clone()); + } + + args.push("--dump-arrays".to_string()); + args.push("--format=fst".to_string()); + args.push(format!("--wave={testbench_name}.fst")); + + if let Some(path) = rust_lib_path { + let envs = vec![("GPI_USERS".to_string(), path.clone())]; + args.push(format!("--load={path}")); + run_cmd(&args, Some(&envs)).await?; + } + else { + run_cmd(&args, None).await?; + } + + Ok(()) +} + diff --git a/vw-lib/src/visitor.rs b/vw-lib/src/visitor.rs index 51f6a25..cbb1cd8 100644 --- a/vw-lib/src/visitor.rs +++ b/vw-lib/src/visitor.rs @@ -13,9 +13,10 @@ //! } //! //! impl Visitor for MyVisitor { -//! fn visit_type_declaration(&mut self, decl: &TypeDeclaration) -> VisitorResult { +//! fn visit_type_declaration(&mut self, decl: &TypeDeclaration, unit: &AnyDesignUnit) -> VisitorResult { //! if matches!(&decl.def, TypeDefinition::Record(_)) { //! self.record_count += 1; +//! println!("Found record type in unit: {:?}", unit); //! } //! VisitorResult::Continue //! } @@ -118,32 +119,32 @@ pub trait Visitor { // ======================================================================== /// Called for each declaration (before dispatching to specific type) - fn visit_declaration(&mut self, decl: &Declaration) -> VisitorResult { + fn visit_declaration(&mut self, decl: &Declaration, unit: &AnyDesignUnit) -> VisitorResult { VisitorResult::Continue } /// Called for type declarations - fn visit_type_declaration(&mut self, decl: &TypeDeclaration) -> VisitorResult { + fn visit_type_declaration(&mut self, decl: &TypeDeclaration, unit: &AnyDesignUnit) -> VisitorResult { VisitorResult::Continue } /// Called for component declarations - fn visit_component(&mut self, comp: &ComponentDeclaration) -> VisitorResult { + fn visit_component(&mut self, comp: &ComponentDeclaration, unit: &AnyDesignUnit) -> VisitorResult { VisitorResult::Continue } /// Called for subprogram declarations (function/procedure specs) - fn visit_subprogram_declaration(&mut self, decl: &SubprogramDeclaration) -> VisitorResult { + fn visit_subprogram_declaration(&mut self, decl: &SubprogramDeclaration, unit: &AnyDesignUnit) -> VisitorResult { VisitorResult::Continue } /// Called for subprogram bodies (function/procedure implementations) - fn visit_subprogram_body(&mut self, body: &SubprogramBody) -> VisitorResult { + fn visit_subprogram_body(&mut self, body: &SubprogramBody, unit: &AnyDesignUnit) -> VisitorResult { VisitorResult::Continue } /// Called for subprogram instantiations - fn visit_subprogram_instantiation(&mut self, inst: &SubprogramInstantiation) -> VisitorResult { + fn visit_subprogram_instantiation(&mut self, inst: &SubprogramInstantiation, unit: &AnyDesignUnit) -> VisitorResult { VisitorResult::Continue } @@ -152,12 +153,12 @@ pub trait Visitor { // ------------------------------------------------------------------------ /// Called for attribute declarations (attribute X : type) - fn visit_attribute_declaration(&mut self, decl: &AttributeDeclaration) -> VisitorResult { + fn visit_attribute_declaration(&mut self, decl: &AttributeDeclaration, unit: &AnyDesignUnit) -> VisitorResult { VisitorResult::Continue } /// Called for attribute specifications (attribute X of Y : class is value) - fn visit_attribute_specification(&mut self, spec: &AttributeSpecification) -> VisitorResult { + fn visit_attribute_specification(&mut self, spec: &AttributeSpecification, unit: &AnyDesignUnit) -> VisitorResult { VisitorResult::Continue } } @@ -184,25 +185,25 @@ pub fn walk_design_unit(visitor: &mut V, unit: &AnyDesignUnit) -> Vi } match unit { - AnyDesignUnit::Primary(primary) => walk_primary_unit(visitor, primary), - AnyDesignUnit::Secondary(secondary) => walk_secondary_unit(visitor, secondary), + AnyDesignUnit::Primary(primary) => walk_primary_unit(visitor, primary, unit), + AnyDesignUnit::Secondary(secondary) => walk_secondary_unit(visitor, secondary, unit), } } /// Walk a primary unit. -fn walk_primary_unit(visitor: &mut V, unit: &AnyPrimaryUnit) -> VisitorResult { +fn walk_primary_unit(visitor: &mut V, unit: &AnyPrimaryUnit, design_unit: &AnyDesignUnit) -> VisitorResult { match unit { AnyPrimaryUnit::Entity(entity) => { if !visitor.visit_entity(entity).should_continue() { return VisitorResult::Stop; } - walk_declarations(visitor, &entity.decl) + walk_declarations(visitor, &entity.decl, design_unit) } AnyPrimaryUnit::Package(package) => { if !visitor.visit_package(package).should_continue() { return VisitorResult::Stop; } - walk_declarations(visitor, &package.decl) + walk_declarations(visitor, &package.decl, design_unit) } AnyPrimaryUnit::PackageInstance(instance) => { visitor.visit_package_instance(instance) @@ -217,19 +218,19 @@ fn walk_primary_unit(visitor: &mut V, unit: &AnyPrimaryUnit) -> Visi } /// Walk a secondary unit. -fn walk_secondary_unit(visitor: &mut V, unit: &AnySecondaryUnit) -> VisitorResult { +fn walk_secondary_unit(visitor: &mut V, unit: &AnySecondaryUnit, design_unit: &AnyDesignUnit) -> VisitorResult { match unit { AnySecondaryUnit::Architecture(arch) => { if !visitor.visit_architecture(arch).should_continue() { return VisitorResult::Stop; } - walk_declarations(visitor, &arch.decl) + walk_declarations(visitor, &arch.decl, design_unit) } AnySecondaryUnit::PackageBody(body) => { if !visitor.visit_package_body(body).should_continue() { return VisitorResult::Stop; } - walk_declarations(visitor, &body.decl) + walk_declarations(visitor, &body.decl, design_unit) } } } @@ -238,9 +239,10 @@ fn walk_secondary_unit(visitor: &mut V, unit: &AnySecondaryUnit) -> fn walk_declarations( visitor: &mut V, decls: &[vhdl_lang::ast::token_range::WithTokenSpan], + unit: &AnyDesignUnit, ) -> VisitorResult { for decl in decls { - if !walk_declaration(visitor, &decl.item).should_continue() { + if !walk_declaration(visitor, &decl.item, unit).should_continue() { return VisitorResult::Stop; } } @@ -248,42 +250,42 @@ fn walk_declarations( } /// Walk a single declaration. -fn walk_declaration(visitor: &mut V, decl: &Declaration) -> VisitorResult { +fn walk_declaration(visitor: &mut V, decl: &Declaration, unit: &AnyDesignUnit) -> VisitorResult { // First call the generic declaration visitor - if !visitor.visit_declaration(decl).should_continue() { + if !visitor.visit_declaration(decl, unit).should_continue() { return VisitorResult::Stop; } // Then dispatch to specific visitors match decl { Declaration::Type(type_decl) => { - visitor.visit_type_declaration(type_decl) + visitor.visit_type_declaration(type_decl, unit) } Declaration::Component(comp) => { - visitor.visit_component(comp) + visitor.visit_component(comp, unit) } Declaration::Attribute(attr) => { match attr { Attribute::Declaration(decl) => { - visitor.visit_attribute_declaration(decl) + visitor.visit_attribute_declaration(decl, unit) } Attribute::Specification(spec) => { - visitor.visit_attribute_specification(spec) + visitor.visit_attribute_specification(spec, unit) } } } Declaration::SubprogramDeclaration(decl) => { - visitor.visit_subprogram_declaration(decl) + visitor.visit_subprogram_declaration(decl, unit) } Declaration::SubprogramBody(body) => { - if !visitor.visit_subprogram_body(body).should_continue() { + if !visitor.visit_subprogram_body(body, unit).should_continue() { return VisitorResult::Stop; } // Recurse into subprogram body declarations - walk_declarations(visitor, &body.declarations) + walk_declarations(visitor, &body.declarations, unit) } Declaration::SubprogramInstantiation(inst) => { - visitor.visit_subprogram_instantiation(inst) + visitor.visit_subprogram_instantiation(inst, unit) } // For other declaration types, just continue _ => VisitorResult::Continue, @@ -323,12 +325,12 @@ mod tests { VisitorResult::Continue } - fn visit_type_declaration(&mut self, _: &TypeDeclaration) -> VisitorResult { + fn visit_type_declaration(&mut self, _: &TypeDeclaration, _: &AnyDesignUnit) -> VisitorResult { self.types += 1; VisitorResult::Continue } - fn visit_attribute_specification(&mut self, _: &AttributeSpecification) -> VisitorResult { + fn visit_attribute_specification(&mut self, _: &AttributeSpecification, _: &AnyDesignUnit) -> VisitorResult { self.attr_specs += 1; VisitorResult::Continue } From 7a52def588edf686936568ec60964a533555d8da Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Wed, 31 Dec 2025 08:38:31 +0000 Subject: [PATCH 04/22] Integrated VHDL->Rust struct generation --- Cargo.lock | 22 +- vw-cli/src/main.rs | 22 +- vw-lib/Cargo.toml | 4 + vw-lib/src/anodizer.rs | 459 +++++++++++++++++++++++++++++++++++++ vw-lib/src/lib.rs | 167 ++++++++++---- vw-lib/src/nvc_helpers.rs | 140 ++++++++--- vw-lib/src/vhdl_printer.rs | 108 +++++++++ 7 files changed, 846 insertions(+), 76 deletions(-) create mode 100644 vw-lib/src/anodizer.rs create mode 100644 vw-lib/src/vhdl_printer.rs diff --git a/Cargo.lock b/Cargo.lock index 83587f8..f657fe9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -387,9 +387,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -506,9 +506,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown", @@ -788,6 +788,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "prettyplease" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -1295,9 +1305,13 @@ dependencies = [ "git2", "glob", "netrc", + "prettyplease", + "proc-macro2", + "quote", "regex", "serde", "serde_json", + "syn", "tempfile", "thiserror 1.0.69", "tokio", diff --git a/vw-cli/src/main.rs b/vw-cli/src/main.rs index 171bd90..c7531d7 100644 --- a/vw-cli/src/main.rs +++ b/vw-cli/src/main.rs @@ -10,7 +10,7 @@ use std::process; use std::collections::HashSet; use vw_lib::{ - add_dependency_with_token, clear_cache, extract_hostname_from_repo_url, + add_dependency_with_token, anodize_only, clear_cache, extract_hostname_from_repo_url, generate_deps_tcl, get_access_credentials_from_netrc, init_workspace, list_dependencies, list_testbenches, load_workspace_config, remove_dependency, run_testbench, update_workspace_with_token, Credentials, @@ -106,6 +106,11 @@ enum Commands { #[arg(long, help = "Build Rust library for testbench before running", requires = "testbench")] build_rust: bool, }, + #[command(name = "anodize", about = "Generate Rust structs from VHDL records tagged with serialize_rust attribute")] + Anodize { + #[arg(long, help = "VHDL standard", default_value_t = CliVhdlStandard::Vhdl2019)] + std: CliVhdlStandard, + }, } /// Helper function to get access credentials for a repository URL from netrc if available @@ -396,5 +401,20 @@ async fn main() { process::exit(1); } } + Commands::Anodize { std } => { + println!("Generating Rust structs from VHDL records..."); + match anodize_only(&cwd, std.into()).await { + Ok(()) => { + println!( + "{} Generated Rust structs successfully!", + "✓".bright_green() + ); + } + Err(e) => { + eprintln!("{} {e}", "error:".bright_red()); + process::exit(1); + } + } + } } } diff --git a/vw-lib/Cargo.toml b/vw-lib/Cargo.toml index cdb5e30..bcb66ed 100644 --- a/vw-lib/Cargo.toml +++ b/vw-lib/Cargo.toml @@ -23,3 +23,7 @@ url.workspace = true glob.workspace = true git2 = "0.18" vhdl_lang = "0.86" +quote = "1" +proc-macro2 = "1" +syn = "2" +prettyplease = "0.2" diff --git a/vw-lib/src/anodizer.rs b/vw-lib/src/anodizer.rs new file mode 100644 index 0000000..88aa107 --- /dev/null +++ b/vw-lib/src/anodizer.rs @@ -0,0 +1,459 @@ +use std::{ + collections::HashMap, + fs, +}; + +use crate::{RecordProcessor, VhdlStandard, nvc_helpers::{run_nvc_elab, run_nvc_sim}}; +use crate::VwError; +use crate::vhdl_printer::expr_to_string; +use crate::nvc_helpers::run_nvc_analysis; + +use quote::{quote, format_ident}; +use proc_macro2::TokenStream; + +use vhdl_lang::ast::{ + AbstractLiteral, + Expression, + Literal, +}; + +enum Side { + Left, + Right +} + +/// struct for identifying a particular +/// range constraint within a field with a struct +struct ConstraintID { + record_index : usize, + field_index : usize, + side : Side +} + + +#[derive(Debug, Clone, Default)] +pub struct ResolvedRange { + left : Option, + right : Option +} + +#[derive(Debug, Clone)] +pub struct ResolvedField { + pub name: String, + pub bit_width: Option, + pub subtype_name: String, +} +impl ResolvedField { + pub fn new(name : &str, subtype : &str, bitwidth : Option) -> Self { + ResolvedField { + name: name.to_string(), + bit_width: bitwidth, + subtype_name: subtype.to_string() + } + } +} + +// Resolved record with all field widths computed +#[derive(Debug, Clone)] +pub struct ResolvedRecord { + pub name: String, + pub fields: Vec, +} + +impl ResolvedRecord { + pub fn new(name: &str) -> Self { + ResolvedRecord { + name: name.to_string(), + fields: Vec::new() + } + } +} + +pub async fn anodize_records( + processor : &RecordProcessor, + referenced_files : &Vec, + generate_dir : String, + build_dir : String, + rust_out_dir : String +) -> Result<(), VwError>{ + let mut tagged_records = Vec::new(); + let mut packages_needed = Vec::new(); + for name in &processor.tagged_names { + match processor.records.get(name) { + Some(record) => { + tagged_records.push(record); + let pkg_name = record.get_pkg_name() + .ok_or_else(|| + VwError::CodeGen { + message: format!("Serialization not supported for \ + records not in packages. Record : {:}", record.get_name() + ) + } + )?; + packages_needed.push(pkg_name.clone()); + } + None => { + return Err(VwError::CodeGen { + message: format!("Tagged type with name {:} not supported", name) + }) + } + } + } + + let mut expr_to_resolve : HashMap> = HashMap::new(); + let mut process_records = Vec::new(); + + // ok we got tagged records. time to collect either their subtypes or their constraints + for (i, record) in tagged_records.iter().enumerate() { + let mut record_resolution = ResolvedRecord::new(record.get_name()); + for (j, field) in record.get_fields().iter().enumerate() { + // possibly FIXME: work for non-ASCII strings? + if field.subtype_name.eq_ignore_ascii_case("std_logic_vector") { + let mut resolve_range = ResolvedRange::default(); + // ok we may need to resolve either the left or right range constraints + if let Some(range) = &field.constraint { + // check the left constraint + // is it possible to immediately derive a value? + if let Expression::Literal( + Literal::AbstractLiteral( + AbstractLiteral::Integer(value) + )) = range.left_expr.item { + // unwrap here because we just put the range in the field + resolve_range.left = Some(value as usize); + } + // ok we have to have VHDL evaluate it + else { + let expr_str = expr_to_string(&range.left_expr.item); + expr_to_resolve.entry(expr_str).or_default().push( + ConstraintID { + record_index: i, + field_index: j, + side: Side::Left + } + ); + } + // check the right constraint + if let Expression::Literal( + Literal::AbstractLiteral( + AbstractLiteral::Integer(value) + )) = range.right_expr.item { + resolve_range.right = Some(value as usize); + } + else { + let expr_str = expr_to_string(&range.right_expr.item); + expr_to_resolve.entry( + expr_str + ).or_default().push( + ConstraintID { + record_index: i, + field_index: j, + side: Side::Right + } + ); + } + let resolve_field = ResolvedField::new( + &field.name, + &field.subtype_name, + Some(resolve_range) + ); + record_resolution.fields.push(resolve_field); + } + else { + return Err(VwError::CodeGen { + message: format!("All fields in serialized structs must be constrained. \ + Found unconstrained field {:} in record {:}", field.name, record.get_name() + ) + }); + } + } + else if field.subtype_name.eq_ignore_ascii_case("std_logic") { + let mut range = ResolvedRange::default(); + range.left = Some(0); + range.right = Some(0); + record_resolution.fields.push(ResolvedField::new( + &field.name, + &field.subtype_name, + Some(range) + )); + } + // make sure the subtype struct is captured too + else { + if !processor.tagged_names.contains(&field.subtype_name) { + return Err(VwError::CodeGen { + message: format!("Subtype {:} not tagged for serialization. Please tag it", field.subtype_name) + }); + } + else { + record_resolution.fields.push(ResolvedField::new( + &field.name, + &field.subtype_name, + None + )); + } + } + } + process_records.push(record_resolution); + } + + + // alright, we've collected all the expressions that need resolving...create a testbench + let exprs = expr_to_resolve.keys().cloned().collect(); + let testbench = create_testbench(&exprs, &packages_needed); + + let generate_path = format!("{}/{}", build_dir, generate_dir); + + fs::create_dir_all(generate_path.clone())?; + fs::write(format!("{}/constraint_tb.vhd", generate_path), &testbench)?; + + let tb_files : Vec = referenced_files.iter().cloned() + .chain(std::iter::once(format!("{}/constraint_tb.vhd", generate_path))) + .collect(); + + // ok and now we have to run the testbench + // analyze the testbench + let (std_out_analysis, std_err_analysis) = run_nvc_analysis( + VhdlStandard::Vhdl2019, + &build_dir, + &"generated".to_string(), + &tb_files, + true + ).await?.unwrap(); + + let stdout_a_path = format!("{}/analysis.out", generate_path); + let stderr_a_path = format!("{}/analysis.err", generate_path); + fs::write(stdout_a_path, &std_out_analysis)?; + fs::write(stderr_a_path, &std_err_analysis)?; + + //elaborate the testbench + let (stdout_elab, stderr_elab) = run_nvc_elab( + VhdlStandard::Vhdl2019, + &build_dir, + &"generated".to_string(), + &"constraint_evaluator".to_string(), + true).await?.unwrap(); + + let stdout_e_path = format!("{}/elab.out", generate_path); + let stderr_e_path = format!("{}/elab.err", generate_path); + fs::write(stdout_e_path, &stdout_elab)?; + fs::write(stderr_e_path, &stderr_elab)?; + + + // run the testbench + + let (stdout_sim, stderr_sim) = run_nvc_sim( + VhdlStandard::Vhdl2019, + &build_dir, + &"generated".to_string(), + &"constraint_evaluator".to_string(), + None, + &Vec::new(), + true + ).await?.unwrap(); + + let stdout_sim_path = format!("{}/sim.out", generate_path); + let stderr_sim_path = format!("{}/sim.err", generate_path); + + fs::write(stdout_sim_path, &stdout_sim)?; + fs::write(stderr_sim_path, &stderr_sim)?; + + // process the sim output to resolve the expressions + process_sim_output( + &exprs, + expr_to_resolve, + &mut process_records, + &stdout_sim + )?; + + + // ok generate Rust code from the resolved records + let rust_content = generate_rust_structs(&process_records)?; + let rust_structs_file = format!("{}/generated_structs.rs", rust_out_dir); + fs::write(rust_structs_file, rust_content)?; + + Ok(()) +} + +fn generate_rust_structs(resolved_recs: &Vec) -> Result { + let structs: Result, VwError> = resolved_recs.iter().map(|record| { + let struct_name = format_ident!("{}", record.name); + + let fields: Result, VwError> = record.fields.iter().map(|field| { + let field_name = format_ident!("{}", field.name); + + if let Some(range) = &field.bit_width { + let left = range.left.ok_or_else(|| VwError::CodeGen { + message: format!("Somehow didn't resolve left expression for field {:}", field.name) + })?; + let right = range.right.ok_or_else(|| VwError::CodeGen { + message: format!("Somehow didn't resolve right expression for field {:}", field.name) + })?; + let bitwidth = left - right + 1; + + Ok(quote! { + pub #field_name: BitfieldWrap<#bitwidth> + }) + } else { + let subtype = format_ident!("{}", field.subtype_name); + Ok(quote! { + pub #field_name: #subtype + }) + } + }).collect(); + + let fields = fields?; + + let constructor_inners = record.fields.iter().map(|field| { + let field_name = format_ident!("{}", field.name); + if let Some(_) = &field.bit_width { + quote!{ + #field_name : BitfieldWrap::new() + } + } + else { + let subtype = format_ident!("{}", field.subtype_name); + quote! { + #field_name : #subtype::new() + } + } + }); + + + Ok(quote! { + #[derive(Debug, Clone, BitStructSerial)] + pub struct #struct_name { + #(#fields),* + } + + impl #struct_name { + pub fn new() -> Self { + #struct_name { + #(#constructor_inners),* + } + } + } + }) + }).collect(); + + let structs = structs?; + + let output = quote! { + use bitfield_derive::BitStructSerial; + use bitfield_struct::{BitStructSerial, BitfieldError, BitfieldWrap}; + + #(#structs)* + }; + + let syntax_tree = syn::parse2(output).map_err(|e| VwError::CodeGen { + message: format!("Failed to parse generated code: {}", e) + })?; + Ok(prettyplease::unparse(&syntax_tree)) +} + +fn process_sim_output( + expr_keys : &Vec, + exprs_to_resolve : HashMap>, + records : &mut Vec, + sim_out : &Vec +) -> Result<(), VwError>{ + let stdout_str = String::from_utf8_lossy(sim_out); + + for line in stdout_str.lines() { + let parts: Vec<&str> = line.splitn(2, ": ").collect(); + + let index: usize = parts[0].strip_prefix("EXPR_").unwrap().parse().map_err(|e| + VwError::CodeGen { message: + format!("Somehow generated an unparseable simulation output : {:}.\ + Look at sim.out", e) + })?; + let value : usize = parts[1].parse().map_err(|e| + VwError::CodeGen { message: + format!("Expression couldn't be evaluated : {}", e) + })?; + + let key = &expr_keys[index]; + let constraint_ids = exprs_to_resolve.get(key) + .ok_or_else(||{ + VwError::CodeGen { message: + format!("Somehow got expression {:} which doesn't exist", key) + }})?; + for id in constraint_ids { + let record = &mut (records[id.record_index]); + let field = &mut (record.fields[id.field_index]); + if let Some(bitfield) = &mut field.bit_width { + match id.side { + Side::Left => bitfield.left = Some(value), + Side::Right => bitfield.right = Some(value) + } + } + else { + return Err(VwError::CodeGen { message: + format!("Somehow tried to generate an expression for \ + field {:} in record {:} which has no expression", + field.name, record.name) + }) + } + } + } + Ok(()) +} + + +fn create_testbench( + exprs_to_resolve : &Vec, + packages_needed : &Vec, +) -> String { + let mut testbench = Vec::new(); + + testbench.push(generate_testbench_imports(packages_needed)); + + testbench.push(String::from( +"entity constraint_evaluator is +end entity constraint_evaluator; + +architecture behavior of constraint_evaluator is +begin + process + variable l : line; + begin + wait for 0ns; +" + )); + + for (i, expr) in exprs_to_resolve.iter().enumerate() { + testbench.push(format!(" + write(l, string'(\"EXPR_{}: \"));\n + write(l, integer'image({}));\n + writeline(OUTPUT, l);\n", + i, expr + )); + } + + testbench.push(String::from( + " wait; + end process; +end architecture behavior; +" + )); + + testbench.join("") +} + +/// Generate VHDL package use statements for a testbench +pub fn generate_testbench_imports(packages_needed : &Vec) -> String { + let mut imports = Vec::new(); + + imports.push(String::from("-- Required packages\n")); + imports.push(String::from("library ieee;\n")); + imports.push(String::from("use ieee.std_logic_1164.all;\n")); + imports.push(String::from("use ieee.numeric_std.all;\n")); + imports.push(String::from("\n")); + + imports.push(String::from("library std;\n")); + imports.push(String::from("use std.textio.all;\n")); + + for package_name in packages_needed { + imports.push(format!("use work.{}.all;\n", package_name)); + } + + imports.join("") +} \ No newline at end of file diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index 1b17920..bc65a0c 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -36,6 +36,7 @@ use camino::{Utf8Path, Utf8PathBuf}; use serde::{Deserialize, Serialize}; use vhdl_lang::{VHDLParser, VHDLStandard}; +use crate::anodizer::anodize_records; use crate::mapping::{RecordData, VwSymbol, VwSymbolFinder}; use crate::nvc_helpers::{run_nvc_analysis, run_nvc_elab, run_nvc_sim}; use crate::visitor::walk_design_file; @@ -48,6 +49,7 @@ pub mod nvc_helpers; // TODO: make this a flag const BUILD_DIR : &str = "vw_build"; +const RUST_GEN_DIR : &str = "bench/test_utils/src"; // ============================================================================ // Error Types @@ -826,20 +828,68 @@ impl RecordProcessor { } } -/// Run a testbench using NVC simulator. -pub async fn run_testbench( +pub async fn anodize_only( workspace_dir: &Utf8Path, - testbench_name: String, - vhdl_std: VhdlStandard, - recurse: bool, - runtime_flags: &[String], - build_rust: bool, + vhdl_std: VhdlStandard ) -> Result<()> { let vhdl_ls_config = load_existing_vhdl_ls_config(workspace_dir)?; let mut processor = RecordProcessor::new(vhdl_std); - + fs::create_dir_all(BUILD_DIR)?; + analyze_ext_libraries(&vhdl_ls_config, &mut processor, vhdl_std).await?; + + // alright...now we need to find packages and search them for records + let defaultlib_files = vhdl_ls_config + .libraries + .get("defaultlib") + .map(|lib| lib.files.clone()) + .unwrap_or_default(); + + let mut relevant_files = HashSet::new(); + + + for file in &defaultlib_files { + let content = + fs::read_to_string(file).map_err(|e| VwError::FileSystem { + message: format!("Failed to read file {file:?}: {e}"), + })?; + let package_re = regex::Regex::new(r"(?i)\bpackage\s+\w+\s+is\b")?; + + if package_re.is_match(&content) { + relevant_files.insert(file.clone()); + // ok it is a package...figure out which files it brings in + let referenced_files = find_referenced_files(file, &defaultlib_files)?; + relevant_files.extend(referenced_files.iter().cloned()); + } + } + + let mut files_vec : Vec = relevant_files.iter().cloned().collect(); + + // with all the relevant files, sort them and then anodize them + sort_files_by_dependencies(&mut processor, &mut files_vec)?; + + let files : Vec = files_vec.iter() + .map(|s| s.to_string_lossy().to_string()) + .collect(); + + anodize_records( + &processor, + &files, + "generate".to_string(), + BUILD_DIR.to_string(), + RUST_GEN_DIR.to_string() + ).await?; + + Ok(()) +} + + +async fn analyze_ext_libraries( + vhdl_ls_config : &VhdlLsConfig, + processor : &mut RecordProcessor, + vhdl_std : VhdlStandard +) -> Result<()> { // First, analyze all non-defaultlib libraries for (lib_name, library) in &vhdl_ls_config.libraries { if lib_name != "defaultlib" { @@ -866,7 +916,7 @@ pub async fn run_testbench( } // Sort files in dependency order (dependencies first) - sort_files_by_dependencies(&mut processor, &mut files)?; + sort_files_by_dependencies(processor, &mut files)?; let file_strings: Vec = files .iter() @@ -878,11 +928,32 @@ pub async fn run_testbench( &BUILD_DIR.to_string(), &nvc_lib_name, &file_strings, + false ).await?; } } + Ok(()) +} + +/// Run a testbench using NVC simulator. +pub async fn run_testbench( + workspace_dir: &Utf8Path, + testbench_name: String, + vhdl_std: VhdlStandard, + recurse: bool, + runtime_flags: &[String], + build_rust: bool, +) -> Result<()> { + let vhdl_ls_config = load_existing_vhdl_ls_config(workspace_dir)?; + let mut processor = RecordProcessor::new(vhdl_std); + + fs::create_dir_all(BUILD_DIR)?; + + // First, analyze all non-defaultlib libraries + analyze_ext_libraries(&vhdl_ls_config, &mut processor, vhdl_std).await?; + // Get defaultlib files for later use let defaultlib_files = vhdl_ls_config .libraries @@ -951,20 +1022,31 @@ pub async fn run_testbench( vhdl_std, &BUILD_DIR.to_string(), &"work".to_string(), - &files + &files, + false ).await?; run_nvc_elab( vhdl_std, &BUILD_DIR.to_string(), &"work".to_string(), - &testbench_name + &testbench_name, + false ).await?; // Build Rust library if requested let rust_lib_path = if build_rust { + let testbench_dir = testbench_file.parent().ok_or_else(|| VwError::Testbench { + message: format!("Testbench file {:?} has no parent directory???", testbench_file), + })?; // generate any structs that have to be generated + anodize_records(&processor, + &files, + "generate".to_string(), + BUILD_DIR.to_string(), + format!("{}/src", testbench_dir.to_string_lossy().to_string()) + ).await?; Some(build_rust_library(&testbench_file).await? .to_string_lossy() .to_string()) @@ -979,7 +1061,8 @@ pub async fn run_testbench( &"work".to_string(), &testbench_name, rust_lib_path, - &runtime_flags.to_vec() + &runtime_flags.to_vec(), + false ).await?; @@ -1186,36 +1269,36 @@ fn sort_files_by_dependencies( Ok(()) } -fn get_file_symbols(file_path: &Path) -> Result> { - let content = - fs::read_to_string(file_path).map_err(|e| VwError::FileSystem { - message: format!("Failed to read file {file_path:?}: {e}"), - })?; - - let mut symbols = Vec::new(); - - // Find package declarations - let package_pattern = r"(?i)\bpackage\s+(\w+)\s+is\b"; - let package_re = regex::Regex::new(package_pattern)?; - - for captures in package_re.captures_iter(&content) { - if let Some(package_name) = captures.get(1) { - symbols.push(VwSymbol::Package(package_name.as_str().to_string())); - } - } - - // Find entity declarations - let entity_pattern = r"(?i)\bentity\s+(\w+)\s+is\b"; - let entity_re = regex::Regex::new(entity_pattern)?; - - for captures in entity_re.captures_iter(&content) { - if let Some(entity_name) = captures.get(1) { - symbols.push(VwSymbol::Entity(entity_name.as_str().to_string())); - } - } - - Ok(symbols) -} +//fn get_file_symbols(file_path: &Path) -> Result> { +// let content = +// fs::read_to_string(file_path).map_err(|e| VwError::FileSystem { +// message: format!("Failed to read file {file_path:?}: {e}"), +// })?; +// +// let mut symbols = Vec::new(); +// +// // Find package declarations +// let package_pattern = r"(?i)\bpackage\s+(\w+)\s+is\b"; +// let package_re = regex::Regex::new(package_pattern)?; +// +// for captures in package_re.captures_iter(&content) { +// if let Some(package_name) = captures.get(1) { +// symbols.push(VwSymbol::Package(package_name.as_str().to_string())); +// } +// } +// +// // Find entity declarations +// let entity_pattern = r"(?i)\bentity\s+(\w+)\s+is\b"; +// let entity_re = regex::Regex::new(entity_pattern)?; +// +// for captures in entity_re.captures_iter(&content) { +// if let Some(entity_name) = captures.get(1) { +// symbols.push(VwSymbol::Entity(entity_name.as_str().to_string())); +// } +// } +// +// Ok(symbols) +//} fn topological_sort( files: Vec, diff --git a/vw-lib/src/nvc_helpers.rs b/vw-lib/src/nvc_helpers.rs index cbb488f..9ec5bdb 100644 --- a/vw-lib/src/nvc_helpers.rs +++ b/vw-lib/src/nvc_helpers.rs @@ -2,7 +2,7 @@ use crate::{VhdlStandard, VwError}; use tokio::process::Command; -use std::process::ExitStatus; +use std::{io::Write, process::{ExitStatus, Output}}; fn get_base_nvc_cmd_args( std: VhdlStandard, @@ -21,6 +21,27 @@ fn get_base_nvc_cmd_args( args } +async fn run_cmd_w_output( + args : &Vec, + envs : Option<&Vec<(String, String)>> +) -> Result { + let mut nvc_cmd = Command::new("nvc"); + for arg in args { + nvc_cmd.arg(arg); + } + + if let Some(vars) = envs { + for (env_var, value) in vars { + nvc_cmd.env(env_var, value); + } + } + + nvc_cmd.output().await.map_err(|e| + VwError::Testbench { + message: format!("nvc command failed : {e}") + }) +} + async fn run_cmd( args : &Vec, envs : Option<&Vec<(String, String)>> @@ -46,8 +67,9 @@ pub async fn run_nvc_analysis( std: VhdlStandard, build_dir: &String, lib_name: &String, - referenced_files : &Vec -) -> Result<(), VwError> { + referenced_files : &Vec, + capture_output : bool +) -> Result, Vec)>, VwError> { let mut args = get_base_nvc_cmd_args(std, build_dir, lib_name); args.push("-a".to_string()); @@ -55,38 +77,71 @@ pub async fn run_nvc_analysis( args.push(file.clone()); } - let status = run_cmd(&args, None).await?; - - if !status.success() { - let cmd_str = format!("nvc {}", args.join(" ")); - return Err(VwError::NvcAnalysis { - library: lib_name.clone(), - command: cmd_str - }) + if capture_output { + let output = run_cmd_w_output(&args, None).await?; + + if !output.status.success() { + let cmd_str = format!("nvc {}", args.join(" ")); + std::io::stdout().write_all(&output.stdout)?; + std::io::stderr().write_all(&output.stderr)?; + return Err(VwError::NvcAnalysis { + library: lib_name.clone(), + command: cmd_str + }) + } + Ok(Some((output.stdout, output.stderr))) } - Ok(()) + else { + let status = run_cmd(&args, None).await?; + + if !status.success() { + let cmd_str = format!("nvc {}", args.join(" ")); + return Err(VwError::NvcAnalysis { + library: lib_name.clone(), + command: cmd_str + }) + } + Ok(None) + } + } pub async fn run_nvc_elab( std: VhdlStandard, build_dir: &String, lib_name: &String, - testbench_name : &String -) -> Result<(), VwError> { + testbench_name : &String, + capture_output : bool +) -> Result, Vec)>, VwError> { let mut args = get_base_nvc_cmd_args(std, build_dir, lib_name); args.push("-e".to_string()); args.push(testbench_name.clone()); - let status = run_cmd(&args, None).await?; + if capture_output { + let output = run_cmd_w_output(&args, None).await?; + if !output.status.success() { + let cmd_str = format!("nvc {}", args.join(" ")); + std::io::stdout().write_all(&output.stdout)?; + std::io::stdout().write_all(&output.stderr)?; + + return Err(VwError::NvcElab { command: cmd_str }); + } + Ok(Some((output.stdout, output.stderr))) + + } + else { + let status = run_cmd(&args, None).await?; + + if !status.success() { + let cmd_str = format!("nvc {}", args.join(" ")); + return Err( + VwError::NvcElab { command: cmd_str } + ); + } - if !status.success() { - let cmd_str = format!("nvc {}", args.join(" ")); - return Err( - VwError::NvcElab { command: cmd_str } - ); + Ok(None) } - Ok(()) } pub async fn run_nvc_sim( @@ -95,8 +150,9 @@ pub async fn run_nvc_sim( lib_name: &String, testbench_name : &String, rust_lib_path : Option, - runtime_flags : &Vec -) -> Result<(), VwError> { + runtime_flags : &Vec, + capture_output : bool +) -> Result, Vec)>, VwError> { let mut args = get_base_nvc_cmd_args(std, build_dir, lib_name); args.push("-r".to_string()); args.push(testbench_name.clone()); @@ -109,15 +165,41 @@ pub async fn run_nvc_sim( args.push("--format=fst".to_string()); args.push(format!("--wave={testbench_name}.fst")); - if let Some(path) = rust_lib_path { - let envs = vec![("GPI_USERS".to_string(), path.clone())]; - args.push(format!("--load={path}")); - run_cmd(&args, Some(&envs)).await?; + let envs = match rust_lib_path { + Some(path) => { + args.push(format!("--load={path}")); + let envs_vec = vec![("GPI_USERS".to_string(), path.clone())]; + Some(envs_vec) + } + None => { + None + } + }; + + if capture_output { + let output = run_cmd_w_output(&args, envs.as_ref()).await?; + + if !output.status.success() { + let cmd_str = format!("nvc {}", args.join(" ")); + std::io::stdout().write_all(&output.stdout)?; + std::io::stdout().write_all(&output.stderr)?; + + return Err(VwError::NvcSimulation { command: cmd_str }); + } + Ok(Some((output.stdout, output.stderr))) + } else { - run_cmd(&args, None).await?; + let status = run_cmd(&args, envs.as_ref()).await?; + + if !status.success() { + let cmd_str = format!("nvc {}", args.join(" ")); + return Err( + VwError::NvcSimulation { command: cmd_str } + ); + } + Ok(None) } - Ok(()) } diff --git a/vw-lib/src/vhdl_printer.rs b/vw-lib/src/vhdl_printer.rs new file mode 100644 index 0000000..d88774e --- /dev/null +++ b/vw-lib/src/vhdl_printer.rs @@ -0,0 +1,108 @@ +// VHDL AST Pretty Printer +// Manually converts VHDL AST nodes back to VHDL source code strings +// +// Note: vhdl_lang provides VHDLFormatter, but its Buffer type is private, +// so we manually reconstruct expressions from the AST instead. + +use vhdl_lang::ast::{Expression, Name, Designator, Literal, Operator}; + +/// Convert an Expression AST node to a VHDL string +pub fn expr_to_string(expr: &Expression) -> String { + match expr { + Expression::Literal(lit) => literal_to_string(lit), + Expression::Name(name) => name_to_string(&**name), + Expression::Binary(op, left, right) => { + // Match on the binary operator enum directly - inline to avoid exposing private types + let op_str = match &op.item.item { + Operator::Plus => "+", + Operator::Minus => "-", + Operator::Times => "*", + Operator::Div => "/", + Operator::Mod => " mod ", + Operator::Rem => " rem ", + Operator::Pow => "**", + Operator::And => " and ", + Operator::Or => " or ", + Operator::Nand => " nand ", + Operator::Nor => " nor ", + Operator::Xor => " xor ", + Operator::Xnor => " xnor ", + Operator::EQ => "=", + Operator::NE => "/=", + Operator::LT => "<", + Operator::LTE => "<=", + Operator::GT => ">", + Operator::GTE => ">=", + Operator::QueEQ => "?=", + Operator::QueNE => "?/=", + Operator::QueLT => "?<", + Operator::QueLTE => "?<=", + Operator::QueGT => "?>", + Operator::QueGTE => "?>=", + Operator::SLL => " sll ", + Operator::SRL => " srl ", + Operator::SLA => " sla ", + Operator::SRA => " sra ", + Operator::ROL => " rol ", + Operator::ROR => " ror ", + Operator::Concat => "&", + _ => " ? ", // Catch-all for unexpected operators + }; + format!("{} {} {}", expr_to_string(&left.item), op_str, expr_to_string(&right.item)) + }, + Expression::Unary(op, operand) => { + let op_str = match &op.item.item { + Operator::Plus => "+", + Operator::Minus => "-", + Operator::Not => "not ", + Operator::Abs => "abs ", + Operator::QueQue => "?? ", + _ => "unary_op ", + }; + format!("{}{}", op_str, expr_to_string(&operand.item)) + }, + Expression::Parenthesized(inner) => { + format!("({})", expr_to_string(&inner.item)) + }, + _ => "complex_expr".to_string(), + } +} + +fn literal_to_string(lit: &Literal) -> String { + match lit { + Literal::AbstractLiteral(al) => al.to_string(), + Literal::String(s) => format!("\"{}\"", s), + Literal::BitString(bs) => format!("{:?}", bs), + Literal::Character(c) => format!("'{}'", c), + Literal::Null => "null".to_string(), + Literal::Physical(val) => format!("{:?}", val), + } +} + +fn name_to_string(name: &Name) -> String { + match name { + Name::Designator(des) => { + match &des.item { + Designator::Identifier(sym) => sym.name_utf8(), + Designator::OperatorSymbol(_) => "operator".to_string(), + Designator::Character(c) => format!("'{}'", c), + Designator::Anonymous(_) => "anonymous".to_string(), + } + }, + Name::Selected(prefix, suffix) => { + let suffix_name = match &suffix.item.item { + Designator::Identifier(sym) => sym.name_utf8(), + _ => "suffix".to_string(), + }; + format!("{}.{}", expr_to_string(&Expression::Name(Box::new(prefix.item.clone()))), suffix_name) + }, + Name::SelectedAll(prefix) => { + format!("{}.all", expr_to_string(&Expression::Name(Box::new(prefix.item.clone())))) + }, + Name::CallOrIndexed(fcall) => { + // For function calls like get_eth_hdr_bits(x) + format!("{}", expr_to_string(&Expression::Name(Box::new(fcall.name.item.clone())))) + }, + _ => "complex_name".to_string(), + } +} From 6e2e1f5e08820a2fb5db9f7d90bc2f807c15b1b9 Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Fri, 16 Jan 2026 10:20:46 +0000 Subject: [PATCH 05/22] Make sure to capture all package dependencies --- vw-lib/src/anodizer.rs | 20 +++- vw-lib/src/lib.rs | 260 +++++++++++++++++++++++------------------ vw-lib/src/mapping.rs | 67 ++++++++--- 3 files changed, 212 insertions(+), 135 deletions(-) diff --git a/vw-lib/src/anodizer.rs b/vw-lib/src/anodizer.rs index 88aa107..52a332e 100644 --- a/vw-lib/src/anodizer.rs +++ b/vw-lib/src/anodizer.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + collections::HashSet, fs, }; @@ -77,7 +78,7 @@ pub async fn anodize_records( rust_out_dir : String ) -> Result<(), VwError>{ let mut tagged_records = Vec::new(); - let mut packages_needed = Vec::new(); + let mut packages_set = HashSet::new(); for name in &processor.tagged_names { match processor.records.get(name) { Some(record) => { @@ -90,7 +91,21 @@ pub async fn anodize_records( ) } )?; - packages_needed.push(pkg_name.clone()); + packages_set.insert(pkg_name.clone()); + + // get the imported packages for the file + let record_filename = processor.record_to_file.get(name) + .ok_or( + VwError::CodeGen { + message: format!("Somehow couldn't find the containing file for record {:}", name) + })?; + let file_data = processor.file_info.get(record_filename) + .ok_or( + VwError::CodeGen { + message: format!("Somehow couldn't find information for file {:}", record_filename) + })?; + let imports = file_data.get_imported_pkgs(); + packages_set.extend(imports.iter().cloned()); } None => { return Err(VwError::CodeGen { @@ -196,6 +211,7 @@ pub async fn anodize_records( } + let packages_needed : Vec = packages_set.iter().cloned().collect(); // alright, we've collected all the expressions that need resolving...create a testbench let exprs = expr_to_resolve.keys().cloned().collect(); let testbench = create_testbench(&exprs, &packages_needed); diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index bc65a0c..d5bc25a 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -29,27 +29,27 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet, VecDeque}; -use std::{fmt, fs}; use std::path::{Path, PathBuf}; +use std::{fmt, fs}; use camino::{Utf8Path, Utf8PathBuf}; use serde::{Deserialize, Serialize}; use vhdl_lang::{VHDLParser, VHDLStandard}; use crate::anodizer::anodize_records; -use crate::mapping::{RecordData, VwSymbol, VwSymbolFinder}; +use crate::mapping::{FileData, RecordData, VwSymbol, VwSymbolFinder}; use crate::nvc_helpers::{run_nvc_analysis, run_nvc_elab, run_nvc_sim}; use crate::visitor::walk_design_file; -pub mod mapping; -pub mod visitor; pub mod anodizer; -pub mod vhdl_printer; +pub mod mapping; pub mod nvc_helpers; +pub mod vhdl_printer; +pub mod visitor; // TODO: make this a flag -const BUILD_DIR : &str = "vw_build"; -const RUST_GEN_DIR : &str = "bench/test_utils/src"; +const BUILD_DIR: &str = "vw_build"; +const RUST_GEN_DIR: &str = "bench/test_utils/src"; // ============================================================================ // Error Types @@ -63,9 +63,9 @@ pub enum VwError { FileSystem { message: String }, Testbench { message: String }, NvcSimulation { command: String }, - NvcElab {command: String}, + NvcElab { command: String }, NvcAnalysis { library: String, command: String }, - CodeGen {message: String}, + CodeGen { message: String }, Io(std::io::Error), Serialization(toml::ser::Error), Deserialization(toml::de::Error), @@ -163,7 +163,7 @@ impl Into for VhdlStandard { match self { VhdlStandard::Vhdl2008 => VHDLStandard::VHDL2008, VhdlStandard::Vhdl2019 => VHDLStandard::VHDL2019, - } + } } } @@ -758,8 +758,8 @@ pub fn generate_deps_tcl(workspace_dir: &Utf8Path) -> Result<()> { /// List all available testbenches in the workspace. pub fn list_testbenches( bench_dir: &Utf8Path, - ignore_dirs : &HashSet, - recurse : bool + ignore_dirs: &HashSet, + recurse: bool, ) -> Result> { let mut testbenches = Vec::new(); @@ -783,14 +783,15 @@ pub fn list_testbenches( } } } - } - else if recurse { - let dir_path: Utf8PathBuf = path.try_into().map_err(|e| VwError::FileSystem { - message: format!("Failed to get dir path: {e}"), - })?; + } else if recurse { + let dir_path: Utf8PathBuf = + path.try_into().map_err(|e| VwError::FileSystem { + message: format!("Failed to get dir path: {e}"), + })?; if let Some(file_name) = dir_path.file_name() { if !ignore_dirs.contains(file_name) { - let mut lower_testbenches = list_testbenches(&dir_path, ignore_dirs, recurse)?; + let mut lower_testbenches = + list_testbenches(&dir_path, ignore_dirs, recurse)?; testbenches.append(&mut lower_testbenches); } } @@ -806,35 +807,36 @@ pub struct TestbenchInfo { pub path: PathBuf, } - pub struct RecordProcessor { - vhdl_std : VhdlStandard, - records : HashMap, - tagged_names : HashSet, - file_to_package : HashMap>, - target_attr : String, + vhdl_std: VhdlStandard, + records: HashMap, + record_to_file: HashMap, + tagged_names: HashSet, + file_info: HashMap, + target_attr: String, } const RECORD_PARSE_ATTRIBUTE: &str = "serialize_rust"; impl RecordProcessor { - pub fn new(std : VhdlStandard) -> Self { + pub fn new(std: VhdlStandard) -> Self { Self { - vhdl_std : std, + vhdl_std: std, records: HashMap::new(), + record_to_file: HashMap::new(), tagged_names: HashSet::new(), - file_to_package : HashMap::new(), - target_attr : RECORD_PARSE_ATTRIBUTE.to_string() + file_info: HashMap::new(), + target_attr: RECORD_PARSE_ATTRIBUTE.to_string(), } } } pub async fn anodize_only( workspace_dir: &Utf8Path, - vhdl_std: VhdlStandard + vhdl_std: VhdlStandard, ) -> Result<()> { let vhdl_ls_config = load_existing_vhdl_ls_config(workspace_dir)?; let mut processor = RecordProcessor::new(vhdl_std); - + fs::create_dir_all(BUILD_DIR)?; analyze_ext_libraries(&vhdl_ls_config, &mut processor, vhdl_std).await?; @@ -848,7 +850,6 @@ pub async fn anodize_only( let mut relevant_files = HashSet::new(); - for file in &defaultlib_files { let content = fs::read_to_string(file).map_err(|e| VwError::FileSystem { @@ -859,36 +860,38 @@ pub async fn anodize_only( if package_re.is_match(&content) { relevant_files.insert(file.clone()); // ok it is a package...figure out which files it brings in - let referenced_files = find_referenced_files(file, &defaultlib_files)?; + let referenced_files = + find_referenced_files(file, &defaultlib_files)?; relevant_files.extend(referenced_files.iter().cloned()); } } - let mut files_vec : Vec = relevant_files.iter().cloned().collect(); + let mut files_vec: Vec = relevant_files.iter().cloned().collect(); // with all the relevant files, sort them and then anodize them sort_files_by_dependencies(&mut processor, &mut files_vec)?; - - let files : Vec = files_vec.iter() + + let files: Vec = files_vec + .iter() .map(|s| s.to_string_lossy().to_string()) .collect(); anodize_records( - &processor, - &files, - "generate".to_string(), + &processor, + &files, + "generate".to_string(), BUILD_DIR.to_string(), - RUST_GEN_DIR.to_string() - ).await?; + RUST_GEN_DIR.to_string(), + ) + .await?; Ok(()) } - async fn analyze_ext_libraries( - vhdl_ls_config : &VhdlLsConfig, - processor : &mut RecordProcessor, - vhdl_std : VhdlStandard + vhdl_ls_config: &VhdlLsConfig, + processor: &mut RecordProcessor, + vhdl_std: VhdlStandard, ) -> Result<()> { // First, analyze all non-defaultlib libraries for (lib_name, library) in &vhdl_ls_config.libraries { @@ -924,13 +927,13 @@ async fn analyze_ext_libraries( .collect(); run_nvc_analysis( - vhdl_std, - &BUILD_DIR.to_string(), - &nvc_lib_name, + vhdl_std, + &BUILD_DIR.to_string(), + &nvc_lib_name, &file_strings, - false - ).await?; - + false, + ) + .await?; } } @@ -969,7 +972,8 @@ pub async fn run_testbench( }); } - let testbench_file = find_testbench_file(&testbench_name, &bench_dir, recurse)?; + let testbench_file = + find_testbench_file(&testbench_name, &bench_dir, recurse)?; // Filter defaultlib files to exclude OTHER testbenches but allow common bench code let bench_dir_abs = workspace_dir.as_std_path().join("bench"); @@ -1012,64 +1016,58 @@ pub async fn run_testbench( // Sort files in dependency order (dependencies first) sort_files_by_dependencies(&mut processor, &mut referenced_files)?; - let mut files : Vec = referenced_files.iter() + let mut files: Vec = referenced_files + .iter() .map(|s| s.to_string_lossy().to_string()) .collect(); files.push(testbench_file.to_string_lossy().to_string()); - + run_nvc_analysis( - vhdl_std, - &BUILD_DIR.to_string(), - &"work".to_string(), + vhdl_std, + &BUILD_DIR.to_string(), + &"work".to_string(), &files, - false - ).await?; + false, + ) + .await?; run_nvc_elab( - vhdl_std, - &BUILD_DIR.to_string(), - &"work".to_string(), + vhdl_std, + &BUILD_DIR.to_string(), + &"work".to_string(), &testbench_name, - false - ).await?; + false, + ) + .await?; - // Build Rust library if requested let rust_lib_path = if build_rust { - let testbench_dir = testbench_file.parent().ok_or_else(|| VwError::Testbench { - message: format!("Testbench file {:?} has no parent directory???", testbench_file), - })?; - // generate any structs that have to be generated - anodize_records(&processor, - &files, - "generate".to_string(), - BUILD_DIR.to_string(), - format!("{}/src", testbench_dir.to_string_lossy().to_string()) - ).await?; - Some(build_rust_library(&testbench_file).await? - .to_string_lossy() - .to_string()) + Some( + build_rust_library(&testbench_file) + .await? + .to_string_lossy() + .to_string(), + ) } else { None }; - + // Run NVC simulation run_nvc_sim( - vhdl_std, - &BUILD_DIR.to_string(), - &"work".to_string(), + vhdl_std, + &BUILD_DIR.to_string(), + &"work".to_string(), &testbench_name, rust_lib_path, &runtime_flags.to_vec(), - false - ).await?; - + false, + ) + .await?; Ok(()) } - // ============================================================================ // Internal Helper Functions // ============================================================================ @@ -1094,7 +1092,6 @@ fn find_referenced_files( referenced_files.push(current_file.clone()); } - let dependencies = find_file_dependencies(¤t_file)?; // Find corresponding files for each dependency @@ -1113,7 +1110,7 @@ fn find_referenced_files( Ok(referenced_files) } -fn get_package_imports(content : &str) -> Result> { +fn get_package_imports(content: &str) -> Result> { // Find 'use work.package_name' statements let use_work_pattern = r"(?i)use\s+work\.(\w+)"; let use_work_re = regex::Regex::new(use_work_pattern)?; @@ -1138,7 +1135,6 @@ fn find_file_dependencies(file_path: &Path) -> Result> { let imports = get_package_imports(&content)?; dependencies.extend(imports); - // Find direct entity instantiations (instance_name: entity work.entity_name) let entity_inst_pattern = r"(?i)\w+\s*:\s*entity\s+work\.(\w+)"; let entity_inst_re = regex::Regex::new(entity_inst_pattern)?; @@ -1202,7 +1198,10 @@ fn file_provides_symbol(file_path: &Path, symbol: &str) -> Result { Ok(false) } -fn analyze_file(processor : &mut RecordProcessor, file: &PathBuf) -> Result> { +fn analyze_file( + processor: &mut RecordProcessor, + file: &PathBuf, +) -> Result> { let parser = VHDLParser::new(processor.vhdl_std.into()); let mut diagnostics = Vec::new(); let (_, design_file) = parser.parse_design_file(file, &mut diagnostics)?; @@ -1211,7 +1210,13 @@ fn analyze_file(processor : &mut RecordProcessor, file: &PathBuf) -> Result Result + processor: &mut RecordProcessor, + files: &mut Vec, ) -> Result<()> { // Build dependency graph let mut dependencies: HashMap> = HashMap::new(); @@ -1236,7 +1241,23 @@ fn sort_files_by_dependencies( match symbol { VwSymbol::Package(name) => { all_symbols.insert(name.clone(), file.clone()); - processor.file_to_package.entry(file.clone()).or_default().push(name); + let entry = processor + .file_info + .entry(file.to_string_lossy().to_string()) + .or_insert_with(|| FileData::new()); + entry.add_defined_pkg(&name); + + let content = fs::read_to_string(file).map_err(|e| { + VwError::FileSystem { + message: format!( + "Failed to read file {file:?}: {e}" + ), + } + })?; + let package_names = get_package_imports(&content)?; + for pkg in package_names { + entry.add_imported_pkg(&pkg); + } } VwSymbol::Entity(name) => { all_symbols.insert(name, file.clone()); @@ -1382,10 +1403,10 @@ fn find_entities_in_file(file_path: &Path) -> Result> { fn find_testbench_file_recurse( testbench_name: &str, bench_dir: &Utf8Path, - recurse : bool + recurse: bool, ) -> Result> { let mut found_files = Vec::new(); - + for entry in fs::read_dir(bench_dir).map_err(|e| VwError::FileSystem { message: format!("Failed to read bench directory: {e}"), })? { @@ -1403,12 +1424,16 @@ fn find_testbench_file_recurse( } } } - } - else if recurse { - let dir_path: Utf8PathBuf = path.try_into().map_err(|e| VwError::FileSystem { - message: format!("Failed to get dir path: {e}"), - })?; - let mut lower_testbenches = find_testbench_file_recurse(testbench_name, &dir_path, recurse)?; + } else if recurse { + let dir_path: Utf8PathBuf = + path.try_into().map_err(|e| VwError::FileSystem { + message: format!("Failed to get dir path: {e}"), + })?; + let mut lower_testbenches = find_testbench_file_recurse( + testbench_name, + &dir_path, + recurse, + )?; found_files.append(&mut lower_testbenches); } } @@ -1418,10 +1443,10 @@ fn find_testbench_file_recurse( fn find_testbench_file( testbench_name: &str, bench_dir: &Utf8Path, - recurse : bool + recurse: bool, ) -> Result { - let found_files = find_testbench_file_recurse(testbench_name, bench_dir, recurse)?; - + let found_files = + find_testbench_file_recurse(testbench_name, bench_dir, recurse)?; match found_files.len() { 0 => Err(VwError::Testbench { @@ -2026,9 +2051,13 @@ fn load_existing_vhdl_ls_config( /// Looks for Cargo.toml in the testbench directory, builds it, and returns the path to the .so file. async fn build_rust_library(testbench_file: &Path) -> Result { // Get the testbench directory - let testbench_dir = testbench_file.parent().ok_or_else(|| VwError::Testbench { - message: format!("Testbench file {:?} has no parent directory???", testbench_file), - })?; + let testbench_dir = + testbench_file.parent().ok_or_else(|| VwError::Testbench { + message: format!( + "Testbench file {:?} has no parent directory???", + testbench_file + ), + })?; // Look for Cargo.toml in the testbench directory let cargo_toml_path = testbench_dir.join("Cargo.toml"); @@ -2042,12 +2071,12 @@ async fn build_rust_library(testbench_file: &Path) -> Result { } // Parse Cargo.toml to get the package name - let cargo_toml_content = fs::read_to_string(&cargo_toml_path).map_err(|e| { - VwError::FileSystem { - message: format!("Failed to read Cargo.toml: {e}"), - } - })?; - + let cargo_toml_content = + fs::read_to_string(&cargo_toml_path).map_err(|e| { + VwError::FileSystem { + message: format!("Failed to read Cargo.toml: {e}"), + } + })?; let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content)?; let package_name = cargo_toml.package.name; @@ -2082,7 +2111,10 @@ async fn build_rust_library(testbench_file: &Path) -> Result { let workspace_target = testbench_dir .parent() .ok_or_else(|| VwError::Testbench { - message: format!("Testbench directory {:?} has no parent", testbench_dir), + message: format!( + "Testbench directory {:?} has no parent", + testbench_dir + ), })? .join("target") .join("debug"); diff --git a/vw-lib/src/mapping.rs b/vw-lib/src/mapping.rs index 271578e..75015a0 100644 --- a/vw-lib/src/mapping.rs +++ b/vw-lib/src/mapping.rs @@ -1,11 +1,13 @@ use vhdl_lang::ast::{ - AnyDesignUnit, AnyPrimaryUnit, AttributeSpecification, Designator, DiscreteRange, ElementDeclaration, EntityClass, EntityDeclaration, EntityName, Name, PackageDeclaration, Range, RangeConstraint, SubtypeConstraint, TypeDeclaration, TypeDefinition::Record + AnyDesignUnit, AnyPrimaryUnit, AttributeSpecification, Designator, + DiscreteRange, ElementDeclaration, EntityClass, EntityDeclaration, + EntityName, Name, PackageDeclaration, Range, RangeConstraint, + SubtypeConstraint, TypeDeclaration, TypeDefinition::Record, }; use crate::visitor::{Visitor, VisitorResult}; - -#[derive(Debug,Clone)] +#[derive(Debug, Clone)] pub enum VwSymbol { Package(String), Entity(String), @@ -15,14 +17,41 @@ pub enum VwSymbol { #[derive(Debug, Clone)] pub struct RecordData { - containing_pkg : Option, + containing_pkg: Option, name: String, fields: Vec, } + +pub struct FileData { + defined_pkgs: Vec, + imported_pkgs: Vec, +} + +impl FileData { + pub fn new() -> Self { + Self { + defined_pkgs : Vec::new(), + imported_pkgs : Vec::new() + } + } + + pub fn add_defined_pkg(&mut self, pkg_name : &str) { + self.defined_pkgs.push(pkg_name.to_string()); + } + + pub fn add_imported_pkg(&mut self, pkg_name : &str) { + self.imported_pkgs.push(pkg_name.to_string()); + } + + pub fn get_imported_pkgs(&self) -> &Vec { + &self.imported_pkgs + } +} + impl RecordData { - pub fn new(containing_pkg : Option, name: &str) -> Self { + pub fn new(containing_pkg: Option, name: &str) -> Self { Self { - containing_pkg : containing_pkg, + containing_pkg: containing_pkg, name: String::from(name), fields: Vec::new(), } @@ -31,7 +60,7 @@ impl RecordData { pub fn get_pkg_name(&self) -> Option<&String> { self.containing_pkg.as_ref() } - + pub fn get_fields(&self) -> &Vec { &self.fields } @@ -39,6 +68,7 @@ impl RecordData { pub fn get_name(&self) -> &str { &self.name } + } #[derive(Debug, Clone)] @@ -73,7 +103,7 @@ impl VwSymbolFinder { pub fn get_records(&self) -> &Vec { &self.records } - + pub fn get_tagged_types(&self) -> &Vec { &self.tagged_types } @@ -112,18 +142,17 @@ impl Visitor for VwSymbolFinder { if let Record(elements) = &decl.def { let name = decl.ident.tree.item.name_utf8(); //figure out where this package was defined - let defining_pkg_name = if let AnyDesignUnit::Primary(primary_unit) = unit { - if let AnyPrimaryUnit::Package(package) = primary_unit { - Some(package.ident.tree.item.name_utf8()) - } - else { + let defining_pkg_name = + if let AnyDesignUnit::Primary(primary_unit) = unit { + if let AnyPrimaryUnit::Package(package) = primary_unit { + Some(package.ident.tree.item.name_utf8()) + } else { + None + } + } else { None - } - } - else { - None - }; - + }; + let mut record_struct = RecordData::new(defining_pkg_name, &name); let fields = get_fields(elements); record_struct.fields = fields; From c71f2067bffd7f8070b020733e2a76fb0056279d Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Thu, 22 Jan 2026 15:33:10 -0800 Subject: [PATCH 06/22] Fix destination of Rust library --- vw-lib/src/lib.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index d5bc25a..8354434 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -1044,7 +1044,7 @@ pub async fn run_testbench( // Build Rust library if requested let rust_lib_path = if build_rust { Some( - build_rust_library(&testbench_file) + build_rust_library(&bench_dir, &testbench_file) .await? .to_string_lossy() .to_string(), @@ -2049,7 +2049,7 @@ fn load_existing_vhdl_ls_config( /// Build a Rust library for a testbench. /// Looks for Cargo.toml in the testbench directory, builds it, and returns the path to the .so file. -async fn build_rust_library(testbench_file: &Path) -> Result { +async fn build_rust_library(bench_dir: &Utf8Path, testbench_file: &Path) -> Result { // Get the testbench directory let testbench_dir = testbench_file.parent().ok_or_else(|| VwError::Testbench { @@ -2108,14 +2108,7 @@ async fn build_rust_library(testbench_file: &Path) -> Result { // Find the .so file in the workspace target directory (parent of testbench dir) let lib_name = format!("lib{}.so", package_name.replace('-', "_")); - let workspace_target = testbench_dir - .parent() - .ok_or_else(|| VwError::Testbench { - message: format!( - "Testbench directory {:?} has no parent", - testbench_dir - ), - })? + let workspace_target = bench_dir .join("target") .join("debug"); @@ -2130,5 +2123,5 @@ async fn build_rust_library(testbench_file: &Path) -> Result { }); } - Ok(lib_path) + Ok(lib_path.into()) } From 37c092f32a780f7a71c2b0c36d64c98aa4b3054f Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Tue, 3 Feb 2026 10:35:35 +0000 Subject: [PATCH 07/22] Support enums --- vw-lib/src/anodizer.rs | 107 +++++++--- vw-lib/src/lib.rs | 443 +++++++++++++++++++++++++++-------------- vw-lib/src/mapping.rs | 90 +++++++-- 3 files changed, 442 insertions(+), 198 deletions(-) diff --git a/vw-lib/src/anodizer.rs b/vw-lib/src/anodizer.rs index 52a332e..3723d64 100644 --- a/vw-lib/src/anodizer.rs +++ b/vw-lib/src/anodizer.rs @@ -6,6 +6,7 @@ use std::{ use crate::{RecordProcessor, VhdlStandard, nvc_helpers::{run_nvc_elab, run_nvc_sim}}; use crate::VwError; +use crate::mapping::VwSymbol; use crate::vhdl_printer::expr_to_string; use crate::nvc_helpers::run_nvc_analysis; @@ -28,7 +29,7 @@ enum Side { struct ConstraintID { record_index : usize, field_index : usize, - side : Side + side : Option, // None for enum size (single value), Some(Left/Right) for range constraints } @@ -80,12 +81,12 @@ pub async fn anodize_records( let mut tagged_records = Vec::new(); let mut packages_set = HashSet::new(); for name in &processor.tagged_names { - match processor.records.get(name) { - Some(record) => { + match processor.symbols.get(name) { + Some(VwSymbol::Record(record)) => { tagged_records.push(record); let pkg_name = record.get_pkg_name() .ok_or_else(|| - VwError::CodeGen { + VwError::CodeGen { message: format!("Serialization not supported for \ records not in packages. Record : {:}", record.get_name() ) @@ -94,22 +95,27 @@ pub async fn anodize_records( packages_set.insert(pkg_name.clone()); // get the imported packages for the file - let record_filename = processor.record_to_file.get(name) + let record_filename = processor.symbol_to_file.get(name) .ok_or( - VwError::CodeGen { - message: format!("Somehow couldn't find the containing file for record {:}", name) + VwError::CodeGen { + message: format!("Somehow couldn't find the containing file for record {:}", name) })?; let file_data = processor.file_info.get(record_filename) .ok_or( - VwError::CodeGen { + VwError::CodeGen { message: format!("Somehow couldn't find information for file {:}", record_filename) - })?; + })?; let imports = file_data.get_imported_pkgs(); packages_set.extend(imports.iter().cloned()); } + Some(_) => { + return Err(VwError::CodeGen { + message: format!("Tagged type {:} is not a record", name) + }) + } None => { - return Err(VwError::CodeGen { - message: format!("Tagged type with name {:} not supported", name) + return Err(VwError::CodeGen { + message: format!("Tagged type with name {:} not found", name) }) } } @@ -140,10 +146,10 @@ pub async fn anodize_records( else { let expr_str = expr_to_string(&range.left_expr.item); expr_to_resolve.entry(expr_str).or_default().push( - ConstraintID { + ConstraintID { record_index: i, field_index: j, - side: Side::Left + side: Some(Side::Left) } ); } @@ -159,10 +165,10 @@ pub async fn anodize_records( expr_to_resolve.entry( expr_str ).or_default().push( - ConstraintID { + ConstraintID { record_index: i, - field_index: j, - side: Side::Right + field_index: j, + side: Some(Side::Right) } ); } @@ -191,19 +197,51 @@ pub async fn anodize_records( Some(range) )); } - // make sure the subtype struct is captured too + // Check if subtype is an enum or a record else { - if !processor.tagged_names.contains(&field.subtype_name) { - return Err(VwError::CodeGen { - message: format!("Subtype {:} not tagged for serialization. Please tag it", field.subtype_name) - }); - } - else { - record_resolution.fields.push(ResolvedField::new( - &field.name, - &field.subtype_name, - None - )); + match processor.symbols.get(&field.subtype_name) { + Some(VwSymbol::Enum(enum_data)) => { + // Check if enum has custom encoding + if enum_data.has_custom_encoding { + return Err(VwError::CodeGen { + message: format!("Enum {:} has custom encoding and cannot be serialized", + field.subtype_name) + }); + } + // Enum field - need to resolve its size via testbench + let expr_str = format!("{}'pos({}'high)", + field.subtype_name, field.subtype_name); + expr_to_resolve.entry(expr_str).or_default().push( + ConstraintID { + record_index: i, + field_index: j, + side: None, // Enum size, not left/right range + } + ); + record_resolution.fields.push(ResolvedField::new( + &field.name, + &field.subtype_name, + Some(ResolvedRange::default()) + )); + } + Some(VwSymbol::Record(_)) => { + // Nested record - must be tagged for serialization + if !processor.tagged_names.contains(&field.subtype_name) { + return Err(VwError::CodeGen { + message: format!("Subtype {:} not tagged for serialization. Please tag it", field.subtype_name) + }); + } + record_resolution.fields.push(ResolvedField::new( + &field.name, + &field.subtype_name, + None + )); + } + _ => { + return Err(VwError::CodeGen { + message: format!("Subtype {:} is not a known record or enum type", field.subtype_name) + }); + } } } } @@ -397,12 +435,19 @@ fn process_sim_output( let field = &mut (record.fields[id.field_index]); if let Some(bitfield) = &mut field.bit_width { match id.side { - Side::Left => bitfield.left = Some(value), - Side::Right => bitfield.right = Some(value) + Some(Side::Left) => bitfield.left = Some(value), + Some(Side::Right) => bitfield.right = Some(value), + None => { + // Enum size - value is max position (0-indexed), so count = value + 1 + let count = value + 1; + let bits = if count <= 1 { 1 } else { (count as f64).log2().ceil() as usize }; + bitfield.left = Some(bits - 1); + bitfield.right = Some(0); + } } } else { - return Err(VwError::CodeGen { message: + return Err(VwError::CodeGen { message: format!("Somehow tried to generate an expression for \ field {:} in record {:} which has no expression", field.name, record.name) diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index 8354434..e5ae78d 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -37,7 +37,7 @@ use serde::{Deserialize, Serialize}; use vhdl_lang::{VHDLParser, VHDLStandard}; use crate::anodizer::anodize_records; -use crate::mapping::{FileData, RecordData, VwSymbol, VwSymbolFinder}; +use crate::mapping::{FileData, VwSymbol, VwSymbolFinder}; use crate::nvc_helpers::{run_nvc_analysis, run_nvc_elab, run_nvc_sim}; use crate::visitor::walk_design_file; @@ -760,6 +760,16 @@ pub fn list_testbenches( bench_dir: &Utf8Path, ignore_dirs: &HashSet, recurse: bool, +) -> Result> { + let mut entities_cache = HashMap::new(); + list_testbenches_impl(bench_dir, ignore_dirs, recurse, &mut entities_cache) +} + +fn list_testbenches_impl( + bench_dir: &Utf8Path, + ignore_dirs: &HashSet, + recurse: bool, + entities_cache: &mut HashMap>, ) -> Result> { let mut testbenches = Vec::new(); @@ -774,10 +784,10 @@ pub fn list_testbenches( if path.is_file() { if let Some(extension) = path.extension() { if extension == "vhd" || extension == "vhdl" { - let entities = find_entities_in_file(&path)?; + let entities = get_cached_entities(&path, entities_cache)?; for entity in entities { testbenches.push(TestbenchInfo { - name: entity, + name: entity.clone(), path: path.clone(), }); } @@ -791,7 +801,7 @@ pub fn list_testbenches( if let Some(file_name) = dir_path.file_name() { if !ignore_dirs.contains(file_name) { let mut lower_testbenches = - list_testbenches(&dir_path, ignore_dirs, recurse)?; + list_testbenches_impl(&dir_path, ignore_dirs, recurse, entities_cache)?; testbenches.append(&mut lower_testbenches); } } @@ -809,8 +819,8 @@ pub struct TestbenchInfo { pub struct RecordProcessor { vhdl_std: VhdlStandard, - records: HashMap, - record_to_file: HashMap, + symbols: HashMap, + symbol_to_file: HashMap, tagged_names: HashSet, file_info: HashMap, target_attr: String, @@ -821,8 +831,8 @@ impl RecordProcessor { pub fn new(std: VhdlStandard) -> Self { Self { vhdl_std: std, - records: HashMap::new(), - record_to_file: HashMap::new(), + symbols: HashMap::new(), + symbol_to_file: HashMap::new(), tagged_names: HashSet::new(), file_info: HashMap::new(), target_attr: RECORD_PARSE_ATTRIBUTE.to_string(), @@ -830,16 +840,181 @@ impl RecordProcessor { } } +// ============================================================================ +// File Cache - Reduces redundant file reads during build +// ============================================================================ + +/// Cache for parsed file data to avoid redundant parsing during builds. +/// Only caches parsed results, not raw file contents. +pub struct FileCache { + dependencies: HashMap>, + provided_symbols: HashMap>, + entities: HashMap>, +} + +impl FileCache { + pub fn new() -> Self { + Self { + dependencies: HashMap::new(), + provided_symbols: HashMap::new(), + entities: HashMap::new(), + } + } + + /// Get cached file dependencies, reading and parsing file if not cached. + pub fn get_dependencies(&mut self, path: &Path) -> Result<&Vec> { + if !self.dependencies.contains_key(path) { + let content = + fs::read_to_string(path).map_err(|e| VwError::FileSystem { + message: format!("Failed to read file {path:?}: {e}"), + })?; + let deps = parse_file_dependencies(&content)?; + self.dependencies.insert(path.to_path_buf(), deps); + } + Ok(self.dependencies.get(path).unwrap()) + } + + /// Get cached provided symbols (packages and entities), reading and parsing if not cached. + pub fn get_provided_symbols( + &mut self, + path: &Path, + ) -> Result<&Vec> { + if !self.provided_symbols.contains_key(path) { + let content = + fs::read_to_string(path).map_err(|e| VwError::FileSystem { + message: format!("Failed to read file {path:?}: {e}"), + })?; + let symbols = parse_provided_symbols(&content)?; + self.provided_symbols.insert(path.to_path_buf(), symbols); + } + Ok(self.provided_symbols.get(path).unwrap()) + } + + /// Get cached entities in file, reading and parsing if not cached. + pub fn get_entities(&mut self, path: &Path) -> Result<&Vec> { + if !self.entities.contains_key(path) { + let content = + fs::read_to_string(path).map_err(|e| VwError::FileSystem { + message: format!("Failed to read file {path:?}: {e}"), + })?; + let entities = parse_entities(&content)?; + self.entities.insert(path.to_path_buf(), entities); + } + Ok(self.entities.get(path).unwrap()) + } + + /// Get mutable access to the entities cache for functions that only need entity lookups. + pub fn entities_cache_mut(&mut self) -> &mut HashMap> { + &mut self.entities + } +} + +impl Default for FileCache { + fn default() -> Self { + Self::new() + } +} + +/// Parse dependencies from file content (extracted for use by FileCache). +fn parse_file_dependencies(content: &str) -> Result> { + let mut dependencies = Vec::new(); + let mut seen = HashSet::new(); + + // Package imports from "use work.package_name" + let imports = get_package_imports(content)?; + for pkg in imports { + let key = format!("pkg:{}", pkg.to_lowercase()); + if seen.insert(key) { + dependencies.push(VwSymbol::Package(pkg)); + } + } + + // Find direct entity instantiations (instance_name: entity work.entity_name) + let entity_inst_pattern = r"(?i)\w+\s*:\s*entity\s+work\.(\w+)"; + let entity_inst_re = regex::Regex::new(entity_inst_pattern)?; + + for captures in entity_inst_re.captures_iter(content) { + if let Some(entity_name) = captures.get(1) { + let name = entity_name.as_str().to_string(); + let key = format!("ent:{}", name.to_lowercase()); + if seen.insert(key) { + dependencies.push(VwSymbol::Entity(name)); + } + } + } + + // Find component declarations + let comp_decl_pattern = r"(?i)component\s+(\w+)"; + let comp_decl_re = regex::Regex::new(comp_decl_pattern)?; + + for captures in comp_decl_re.captures_iter(content) { + if let Some(comp_name) = captures.get(1) { + let name = comp_name.as_str().to_string(); + let key = format!("ent:{}", name.to_lowercase()); + if seen.insert(key) { + dependencies.push(VwSymbol::Entity(name)); + } + } + } + + Ok(dependencies) +} + +/// Parse provided symbols (packages and entities) from file content. +fn parse_provided_symbols(content: &str) -> Result> { + let mut symbols = Vec::new(); + + // Find package declarations + let package_pattern = r"(?i)\bpackage\s+(\w+)\s+is\b"; + let package_re = regex::Regex::new(package_pattern)?; + + for captures in package_re.captures_iter(content) { + if let Some(package_name) = captures.get(1) { + symbols.push(VwSymbol::Package(package_name.as_str().to_string())); + } + } + + // Find entity declarations + let entity_pattern = r"(?i)\bentity\s+(\w+)\s+is\b"; + let entity_re = regex::Regex::new(entity_pattern)?; + + for captures in entity_re.captures_iter(content) { + if let Some(entity_name) = captures.get(1) { + symbols.push(VwSymbol::Entity(entity_name.as_str().to_string())); + } + } + + Ok(symbols) +} + +/// Parse entity declarations from file content. +fn parse_entities(content: &str) -> Result> { + let mut entities = Vec::new(); + + let entity_pattern = r"(?i)\bentity\s+(\w+)\s+is\b"; + let re = regex::Regex::new(entity_pattern)?; + + for captures in re.captures_iter(content) { + if let Some(entity_name) = captures.get(1) { + entities.push(entity_name.as_str().to_string()); + } + } + + Ok(entities) +} + pub async fn anodize_only( workspace_dir: &Utf8Path, vhdl_std: VhdlStandard, ) -> Result<()> { let vhdl_ls_config = load_existing_vhdl_ls_config(workspace_dir)?; let mut processor = RecordProcessor::new(vhdl_std); + let mut cache = FileCache::new(); fs::create_dir_all(BUILD_DIR)?; - analyze_ext_libraries(&vhdl_ls_config, &mut processor, vhdl_std).await?; + analyze_ext_libraries(&vhdl_ls_config, &mut processor, vhdl_std, &mut cache) + .await?; // alright...now we need to find packages and search them for records let defaultlib_files = vhdl_ls_config @@ -851,17 +1026,17 @@ pub async fn anodize_only( let mut relevant_files = HashSet::new(); for file in &defaultlib_files { - let content = - fs::read_to_string(file).map_err(|e| VwError::FileSystem { - message: format!("Failed to read file {file:?}: {e}"), - })?; - let package_re = regex::Regex::new(r"(?i)\bpackage\s+\w+\s+is\b")?; - - if package_re.is_match(&content) { + // Use cache to check for package declarations + let provided = cache.get_provided_symbols(file)?; + // Check if file contains any package declarations + let has_package = provided + .iter() + .any(|s| matches!(s, VwSymbol::Package(_))); + if has_package { relevant_files.insert(file.clone()); // ok it is a package...figure out which files it brings in let referenced_files = - find_referenced_files(file, &defaultlib_files)?; + find_referenced_files(file, &defaultlib_files, &mut cache)?; relevant_files.extend(referenced_files.iter().cloned()); } } @@ -869,7 +1044,7 @@ pub async fn anodize_only( let mut files_vec: Vec = relevant_files.iter().cloned().collect(); // with all the relevant files, sort them and then anodize them - sort_files_by_dependencies(&mut processor, &mut files_vec)?; + sort_files_by_dependencies(&mut processor, &mut files_vec, &mut cache)?; let files: Vec = files_vec .iter() @@ -892,6 +1067,7 @@ async fn analyze_ext_libraries( vhdl_ls_config: &VhdlLsConfig, processor: &mut RecordProcessor, vhdl_std: VhdlStandard, + cache: &mut FileCache, ) -> Result<()> { // First, analyze all non-defaultlib libraries for (lib_name, library) in &vhdl_ls_config.libraries { @@ -919,7 +1095,7 @@ async fn analyze_ext_libraries( } // Sort files in dependency order (dependencies first) - sort_files_by_dependencies(processor, &mut files)?; + sort_files_by_dependencies(processor, &mut files, cache)?; let file_strings: Vec = files .iter() @@ -951,11 +1127,13 @@ pub async fn run_testbench( ) -> Result<()> { let vhdl_ls_config = load_existing_vhdl_ls_config(workspace_dir)?; let mut processor = RecordProcessor::new(vhdl_std); + let mut cache = FileCache::new(); fs::create_dir_all(BUILD_DIR)?; // First, analyze all non-defaultlib libraries - analyze_ext_libraries(&vhdl_ls_config, &mut processor, vhdl_std).await?; + analyze_ext_libraries(&vhdl_ls_config, &mut processor, vhdl_std, &mut cache) + .await?; // Get defaultlib files for later use let defaultlib_files = vhdl_ls_config @@ -973,10 +1151,26 @@ pub async fn run_testbench( } let testbench_file = - find_testbench_file(&testbench_name, &bench_dir, recurse)?; + find_testbench_file(&testbench_name, &bench_dir, recurse, cache.entities_cache_mut())?; // Filter defaultlib files to exclude OTHER testbenches but allow common bench code let bench_dir_abs = workspace_dir.as_std_path().join("bench"); + + // Pre-compute entities for bench files to avoid mutable borrow in closure + let mut bench_file_entities: HashMap> = HashMap::new(); + for file_path in &defaultlib_files { + let absolute_path = if file_path.is_relative() { + workspace_dir.as_std_path().join(file_path) + } else { + file_path.clone() + }; + if absolute_path.starts_with(&bench_dir_abs) { + if let Ok(entities) = cache.get_entities(&absolute_path) { + bench_file_entities.insert(absolute_path, entities.clone()); + } + } + } + let filtered_defaultlib_files: Vec = defaultlib_files .into_iter() .filter(|file_path| { @@ -993,11 +1187,11 @@ pub async fn run_testbench( } // If it's in the bench directory, check if it's a different testbench - if let Ok(entities) = find_entities_in_file(&absolute_path) { + if let Some(entities) = bench_file_entities.get(&absolute_path) { // Exclude files that contain testbench entities other than the one we're running for entity in entities { if entity.to_lowercase().ends_with("_tb") - && entity != testbench_name + && entity != &testbench_name { return false; // This is a different testbench, exclude it } @@ -1011,10 +1205,10 @@ pub async fn run_testbench( // Find only the defaultlib files that are actually referenced by this testbench let mut referenced_files = - find_referenced_files(&testbench_file, &filtered_defaultlib_files)?; + find_referenced_files(&testbench_file, &filtered_defaultlib_files, &mut cache)?; // Sort files in dependency order (dependencies first) - sort_files_by_dependencies(&mut processor, &mut referenced_files)?; + sort_files_by_dependencies(&mut processor, &mut referenced_files, &mut cache)?; let mut files: Vec = referenced_files .iter() @@ -1075,6 +1269,7 @@ pub async fn run_testbench( fn find_referenced_files( testbench_file: &Path, available_files: &[PathBuf], + cache: &mut FileCache, ) -> Result> { let mut referenced_files = Vec::new(); let mut processed_files = HashSet::new(); @@ -1092,12 +1287,12 @@ fn find_referenced_files( referenced_files.push(current_file.clone()); } - let dependencies = find_file_dependencies(¤t_file)?; + let dependencies = cache.get_dependencies(¤t_file)?.clone(); // Find corresponding files for each dependency for dep in dependencies { for available_file in available_files { - if file_provides_symbol(available_file, &dep)? { + if file_provides_symbol(available_file, &dep, cache)? { if !processed_files.contains(available_file) { files_to_process.push(available_file.clone()); } @@ -1124,78 +1319,23 @@ fn get_package_imports(content: &str) -> Result> { Ok(imports) } -fn find_file_dependencies(file_path: &Path) -> Result> { - let content = - fs::read_to_string(file_path).map_err(|e| VwError::FileSystem { - message: format!("Failed to read file {file_path:?}: {e}"), - })?; - - let mut dependencies = HashSet::new(); - - let imports = get_package_imports(&content)?; - dependencies.extend(imports); - - // Find direct entity instantiations (instance_name: entity work.entity_name) - let entity_inst_pattern = r"(?i)\w+\s*:\s*entity\s+work\.(\w+)"; - let entity_inst_re = regex::Regex::new(entity_inst_pattern)?; - - for captures in entity_inst_re.captures_iter(&content) { - if let Some(entity_name) = captures.get(1) { - dependencies.insert(entity_name.as_str().to_string()); +fn file_provides_symbol( + file_path: &Path, + needed: &VwSymbol, + cache: &mut FileCache, +) -> Result { + let provided = cache.get_provided_symbols(file_path)?; + Ok(provided.iter().any(|s| match (needed, s) { + // Package dependency matches package declaration + (VwSymbol::Package(need), VwSymbol::Package(have)) => { + need.eq_ignore_ascii_case(have) } - } - - // Find component instantiations (component_name : entity_name) - let component_pattern = r"(?i)(\w+)\s*:\s*(\w+)"; - let component_re = regex::Regex::new(component_pattern)?; - - for captures in component_re.captures_iter(&content) { - if let Some(entity_name) = captures.get(2) { - // Skip if this looks like an entity instantiation (already handled above) - if !entity_name.as_str().eq_ignore_ascii_case("entity") { - dependencies.insert(entity_name.as_str().to_string()); - } + // Entity dependency matches entity declaration + (VwSymbol::Entity(need), VwSymbol::Entity(have)) => { + need.eq_ignore_ascii_case(have) } - } - - // Find component declarations - let comp_decl_pattern = r"(?i)component\s+(\w+)"; - let comp_decl_re = regex::Regex::new(comp_decl_pattern)?; - - for captures in comp_decl_re.captures_iter(&content) { - if let Some(comp_name) = captures.get(1) { - dependencies.insert(comp_name.as_str().to_string()); - } - } - - Ok(dependencies.into_iter().collect()) -} - -fn file_provides_symbol(file_path: &Path, symbol: &str) -> Result { - let content = - fs::read_to_string(file_path).map_err(|e| VwError::FileSystem { - message: format!("Failed to read file {file_path:?}: {e}"), - })?; - - // Check for package declaration - let package_pattern = - format!(r"(?i)\bpackage\s+{}\s+is\b", regex::escape(symbol)); - let package_re = regex::Regex::new(&package_pattern)?; - - if package_re.is_match(&content) { - return Ok(true); - } - - // Check for entity declaration - let entity_pattern = - format!(r"(?i)\bentity\s+{}\s+is\b", regex::escape(symbol)); - let entity_re = regex::Regex::new(&entity_pattern)?; - - if entity_re.is_match(&content) { - return Ok(true); - } - - Ok(false) + _ => false, + })) } fn analyze_file( @@ -1209,14 +1349,22 @@ fn analyze_file( let mut file_finder = VwSymbolFinder::new(&processor.target_attr); walk_design_file(&mut file_finder, &design_file); + let file_str = file.to_string_lossy().to_string(); + + // Add records to symbols map for record in file_finder.get_records() { - processor - .records - .insert(record.get_name().to_string(), record.clone()); - processor.record_to_file.insert( - record.get_name().to_string(), - file.to_string_lossy().to_string(), - ); + let name = record.get_name().to_string(); + processor.symbols.insert(name.clone(), VwSymbol::Record(record.clone())); + processor.symbol_to_file.insert(name, file_str.clone()); + } + + // Add enums from symbols (they're already VwSymbol::Enum) + for symbol in file_finder.get_symbols() { + if let VwSymbol::Enum(enum_data) = symbol { + let name = enum_data.get_name().to_string(); + processor.symbols.insert(name.clone(), symbol.clone()); + processor.symbol_to_file.insert(name, file_str.clone()); + } } for tagged_type in file_finder.get_tagged_types() { @@ -1229,6 +1377,7 @@ fn analyze_file( fn sort_files_by_dependencies( processor: &mut RecordProcessor, files: &mut Vec, + cache: &mut FileCache, ) -> Result<()> { // Build dependency graph let mut dependencies: HashMap> = HashMap::new(); @@ -1247,16 +1396,12 @@ fn sort_files_by_dependencies( .or_insert_with(|| FileData::new()); entry.add_defined_pkg(&name); - let content = fs::read_to_string(file).map_err(|e| { - VwError::FileSystem { - message: format!( - "Failed to read file {file:?}: {e}" - ), + // Use cache to get package imports only + let deps = cache.get_dependencies(file)?; + for dep in deps { + if let VwSymbol::Package(pkg_name) = dep { + entry.add_imported_pkg(pkg_name); } - })?; - let package_names = get_package_imports(&content)?; - for pkg in package_names { - entry.add_imported_pkg(&pkg); } } VwSymbol::Entity(name) => { @@ -1269,11 +1414,15 @@ fn sort_files_by_dependencies( // Second pass: find dependencies for each file for file in files.iter() { - let deps = find_file_dependencies(file)?; + let deps = cache.get_dependencies(file)?.clone(); let mut file_deps = Vec::new(); for dep in deps { - if let Some(provider_file) = all_symbols.get(&dep) { + let dep_name = match &dep { + VwSymbol::Package(name) | VwSymbol::Entity(name) => name, + _ => continue, + }; + if let Some(provider_file) = all_symbols.get(dep_name) { if provider_file != file { file_deps.push(provider_file.clone()); } @@ -1379,31 +1528,11 @@ fn topological_sort( Ok(result) } -fn find_entities_in_file(file_path: &Path) -> Result> { - let content = - fs::read_to_string(file_path).map_err(|e| VwError::FileSystem { - message: format!("Failed to read file {file_path:?}: {e}"), - })?; - - let mut entities = Vec::new(); - - // Regex to find entity declarations - let entity_pattern = r"(?i)\bentity\s+(\w+)\s+is\b"; - let re = regex::Regex::new(entity_pattern)?; - - for captures in re.captures_iter(&content) { - if let Some(entity_name) = captures.get(1) { - entities.push(entity_name.as_str().to_string()); - } - } - - Ok(entities) -} - fn find_testbench_file_recurse( testbench_name: &str, bench_dir: &Utf8Path, recurse: bool, + entities_cache: &mut HashMap>, ) -> Result> { let mut found_files = Vec::new(); @@ -1419,7 +1548,7 @@ fn find_testbench_file_recurse( if let Some(extension) = path.extension() { if extension == "vhd" || extension == "vhdl" { // Check if this file contains the entity we're looking for - if file_contains_entity(&path, testbench_name)? { + if file_contains_entity(&path, testbench_name, entities_cache)? { found_files.push(path); } } @@ -1433,6 +1562,7 @@ fn find_testbench_file_recurse( testbench_name, &dir_path, recurse, + entities_cache, )?; found_files.append(&mut lower_testbenches); } @@ -1444,9 +1574,10 @@ fn find_testbench_file( testbench_name: &str, bench_dir: &Utf8Path, recurse: bool, + entities_cache: &mut HashMap>, ) -> Result { let found_files = - find_testbench_file_recurse(testbench_name, bench_dir, recurse)?; + find_testbench_file_recurse(testbench_name, bench_dir, recurse, entities_cache)?; match found_files.len() { 0 => Err(VwError::Testbench { @@ -1459,19 +1590,31 @@ fn find_testbench_file( } } -fn file_contains_entity(file_path: &Path, entity_name: &str) -> Result { - let content = - fs::read_to_string(file_path).map_err(|e| VwError::FileSystem { - message: format!("Failed to read file {file_path:?}: {e}"), - })?; - - // Simple regex to find entity declarations - // This is a basic implementation that looks for "entity is" - let entity_pattern = - format!(r"(?i)\bentity\s+{}\s+is\b", regex::escape(entity_name)); - let re = regex::Regex::new(&entity_pattern)?; +fn file_contains_entity( + file_path: &Path, + entity_name: &str, + entities_cache: &mut HashMap>, +) -> Result { + let entities = get_cached_entities(file_path, entities_cache)?; + Ok(entities + .iter() + .any(|e| e.eq_ignore_ascii_case(entity_name))) +} - Ok(re.is_match(&content)) +/// Get entities from cache, parsing and caching if not present. +fn get_cached_entities<'a>( + path: &Path, + entities_cache: &'a mut HashMap>, +) -> Result<&'a Vec> { + if !entities_cache.contains_key(path) { + let content = + fs::read_to_string(path).map_err(|e| VwError::FileSystem { + message: format!("Failed to read file {path:?}: {e}"), + })?; + let entities = parse_entities(&content)?; + entities_cache.insert(path.to_path_buf(), entities); + } + Ok(entities_cache.get(path).unwrap()) } fn make_path_portable(path: PathBuf) -> PathBuf { diff --git a/vw-lib/src/mapping.rs b/vw-lib/src/mapping.rs index 75015a0..d94ca03 100644 --- a/vw-lib/src/mapping.rs +++ b/vw-lib/src/mapping.rs @@ -2,7 +2,7 @@ use vhdl_lang::ast::{ AnyDesignUnit, AnyPrimaryUnit, AttributeSpecification, Designator, DiscreteRange, ElementDeclaration, EntityClass, EntityDeclaration, EntityName, Name, PackageDeclaration, Range, RangeConstraint, - SubtypeConstraint, TypeDeclaration, TypeDefinition::Record, + SubtypeConstraint, TypeDeclaration, TypeDefinition, }; use crate::visitor::{Visitor, VisitorResult}; @@ -13,6 +13,32 @@ pub enum VwSymbol { Entity(String), Constant(String), Record(RecordData), + Enum(EnumData), +} + +#[derive(Debug, Clone)] +pub struct EnumData { + pub containing_pkg: Option, + pub name: String, + pub has_custom_encoding: bool, +} + +impl EnumData { + pub fn new(containing_pkg: Option, name: &str) -> Self { + Self { + containing_pkg, + name: String::from(name), + has_custom_encoding: false, + } + } + + pub fn get_pkg_name(&self) -> Option<&String> { + self.containing_pkg.as_ref() + } + + pub fn get_name(&self) -> &str { + &self.name + } } #[derive(Debug, Clone)] @@ -115,8 +141,30 @@ impl Visitor for VwSymbolFinder { spec: &AttributeSpecification, _unit: &AnyDesignUnit, ) -> VisitorResult { + let attr_name = spec.ident.item.item.name_utf8(); + + // Check for custom enum encoding + if attr_name == "enum_encoding" { + if let EntityClass::Type = spec.entity_class { + if let EntityName::Name(tag) = &spec.entity_name { + if let Designator::Identifier(id) = &tag.designator.item.item { + let type_name = id.name_utf8(); + // Find the enum and set its flag + for symbol in &mut self.symbols { + if let VwSymbol::Enum(enum_data) = symbol { + if enum_data.name == type_name { + enum_data.has_custom_encoding = true; + break; + } + } + } + } + } + } + } + // if we found the attribute with the right name - if spec.ident.item.item.name_utf8() == self.target_attr { + if attr_name == self.target_attr { // if we tagged a type (like a record) if let EntityClass::Type = spec.entity_class { // get the entity name @@ -139,24 +187,32 @@ impl Visitor for VwSymbolFinder { decl: &TypeDeclaration, unit: &AnyDesignUnit, ) -> VisitorResult { - if let Record(elements) = &decl.def { - let name = decl.ident.tree.item.name_utf8(); - //figure out where this package was defined - let defining_pkg_name = - if let AnyDesignUnit::Primary(primary_unit) = unit { - if let AnyPrimaryUnit::Package(package) = primary_unit { - Some(package.ident.tree.item.name_utf8()) - } else { - None - } + let name = decl.ident.tree.item.name_utf8(); + + // Figure out where this type was defined (containing package) + let defining_pkg_name = + if let AnyDesignUnit::Primary(primary_unit) = unit { + if let AnyPrimaryUnit::Package(package) = primary_unit { + Some(package.ident.tree.item.name_utf8()) } else { None - }; + } + } else { + None + }; - let mut record_struct = RecordData::new(defining_pkg_name, &name); - let fields = get_fields(elements); - record_struct.fields = fields; - self.records.push(record_struct); + match &decl.def { + TypeDefinition::Record(elements) => { + let mut record_struct = RecordData::new(defining_pkg_name, &name); + let fields = get_fields(elements); + record_struct.fields = fields; + self.records.push(record_struct); + } + TypeDefinition::Enumeration(_) => { + let enum_data = EnumData::new(defining_pkg_name, &name); + self.symbols.push(VwSymbol::Enum(enum_data)); + } + _ => {} } VisitorResult::Continue } From 76ff984800c808c0cc8032fc91058459de01910b Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Wed, 19 Nov 2025 22:05:53 -0800 Subject: [PATCH 08/22] add sim_only option to deps (#9) these deps are skipped when generating vivado tcl import files --- example/vw.toml | 3 +++ vw-cli/src/main.rs | 4 ++++ vw-lib/src/lib.rs | 17 +++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/example/vw.toml b/example/vw.toml index 92f2c44..1e1b275 100644 --- a/example/vw.toml +++ b/example/vw.toml @@ -18,3 +18,6 @@ recursive = true # Specific files by pattern: # src = "hdl/**/pkg_*.vhd" + +# Simulation-only dependency (excluded from deps.tcl): +# sim_only = true diff --git a/vw-cli/src/main.rs b/vw-cli/src/main.rs index c7531d7..528f6b4 100644 --- a/vw-cli/src/main.rs +++ b/vw-cli/src/main.rs @@ -77,6 +77,8 @@ enum Commands { help = "Recursively include VHDL files from subdirectories" )] recursive: bool, + #[arg(long, help = "Mark as simulation-only (excluded from deps.tcl)")] + sim_only: bool, }, #[command(about = "Remove a dependency")] Remove { @@ -212,6 +214,7 @@ async fn main() { src, name, recursive, + sim_only, } => { let access_creds = get_access_credentials_for_repo(&repo).await; match add_dependency_with_token( @@ -222,6 +225,7 @@ async fn main() { src, name.clone(), recursive, + sim_only, access_creds, ) .await diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index e5ae78d..c7537e3 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -205,6 +205,8 @@ pub struct Dependency { pub src: String, #[serde(default)] pub recursive: bool, + #[serde(default)] + pub sim_only: bool, } #[derive(Debug, Serialize, Deserialize)] @@ -220,6 +222,8 @@ pub struct LockedDependency { pub path: PathBuf, #[serde(default)] pub recursive: bool, + #[serde(default)] + pub sim_only: bool, } #[derive(Debug, Serialize, Deserialize)] @@ -478,6 +482,7 @@ pub async fn update_workspace_with_token( src: dep.src.clone(), path: dep_path.clone(), recursive: dep.recursive, + sim_only: dep.sim_only, }, ); @@ -506,6 +511,7 @@ pub async fn update_workspace_with_token( } /// Add a new dependency to the workspace configuration. +#[allow(clippy::too_many_arguments)] pub async fn add_dependency( workspace_dir: &Utf8Path, repo: String, @@ -514,6 +520,7 @@ pub async fn add_dependency( src: Option, name: Option, recursive: bool, + sim_only: bool, ) -> Result<()> { add_dependency_with_token( workspace_dir, @@ -523,6 +530,7 @@ pub async fn add_dependency( src, name, recursive, + sim_only, None, ) .await @@ -538,6 +546,7 @@ pub async fn add_dependency( /// * `src` - Optional source path within the repository /// * `name` - Optional dependency name /// * `recursive` - Whether to recursively include VHDL files +/// * `sim_only` - Whether this dependency is only for simulation (excluded from deps.tcl) /// * `credentials` - Optional credentials for authentication #[allow(clippy::too_many_arguments)] pub async fn add_dependency_with_token( @@ -548,6 +557,7 @@ pub async fn add_dependency_with_token( src: Option, name: Option, recursive: bool, + sim_only: bool, _credentials: Option, ) -> Result<()> { let mut config = @@ -577,6 +587,7 @@ pub async fn add_dependency_with_token( commit, src: src_path, recursive, + sim_only, }; config.dependencies.insert(dep_name.clone(), dependency); @@ -719,6 +730,12 @@ pub fn generate_deps_tcl(workspace_dir: &Utf8Path) -> Result<()> { for dep_name in dep_names { let locked_dep = &lock_file.dependencies[dep_name]; + + // Skip sim-only dependencies + if locked_dep.sim_only { + continue; + } + let vhdl_files = find_vhdl_files(&locked_dep.path, locked_dep.recursive)?; From 5805824883912cbd6be974c8f6c1c11a1e33e1bb Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Wed, 18 Feb 2026 08:06:45 +0000 Subject: [PATCH 09/22] Make generated code use BitSet --- vw-lib/src/anodizer.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vw-lib/src/anodizer.rs b/vw-lib/src/anodizer.rs index 3723d64..1ca1931 100644 --- a/vw-lib/src/anodizer.rs +++ b/vw-lib/src/anodizer.rs @@ -344,7 +344,7 @@ fn generate_rust_structs(resolved_recs: &Vec) -> Result + pub #field_name: BitSet<#bitwidth> }) } else { let subtype = format_ident!("{}", field.subtype_name); @@ -360,7 +360,7 @@ fn generate_rust_structs(resolved_recs: &Vec) -> Result) -> Result Date: Wed, 18 Feb 2026 08:18:40 +0000 Subject: [PATCH 10/22] Formatting --- vw-cli/src/main.rs | 62 ++++--- vw-lib/src/anodizer.rs | 321 ++++++++++++++++++++----------------- vw-lib/src/lib.rs | 83 +++++++--- vw-lib/src/mapping.rs | 16 +- vw-lib/src/nvc_helpers.rs | 98 +++++------ vw-lib/src/vhdl_printer.rs | 55 ++++--- vw-lib/src/visitor.rs | 141 +++++++++++----- 7 files changed, 465 insertions(+), 311 deletions(-) diff --git a/vw-cli/src/main.rs b/vw-cli/src/main.rs index 528f6b4..b6ebc99 100644 --- a/vw-cli/src/main.rs +++ b/vw-cli/src/main.rs @@ -5,16 +5,16 @@ use camino::Utf8PathBuf; use clap::{Parser, Subcommand, ValueEnum}; use colored::*; +use std::collections::HashSet; use std::fmt; use std::process; -use std::collections::HashSet; use vw_lib::{ - add_dependency_with_token, anodize_only, clear_cache, extract_hostname_from_repo_url, - generate_deps_tcl, get_access_credentials_from_netrc, init_workspace, - list_dependencies, list_testbenches, load_workspace_config, - remove_dependency, run_testbench, update_workspace_with_token, Credentials, - VersionInfo, VhdlStandard, + add_dependency_with_token, anodize_only, clear_cache, + extract_hostname_from_repo_url, generate_deps_tcl, + get_access_credentials_from_netrc, init_workspace, list_dependencies, + list_testbenches, load_workspace_config, remove_dependency, run_testbench, + update_workspace_with_token, Credentials, VersionInfo, VhdlStandard, }; #[derive(Clone, Copy, Debug, ValueEnum)] @@ -99,16 +99,35 @@ enum Commands { std: CliVhdlStandard, #[arg(long, help = "List all available testbenches")] list: bool, - #[arg(long, help = "Enable recursive search when looking for testbenches")] + #[arg( + long, + help = "Enable recursive search when looking for testbenches" + )] recurse: bool, - #[arg(long, value_delimiter = ',', help = "Ignore directories matching these names (comma-separated or use multiple times)")] + #[arg( + long, + value_delimiter = ',', + help = "Ignore directories matching these names (comma-separated or use multiple times)" + )] ignore: Vec, - #[arg(long, value_delimiter = ',', help = "Runtime flags to pass to NVC (comma-separated or use multiple times)", requires = "testbench")] + #[arg( + long, + value_delimiter = ',', + help = "Runtime flags to pass to NVC (comma-separated or use multiple times)", + requires = "testbench" + )] runtime_flags: Vec, - #[arg(long, help = "Build Rust library for testbench before running", requires = "testbench")] + #[arg( + long, + help = "Build Rust library for testbench before running", + requires = "testbench" + )] build_rust: bool, }, - #[command(name = "anodize", about = "Generate Rust structs from VHDL records tagged with serialize_rust attribute")] + #[command( + name = "anodize", + about = "Generate Rust structs from VHDL records tagged with serialize_rust attribute" + )] Anodize { #[arg(long, help = "VHDL standard", default_value_t = CliVhdlStandard::Vhdl2019)] std: CliVhdlStandard, @@ -344,9 +363,8 @@ async fn main() { let bench_dir = cwd.join("bench"); if !bench_dir.exists() { println!("No bench dir found in {:}", bench_dir.as_str()); - } - else { - let mut ignore_set : HashSet = HashSet::new(); + } else { + let mut ignore_set: HashSet = HashSet::new(); for ignore_pattern in ignore { ignore_set.insert(ignore_pattern); } @@ -354,7 +372,9 @@ async fn main() { match list_testbenches(&bench_dir, &ignore_set, recurse) { Ok(testbenches) => { if testbenches.is_empty() { - println!("No testbenches found in bench directory"); + println!( + "No testbenches found in bench directory" + ); } else { println!("Available testbenches:"); for tb in testbenches { @@ -375,11 +395,17 @@ async fn main() { } } } - } else if let Some(testbench_name) = testbench { println!("Running testbench: {}", testbench_name.cyan()); - match run_testbench(&cwd, testbench_name.clone(), std.into(), recurse, &runtime_flags, build_rust) - .await + match run_testbench( + &cwd, + testbench_name.clone(), + std.into(), + recurse, + &runtime_flags, + build_rust, + ) + .await { Ok(()) => { println!( diff --git a/vw-lib/src/anodizer.rs b/vw-lib/src/anodizer.rs index 1ca1931..54440c0 100644 --- a/vw-lib/src/anodizer.rs +++ b/vw-lib/src/anodizer.rs @@ -1,42 +1,36 @@ -use std::{ - collections::HashMap, - collections::HashSet, - fs, -}; +use std::{collections::HashMap, collections::HashSet, fs}; -use crate::{RecordProcessor, VhdlStandard, nvc_helpers::{run_nvc_elab, run_nvc_sim}}; -use crate::VwError; use crate::mapping::VwSymbol; -use crate::vhdl_printer::expr_to_string; use crate::nvc_helpers::run_nvc_analysis; +use crate::vhdl_printer::expr_to_string; +use crate::VwError; +use crate::{ + nvc_helpers::{run_nvc_elab, run_nvc_sim}, + RecordProcessor, VhdlStandard, +}; -use quote::{quote, format_ident}; use proc_macro2::TokenStream; +use quote::{format_ident, quote}; -use vhdl_lang::ast::{ - AbstractLiteral, - Expression, - Literal, -}; +use vhdl_lang::ast::{AbstractLiteral, Expression, Literal}; enum Side { Left, - Right + Right, } /// struct for identifying a particular /// range constraint within a field with a struct struct ConstraintID { - record_index : usize, - field_index : usize, - side : Option, // None for enum size (single value), Some(Left/Right) for range constraints + record_index: usize, + field_index: usize, + side: Option, // None for enum size (single value), Some(Left/Right) for range constraints } - #[derive(Debug, Clone, Default)] pub struct ResolvedRange { - left : Option, - right : Option + left: Option, + right: Option, } #[derive(Debug, Clone)] @@ -46,11 +40,15 @@ pub struct ResolvedField { pub subtype_name: String, } impl ResolvedField { - pub fn new(name : &str, subtype : &str, bitwidth : Option) -> Self { - ResolvedField { + pub fn new( + name: &str, + subtype: &str, + bitwidth: Option, + ) -> Self { + ResolvedField { name: name.to_string(), - bit_width: bitwidth, - subtype_name: subtype.to_string() + bit_width: bitwidth, + subtype_name: subtype.to_string(), } } } @@ -64,34 +62,34 @@ pub struct ResolvedRecord { impl ResolvedRecord { pub fn new(name: &str) -> Self { - ResolvedRecord { - name: name.to_string(), - fields: Vec::new() + ResolvedRecord { + name: name.to_string(), + fields: Vec::new(), } } } pub async fn anodize_records( - processor : &RecordProcessor, - referenced_files : &Vec, - generate_dir : String, - build_dir : String, - rust_out_dir : String -) -> Result<(), VwError>{ + processor: &RecordProcessor, + referenced_files: &Vec, + generate_dir: String, + build_dir: String, + rust_out_dir: String, +) -> Result<(), VwError> { let mut tagged_records = Vec::new(); let mut packages_set = HashSet::new(); for name in &processor.tagged_names { match processor.symbols.get(name) { Some(VwSymbol::Record(record)) => { tagged_records.push(record); - let pkg_name = record.get_pkg_name() - .ok_or_else(|| - VwError::CodeGen { - message: format!("Serialization not supported for \ - records not in packages. Record : {:}", record.get_name() - ) - } - )?; + let pkg_name = + record.get_pkg_name().ok_or_else(|| VwError::CodeGen { + message: format!( + "Serialization not supported for \ + records not in packages. Record : {:}", + record.get_name() + ), + })?; packages_set.insert(pkg_name.clone()); // get the imported packages for the file @@ -100,28 +98,36 @@ pub async fn anodize_records( VwError::CodeGen { message: format!("Somehow couldn't find the containing file for record {:}", name) })?; - let file_data = processor.file_info.get(record_filename) - .ok_or( - VwError::CodeGen { - message: format!("Somehow couldn't find information for file {:}", record_filename) + let file_data = processor + .file_info + .get(record_filename) + .ok_or(VwError::CodeGen { + message: format!( + "Somehow couldn't find information for file {:}", + record_filename + ), })?; let imports = file_data.get_imported_pkgs(); packages_set.extend(imports.iter().cloned()); } Some(_) => { return Err(VwError::CodeGen { - message: format!("Tagged type {:} is not a record", name) + message: format!("Tagged type {:} is not a record", name), }) } None => { return Err(VwError::CodeGen { - message: format!("Tagged type with name {:} not found", name) + message: format!( + "Tagged type with name {:} not found", + name + ), }) } } } - - let mut expr_to_resolve : HashMap> = HashMap::new(); + + let mut expr_to_resolve: HashMap> = + HashMap::new(); let mut process_records = Vec::new(); // ok we got tagged records. time to collect either their subtypes or their constraints @@ -130,15 +136,15 @@ pub async fn anodize_records( for (j, field) in record.get_fields().iter().enumerate() { // possibly FIXME: work for non-ASCII strings? if field.subtype_name.eq_ignore_ascii_case("std_logic_vector") { - let mut resolve_range = ResolvedRange::default(); + let mut resolve_range = ResolvedRange::default(); // ok we may need to resolve either the left or right range constraints if let Some(range) = &field.constraint { // check the left constraint // is it possible to immediately derive a value? - if let Expression::Literal( - Literal::AbstractLiteral( - AbstractLiteral::Integer(value) - )) = range.left_expr.item { + if let Expression::Literal(Literal::AbstractLiteral( + AbstractLiteral::Integer(value), + )) = range.left_expr.item + { // unwrap here because we just put the range in the field resolve_range.left = Some(value as usize); } @@ -149,53 +155,48 @@ pub async fn anodize_records( ConstraintID { record_index: i, field_index: j, - side: Some(Side::Left) - } + side: Some(Side::Left), + }, ); } // check the right constraint - if let Expression::Literal( - Literal::AbstractLiteral( - AbstractLiteral::Integer(value) - )) = range.right_expr.item { + if let Expression::Literal(Literal::AbstractLiteral( + AbstractLiteral::Integer(value), + )) = range.right_expr.item + { resolve_range.right = Some(value as usize); - } - else { + } else { let expr_str = expr_to_string(&range.right_expr.item); - expr_to_resolve.entry( - expr_str - ).or_default().push( + expr_to_resolve.entry(expr_str).or_default().push( ConstraintID { record_index: i, field_index: j, - side: Some(Side::Right) - } + side: Some(Side::Right), + }, ); } let resolve_field = ResolvedField::new( - &field.name, + &field.name, &field.subtype_name, - Some(resolve_range) + Some(resolve_range), ); record_resolution.fields.push(resolve_field); - } - else { - return Err(VwError::CodeGen { + } else { + return Err(VwError::CodeGen { message: format!("All fields in serialized structs must be constrained. \ Found unconstrained field {:} in record {:}", field.name, record.get_name() ) }); } - } - else if field.subtype_name.eq_ignore_ascii_case("std_logic") { + } else if field.subtype_name.eq_ignore_ascii_case("std_logic") { let mut range = ResolvedRange::default(); range.left = Some(0); range.right = Some(0); record_resolution.fields.push(ResolvedField::new( &field.name, &field.subtype_name, - Some(range) - )); + Some(range), + )); } // Check if subtype is an enum or a record else { @@ -209,24 +210,27 @@ pub async fn anodize_records( }); } // Enum field - need to resolve its size via testbench - let expr_str = format!("{}'pos({}'high)", - field.subtype_name, field.subtype_name); + let expr_str = format!( + "{}'pos({}'high)", + field.subtype_name, field.subtype_name + ); expr_to_resolve.entry(expr_str).or_default().push( ConstraintID { record_index: i, field_index: j, - side: None, // Enum size, not left/right range - } + side: None, // Enum size, not left/right range + }, ); record_resolution.fields.push(ResolvedField::new( &field.name, &field.subtype_name, - Some(ResolvedRange::default()) + Some(ResolvedRange::default()), )); } Some(VwSymbol::Record(_)) => { // Nested record - must be tagged for serialization - if !processor.tagged_names.contains(&field.subtype_name) { + if !processor.tagged_names.contains(&field.subtype_name) + { return Err(VwError::CodeGen { message: format!("Subtype {:} not tagged for serialization. Please tag it", field.subtype_name) }); @@ -234,7 +238,7 @@ pub async fn anodize_records( record_resolution.fields.push(ResolvedField::new( &field.name, &field.subtype_name, - None + None, )); } _ => { @@ -248,8 +252,7 @@ pub async fn anodize_records( process_records.push(record_resolution); } - - let packages_needed : Vec = packages_set.iter().cloned().collect(); + let packages_needed: Vec = packages_set.iter().cloned().collect(); // alright, we've collected all the expressions that need resolving...create a testbench let exprs = expr_to_resolve.keys().cloned().collect(); let testbench = create_testbench(&exprs, &packages_needed); @@ -259,8 +262,13 @@ pub async fn anodize_records( fs::create_dir_all(generate_path.clone())?; fs::write(format!("{}/constraint_tb.vhd", generate_path), &testbench)?; - let tb_files : Vec = referenced_files.iter().cloned() - .chain(std::iter::once(format!("{}/constraint_tb.vhd", generate_path))) + let tb_files: Vec = referenced_files + .iter() + .cloned() + .chain(std::iter::once(format!( + "{}/constraint_tb.vhd", + generate_path + ))) .collect(); // ok and now we have to run the testbench @@ -270,8 +278,10 @@ pub async fn anodize_records( &build_dir, &"generated".to_string(), &tb_files, - true - ).await?.unwrap(); + true, + ) + .await? + .unwrap(); let stdout_a_path = format!("{}/analysis.out", generate_path); let stderr_a_path = format!("{}/analysis.err", generate_path); @@ -280,29 +290,33 @@ pub async fn anodize_records( //elaborate the testbench let (stdout_elab, stderr_elab) = run_nvc_elab( - VhdlStandard::Vhdl2019, - &build_dir, - &"generated".to_string(), - &"constraint_evaluator".to_string(), - true).await?.unwrap(); + VhdlStandard::Vhdl2019, + &build_dir, + &"generated".to_string(), + &"constraint_evaluator".to_string(), + true, + ) + .await? + .unwrap(); let stdout_e_path = format!("{}/elab.out", generate_path); let stderr_e_path = format!("{}/elab.err", generate_path); fs::write(stdout_e_path, &stdout_elab)?; fs::write(stderr_e_path, &stderr_elab)?; - // run the testbench let (stdout_sim, stderr_sim) = run_nvc_sim( - VhdlStandard::Vhdl2019, - &build_dir, - &"generated".to_string(), - &"constraint_evaluator".to_string(), - None, - &Vec::new(), - true - ).await?.unwrap(); + VhdlStandard::Vhdl2019, + &build_dir, + &"generated".to_string(), + &"constraint_evaluator".to_string(), + None, + &Vec::new(), + true, + ) + .await? + .unwrap(); let stdout_sim_path = format!("{}/sim.out", generate_path); let stderr_sim_path = format!("{}/sim.err", generate_path); @@ -312,12 +326,11 @@ pub async fn anodize_records( // process the sim output to resolve the expressions process_sim_output( - &exprs, - expr_to_resolve, - &mut process_records, - &stdout_sim + &exprs, + expr_to_resolve, + &mut process_records, + &stdout_sim, )?; - // ok generate Rust code from the resolved records let rust_content = generate_rust_structs(&process_records)?; @@ -327,7 +340,9 @@ pub async fn anodize_records( Ok(()) } -fn generate_rust_structs(resolved_recs: &Vec) -> Result { +fn generate_rust_structs( + resolved_recs: &Vec, +) -> Result { let structs: Result, VwError> = resolved_recs.iter().map(|record| { let struct_name = format_ident!("{}", record.name); @@ -399,38 +414,45 @@ fn generate_rust_structs(resolved_recs: &Vec) -> Result, - exprs_to_resolve : HashMap>, - records : &mut Vec, - sim_out : &Vec -) -> Result<(), VwError>{ + expr_keys: &Vec, + exprs_to_resolve: HashMap>, + records: &mut Vec, + sim_out: &Vec, +) -> Result<(), VwError> { let stdout_str = String::from_utf8_lossy(sim_out); for line in stdout_str.lines() { let parts: Vec<&str> = line.splitn(2, ": ").collect(); - let index: usize = parts[0].strip_prefix("EXPR_").unwrap().parse().map_err(|e| - VwError::CodeGen { message: - format!("Somehow generated an unparseable simulation output : {:}.\ - Look at sim.out", e) - })?; - let value : usize = parts[1].parse().map_err(|e| - VwError::CodeGen { message: - format!("Expression couldn't be evaluated : {}", e) + let index: usize = parts[0] + .strip_prefix("EXPR_") + .unwrap() + .parse() + .map_err(|e| VwError::CodeGen { + message: format!( + "Somehow generated an unparseable simulation output : {:}.\ + Look at sim.out", + e + ), + })?; + let value: usize = parts[1].parse().map_err(|e| VwError::CodeGen { + message: format!("Expression couldn't be evaluated : {}", e), })?; let key = &expr_keys[index]; - let constraint_ids = exprs_to_resolve.get(key) - .ok_or_else(||{ - VwError::CodeGen { message: - format!("Somehow got expression {:} which doesn't exist", key) - }})?; + let constraint_ids = + exprs_to_resolve.get(key).ok_or_else(|| VwError::CodeGen { + message: format!( + "Somehow got expression {:} which doesn't exist", + key + ), + })?; for id in constraint_ids { let record = &mut (records[id.record_index]); let field = &mut (record.fields[id.field_index]); @@ -441,35 +463,39 @@ fn process_sim_output( None => { // Enum size - value is max position (0-indexed), so count = value + 1 let count = value + 1; - let bits = if count <= 1 { 1 } else { (count as f64).log2().ceil() as usize }; + let bits = if count <= 1 { + 1 + } else { + (count as f64).log2().ceil() as usize + }; bitfield.left = Some(bits - 1); bitfield.right = Some(0); } } - } - else { - return Err(VwError::CodeGen { message: - format!("Somehow tried to generate an expression for \ + } else { + return Err(VwError::CodeGen { + message: format!( + "Somehow tried to generate an expression for \ field {:} in record {:} which has no expression", - field.name, record.name) - }) + field.name, record.name + ), + }); } } } Ok(()) } - fn create_testbench( - exprs_to_resolve : &Vec, - packages_needed : &Vec, + exprs_to_resolve: &Vec, + packages_needed: &Vec, ) -> String { let mut testbench = Vec::new(); testbench.push(generate_testbench_imports(packages_needed)); testbench.push(String::from( -"entity constraint_evaluator is + "entity constraint_evaluator is end entity constraint_evaluator; architecture behavior of constraint_evaluator is @@ -478,30 +504,31 @@ begin variable l : line; begin wait for 0ns; -" +", )); for (i, expr) in exprs_to_resolve.iter().enumerate() { - testbench.push(format!(" + testbench.push(format!( + " write(l, string'(\"EXPR_{}: \"));\n write(l, integer'image({}));\n writeline(OUTPUT, l);\n", i, expr )); } - + testbench.push(String::from( " wait; end process; end architecture behavior; -" +", )); testbench.join("") } - + /// Generate VHDL package use statements for a testbench -pub fn generate_testbench_imports(packages_needed : &Vec) -> String { +pub fn generate_testbench_imports(packages_needed: &Vec) -> String { let mut imports = Vec::new(); imports.push(String::from("-- Required packages\n")); @@ -518,4 +545,4 @@ pub fn generate_testbench_imports(packages_needed : &Vec) -> String { } imports.join("") -} \ No newline at end of file +} diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index c7537e3..12fed4f 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -817,8 +817,12 @@ fn list_testbenches_impl( })?; if let Some(file_name) = dir_path.file_name() { if !ignore_dirs.contains(file_name) { - let mut lower_testbenches = - list_testbenches_impl(&dir_path, ignore_dirs, recurse, entities_cache)?; + let mut lower_testbenches = list_testbenches_impl( + &dir_path, + ignore_dirs, + recurse, + entities_cache, + )?; testbenches.append(&mut lower_testbenches); } } @@ -1030,8 +1034,13 @@ pub async fn anodize_only( fs::create_dir_all(BUILD_DIR)?; - analyze_ext_libraries(&vhdl_ls_config, &mut processor, vhdl_std, &mut cache) - .await?; + analyze_ext_libraries( + &vhdl_ls_config, + &mut processor, + vhdl_std, + &mut cache, + ) + .await?; // alright...now we need to find packages and search them for records let defaultlib_files = vhdl_ls_config @@ -1046,9 +1055,8 @@ pub async fn anodize_only( // Use cache to check for package declarations let provided = cache.get_provided_symbols(file)?; // Check if file contains any package declarations - let has_package = provided - .iter() - .any(|s| matches!(s, VwSymbol::Package(_))); + let has_package = + provided.iter().any(|s| matches!(s, VwSymbol::Package(_))); if has_package { relevant_files.insert(file.clone()); // ok it is a package...figure out which files it brings in @@ -1149,8 +1157,13 @@ pub async fn run_testbench( fs::create_dir_all(BUILD_DIR)?; // First, analyze all non-defaultlib libraries - analyze_ext_libraries(&vhdl_ls_config, &mut processor, vhdl_std, &mut cache) - .await?; + analyze_ext_libraries( + &vhdl_ls_config, + &mut processor, + vhdl_std, + &mut cache, + ) + .await?; // Get defaultlib files for later use let defaultlib_files = vhdl_ls_config @@ -1167,8 +1180,12 @@ pub async fn run_testbench( }); } - let testbench_file = - find_testbench_file(&testbench_name, &bench_dir, recurse, cache.entities_cache_mut())?; + let testbench_file = find_testbench_file( + &testbench_name, + &bench_dir, + recurse, + cache.entities_cache_mut(), + )?; // Filter defaultlib files to exclude OTHER testbenches but allow common bench code let bench_dir_abs = workspace_dir.as_std_path().join("bench"); @@ -1221,11 +1238,18 @@ pub async fn run_testbench( .collect(); // Find only the defaultlib files that are actually referenced by this testbench - let mut referenced_files = - find_referenced_files(&testbench_file, &filtered_defaultlib_files, &mut cache)?; + let mut referenced_files = find_referenced_files( + &testbench_file, + &filtered_defaultlib_files, + &mut cache, + )?; // Sort files in dependency order (dependencies first) - sort_files_by_dependencies(&mut processor, &mut referenced_files, &mut cache)?; + sort_files_by_dependencies( + &mut processor, + &mut referenced_files, + &mut cache, + )?; let mut files: Vec = referenced_files .iter() @@ -1371,7 +1395,9 @@ fn analyze_file( // Add records to symbols map for record in file_finder.get_records() { let name = record.get_name().to_string(); - processor.symbols.insert(name.clone(), VwSymbol::Record(record.clone())); + processor + .symbols + .insert(name.clone(), VwSymbol::Record(record.clone())); processor.symbol_to_file.insert(name, file_str.clone()); } @@ -1565,7 +1591,11 @@ fn find_testbench_file_recurse( if let Some(extension) = path.extension() { if extension == "vhd" || extension == "vhdl" { // Check if this file contains the entity we're looking for - if file_contains_entity(&path, testbench_name, entities_cache)? { + if file_contains_entity( + &path, + testbench_name, + entities_cache, + )? { found_files.push(path); } } @@ -1593,8 +1623,12 @@ fn find_testbench_file( recurse: bool, entities_cache: &mut HashMap>, ) -> Result { - let found_files = - find_testbench_file_recurse(testbench_name, bench_dir, recurse, entities_cache)?; + let found_files = find_testbench_file_recurse( + testbench_name, + bench_dir, + recurse, + entities_cache, + )?; match found_files.len() { 0 => Err(VwError::Testbench { @@ -1613,9 +1647,7 @@ fn file_contains_entity( entities_cache: &mut HashMap>, ) -> Result { let entities = get_cached_entities(file_path, entities_cache)?; - Ok(entities - .iter() - .any(|e| e.eq_ignore_ascii_case(entity_name))) + Ok(entities.iter().any(|e| e.eq_ignore_ascii_case(entity_name))) } /// Get entities from cache, parsing and caching if not present. @@ -2209,7 +2241,10 @@ fn load_existing_vhdl_ls_config( /// Build a Rust library for a testbench. /// Looks for Cargo.toml in the testbench directory, builds it, and returns the path to the .so file. -async fn build_rust_library(bench_dir: &Utf8Path, testbench_file: &Path) -> Result { +async fn build_rust_library( + bench_dir: &Utf8Path, + testbench_file: &Path, +) -> Result { // Get the testbench directory let testbench_dir = testbench_file.parent().ok_or_else(|| VwError::Testbench { @@ -2268,9 +2303,7 @@ async fn build_rust_library(bench_dir: &Utf8Path, testbench_file: &Path) -> Resu // Find the .so file in the workspace target directory (parent of testbench dir) let lib_name = format!("lib{}.so", package_name.replace('-', "_")); - let workspace_target = bench_dir - .join("target") - .join("debug"); + let workspace_target = bench_dir.join("target").join("debug"); let lib_path = workspace_target.join(&lib_name); diff --git a/vw-lib/src/mapping.rs b/vw-lib/src/mapping.rs index d94ca03..ba530c2 100644 --- a/vw-lib/src/mapping.rs +++ b/vw-lib/src/mapping.rs @@ -56,16 +56,16 @@ pub struct FileData { impl FileData { pub fn new() -> Self { Self { - defined_pkgs : Vec::new(), - imported_pkgs : Vec::new() + defined_pkgs: Vec::new(), + imported_pkgs: Vec::new(), } } - pub fn add_defined_pkg(&mut self, pkg_name : &str) { + pub fn add_defined_pkg(&mut self, pkg_name: &str) { self.defined_pkgs.push(pkg_name.to_string()); } - pub fn add_imported_pkg(&mut self, pkg_name : &str) { + pub fn add_imported_pkg(&mut self, pkg_name: &str) { self.imported_pkgs.push(pkg_name.to_string()); } @@ -94,7 +94,6 @@ impl RecordData { pub fn get_name(&self) -> &str { &self.name } - } #[derive(Debug, Clone)] @@ -147,7 +146,9 @@ impl Visitor for VwSymbolFinder { if attr_name == "enum_encoding" { if let EntityClass::Type = spec.entity_class { if let EntityName::Name(tag) = &spec.entity_name { - if let Designator::Identifier(id) = &tag.designator.item.item { + if let Designator::Identifier(id) = + &tag.designator.item.item + { let type_name = id.name_utf8(); // Find the enum and set its flag for symbol in &mut self.symbols { @@ -203,7 +204,8 @@ impl Visitor for VwSymbolFinder { match &decl.def { TypeDefinition::Record(elements) => { - let mut record_struct = RecordData::new(defining_pkg_name, &name); + let mut record_struct = + RecordData::new(defining_pkg_name, &name); let fields = get_fields(elements); record_struct.fields = fields; self.records.push(record_struct); diff --git a/vw-lib/src/nvc_helpers.rs b/vw-lib/src/nvc_helpers.rs index 9ec5bdb..fee0230 100644 --- a/vw-lib/src/nvc_helpers.rs +++ b/vw-lib/src/nvc_helpers.rs @@ -2,10 +2,13 @@ use crate::{VhdlStandard, VwError}; use tokio::process::Command; -use std::{io::Write, process::{ExitStatus, Output}}; +use std::{ + io::Write, + process::{ExitStatus, Output}, +}; fn get_base_nvc_cmd_args( - std: VhdlStandard, + std: VhdlStandard, build_dir: &String, lib_name: &String, ) -> Vec { @@ -16,59 +19,57 @@ fn get_base_nvc_cmd_args( "-M".to_string(), "256m".to_string(), "-L".to_string(), - build_dir.clone() + build_dir.clone(), ]; args } async fn run_cmd_w_output( - args : &Vec, - envs : Option<&Vec<(String, String)>> + args: &Vec, + envs: Option<&Vec<(String, String)>>, ) -> Result { let mut nvc_cmd = Command::new("nvc"); for arg in args { nvc_cmd.arg(arg); } - + if let Some(vars) = envs { for (env_var, value) in vars { nvc_cmd.env(env_var, value); } } - nvc_cmd.output().await.map_err(|e| - VwError::Testbench { - message: format!("nvc command failed : {e}") + nvc_cmd.output().await.map_err(|e| VwError::Testbench { + message: format!("nvc command failed : {e}"), }) } async fn run_cmd( - args : &Vec, - envs : Option<&Vec<(String, String)>> -) -> Result{ + args: &Vec, + envs: Option<&Vec<(String, String)>>, +) -> Result { let mut nvc_cmd = Command::new("nvc"); for arg in args { nvc_cmd.arg(arg); } - + if let Some(vars) = envs { for (env_var, value) in vars { nvc_cmd.env(env_var, value); } } - nvc_cmd.status().await.map_err(|e| - VwError::Testbench { - message: format!("nvc command failed : {e}") + nvc_cmd.status().await.map_err(|e| VwError::Testbench { + message: format!("nvc command failed : {e}"), }) } pub async fn run_nvc_analysis( - std: VhdlStandard, + std: VhdlStandard, build_dir: &String, lib_name: &String, - referenced_files : &Vec, - capture_output : bool + referenced_files: &Vec, + capture_output: bool, ) -> Result, Vec)>, VwError> { let mut args = get_base_nvc_cmd_args(std, build_dir, lib_name); args.push("-a".to_string()); @@ -84,34 +85,32 @@ pub async fn run_nvc_analysis( let cmd_str = format!("nvc {}", args.join(" ")); std::io::stdout().write_all(&output.stdout)?; std::io::stderr().write_all(&output.stderr)?; - return Err(VwError::NvcAnalysis { - library: lib_name.clone(), - command: cmd_str - }) + return Err(VwError::NvcAnalysis { + library: lib_name.clone(), + command: cmd_str, + }); } Ok(Some((output.stdout, output.stderr))) - } - else { + } else { let status = run_cmd(&args, None).await?; if !status.success() { let cmd_str = format!("nvc {}", args.join(" ")); - return Err(VwError::NvcAnalysis { - library: lib_name.clone(), - command: cmd_str - }) + return Err(VwError::NvcAnalysis { + library: lib_name.clone(), + command: cmd_str, + }); } Ok(None) } - } pub async fn run_nvc_elab( - std: VhdlStandard, + std: VhdlStandard, build_dir: &String, lib_name: &String, - testbench_name : &String, - capture_output : bool + testbench_name: &String, + capture_output: bool, ) -> Result, Vec)>, VwError> { let mut args = get_base_nvc_cmd_args(std, build_dir, lib_name); args.push("-e".to_string()); @@ -127,31 +126,26 @@ pub async fn run_nvc_elab( return Err(VwError::NvcElab { command: cmd_str }); } Ok(Some((output.stdout, output.stderr))) - - } - else { + } else { let status = run_cmd(&args, None).await?; if !status.success() { let cmd_str = format!("nvc {}", args.join(" ")); - return Err( - VwError::NvcElab { command: cmd_str } - ); + return Err(VwError::NvcElab { command: cmd_str }); } Ok(None) } - } pub async fn run_nvc_sim( - std: VhdlStandard, + std: VhdlStandard, build_dir: &String, lib_name: &String, - testbench_name : &String, - rust_lib_path : Option, - runtime_flags : &Vec, - capture_output : bool + testbench_name: &String, + rust_lib_path: Option, + runtime_flags: &Vec, + capture_output: bool, ) -> Result, Vec)>, VwError> { let mut args = get_base_nvc_cmd_args(std, build_dir, lib_name); args.push("-r".to_string()); @@ -171,9 +165,7 @@ pub async fn run_nvc_sim( let envs_vec = vec![("GPI_USERS".to_string(), path.clone())]; Some(envs_vec) } - None => { - None - } + None => None, }; if capture_output { @@ -187,19 +179,13 @@ pub async fn run_nvc_sim( return Err(VwError::NvcSimulation { command: cmd_str }); } Ok(Some((output.stdout, output.stderr))) - - } - else { + } else { let status = run_cmd(&args, envs.as_ref()).await?; if !status.success() { let cmd_str = format!("nvc {}", args.join(" ")); - return Err( - VwError::NvcSimulation { command: cmd_str } - ); + return Err(VwError::NvcSimulation { command: cmd_str }); } Ok(None) } - } - diff --git a/vw-lib/src/vhdl_printer.rs b/vw-lib/src/vhdl_printer.rs index d88774e..9c2230d 100644 --- a/vw-lib/src/vhdl_printer.rs +++ b/vw-lib/src/vhdl_printer.rs @@ -4,7 +4,7 @@ // Note: vhdl_lang provides VHDLFormatter, but its Buffer type is private, // so we manually reconstruct expressions from the AST instead. -use vhdl_lang::ast::{Expression, Name, Designator, Literal, Operator}; +use vhdl_lang::ast::{Designator, Expression, Literal, Name, Operator}; /// Convert an Expression AST node to a VHDL string pub fn expr_to_string(expr: &Expression) -> String { @@ -48,8 +48,13 @@ pub fn expr_to_string(expr: &Expression) -> String { Operator::Concat => "&", _ => " ? ", // Catch-all for unexpected operators }; - format!("{} {} {}", expr_to_string(&left.item), op_str, expr_to_string(&right.item)) - }, + format!( + "{} {} {}", + expr_to_string(&left.item), + op_str, + expr_to_string(&right.item) + ) + } Expression::Unary(op, operand) => { let op_str = match &op.item.item { Operator::Plus => "+", @@ -60,10 +65,10 @@ pub fn expr_to_string(expr: &Expression) -> String { _ => "unary_op ", }; format!("{}{}", op_str, expr_to_string(&operand.item)) - }, + } Expression::Parenthesized(inner) => { format!("({})", expr_to_string(&inner.item)) - }, + } _ => "complex_expr".to_string(), } } @@ -81,28 +86,42 @@ fn literal_to_string(lit: &Literal) -> String { fn name_to_string(name: &Name) -> String { match name { - Name::Designator(des) => { - match &des.item { - Designator::Identifier(sym) => sym.name_utf8(), - Designator::OperatorSymbol(_) => "operator".to_string(), - Designator::Character(c) => format!("'{}'", c), - Designator::Anonymous(_) => "anonymous".to_string(), - } + Name::Designator(des) => match &des.item { + Designator::Identifier(sym) => sym.name_utf8(), + Designator::OperatorSymbol(_) => "operator".to_string(), + Designator::Character(c) => format!("'{}'", c), + Designator::Anonymous(_) => "anonymous".to_string(), }, Name::Selected(prefix, suffix) => { let suffix_name = match &suffix.item.item { Designator::Identifier(sym) => sym.name_utf8(), _ => "suffix".to_string(), }; - format!("{}.{}", expr_to_string(&Expression::Name(Box::new(prefix.item.clone()))), suffix_name) - }, + format!( + "{}.{}", + expr_to_string(&Expression::Name(Box::new( + prefix.item.clone() + ))), + suffix_name + ) + } Name::SelectedAll(prefix) => { - format!("{}.all", expr_to_string(&Expression::Name(Box::new(prefix.item.clone())))) - }, + format!( + "{}.all", + expr_to_string(&Expression::Name(Box::new( + prefix.item.clone() + ))) + ) + } Name::CallOrIndexed(fcall) => { // For function calls like get_eth_hdr_bits(x) - format!("{}", expr_to_string(&Expression::Name(Box::new(fcall.name.item.clone())))) - }, + format!( + "{}", + expr_to_string(&Expression::Name(Box::new( + fcall.name.item.clone() + ))) + ) + } _ => "complex_name".to_string(), } } diff --git a/vw-lib/src/visitor.rs b/vw-lib/src/visitor.rs index cbb1cd8..5a2f0bf 100644 --- a/vw-lib/src/visitor.rs +++ b/vw-lib/src/visitor.rs @@ -24,13 +24,12 @@ //! ``` use vhdl_lang::ast::{ - AnyDesignUnit, AnyPrimaryUnit, AnySecondaryUnit, - ArchitectureBody, Attribute, AttributeDeclaration, AttributeSpecification, + AnyDesignUnit, AnyPrimaryUnit, AnySecondaryUnit, ArchitectureBody, + Attribute, AttributeDeclaration, AttributeSpecification, ComponentDeclaration, ConfigurationDeclaration, ContextDeclaration, - Declaration, DesignFile, EntityDeclaration, - PackageBody, PackageDeclaration, PackageInstantiation, - SubprogramBody, SubprogramDeclaration, SubprogramInstantiation, - TypeDeclaration, + Declaration, DesignFile, EntityDeclaration, PackageBody, + PackageDeclaration, PackageInstantiation, SubprogramBody, + SubprogramDeclaration, SubprogramInstantiation, TypeDeclaration, }; /// Controls whether AST traversal should continue or stop. @@ -86,7 +85,10 @@ pub trait Visitor { } /// Called for package instantiations - fn visit_package_instance(&mut self, instance: &PackageInstantiation) -> VisitorResult { + fn visit_package_instance( + &mut self, + instance: &PackageInstantiation, + ) -> VisitorResult { VisitorResult::Continue } @@ -96,7 +98,10 @@ pub trait Visitor { } /// Called for configuration declarations - fn visit_configuration(&mut self, config: &ConfigurationDeclaration) -> VisitorResult { + fn visit_configuration( + &mut self, + config: &ConfigurationDeclaration, + ) -> VisitorResult { VisitorResult::Continue } @@ -119,32 +124,56 @@ pub trait Visitor { // ======================================================================== /// Called for each declaration (before dispatching to specific type) - fn visit_declaration(&mut self, decl: &Declaration, unit: &AnyDesignUnit) -> VisitorResult { + fn visit_declaration( + &mut self, + decl: &Declaration, + unit: &AnyDesignUnit, + ) -> VisitorResult { VisitorResult::Continue } /// Called for type declarations - fn visit_type_declaration(&mut self, decl: &TypeDeclaration, unit: &AnyDesignUnit) -> VisitorResult { + fn visit_type_declaration( + &mut self, + decl: &TypeDeclaration, + unit: &AnyDesignUnit, + ) -> VisitorResult { VisitorResult::Continue } /// Called for component declarations - fn visit_component(&mut self, comp: &ComponentDeclaration, unit: &AnyDesignUnit) -> VisitorResult { + fn visit_component( + &mut self, + comp: &ComponentDeclaration, + unit: &AnyDesignUnit, + ) -> VisitorResult { VisitorResult::Continue } /// Called for subprogram declarations (function/procedure specs) - fn visit_subprogram_declaration(&mut self, decl: &SubprogramDeclaration, unit: &AnyDesignUnit) -> VisitorResult { + fn visit_subprogram_declaration( + &mut self, + decl: &SubprogramDeclaration, + unit: &AnyDesignUnit, + ) -> VisitorResult { VisitorResult::Continue } /// Called for subprogram bodies (function/procedure implementations) - fn visit_subprogram_body(&mut self, body: &SubprogramBody, unit: &AnyDesignUnit) -> VisitorResult { + fn visit_subprogram_body( + &mut self, + body: &SubprogramBody, + unit: &AnyDesignUnit, + ) -> VisitorResult { VisitorResult::Continue } /// Called for subprogram instantiations - fn visit_subprogram_instantiation(&mut self, inst: &SubprogramInstantiation, unit: &AnyDesignUnit) -> VisitorResult { + fn visit_subprogram_instantiation( + &mut self, + inst: &SubprogramInstantiation, + unit: &AnyDesignUnit, + ) -> VisitorResult { VisitorResult::Continue } @@ -153,18 +182,29 @@ pub trait Visitor { // ------------------------------------------------------------------------ /// Called for attribute declarations (attribute X : type) - fn visit_attribute_declaration(&mut self, decl: &AttributeDeclaration, unit: &AnyDesignUnit) -> VisitorResult { + fn visit_attribute_declaration( + &mut self, + decl: &AttributeDeclaration, + unit: &AnyDesignUnit, + ) -> VisitorResult { VisitorResult::Continue } /// Called for attribute specifications (attribute X of Y : class is value) - fn visit_attribute_specification(&mut self, spec: &AttributeSpecification, unit: &AnyDesignUnit) -> VisitorResult { + fn visit_attribute_specification( + &mut self, + spec: &AttributeSpecification, + unit: &AnyDesignUnit, + ) -> VisitorResult { VisitorResult::Continue } } /// Walk a design file, calling visitor methods for each node. -pub fn walk_design_file(visitor: &mut V, file: &DesignFile) -> VisitorResult { +pub fn walk_design_file( + visitor: &mut V, + file: &DesignFile, +) -> VisitorResult { if !visitor.visit_design_file(file).should_continue() { return VisitorResult::Stop; } @@ -179,19 +219,30 @@ pub fn walk_design_file(visitor: &mut V, file: &DesignFile) -> Visit } /// Walk a design unit, calling visitor methods for each node. -pub fn walk_design_unit(visitor: &mut V, unit: &AnyDesignUnit) -> VisitorResult { +pub fn walk_design_unit( + visitor: &mut V, + unit: &AnyDesignUnit, +) -> VisitorResult { if !visitor.visit_design_unit(unit).should_continue() { return VisitorResult::Stop; } match unit { - AnyDesignUnit::Primary(primary) => walk_primary_unit(visitor, primary, unit), - AnyDesignUnit::Secondary(secondary) => walk_secondary_unit(visitor, secondary, unit), + AnyDesignUnit::Primary(primary) => { + walk_primary_unit(visitor, primary, unit) + } + AnyDesignUnit::Secondary(secondary) => { + walk_secondary_unit(visitor, secondary, unit) + } } } /// Walk a primary unit. -fn walk_primary_unit(visitor: &mut V, unit: &AnyPrimaryUnit, design_unit: &AnyDesignUnit) -> VisitorResult { +fn walk_primary_unit( + visitor: &mut V, + unit: &AnyPrimaryUnit, + design_unit: &AnyDesignUnit, +) -> VisitorResult { match unit { AnyPrimaryUnit::Entity(entity) => { if !visitor.visit_entity(entity).should_continue() { @@ -208,9 +259,7 @@ fn walk_primary_unit(visitor: &mut V, unit: &AnyPrimaryUnit, design_ AnyPrimaryUnit::PackageInstance(instance) => { visitor.visit_package_instance(instance) } - AnyPrimaryUnit::Context(context) => { - visitor.visit_context(context) - } + AnyPrimaryUnit::Context(context) => visitor.visit_context(context), AnyPrimaryUnit::Configuration(config) => { visitor.visit_configuration(config) } @@ -218,7 +267,11 @@ fn walk_primary_unit(visitor: &mut V, unit: &AnyPrimaryUnit, design_ } /// Walk a secondary unit. -fn walk_secondary_unit(visitor: &mut V, unit: &AnySecondaryUnit, design_unit: &AnyDesignUnit) -> VisitorResult { +fn walk_secondary_unit( + visitor: &mut V, + unit: &AnySecondaryUnit, + design_unit: &AnyDesignUnit, +) -> VisitorResult { match unit { AnySecondaryUnit::Architecture(arch) => { if !visitor.visit_architecture(arch).should_continue() { @@ -250,7 +303,11 @@ fn walk_declarations( } /// Walk a single declaration. -fn walk_declaration(visitor: &mut V, decl: &Declaration, unit: &AnyDesignUnit) -> VisitorResult { +fn walk_declaration( + visitor: &mut V, + decl: &Declaration, + unit: &AnyDesignUnit, +) -> VisitorResult { // First call the generic declaration visitor if !visitor.visit_declaration(decl, unit).should_continue() { return VisitorResult::Stop; @@ -261,19 +318,15 @@ fn walk_declaration(visitor: &mut V, decl: &Declaration, unit: &AnyD Declaration::Type(type_decl) => { visitor.visit_type_declaration(type_decl, unit) } - Declaration::Component(comp) => { - visitor.visit_component(comp, unit) - } - Declaration::Attribute(attr) => { - match attr { - Attribute::Declaration(decl) => { - visitor.visit_attribute_declaration(decl, unit) - } - Attribute::Specification(spec) => { - visitor.visit_attribute_specification(spec, unit) - } + Declaration::Component(comp) => visitor.visit_component(comp, unit), + Declaration::Attribute(attr) => match attr { + Attribute::Declaration(decl) => { + visitor.visit_attribute_declaration(decl, unit) } - } + Attribute::Specification(spec) => { + visitor.visit_attribute_specification(spec, unit) + } + }, Declaration::SubprogramDeclaration(decl) => { visitor.visit_subprogram_declaration(decl, unit) } @@ -325,12 +378,20 @@ mod tests { VisitorResult::Continue } - fn visit_type_declaration(&mut self, _: &TypeDeclaration, _: &AnyDesignUnit) -> VisitorResult { + fn visit_type_declaration( + &mut self, + _: &TypeDeclaration, + _: &AnyDesignUnit, + ) -> VisitorResult { self.types += 1; VisitorResult::Continue } - fn visit_attribute_specification(&mut self, _: &AttributeSpecification, _: &AnyDesignUnit) -> VisitorResult { + fn visit_attribute_specification( + &mut self, + _: &AttributeSpecification, + _: &AnyDesignUnit, + ) -> VisitorResult { self.attr_specs += 1; VisitorResult::Continue } From 8373a0d6c924213d24125988033e5e624e9c5e7b Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Tue, 17 Feb 2026 14:02:48 -0800 Subject: [PATCH 11/22] Allow anodize to specify the output directory --- vw-cli/src/main.rs | 11 +++++++++-- vw-lib/src/lib.rs | 5 ++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/vw-cli/src/main.rs b/vw-cli/src/main.rs index b6ebc99..ce114b6 100644 --- a/vw-cli/src/main.rs +++ b/vw-cli/src/main.rs @@ -131,6 +131,13 @@ enum Commands { Anodize { #[arg(long, help = "VHDL standard", default_value_t = CliVhdlStandard::Vhdl2019)] std: CliVhdlStandard, + #[arg( + long, + short, + help = "Output directory for generated Rust structs", + default_value = "bench/test_utils/src" + )] + output: String, }, } @@ -431,9 +438,9 @@ async fn main() { process::exit(1); } } - Commands::Anodize { std } => { + Commands::Anodize { std, output } => { println!("Generating Rust structs from VHDL records..."); - match anodize_only(&cwd, std.into()).await { + match anodize_only(&cwd, std.into(), &output).await { Ok(()) => { println!( "{} Generated Rust structs successfully!", diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index 12fed4f..b541934 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -47,9 +47,7 @@ pub mod nvc_helpers; pub mod vhdl_printer; pub mod visitor; -// TODO: make this a flag const BUILD_DIR: &str = "vw_build"; -const RUST_GEN_DIR: &str = "bench/test_utils/src"; // ============================================================================ // Error Types @@ -1027,6 +1025,7 @@ fn parse_entities(content: &str) -> Result> { pub async fn anodize_only( workspace_dir: &Utf8Path, vhdl_std: VhdlStandard, + rust_out_dir: &str, ) -> Result<()> { let vhdl_ls_config = load_existing_vhdl_ls_config(workspace_dir)?; let mut processor = RecordProcessor::new(vhdl_std); @@ -1081,7 +1080,7 @@ pub async fn anodize_only( &files, "generate".to_string(), BUILD_DIR.to_string(), - RUST_GEN_DIR.to_string(), + rust_out_dir.to_string(), ) .await?; From ef4d357f51c85648a4725ef5d9639230db278d21 Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Wed, 18 Feb 2026 14:39:36 -0800 Subject: [PATCH 12/22] Paying my dues to clippy --- vw-lib/src/anodizer.rs | 36 +++++++------- vw-lib/src/lib.rs | 36 +++++--------- vw-lib/src/mapping.rs | 17 +++---- vw-lib/src/nvc_helpers.rs | 28 +++++------ vw-lib/src/vhdl_printer.rs | 10 ++-- vw-lib/src/visitor.rs | 98 +++++++++++++++++++------------------- 6 files changed, 105 insertions(+), 120 deletions(-) diff --git a/vw-lib/src/anodizer.rs b/vw-lib/src/anodizer.rs index 54440c0..331cb4c 100644 --- a/vw-lib/src/anodizer.rs +++ b/vw-lib/src/anodizer.rs @@ -71,7 +71,7 @@ impl ResolvedRecord { pub async fn anodize_records( processor: &RecordProcessor, - referenced_files: &Vec, + referenced_files: &[String], generate_dir: String, build_dir: String, rust_out_dir: String, @@ -189,9 +189,10 @@ pub async fn anodize_records( }); } } else if field.subtype_name.eq_ignore_ascii_case("std_logic") { - let mut range = ResolvedRange::default(); - range.left = Some(0); - range.right = Some(0); + let range = ResolvedRange { + left: Some(0), + right: Some(0), + }; record_resolution.fields.push(ResolvedField::new( &field.name, &field.subtype_name, @@ -254,7 +255,7 @@ pub async fn anodize_records( let packages_needed: Vec = packages_set.iter().cloned().collect(); // alright, we've collected all the expressions that need resolving...create a testbench - let exprs = expr_to_resolve.keys().cloned().collect(); + let exprs: Vec = expr_to_resolve.keys().cloned().collect(); let testbench = create_testbench(&exprs, &packages_needed); let generate_path = format!("{}/{}", build_dir, generate_dir); @@ -276,7 +277,7 @@ pub async fn anodize_records( let (std_out_analysis, std_err_analysis) = run_nvc_analysis( VhdlStandard::Vhdl2019, &build_dir, - &"generated".to_string(), + "generated", &tb_files, true, ) @@ -292,8 +293,8 @@ pub async fn anodize_records( let (stdout_elab, stderr_elab) = run_nvc_elab( VhdlStandard::Vhdl2019, &build_dir, - &"generated".to_string(), - &"constraint_evaluator".to_string(), + "generated", + "constraint_evaluator", true, ) .await? @@ -309,7 +310,7 @@ pub async fn anodize_records( let (stdout_sim, stderr_sim) = run_nvc_sim( VhdlStandard::Vhdl2019, &build_dir, - &"generated".to_string(), + "generated", &"constraint_evaluator".to_string(), None, &Vec::new(), @@ -341,7 +342,7 @@ pub async fn anodize_records( } fn generate_rust_structs( - resolved_recs: &Vec, + resolved_recs: &[ResolvedRecord], ) -> Result { let structs: Result, VwError> = resolved_recs.iter().map(|record| { let struct_name = format_ident!("{}", record.name); @@ -373,7 +374,7 @@ fn generate_rust_structs( let constructor_inners = record.fields.iter().map(|field| { let field_name = format_ident!("{}", field.name); - if let Some(_) = &field.bit_width { + if field.bit_width.is_some() { quote!{ #field_name : BitSet::default() } @@ -420,10 +421,10 @@ fn generate_rust_structs( } fn process_sim_output( - expr_keys: &Vec, + expr_keys: &[String], exprs_to_resolve: HashMap>, - records: &mut Vec, - sim_out: &Vec, + records: &mut [ResolvedRecord], + sim_out: &[u8], ) -> Result<(), VwError> { let stdout_str = String::from_utf8_lossy(sim_out); @@ -487,8 +488,8 @@ fn process_sim_output( } fn create_testbench( - exprs_to_resolve: &Vec, - packages_needed: &Vec, + exprs_to_resolve: &[String], + packages_needed: &[String], ) -> String { let mut testbench = Vec::new(); @@ -527,8 +528,9 @@ end architecture behavior; testbench.join("") } +#[allow(clippy::vec_init_then_push)] /// Generate VHDL package use statements for a testbench -pub fn generate_testbench_imports(packages_needed: &Vec) -> String { +pub fn generate_testbench_imports(packages_needed: &[String]) -> String { let mut imports = Vec::new(); imports.push(String::from("-- Required packages\n")); diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index b541934..3f44f30 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -156,9 +156,9 @@ pub enum VhdlStandard { Vhdl2019, } -impl Into for VhdlStandard { - fn into(self) -> VHDLStandard { - match self { +impl From for VHDLStandard { + fn from(val: VhdlStandard) -> Self { + match val { VhdlStandard::Vhdl2008 => VHDLStandard::VHDL2008, VhdlStandard::Vhdl2019 => VHDLStandard::VHDL2019, } @@ -1128,7 +1128,7 @@ async fn analyze_ext_libraries( run_nvc_analysis( vhdl_std, - &BUILD_DIR.to_string(), + BUILD_DIR, &nvc_lib_name, &file_strings, false, @@ -1257,23 +1257,9 @@ pub async fn run_testbench( files.push(testbench_file.to_string_lossy().to_string()); - run_nvc_analysis( - vhdl_std, - &BUILD_DIR.to_string(), - &"work".to_string(), - &files, - false, - ) - .await?; + run_nvc_analysis(vhdl_std, BUILD_DIR, "work", &files, false).await?; - run_nvc_elab( - vhdl_std, - &BUILD_DIR.to_string(), - &"work".to_string(), - &testbench_name, - false, - ) - .await?; + run_nvc_elab(vhdl_std, BUILD_DIR, "work", &testbench_name, false).await?; // Build Rust library if requested let rust_lib_path = if build_rust { @@ -1290,8 +1276,8 @@ pub async fn run_testbench( // Run NVC simulation run_nvc_sim( vhdl_std, - &BUILD_DIR.to_string(), - &"work".to_string(), + BUILD_DIR, + "work", &testbench_name, rust_lib_path, &runtime_flags.to_vec(), @@ -1351,7 +1337,7 @@ fn get_package_imports(content: &str) -> Result> { let use_work_re = regex::Regex::new(use_work_pattern)?; let mut imports = Vec::new(); - for captures in use_work_re.captures_iter(&content) { + for captures in use_work_re.captures_iter(content) { if let Some(package_name) = captures.get(1) { imports.push(package_name.as_str().to_string()); } @@ -1380,7 +1366,7 @@ fn file_provides_symbol( fn analyze_file( processor: &mut RecordProcessor, - file: &PathBuf, + file: &Path, ) -> Result> { let parser = VHDLParser::new(processor.vhdl_std.into()); let mut diagnostics = Vec::new(); @@ -1435,7 +1421,7 @@ fn sort_files_by_dependencies( let entry = processor .file_info .entry(file.to_string_lossy().to_string()) - .or_insert_with(|| FileData::new()); + .or_default(); entry.add_defined_pkg(&name); // Use cache to get package imports only diff --git a/vw-lib/src/mapping.rs b/vw-lib/src/mapping.rs index ba530c2..fea6eb6 100644 --- a/vw-lib/src/mapping.rs +++ b/vw-lib/src/mapping.rs @@ -48,6 +48,7 @@ pub struct RecordData { fields: Vec, } +#[derive(Debug, Default)] pub struct FileData { defined_pkgs: Vec, imported_pkgs: Vec, @@ -77,7 +78,7 @@ impl FileData { impl RecordData { pub fn new(containing_pkg: Option, name: &str) -> Self { Self { - containing_pkg: containing_pkg, + containing_pkg, name: String::from(name), fields: Vec::new(), } @@ -183,6 +184,7 @@ impl Visitor for VwSymbolFinder { VisitorResult::Continue } + #[allow(clippy::collapsible_match)] fn visit_type_declaration( &mut self, decl: &TypeDeclaration, @@ -252,12 +254,11 @@ fn get_fields(elements: &Vec) -> Vec { } .unwrap(); - let element_constraint = - if let Some(constraint) = &element.subtype.constraint { - Some(get_range_constraint(&constraint.item)) - } else { - None - }; + let element_constraint = element + .subtype + .constraint + .as_ref() + .map(|constraint| get_range_constraint(&constraint.item)); fields.push(FieldData { name: element_name, @@ -273,7 +274,7 @@ fn get_range_constraint(constraint: &SubtypeConstraint) -> RangeConstraint { if let SubtypeConstraint::Array(array_range, _) = constraint { if let DiscreteRange::Range(discrete_range) = &array_range[0].item { if let Range::Range(constraint) = discrete_range { - return constraint.clone(); + constraint.clone() } else { panic!("We don't handle other range types") } diff --git a/vw-lib/src/nvc_helpers.rs b/vw-lib/src/nvc_helpers.rs index fee0230..97a3f63 100644 --- a/vw-lib/src/nvc_helpers.rs +++ b/vw-lib/src/nvc_helpers.rs @@ -9,17 +9,17 @@ use std::{ fn get_base_nvc_cmd_args( std: VhdlStandard, - build_dir: &String, - lib_name: &String, + build_dir: &str, + lib_name: &str, ) -> Vec { - let lib_dir = build_dir.clone() + "/" + lib_name; + let lib_dir = build_dir.to_owned() + "/" + lib_name; let args = vec![ format!("--std={std}"), format!("--work={lib_dir}"), "-M".to_string(), "256m".to_string(), "-L".to_string(), - build_dir.clone(), + build_dir.to_owned(), ]; args } @@ -66,8 +66,8 @@ async fn run_cmd( pub async fn run_nvc_analysis( std: VhdlStandard, - build_dir: &String, - lib_name: &String, + build_dir: &str, + lib_name: &str, referenced_files: &Vec, capture_output: bool, ) -> Result, Vec)>, VwError> { @@ -86,7 +86,7 @@ pub async fn run_nvc_analysis( std::io::stdout().write_all(&output.stdout)?; std::io::stderr().write_all(&output.stderr)?; return Err(VwError::NvcAnalysis { - library: lib_name.clone(), + library: lib_name.to_owned(), command: cmd_str, }); } @@ -97,7 +97,7 @@ pub async fn run_nvc_analysis( if !status.success() { let cmd_str = format!("nvc {}", args.join(" ")); return Err(VwError::NvcAnalysis { - library: lib_name.clone(), + library: lib_name.to_owned(), command: cmd_str, }); } @@ -107,14 +107,14 @@ pub async fn run_nvc_analysis( pub async fn run_nvc_elab( std: VhdlStandard, - build_dir: &String, - lib_name: &String, - testbench_name: &String, + build_dir: &str, + lib_name: &str, + testbench_name: &str, capture_output: bool, ) -> Result, Vec)>, VwError> { let mut args = get_base_nvc_cmd_args(std, build_dir, lib_name); args.push("-e".to_string()); - args.push(testbench_name.clone()); + args.push(testbench_name.to_owned()); if capture_output { let output = run_cmd_w_output(&args, None).await?; @@ -140,8 +140,8 @@ pub async fn run_nvc_elab( pub async fn run_nvc_sim( std: VhdlStandard, - build_dir: &String, - lib_name: &String, + build_dir: &str, + lib_name: &str, testbench_name: &String, rust_lib_path: Option, runtime_flags: &Vec, diff --git a/vw-lib/src/vhdl_printer.rs b/vw-lib/src/vhdl_printer.rs index 9c2230d..7968cd3 100644 --- a/vw-lib/src/vhdl_printer.rs +++ b/vw-lib/src/vhdl_printer.rs @@ -10,7 +10,7 @@ use vhdl_lang::ast::{Designator, Expression, Literal, Name, Operator}; pub fn expr_to_string(expr: &Expression) -> String { match expr { Expression::Literal(lit) => literal_to_string(lit), - Expression::Name(name) => name_to_string(&**name), + Expression::Name(name) => name_to_string(name), Expression::Binary(op, left, right) => { // Match on the binary operator enum directly - inline to avoid exposing private types let op_str = match &op.item.item { @@ -115,12 +115,8 @@ fn name_to_string(name: &Name) -> String { } Name::CallOrIndexed(fcall) => { // For function calls like get_eth_hdr_bits(x) - format!( - "{}", - expr_to_string(&Expression::Name(Box::new( - fcall.name.item.clone() - ))) - ) + expr_to_string(&Expression::Name(Box::new(fcall.name.item.clone()))) + .to_string() } _ => "complex_name".to_string(), } diff --git a/vw-lib/src/visitor.rs b/vw-lib/src/visitor.rs index 5a2f0bf..149193d 100644 --- a/vw-lib/src/visitor.rs +++ b/vw-lib/src/visitor.rs @@ -347,53 +347,53 @@ fn walk_declaration( #[cfg(test)] mod tests { - use super::*; - - struct CountingVisitor { - entities: usize, - packages: usize, - types: usize, - attr_specs: usize, - } - - impl CountingVisitor { - fn new() -> Self { - Self { - entities: 0, - packages: 0, - types: 0, - attr_specs: 0, - } - } - } - - impl Visitor for CountingVisitor { - fn visit_entity(&mut self, _: &EntityDeclaration) -> VisitorResult { - self.entities += 1; - VisitorResult::Continue - } - - fn visit_package(&mut self, _: &PackageDeclaration) -> VisitorResult { - self.packages += 1; - VisitorResult::Continue - } - - fn visit_type_declaration( - &mut self, - _: &TypeDeclaration, - _: &AnyDesignUnit, - ) -> VisitorResult { - self.types += 1; - VisitorResult::Continue - } - - fn visit_attribute_specification( - &mut self, - _: &AttributeSpecification, - _: &AnyDesignUnit, - ) -> VisitorResult { - self.attr_specs += 1; - VisitorResult::Continue - } - } + //use super::*; + + //struct CountingVisitor { + // entities: usize, + // packages: usize, + // types: usize, + // attr_specs: usize, + //} + + //impl CountingVisitor { + // fn new() -> Self { + // Self { + // entities: 0, + // packages: 0, + // types: 0, + // attr_specs: 0, + // } + // } + //} + + //impl Visitor for CountingVisitor { + // fn visit_entity(&mut self, _: &EntityDeclaration) -> VisitorResult { + // self.entities += 1; + // VisitorResult::Continue + // } + + // fn visit_package(&mut self, _: &PackageDeclaration) -> VisitorResult { + // self.packages += 1; + // VisitorResult::Continue + // } + + // fn visit_type_declaration( + // &mut self, + // _: &TypeDeclaration, + // _: &AnyDesignUnit, + // ) -> VisitorResult { + // self.types += 1; + // VisitorResult::Continue + // } + + // fn visit_attribute_specification( + // &mut self, + // _: &AttributeSpecification, + // _: &AnyDesignUnit, + // ) -> VisitorResult { + // self.attr_specs += 1; + // VisitorResult::Continue + // } + //} } From 75522db82f880447f0c8e4196df77b234088da75 Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Fri, 20 Feb 2026 17:35:31 -0800 Subject: [PATCH 13/22] Refactor for codegen repo --- vw-cli/src/main.rs | 40 +-- vw-lib/src/anodizer.rs | 550 ------------------------------------- vw-lib/src/lib.rs | 299 +++++++------------- vw-lib/src/vhdl_printer.rs | 123 --------- 4 files changed, 105 insertions(+), 907 deletions(-) delete mode 100644 vw-lib/src/anodizer.rs delete mode 100644 vw-lib/src/vhdl_printer.rs diff --git a/vw-cli/src/main.rs b/vw-cli/src/main.rs index ce114b6..447bf32 100644 --- a/vw-cli/src/main.rs +++ b/vw-cli/src/main.rs @@ -10,11 +10,11 @@ use std::fmt; use std::process; use vw_lib::{ - add_dependency_with_token, anodize_only, clear_cache, - extract_hostname_from_repo_url, generate_deps_tcl, - get_access_credentials_from_netrc, init_workspace, list_dependencies, - list_testbenches, load_workspace_config, remove_dependency, run_testbench, - update_workspace_with_token, Credentials, VersionInfo, VhdlStandard, + add_dependency_with_token, clear_cache, extract_hostname_from_repo_url, + generate_deps_tcl, get_access_credentials_from_netrc, init_workspace, + list_dependencies, list_testbenches, load_workspace_config, + remove_dependency, run_testbench, update_workspace_with_token, Credentials, + VersionInfo, VhdlStandard, }; #[derive(Clone, Copy, Debug, ValueEnum)] @@ -124,21 +124,6 @@ enum Commands { )] build_rust: bool, }, - #[command( - name = "anodize", - about = "Generate Rust structs from VHDL records tagged with serialize_rust attribute" - )] - Anodize { - #[arg(long, help = "VHDL standard", default_value_t = CliVhdlStandard::Vhdl2019)] - std: CliVhdlStandard, - #[arg( - long, - short, - help = "Output directory for generated Rust structs", - default_value = "bench/test_utils/src" - )] - output: String, - }, } /// Helper function to get access credentials for a repository URL from netrc if available @@ -438,20 +423,5 @@ async fn main() { process::exit(1); } } - Commands::Anodize { std, output } => { - println!("Generating Rust structs from VHDL records..."); - match anodize_only(&cwd, std.into(), &output).await { - Ok(()) => { - println!( - "{} Generated Rust structs successfully!", - "✓".bright_green() - ); - } - Err(e) => { - eprintln!("{} {e}", "error:".bright_red()); - process::exit(1); - } - } - } } } diff --git a/vw-lib/src/anodizer.rs b/vw-lib/src/anodizer.rs deleted file mode 100644 index 331cb4c..0000000 --- a/vw-lib/src/anodizer.rs +++ /dev/null @@ -1,550 +0,0 @@ -use std::{collections::HashMap, collections::HashSet, fs}; - -use crate::mapping::VwSymbol; -use crate::nvc_helpers::run_nvc_analysis; -use crate::vhdl_printer::expr_to_string; -use crate::VwError; -use crate::{ - nvc_helpers::{run_nvc_elab, run_nvc_sim}, - RecordProcessor, VhdlStandard, -}; - -use proc_macro2::TokenStream; -use quote::{format_ident, quote}; - -use vhdl_lang::ast::{AbstractLiteral, Expression, Literal}; - -enum Side { - Left, - Right, -} - -/// struct for identifying a particular -/// range constraint within a field with a struct -struct ConstraintID { - record_index: usize, - field_index: usize, - side: Option, // None for enum size (single value), Some(Left/Right) for range constraints -} - -#[derive(Debug, Clone, Default)] -pub struct ResolvedRange { - left: Option, - right: Option, -} - -#[derive(Debug, Clone)] -pub struct ResolvedField { - pub name: String, - pub bit_width: Option, - pub subtype_name: String, -} -impl ResolvedField { - pub fn new( - name: &str, - subtype: &str, - bitwidth: Option, - ) -> Self { - ResolvedField { - name: name.to_string(), - bit_width: bitwidth, - subtype_name: subtype.to_string(), - } - } -} - -// Resolved record with all field widths computed -#[derive(Debug, Clone)] -pub struct ResolvedRecord { - pub name: String, - pub fields: Vec, -} - -impl ResolvedRecord { - pub fn new(name: &str) -> Self { - ResolvedRecord { - name: name.to_string(), - fields: Vec::new(), - } - } -} - -pub async fn anodize_records( - processor: &RecordProcessor, - referenced_files: &[String], - generate_dir: String, - build_dir: String, - rust_out_dir: String, -) -> Result<(), VwError> { - let mut tagged_records = Vec::new(); - let mut packages_set = HashSet::new(); - for name in &processor.tagged_names { - match processor.symbols.get(name) { - Some(VwSymbol::Record(record)) => { - tagged_records.push(record); - let pkg_name = - record.get_pkg_name().ok_or_else(|| VwError::CodeGen { - message: format!( - "Serialization not supported for \ - records not in packages. Record : {:}", - record.get_name() - ), - })?; - packages_set.insert(pkg_name.clone()); - - // get the imported packages for the file - let record_filename = processor.symbol_to_file.get(name) - .ok_or( - VwError::CodeGen { - message: format!("Somehow couldn't find the containing file for record {:}", name) - })?; - let file_data = processor - .file_info - .get(record_filename) - .ok_or(VwError::CodeGen { - message: format!( - "Somehow couldn't find information for file {:}", - record_filename - ), - })?; - let imports = file_data.get_imported_pkgs(); - packages_set.extend(imports.iter().cloned()); - } - Some(_) => { - return Err(VwError::CodeGen { - message: format!("Tagged type {:} is not a record", name), - }) - } - None => { - return Err(VwError::CodeGen { - message: format!( - "Tagged type with name {:} not found", - name - ), - }) - } - } - } - - let mut expr_to_resolve: HashMap> = - HashMap::new(); - let mut process_records = Vec::new(); - - // ok we got tagged records. time to collect either their subtypes or their constraints - for (i, record) in tagged_records.iter().enumerate() { - let mut record_resolution = ResolvedRecord::new(record.get_name()); - for (j, field) in record.get_fields().iter().enumerate() { - // possibly FIXME: work for non-ASCII strings? - if field.subtype_name.eq_ignore_ascii_case("std_logic_vector") { - let mut resolve_range = ResolvedRange::default(); - // ok we may need to resolve either the left or right range constraints - if let Some(range) = &field.constraint { - // check the left constraint - // is it possible to immediately derive a value? - if let Expression::Literal(Literal::AbstractLiteral( - AbstractLiteral::Integer(value), - )) = range.left_expr.item - { - // unwrap here because we just put the range in the field - resolve_range.left = Some(value as usize); - } - // ok we have to have VHDL evaluate it - else { - let expr_str = expr_to_string(&range.left_expr.item); - expr_to_resolve.entry(expr_str).or_default().push( - ConstraintID { - record_index: i, - field_index: j, - side: Some(Side::Left), - }, - ); - } - // check the right constraint - if let Expression::Literal(Literal::AbstractLiteral( - AbstractLiteral::Integer(value), - )) = range.right_expr.item - { - resolve_range.right = Some(value as usize); - } else { - let expr_str = expr_to_string(&range.right_expr.item); - expr_to_resolve.entry(expr_str).or_default().push( - ConstraintID { - record_index: i, - field_index: j, - side: Some(Side::Right), - }, - ); - } - let resolve_field = ResolvedField::new( - &field.name, - &field.subtype_name, - Some(resolve_range), - ); - record_resolution.fields.push(resolve_field); - } else { - return Err(VwError::CodeGen { - message: format!("All fields in serialized structs must be constrained. \ - Found unconstrained field {:} in record {:}", field.name, record.get_name() - ) - }); - } - } else if field.subtype_name.eq_ignore_ascii_case("std_logic") { - let range = ResolvedRange { - left: Some(0), - right: Some(0), - }; - record_resolution.fields.push(ResolvedField::new( - &field.name, - &field.subtype_name, - Some(range), - )); - } - // Check if subtype is an enum or a record - else { - match processor.symbols.get(&field.subtype_name) { - Some(VwSymbol::Enum(enum_data)) => { - // Check if enum has custom encoding - if enum_data.has_custom_encoding { - return Err(VwError::CodeGen { - message: format!("Enum {:} has custom encoding and cannot be serialized", - field.subtype_name) - }); - } - // Enum field - need to resolve its size via testbench - let expr_str = format!( - "{}'pos({}'high)", - field.subtype_name, field.subtype_name - ); - expr_to_resolve.entry(expr_str).or_default().push( - ConstraintID { - record_index: i, - field_index: j, - side: None, // Enum size, not left/right range - }, - ); - record_resolution.fields.push(ResolvedField::new( - &field.name, - &field.subtype_name, - Some(ResolvedRange::default()), - )); - } - Some(VwSymbol::Record(_)) => { - // Nested record - must be tagged for serialization - if !processor.tagged_names.contains(&field.subtype_name) - { - return Err(VwError::CodeGen { - message: format!("Subtype {:} not tagged for serialization. Please tag it", field.subtype_name) - }); - } - record_resolution.fields.push(ResolvedField::new( - &field.name, - &field.subtype_name, - None, - )); - } - _ => { - return Err(VwError::CodeGen { - message: format!("Subtype {:} is not a known record or enum type", field.subtype_name) - }); - } - } - } - } - process_records.push(record_resolution); - } - - let packages_needed: Vec = packages_set.iter().cloned().collect(); - // alright, we've collected all the expressions that need resolving...create a testbench - let exprs: Vec = expr_to_resolve.keys().cloned().collect(); - let testbench = create_testbench(&exprs, &packages_needed); - - let generate_path = format!("{}/{}", build_dir, generate_dir); - - fs::create_dir_all(generate_path.clone())?; - fs::write(format!("{}/constraint_tb.vhd", generate_path), &testbench)?; - - let tb_files: Vec = referenced_files - .iter() - .cloned() - .chain(std::iter::once(format!( - "{}/constraint_tb.vhd", - generate_path - ))) - .collect(); - - // ok and now we have to run the testbench - // analyze the testbench - let (std_out_analysis, std_err_analysis) = run_nvc_analysis( - VhdlStandard::Vhdl2019, - &build_dir, - "generated", - &tb_files, - true, - ) - .await? - .unwrap(); - - let stdout_a_path = format!("{}/analysis.out", generate_path); - let stderr_a_path = format!("{}/analysis.err", generate_path); - fs::write(stdout_a_path, &std_out_analysis)?; - fs::write(stderr_a_path, &std_err_analysis)?; - - //elaborate the testbench - let (stdout_elab, stderr_elab) = run_nvc_elab( - VhdlStandard::Vhdl2019, - &build_dir, - "generated", - "constraint_evaluator", - true, - ) - .await? - .unwrap(); - - let stdout_e_path = format!("{}/elab.out", generate_path); - let stderr_e_path = format!("{}/elab.err", generate_path); - fs::write(stdout_e_path, &stdout_elab)?; - fs::write(stderr_e_path, &stderr_elab)?; - - // run the testbench - - let (stdout_sim, stderr_sim) = run_nvc_sim( - VhdlStandard::Vhdl2019, - &build_dir, - "generated", - &"constraint_evaluator".to_string(), - None, - &Vec::new(), - true, - ) - .await? - .unwrap(); - - let stdout_sim_path = format!("{}/sim.out", generate_path); - let stderr_sim_path = format!("{}/sim.err", generate_path); - - fs::write(stdout_sim_path, &stdout_sim)?; - fs::write(stderr_sim_path, &stderr_sim)?; - - // process the sim output to resolve the expressions - process_sim_output( - &exprs, - expr_to_resolve, - &mut process_records, - &stdout_sim, - )?; - - // ok generate Rust code from the resolved records - let rust_content = generate_rust_structs(&process_records)?; - let rust_structs_file = format!("{}/generated_structs.rs", rust_out_dir); - fs::write(rust_structs_file, rust_content)?; - - Ok(()) -} - -fn generate_rust_structs( - resolved_recs: &[ResolvedRecord], -) -> Result { - let structs: Result, VwError> = resolved_recs.iter().map(|record| { - let struct_name = format_ident!("{}", record.name); - - let fields: Result, VwError> = record.fields.iter().map(|field| { - let field_name = format_ident!("{}", field.name); - - if let Some(range) = &field.bit_width { - let left = range.left.ok_or_else(|| VwError::CodeGen { - message: format!("Somehow didn't resolve left expression for field {:}", field.name) - })?; - let right = range.right.ok_or_else(|| VwError::CodeGen { - message: format!("Somehow didn't resolve right expression for field {:}", field.name) - })?; - let bitwidth = left - right + 1; - - Ok(quote! { - pub #field_name: BitSet<#bitwidth> - }) - } else { - let subtype = format_ident!("{}", field.subtype_name); - Ok(quote! { - pub #field_name: #subtype - }) - } - }).collect(); - - let fields = fields?; - - let constructor_inners = record.fields.iter().map(|field| { - let field_name = format_ident!("{}", field.name); - if field.bit_width.is_some() { - quote!{ - #field_name : BitSet::default() - } - } - else { - let subtype = format_ident!("{}", field.subtype_name); - quote! { - #field_name : #subtype::new() - } - } - }); - - - Ok(quote! { - #[derive(Debug, Clone, BitStructSerial)] - pub struct #struct_name { - #(#fields),* - } - - impl #struct_name { - pub fn new() -> Self { - #struct_name { - #(#constructor_inners),* - } - } - } - }) - }).collect(); - - let structs = structs?; - - let output = quote! { - use bitfield_derive::BitStructSerial; - use bitfield_struct::{BitStructSerial, BitfieldError}; - use bitset::BitSet; - - #(#structs)* - }; - - let syntax_tree = syn::parse2(output).map_err(|e| VwError::CodeGen { - message: format!("Failed to parse generated code: {}", e), - })?; - Ok(prettyplease::unparse(&syntax_tree)) -} - -fn process_sim_output( - expr_keys: &[String], - exprs_to_resolve: HashMap>, - records: &mut [ResolvedRecord], - sim_out: &[u8], -) -> Result<(), VwError> { - let stdout_str = String::from_utf8_lossy(sim_out); - - for line in stdout_str.lines() { - let parts: Vec<&str> = line.splitn(2, ": ").collect(); - - let index: usize = parts[0] - .strip_prefix("EXPR_") - .unwrap() - .parse() - .map_err(|e| VwError::CodeGen { - message: format!( - "Somehow generated an unparseable simulation output : {:}.\ - Look at sim.out", - e - ), - })?; - let value: usize = parts[1].parse().map_err(|e| VwError::CodeGen { - message: format!("Expression couldn't be evaluated : {}", e), - })?; - - let key = &expr_keys[index]; - let constraint_ids = - exprs_to_resolve.get(key).ok_or_else(|| VwError::CodeGen { - message: format!( - "Somehow got expression {:} which doesn't exist", - key - ), - })?; - for id in constraint_ids { - let record = &mut (records[id.record_index]); - let field = &mut (record.fields[id.field_index]); - if let Some(bitfield) = &mut field.bit_width { - match id.side { - Some(Side::Left) => bitfield.left = Some(value), - Some(Side::Right) => bitfield.right = Some(value), - None => { - // Enum size - value is max position (0-indexed), so count = value + 1 - let count = value + 1; - let bits = if count <= 1 { - 1 - } else { - (count as f64).log2().ceil() as usize - }; - bitfield.left = Some(bits - 1); - bitfield.right = Some(0); - } - } - } else { - return Err(VwError::CodeGen { - message: format!( - "Somehow tried to generate an expression for \ - field {:} in record {:} which has no expression", - field.name, record.name - ), - }); - } - } - } - Ok(()) -} - -fn create_testbench( - exprs_to_resolve: &[String], - packages_needed: &[String], -) -> String { - let mut testbench = Vec::new(); - - testbench.push(generate_testbench_imports(packages_needed)); - - testbench.push(String::from( - "entity constraint_evaluator is -end entity constraint_evaluator; - -architecture behavior of constraint_evaluator is -begin - process - variable l : line; - begin - wait for 0ns; -", - )); - - for (i, expr) in exprs_to_resolve.iter().enumerate() { - testbench.push(format!( - " - write(l, string'(\"EXPR_{}: \"));\n - write(l, integer'image({}));\n - writeline(OUTPUT, l);\n", - i, expr - )); - } - - testbench.push(String::from( - " wait; - end process; -end architecture behavior; -", - )); - - testbench.join("") -} - -#[allow(clippy::vec_init_then_push)] -/// Generate VHDL package use statements for a testbench -pub fn generate_testbench_imports(packages_needed: &[String]) -> String { - let mut imports = Vec::new(); - - imports.push(String::from("-- Required packages\n")); - imports.push(String::from("library ieee;\n")); - imports.push(String::from("use ieee.std_logic_1164.all;\n")); - imports.push(String::from("use ieee.numeric_std.all;\n")); - imports.push(String::from("\n")); - - imports.push(String::from("library std;\n")); - imports.push(String::from("use std.textio.all;\n")); - - for package_name in packages_needed { - imports.push(format!("use work.{}.all;\n", package_name)); - } - - imports.join("") -} diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index 3f44f30..6d525fa 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -36,15 +36,12 @@ use camino::{Utf8Path, Utf8PathBuf}; use serde::{Deserialize, Serialize}; use vhdl_lang::{VHDLParser, VHDLStandard}; -use crate::anodizer::anodize_records; use crate::mapping::{FileData, VwSymbol, VwSymbolFinder}; use crate::nvc_helpers::{run_nvc_analysis, run_nvc_elab, run_nvc_sim}; use crate::visitor::walk_design_file; -pub mod anodizer; pub mod mapping; pub mod nvc_helpers; -pub mod vhdl_printer; pub mod visitor; const BUILD_DIR: &str = "vw_build"; @@ -837,12 +834,12 @@ pub struct TestbenchInfo { } pub struct RecordProcessor { - vhdl_std: VhdlStandard, - symbols: HashMap, - symbol_to_file: HashMap, - tagged_names: HashSet, - file_info: HashMap, - target_attr: String, + pub vhdl_std: VhdlStandard, + pub symbols: HashMap, + pub symbol_to_file: HashMap, + pub tagged_names: HashSet, + pub file_info: HashMap, + pub target_attr: String, } const RECORD_PARSE_ATTRIBUTE: &str = "serialize_rust"; @@ -1022,72 +1019,7 @@ fn parse_entities(content: &str) -> Result> { Ok(entities) } -pub async fn anodize_only( - workspace_dir: &Utf8Path, - vhdl_std: VhdlStandard, - rust_out_dir: &str, -) -> Result<()> { - let vhdl_ls_config = load_existing_vhdl_ls_config(workspace_dir)?; - let mut processor = RecordProcessor::new(vhdl_std); - let mut cache = FileCache::new(); - - fs::create_dir_all(BUILD_DIR)?; - - analyze_ext_libraries( - &vhdl_ls_config, - &mut processor, - vhdl_std, - &mut cache, - ) - .await?; - - // alright...now we need to find packages and search them for records - let defaultlib_files = vhdl_ls_config - .libraries - .get("defaultlib") - .map(|lib| lib.files.clone()) - .unwrap_or_default(); - - let mut relevant_files = HashSet::new(); - - for file in &defaultlib_files { - // Use cache to check for package declarations - let provided = cache.get_provided_symbols(file)?; - // Check if file contains any package declarations - let has_package = - provided.iter().any(|s| matches!(s, VwSymbol::Package(_))); - if has_package { - relevant_files.insert(file.clone()); - // ok it is a package...figure out which files it brings in - let referenced_files = - find_referenced_files(file, &defaultlib_files, &mut cache)?; - relevant_files.extend(referenced_files.iter().cloned()); - } - } - - let mut files_vec: Vec = relevant_files.iter().cloned().collect(); - - // with all the relevant files, sort them and then anodize them - sort_files_by_dependencies(&mut processor, &mut files_vec, &mut cache)?; - - let files: Vec = files_vec - .iter() - .map(|s| s.to_string_lossy().to_string()) - .collect(); - - anodize_records( - &processor, - &files, - "generate".to_string(), - BUILD_DIR.to_string(), - rust_out_dir.to_string(), - ) - .await?; - - Ok(()) -} - -async fn analyze_ext_libraries( +pub async fn analyze_ext_libraries( vhdl_ls_config: &VhdlLsConfig, processor: &mut RecordProcessor, vhdl_std: VhdlStandard, @@ -1288,11 +1220,7 @@ pub async fn run_testbench( Ok(()) } -// ============================================================================ -// Internal Helper Functions -// ============================================================================ - -fn find_referenced_files( +pub fn find_referenced_files( testbench_file: &Path, available_files: &[PathBuf], cache: &mut FileCache, @@ -1331,6 +1259,98 @@ fn find_referenced_files( Ok(referenced_files) } +pub fn sort_files_by_dependencies( + processor: &mut RecordProcessor, + files: &mut Vec, + cache: &mut FileCache, +) -> Result<()> { + // Build dependency graph + let mut dependencies: HashMap> = HashMap::new(); + let mut all_symbols: HashMap = HashMap::new(); + + // First pass: collect all symbols provided by each file + for file in files.iter() { + let symbols = analyze_file(processor, file)?; + for symbol in symbols { + match symbol { + VwSymbol::Package(name) => { + all_symbols.insert(name.clone(), file.clone()); + let entry = processor + .file_info + .entry(file.to_string_lossy().to_string()) + .or_default(); + entry.add_defined_pkg(&name); + + // Use cache to get package imports only + let deps = cache.get_dependencies(file)?; + for dep in deps { + if let VwSymbol::Package(pkg_name) = dep { + entry.add_imported_pkg(pkg_name); + } + } + } + VwSymbol::Entity(name) => { + all_symbols.insert(name, file.clone()); + } + _ => {} + } + } + } + + // Second pass: find dependencies for each file + for file in files.iter() { + let deps = cache.get_dependencies(file)?.clone(); + let mut file_deps = Vec::new(); + + for dep in deps { + let dep_name = match &dep { + VwSymbol::Package(name) | VwSymbol::Entity(name) => name, + _ => continue, + }; + if let Some(provider_file) = all_symbols.get(dep_name) { + if provider_file != file { + file_deps.push(provider_file.clone()); + } + } + } + + dependencies.insert(file.clone(), file_deps); + } + + // Topological sort using Kahn's algorithm + let sorted = topological_sort(files.clone(), dependencies)?; + *files = sorted; + + Ok(()) +} + +pub fn load_existing_vhdl_ls_config( + workspace_dir: &Utf8Path, +) -> Result { + let config_path = workspace_dir.join("vhdl_ls.toml"); + if config_path.exists() { + let config_content = fs::read_to_string(&config_path).map_err(|e| { + VwError::FileSystem { + message: format!("Failed to read existing vhdl_ls.toml: {e}"), + } + })?; + + let config: VhdlLsConfig = toml::from_str(&config_content)?; + + Ok(config) + } else { + Ok(VhdlLsConfig { + standard: None, + libraries: HashMap::new(), + lint: None, + }) + } +} + +// ============================================================================ +// Internal Helper Functions +// ============================================================================ + fn get_package_imports(content: &str) -> Result> { // Find 'use work.package_name' statements let use_work_pattern = r"(?i)use\s+work\.(\w+)"; @@ -1402,102 +1422,6 @@ fn analyze_file( Ok(file_finder.get_symbols().clone()) } -fn sort_files_by_dependencies( - processor: &mut RecordProcessor, - files: &mut Vec, - cache: &mut FileCache, -) -> Result<()> { - // Build dependency graph - let mut dependencies: HashMap> = HashMap::new(); - let mut all_symbols: HashMap = HashMap::new(); - - // First pass: collect all symbols provided by each file - for file in files.iter() { - let symbols = analyze_file(processor, file)?; - for symbol in symbols { - match symbol { - VwSymbol::Package(name) => { - all_symbols.insert(name.clone(), file.clone()); - let entry = processor - .file_info - .entry(file.to_string_lossy().to_string()) - .or_default(); - entry.add_defined_pkg(&name); - - // Use cache to get package imports only - let deps = cache.get_dependencies(file)?; - for dep in deps { - if let VwSymbol::Package(pkg_name) = dep { - entry.add_imported_pkg(pkg_name); - } - } - } - VwSymbol::Entity(name) => { - all_symbols.insert(name, file.clone()); - } - _ => {} - } - } - } - - // Second pass: find dependencies for each file - for file in files.iter() { - let deps = cache.get_dependencies(file)?.clone(); - let mut file_deps = Vec::new(); - - for dep in deps { - let dep_name = match &dep { - VwSymbol::Package(name) | VwSymbol::Entity(name) => name, - _ => continue, - }; - if let Some(provider_file) = all_symbols.get(dep_name) { - if provider_file != file { - file_deps.push(provider_file.clone()); - } - } - } - - dependencies.insert(file.clone(), file_deps); - } - - // Topological sort using Kahn's algorithm - let sorted = topological_sort(files.clone(), dependencies)?; - *files = sorted; - - Ok(()) -} - -//fn get_file_symbols(file_path: &Path) -> Result> { -// let content = -// fs::read_to_string(file_path).map_err(|e| VwError::FileSystem { -// message: format!("Failed to read file {file_path:?}: {e}"), -// })?; -// -// let mut symbols = Vec::new(); -// -// // Find package declarations -// let package_pattern = r"(?i)\bpackage\s+(\w+)\s+is\b"; -// let package_re = regex::Regex::new(package_pattern)?; -// -// for captures in package_re.captures_iter(&content) { -// if let Some(package_name) = captures.get(1) { -// symbols.push(VwSymbol::Package(package_name.as_str().to_string())); -// } -// } -// -// // Find entity declarations -// let entity_pattern = r"(?i)\bentity\s+(\w+)\s+is\b"; -// let entity_re = regex::Regex::new(entity_pattern)?; -// -// for captures in entity_re.captures_iter(&content) { -// if let Some(entity_name) = captures.get(1) { -// symbols.push(VwSymbol::Entity(entity_name.as_str().to_string())); -// } -// } -// -// Ok(symbols) -//} - fn topological_sort( files: Vec, dependencies: HashMap>, @@ -2201,29 +2125,6 @@ fn write_vhdl_ls_config( Ok(()) } -fn load_existing_vhdl_ls_config( - workspace_dir: &Utf8Path, -) -> Result { - let config_path = workspace_dir.join("vhdl_ls.toml"); - if config_path.exists() { - let config_content = fs::read_to_string(&config_path).map_err(|e| { - VwError::FileSystem { - message: format!("Failed to read existing vhdl_ls.toml: {e}"), - } - })?; - - let config: VhdlLsConfig = toml::from_str(&config_content)?; - - Ok(config) - } else { - Ok(VhdlLsConfig { - standard: None, - libraries: HashMap::new(), - lint: None, - }) - } -} - /// Build a Rust library for a testbench. /// Looks for Cargo.toml in the testbench directory, builds it, and returns the path to the .so file. async fn build_rust_library( diff --git a/vw-lib/src/vhdl_printer.rs b/vw-lib/src/vhdl_printer.rs deleted file mode 100644 index 7968cd3..0000000 --- a/vw-lib/src/vhdl_printer.rs +++ /dev/null @@ -1,123 +0,0 @@ -// VHDL AST Pretty Printer -// Manually converts VHDL AST nodes back to VHDL source code strings -// -// Note: vhdl_lang provides VHDLFormatter, but its Buffer type is private, -// so we manually reconstruct expressions from the AST instead. - -use vhdl_lang::ast::{Designator, Expression, Literal, Name, Operator}; - -/// Convert an Expression AST node to a VHDL string -pub fn expr_to_string(expr: &Expression) -> String { - match expr { - Expression::Literal(lit) => literal_to_string(lit), - Expression::Name(name) => name_to_string(name), - Expression::Binary(op, left, right) => { - // Match on the binary operator enum directly - inline to avoid exposing private types - let op_str = match &op.item.item { - Operator::Plus => "+", - Operator::Minus => "-", - Operator::Times => "*", - Operator::Div => "/", - Operator::Mod => " mod ", - Operator::Rem => " rem ", - Operator::Pow => "**", - Operator::And => " and ", - Operator::Or => " or ", - Operator::Nand => " nand ", - Operator::Nor => " nor ", - Operator::Xor => " xor ", - Operator::Xnor => " xnor ", - Operator::EQ => "=", - Operator::NE => "/=", - Operator::LT => "<", - Operator::LTE => "<=", - Operator::GT => ">", - Operator::GTE => ">=", - Operator::QueEQ => "?=", - Operator::QueNE => "?/=", - Operator::QueLT => "?<", - Operator::QueLTE => "?<=", - Operator::QueGT => "?>", - Operator::QueGTE => "?>=", - Operator::SLL => " sll ", - Operator::SRL => " srl ", - Operator::SLA => " sla ", - Operator::SRA => " sra ", - Operator::ROL => " rol ", - Operator::ROR => " ror ", - Operator::Concat => "&", - _ => " ? ", // Catch-all for unexpected operators - }; - format!( - "{} {} {}", - expr_to_string(&left.item), - op_str, - expr_to_string(&right.item) - ) - } - Expression::Unary(op, operand) => { - let op_str = match &op.item.item { - Operator::Plus => "+", - Operator::Minus => "-", - Operator::Not => "not ", - Operator::Abs => "abs ", - Operator::QueQue => "?? ", - _ => "unary_op ", - }; - format!("{}{}", op_str, expr_to_string(&operand.item)) - } - Expression::Parenthesized(inner) => { - format!("({})", expr_to_string(&inner.item)) - } - _ => "complex_expr".to_string(), - } -} - -fn literal_to_string(lit: &Literal) -> String { - match lit { - Literal::AbstractLiteral(al) => al.to_string(), - Literal::String(s) => format!("\"{}\"", s), - Literal::BitString(bs) => format!("{:?}", bs), - Literal::Character(c) => format!("'{}'", c), - Literal::Null => "null".to_string(), - Literal::Physical(val) => format!("{:?}", val), - } -} - -fn name_to_string(name: &Name) -> String { - match name { - Name::Designator(des) => match &des.item { - Designator::Identifier(sym) => sym.name_utf8(), - Designator::OperatorSymbol(_) => "operator".to_string(), - Designator::Character(c) => format!("'{}'", c), - Designator::Anonymous(_) => "anonymous".to_string(), - }, - Name::Selected(prefix, suffix) => { - let suffix_name = match &suffix.item.item { - Designator::Identifier(sym) => sym.name_utf8(), - _ => "suffix".to_string(), - }; - format!( - "{}.{}", - expr_to_string(&Expression::Name(Box::new( - prefix.item.clone() - ))), - suffix_name - ) - } - Name::SelectedAll(prefix) => { - format!( - "{}.all", - expr_to_string(&Expression::Name(Box::new( - prefix.item.clone() - ))) - ) - } - Name::CallOrIndexed(fcall) => { - // For function calls like get_eth_hdr_bits(x) - expr_to_string(&Expression::Name(Box::new(fcall.name.item.clone()))) - .to_string() - } - _ => "complex_name".to_string(), - } -} From 08fbf5ae6800b2a15762abaad857a3c77700dc56 Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Fri, 20 Feb 2026 21:39:19 -0800 Subject: [PATCH 14/22] A bunch of features for repo handling - Allow multiple src patterns for one library. Code from other repos can rely on files in other folders, but expects to be in the same library - Exclude patterns: we need to be able to exclude testbenches/other test code - Bring in submodules: quartz has XPM as a submodule and we need that for top-level testbenches --- vw-lib/src/lib.rs | 128 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 118 insertions(+), 10 deletions(-) diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index 6d525fa..709fa11 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -190,6 +190,44 @@ pub struct WorkspaceInfo { pub version: String, } +/// Helper to deserialize a field that can be either a string or array of strings +fn string_or_vec<'de, D>(deserializer: D) -> std::result::Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + use serde::de::{self, SeqAccess, Visitor}; + + struct StringOrVec; + + impl<'de> Visitor<'de> for StringOrVec { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string or array of strings") + } + + fn visit_str(self, value: &str) -> std::result::Result, E> + where + E: de::Error, + { + Ok(vec![value.to_owned()]) + } + + fn visit_seq(self, mut seq: S) -> std::result::Result, S::Error> + where + S: SeqAccess<'de>, + { + let mut vec = Vec::new(); + while let Some(value) = seq.next_element()? { + vec.push(value); + } + Ok(vec) + } + } + + deserializer.deserialize_any(StringOrVec) +} + #[derive(Debug, Deserialize, Serialize)] pub struct Dependency { pub repo: String, @@ -197,11 +235,16 @@ pub struct Dependency { pub branch: Option, #[serde(default)] pub commit: Option, - pub src: String, + #[serde(deserialize_with = "string_or_vec")] + pub src: Vec, #[serde(default)] pub recursive: bool, #[serde(default)] pub sim_only: bool, + #[serde(default)] + pub submodules: bool, + #[serde(default)] + pub exclude: Vec, } #[derive(Debug, Serialize, Deserialize)] @@ -213,12 +256,17 @@ pub struct LockFile { pub struct LockedDependency { pub repo: String, pub commit: String, - pub src: String, + #[serde(deserialize_with = "string_or_vec")] + pub src: Vec, pub path: PathBuf, #[serde(default)] pub recursive: bool, #[serde(default)] pub sim_only: bool, + #[serde(default)] + pub submodules: bool, + #[serde(default)] + pub exclude: Vec, } #[derive(Debug, Serialize, Deserialize)] @@ -455,6 +503,8 @@ pub async fn update_workspace_with_token( &dep.src, &dep_path, dep.recursive, + &dep.exclude, + dep.submodules, creds, ) .await @@ -478,11 +528,13 @@ pub async fn update_workspace_with_token( path: dep_path.clone(), recursive: dep.recursive, sim_only: dep.sim_only, + submodules: dep.submodules, + exclude: dep.exclude.clone(), }, ); // Find VHDL files in the cached dependency directory - let vhdl_files = find_vhdl_files(&dep_path, dep.recursive)?; + let vhdl_files = find_vhdl_files(&dep_path, dep.recursive, &dep.exclude)?; if !vhdl_files.is_empty() { let portable_files = vhdl_files.into_iter().map(make_path_portable).collect(); @@ -574,15 +626,17 @@ pub async fn add_dependency_with_token( } let dep_name = name.unwrap_or_else(|| extract_repo_name(&repo)); - let src_path = src.unwrap_or_else(|| ".".to_string()); + let src_paths = vec![src.unwrap_or_else(|| ".".to_string())]; let dependency = Dependency { repo: repo.clone(), branch, commit, - src: src_path, + src: src_paths, recursive, sim_only, + submodules: false, + exclude: Vec::new(), }; config.dependencies.insert(dep_name.clone(), dependency); @@ -732,7 +786,7 @@ pub fn generate_deps_tcl(workspace_dir: &Utf8Path) -> Result<()> { } let vhdl_files = - find_vhdl_files(&locked_dep.path, locked_dep.recursive)?; + find_vhdl_files(&locked_dep.path, locked_dep.recursive, &locked_dep.exclude)?; // Create array entry for this library tcl_content.push_str(&format!("set dep_files({dep_name}) [list")); @@ -1799,12 +1853,15 @@ async fn get_branch_head_commit( })? } +#[allow(clippy::too_many_arguments)] async fn download_dependency( repo_url: &str, commit: &str, - src_path: &str, + src_paths: &[String], dest_path: &Path, recursive: bool, + exclude: &[String], + submodules: bool, credentials: Option<(&str, &str)>, // (username, password) ) -> Result<()> { let temp_dir = tempfile::tempdir().map_err(|e| VwError::FileSystem { @@ -1821,7 +1878,7 @@ async fn download_dependency( let commit = commit.to_string(); let temp_path = temp_dir.path().to_path_buf(); - let src_path = src_path.to_string(); + let src_paths = src_paths.to_vec(); let credentials = credentials.map(|(u, p)| (u.to_string(), p.to_string())); tokio::task::spawn_blocking(move || { @@ -1913,6 +1970,26 @@ async fn download_dependency( ), })?; + // Initialize and update submodules if requested + if submodules { + for mut submodule in repo.submodules().map_err(|e| VwError::Git { + message: format!("Failed to list submodules: {e}"), + })? { + submodule.init(false).map_err(|e| VwError::Git { + message: format!( + "Failed to init submodule '{}': {e}", + submodule.name().unwrap_or("unknown") + ), + })?; + submodule.update(true, None).map_err(|e| VwError::Git { + message: format!( + "Failed to update submodule '{}': {e}", + submodule.name().unwrap_or("unknown") + ), + })?; + } + } + Ok::<(), VwError>(()) }) .await @@ -1925,7 +2002,9 @@ async fn download_dependency( })?; // Treat all src values as globs (handles files, directories, and patterns) - copy_vhdl_files_glob(temp_dir.path(), &src_path, dest_path, recursive)?; + for src_path in &src_paths { + copy_vhdl_files_glob(temp_dir.path(), src_path, dest_path, recursive, exclude)?; + } Ok(()) } @@ -1935,12 +2014,19 @@ fn copy_vhdl_files_glob( src_pattern: &str, dest: &Path, recursive: bool, + exclude: &[String], ) -> Result<()> { // Build patterns to match let src_path = repo_root.join(src_pattern); let mut patterns = Vec::new(); let strip_prefix: PathBuf; + // Compile exclude patterns + let exclude_patterns: Vec = exclude + .iter() + .filter_map(|p| glob::Pattern::new(p).ok()) + .collect(); + // Check if src_pattern points to a directory if src_path.is_dir() { // It's a directory - create appropriate glob patterns @@ -2018,6 +2104,12 @@ fn copy_vhdl_files_glob( } })?; + // Check if file matches any exclude pattern + let path_str = relative_path.to_string_lossy(); + if exclude_patterns.iter().any(|p| p.matches(&path_str)) { + continue; // Skip excluded files + } + let dest_file = dest.join(relative_path); // Create parent directories if needed @@ -2054,9 +2146,25 @@ fn copy_vhdl_files_glob( Ok(()) } -fn find_vhdl_files(dir: &Path, recursive: bool) -> Result> { +fn find_vhdl_files(dir: &Path, recursive: bool, exclude: &[String]) -> Result> { let mut vhdl_files = Vec::new(); find_vhdl_files_impl(dir, &mut vhdl_files, recursive)?; + + // Filter out excluded files + if !exclude.is_empty() { + let exclude_patterns: Vec = exclude + .iter() + .filter_map(|p| glob::Pattern::new(p).ok()) + .collect(); + + vhdl_files.retain(|file| { + // Match against path relative to the base directory + let relative = file.strip_prefix(dir).unwrap_or(file); + let path_str = relative.to_string_lossy(); + !exclude_patterns.iter().any(|pattern| pattern.matches(&path_str)) + }); + } + Ok(vhdl_files) } From 9737e47e16048edf3546c0abb1ff3a5081c31f56 Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Fri, 20 Feb 2026 21:47:29 -0800 Subject: [PATCH 15/22] Sort external libraries by dependency - Quartz relies on XPM, so XPM needs to be built first... --- vw-lib/src/lib.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 3 deletions(-) diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index 709fa11..e9d1f0a 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -1079,9 +1079,94 @@ pub async fn analyze_ext_libraries( vhdl_std: VhdlStandard, cache: &mut FileCache, ) -> Result<()> { - // First, analyze all non-defaultlib libraries - for (lib_name, library) in &vhdl_ls_config.libraries { - if lib_name != "defaultlib" { + // Collect non-defaultlib library names + let ext_lib_names: Vec = vhdl_ls_config + .libraries + .keys() + .filter(|k| k.as_str() != "defaultlib") + .cloned() + .collect(); + + // Build inter-library dependency graph by scanning for `library ;` + let ext_lib_set: HashSet = ext_lib_names.iter().cloned().collect(); + let mut lib_deps: HashMap> = HashMap::new(); + for lib_name in &ext_lib_names { + let mut deps = Vec::new(); + if let Some(library) = vhdl_ls_config.libraries.get(lib_name) { + for file_path in &library.files { + let expanded = if file_path.starts_with("$HOME") { + if let Some(home) = dirs::home_dir() { + home.join( + file_path + .strip_prefix("$HOME/") + .unwrap_or(file_path), + ) + } else { + PathBuf::from(file_path) + } + } else { + PathBuf::from(file_path) + }; + if let Ok(contents) = fs::read_to_string(&expanded) { + for line in contents.lines() { + let trimmed = line.trim().to_lowercase(); + if let Some(rest) = trimmed.strip_prefix("library ") { + let dep_lib = rest.trim_end_matches(';').trim(); + if ext_lib_set.contains(dep_lib) + && dep_lib != lib_name.to_lowercase() + { + deps.push(dep_lib.to_string()); + } + } + } + } + } + } + lib_deps.insert(lib_name.clone(), deps); + } + + // Topological sort of library names (Kahn's algorithm) + let mut in_degree: HashMap = + ext_lib_names.iter().map(|n| (n.clone(), 0)).collect(); + let mut adj: HashMap> = + ext_lib_names.iter().map(|n| (n.clone(), Vec::new())).collect(); + for (lib, deps) in &lib_deps { + for dep in deps { + if let Some(neighbors) = adj.get_mut(dep) { + neighbors.push(lib.clone()); + } + if let Some(deg) = in_degree.get_mut(lib) { + *deg += 1; + } + } + } + let mut queue: VecDeque = in_degree + .iter() + .filter(|(_, &d)| d == 0) + .map(|(n, _)| n.clone()) + .collect(); + let mut sorted_libs = Vec::new(); + while let Some(current) = queue.pop_front() { + sorted_libs.push(current.clone()); + if let Some(neighbors) = adj.get(¤t) { + for neighbor in neighbors { + if let Some(deg) = in_degree.get_mut(neighbor) { + *deg -= 1; + if *deg == 0 { + queue.push_back(neighbor.clone()); + } + } + } + } + } + // Fall back to unsorted if cycle detected + if sorted_libs.len() != ext_lib_names.len() { + sorted_libs = ext_lib_names; + } + + // Analyze libraries in dependency order + for lib_name in &sorted_libs { + if let Some(library) = vhdl_ls_config.libraries.get(lib_name) { // Convert library name to be NVC-compatible (no hyphens) let nvc_lib_name = lib_name.replace('-', "_"); From dac17f584b0f527edf0a87a051c2fb4f22c08903 Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Wed, 11 Mar 2026 21:08:36 +0000 Subject: [PATCH 16/22] Add timeouts --- vw-lib/src/lib.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index 2850ed4..3b4e4fd 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -1849,7 +1849,9 @@ async fn get_branch_head_commit( let branch = branch.to_string(); let credentials = credentials.map(|(u, p)| (u.to_string(), p.to_string())); - tokio::task::spawn_blocking(move || { + tokio::time::timeout( + std::time::Duration::from_secs(30), + tokio::task::spawn_blocking(move || { // Create a temporary directory for the operation let temp_dir = tempfile::tempdir().map_err(|e| VwError::FileSystem { @@ -1945,8 +1947,12 @@ async fn get_branch_head_commit( "Branch '{branch}' not found in remote repository" ), }) - }) + }), + ) .await + .map_err(|_| VwError::Git { + message: "Git ls-remote timed out after 30 seconds".to_string(), + })? .map_err(|e| VwError::Git { message: format!("Failed to execute git ls-remote task: {e}"), })? @@ -1980,7 +1986,9 @@ async fn download_dependency( let src_paths = src_paths.to_vec(); let credentials = credentials.map(|(u, p)| (u.to_string(), p.to_string())); - tokio::task::spawn_blocking(move || { + tokio::time::timeout( + std::time::Duration::from_secs(120), + tokio::task::spawn_blocking(move || { // Set up clone options with authentication let mut builder = git2::build::RepoBuilder::new(); @@ -2032,6 +2040,7 @@ async fn download_dependency( }); let mut fetch_options = git2::FetchOptions::new(); + fetch_options.depth(1); // shallow clone — only need one commit fetch_options.remote_callbacks(callbacks); builder.fetch_options(fetch_options); @@ -2092,8 +2101,12 @@ async fn download_dependency( } Ok::<(), VwError>(()) - }) + }), + ) .await + .map_err(|_| VwError::Git { + message: "Git clone timed out after 120 seconds".to_string(), + })? .map_err(|e| VwError::Git { message: format!("Failed to execute git operations: {e}"), })??; From 630b36f3b8888950a93a31d938297054887da699 Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Wed, 11 Mar 2026 21:18:20 +0000 Subject: [PATCH 17/22] F o r m a t t i n g --- vw-lib/src/lib.rs | 360 ++++++++++++++++++++++++---------------------- 1 file changed, 189 insertions(+), 171 deletions(-) diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index 3b4e4fd..d9caeab 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -1852,102 +1852,111 @@ async fn get_branch_head_commit( tokio::time::timeout( std::time::Duration::from_secs(30), tokio::task::spawn_blocking(move || { - // Create a temporary directory for the operation - let temp_dir = - tempfile::tempdir().map_err(|e| VwError::FileSystem { - message: format!("Failed to create temporary directory: {e}"), - })?; - - // Create an empty repository to work with remotes - let repo = - git2::Repository::init_bare(temp_dir.path()).map_err(|e| { - VwError::Git { + // Create a temporary directory for the operation + let temp_dir = + tempfile::tempdir().map_err(|e| VwError::FileSystem { message: format!( - "Failed to initialize temporary repository: {e}" + "Failed to create temporary directory: {e}" ), - } - })?; + })?; - // Create a remote - let mut remote = - repo.remote_anonymous(&normalized_repo_url).map_err(|e| { - VwError::Git { - message: format!("Failed to create remote: {e}"), - } - })?; + // Create an empty repository to work with remotes + let repo = + git2::Repository::init_bare(temp_dir.path()).map_err(|e| { + VwError::Git { + message: format!( + "Failed to initialize temporary repository: {e}" + ), + } + })?; - // Connect and list references - // Always set a credentials callback so git2 doesn't fail with "no callback set". - // The callback will try explicit credentials first, then fall back to git's - // credential helper system (which includes .netrc support). - let mut callbacks = git2::RemoteCallbacks::new(); - let attempt_count = RefCell::new(0); + // Create a remote + let mut remote = repo + .remote_anonymous(&normalized_repo_url) + .map_err(|e| VwError::Git { + message: format!("Failed to create remote: {e}"), + })?; - callbacks.credentials(move |url, username_from_url, allowed_types| { - let mut attempts = attempt_count.borrow_mut(); - *attempts += 1; + // Connect and list references + // Always set a credentials callback so git2 doesn't fail with "no callback set". + // The callback will try explicit credentials first, then fall back to git's + // credential helper system (which includes .netrc support). + let mut callbacks = git2::RemoteCallbacks::new(); + let attempt_count = RefCell::new(0); + + callbacks.credentials( + move |url, username_from_url, allowed_types| { + let mut attempts = attempt_count.borrow_mut(); + *attempts += 1; + + // Limit attempts to prevent infinite loops + if *attempts > 1 { + return git2::Cred::default(); + } - // Limit attempts to prevent infinite loops - if *attempts > 1 { - return git2::Cred::default(); - } + // First, try explicit credentials from netrc if available + if allowed_types + .contains(git2::CredentialType::USER_PASS_PLAINTEXT) + { + if let Some((ref username, ref password)) = credentials + { + // Use both username and password from netrc + return git2::Cred::userpass_plaintext( + username, password, + ); + } + } - // First, try explicit credentials from netrc if available - if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) - { - if let Some((ref username, ref password)) = credentials { - // Use both username and password from netrc - return git2::Cred::userpass_plaintext(username, password); - } - } + // Try SSH key if available + if allowed_types.contains(git2::CredentialType::SSH_KEY) { + if let Some(username) = username_from_url { + if let Ok(cred) = + git2::Cred::ssh_key_from_agent(username) + { + return Ok(cred); + } + } + } - // Try SSH key if available - if allowed_types.contains(git2::CredentialType::SSH_KEY) { - if let Some(username) = username_from_url { - if let Ok(cred) = git2::Cred::ssh_key_from_agent(username) { - return Ok(cred); + // Fall back to git's credential helper system (includes .netrc) + if let Ok(config) = git2::Config::open_default() { + if let Ok(cred) = git2::Cred::credential_helper( + &config, + url, + username_from_url, + ) { + return Ok(cred); + } } - } - } - // Fall back to git's credential helper system (includes .netrc) - if let Ok(config) = git2::Config::open_default() { - if let Ok(cred) = git2::Cred::credential_helper( - &config, - url, - username_from_url, - ) { - return Ok(cred); - } - } + git2::Cred::default() + }, + ); - git2::Cred::default() - }); + remote + .connect_auth(git2::Direction::Fetch, Some(callbacks), None) + .map_err(|e| VwError::Git { + message: format!("Failed to connect to remote: {e}"), + })?; - remote - .connect_auth(git2::Direction::Fetch, Some(callbacks), None) - .map_err(|e| VwError::Git { - message: format!("Failed to connect to remote: {e}"), + let refs = remote.list().map_err(|e| VwError::Git { + message: format!("Failed to list remote references: {e}"), })?; - let refs = remote.list().map_err(|e| VwError::Git { - message: format!("Failed to list remote references: {e}"), - })?; - - // Look for the specific branch reference - let ref_name = format!("refs/heads/{branch}"); - for remote_head in refs { - if remote_head.name() == ref_name { - return Ok(remote_head.oid().to_string()); + // Look for the specific branch reference + let ref_name = format!("refs/heads/{branch}"); + for remote_head in refs { + if remote_head.name() == ref_name { + return Ok(remote_head.oid().to_string()); + } } - } - Err(VwError::Git { - message: format!( - "Branch '{branch}' not found in remote repository" - ), - }) - }), + Err(VwError::Git { + message: format!( + "Branch '{branch}' not found in remote repository" + ), + }) + }), ) .await .map_err(|_| VwError::Git { @@ -1989,119 +1998,128 @@ async fn download_dependency( tokio::time::timeout( std::time::Duration::from_secs(120), tokio::task::spawn_blocking(move || { - // Set up clone options with authentication - let mut builder = git2::build::RepoBuilder::new(); - - // Always set a credentials callback so git2 doesn't fail with "no callback set". - // The callback will try explicit credentials first, then fall back to git's - // credential helper system (which includes .netrc support). - let mut callbacks = git2::RemoteCallbacks::new(); - let attempt_count = RefCell::new(0); - - callbacks.credentials(move |url, username_from_url, allowed_types| { - let mut attempts = attempt_count.borrow_mut(); - *attempts += 1; - - // Limit attempts to prevent infinite loops - if *attempts > 1 { - return git2::Cred::default(); - } + // Set up clone options with authentication + let mut builder = git2::build::RepoBuilder::new(); + + // Always set a credentials callback so git2 doesn't fail with "no callback set". + // The callback will try explicit credentials first, then fall back to git's + // credential helper system (which includes .netrc support). + let mut callbacks = git2::RemoteCallbacks::new(); + let attempt_count = RefCell::new(0); + + callbacks.credentials( + move |url, username_from_url, allowed_types| { + let mut attempts = attempt_count.borrow_mut(); + *attempts += 1; + + // Limit attempts to prevent infinite loops + if *attempts > 1 { + return git2::Cred::default(); + } - // First, try explicit credentials from netrc if available - if allowed_types.contains(git2::CredentialType::USER_PASS_PLAINTEXT) - { - if let Some((ref username, ref password)) = credentials { - // Use both username and password from netrc - return git2::Cred::userpass_plaintext(username, password); - } - } + // First, try explicit credentials from netrc if available + if allowed_types + .contains(git2::CredentialType::USER_PASS_PLAINTEXT) + { + if let Some((ref username, ref password)) = credentials + { + // Use both username and password from netrc + return git2::Cred::userpass_plaintext( + username, password, + ); + } + } - // Try SSH key if available - if allowed_types.contains(git2::CredentialType::SSH_KEY) { - if let Some(username) = username_from_url { - if let Ok(cred) = git2::Cred::ssh_key_from_agent(username) { - return Ok(cred); + // Try SSH key if available + if allowed_types.contains(git2::CredentialType::SSH_KEY) { + if let Some(username) = username_from_url { + if let Ok(cred) = + git2::Cred::ssh_key_from_agent(username) + { + return Ok(cred); + } + } } - } - } - // Fall back to git's credential helper system (includes .netrc) - if let Ok(config) = git2::Config::open_default() { - if let Ok(cred) = git2::Cred::credential_helper( - &config, - url, - username_from_url, - ) { - return Ok(cred); - } - } + // Fall back to git's credential helper system (includes .netrc) + if let Ok(config) = git2::Config::open_default() { + if let Ok(cred) = git2::Cred::credential_helper( + &config, + url, + username_from_url, + ) { + return Ok(cred); + } + } - git2::Cred::default() - }); + git2::Cred::default() + }, + ); - let mut fetch_options = git2::FetchOptions::new(); - fetch_options.depth(1); // shallow clone — only need one commit - fetch_options.remote_callbacks(callbacks); - builder.fetch_options(fetch_options); + let mut fetch_options = git2::FetchOptions::new(); + fetch_options.depth(1); // shallow clone — only need one commit + fetch_options.remote_callbacks(callbacks); + builder.fetch_options(fetch_options); - // Clone the repository - let repo = - builder + // Clone the repository + let repo = builder .clone(&normalized_repo_url, &temp_path) .map_err(|e| VwError::Git { message: format!("Failed to clone repository: {e}"), })?; - // Parse the commit SHA - let commit_oid = - git2::Oid::from_str(&commit).map_err(|e| VwError::Git { - message: format!("Invalid commit SHA '{commit}': {e}"), - })?; - - // Find the commit object - let commit_obj = - repo.find_commit(commit_oid).map_err(|e| VwError::Git { - message: format!("Commit '{commit}' not found: {e}"), - })?; - - // Checkout the specific commit - repo.checkout_tree(commit_obj.as_object(), None) - .map_err(|e| VwError::Git { - message: format!("Failed to checkout commit '{commit}': {e}"), - })?; + // Parse the commit SHA + let commit_oid = + git2::Oid::from_str(&commit).map_err(|e| VwError::Git { + message: format!("Invalid commit SHA '{commit}': {e}"), + })?; - // Set HEAD to the commit - repo.set_head_detached(commit_oid) - .map_err(|e| VwError::Git { - message: format!( - "Failed to set HEAD to commit '{commit}': {e}" - ), - })?; + // Find the commit object + let commit_obj = + repo.find_commit(commit_oid).map_err(|e| VwError::Git { + message: format!("Commit '{commit}' not found: {e}"), + })?; - // Initialize and update submodules if requested - if submodules { - for mut submodule in - repo.submodules().map_err(|e| VwError::Git { - message: format!("Failed to list submodules: {e}"), - })? - { - submodule.init(false).map_err(|e| VwError::Git { + // Checkout the specific commit + repo.checkout_tree(commit_obj.as_object(), None) + .map_err(|e| VwError::Git { message: format!( - "Failed to init submodule '{}': {e}", - submodule.name().unwrap_or("unknown") + "Failed to checkout commit '{commit}': {e}" ), })?; - submodule.update(true, None).map_err(|e| VwError::Git { + + // Set HEAD to the commit + repo.set_head_detached(commit_oid) + .map_err(|e| VwError::Git { message: format!( - "Failed to update submodule '{}': {e}", - submodule.name().unwrap_or("unknown") + "Failed to set HEAD to commit '{commit}': {e}" ), })?; + + // Initialize and update submodules if requested + if submodules { + for mut submodule in + repo.submodules().map_err(|e| VwError::Git { + message: format!("Failed to list submodules: {e}"), + })? + { + submodule.init(false).map_err(|e| VwError::Git { + message: format!( + "Failed to init submodule '{}': {e}", + submodule.name().unwrap_or("unknown") + ), + })?; + submodule.update(true, None).map_err(|e| VwError::Git { + message: format!( + "Failed to update submodule '{}': {e}", + submodule.name().unwrap_or("unknown") + ), + })?; + } } - } - Ok::<(), VwError>(()) - }), + Ok::<(), VwError>(()) + }), ) .await .map_err(|_| VwError::Git { From 2449a7825c1f882e8eaa300bb2f9292d2e8f7983 Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Wed, 18 Mar 2026 12:30:48 -0700 Subject: [PATCH 18/22] Use only vectors for src patterns --- vw-lib/src/lib.rs | 50 ++--------------------------------------------- 1 file changed, 2 insertions(+), 48 deletions(-) diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index d9caeab..f06d3db 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -190,52 +190,6 @@ pub struct WorkspaceInfo { pub version: String, } -/// Helper to deserialize a field that can be either a string or array of strings -fn string_or_vec<'de, D>( - deserializer: D, -) -> std::result::Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - use serde::de::{self, SeqAccess, Visitor}; - - struct StringOrVec; - - impl<'de> Visitor<'de> for StringOrVec { - type Value = Vec; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("string or array of strings") - } - - fn visit_str( - self, - value: &str, - ) -> std::result::Result, E> - where - E: de::Error, - { - Ok(vec![value.to_owned()]) - } - - fn visit_seq( - self, - mut seq: S, - ) -> std::result::Result, S::Error> - where - S: SeqAccess<'de>, - { - let mut vec = Vec::new(); - while let Some(value) = seq.next_element()? { - vec.push(value); - } - Ok(vec) - } - } - - deserializer.deserialize_any(StringOrVec) -} - #[derive(Debug, Deserialize, Serialize)] pub struct Dependency { pub repo: String, @@ -243,7 +197,7 @@ pub struct Dependency { pub branch: Option, #[serde(default)] pub commit: Option, - #[serde(deserialize_with = "string_or_vec")] + #[serde(default)] pub src: Vec, #[serde(default)] pub recursive: bool, @@ -264,7 +218,7 @@ pub struct LockFile { pub struct LockedDependency { pub repo: String, pub commit: String, - #[serde(deserialize_with = "string_or_vec")] + #[serde(default)] pub src: Vec, pub path: PathBuf, #[serde(default)] From 25930ba28f6acf56e215caf3fb7a4334d254fb95 Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Wed, 18 Mar 2026 21:48:18 -0700 Subject: [PATCH 19/22] Use entry to get HashMap values --- vw-lib/src/lib.rs | 97 +++++++++++++++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 33 deletions(-) diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index f06d3db..80694d2 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -28,7 +28,7 @@ //! ``` use std::cell::RefCell; -use std::collections::{HashMap, HashSet, VecDeque}; +use std::collections::{hash_map::Entry, HashMap, HashSet, VecDeque}; use std::path::{Path, PathBuf}; use std::{fmt, fs}; @@ -899,15 +899,18 @@ impl FileCache { /// Get cached file dependencies, reading and parsing file if not cached. pub fn get_dependencies(&mut self, path: &Path) -> Result<&Vec> { - if !self.dependencies.contains_key(path) { - let content = - fs::read_to_string(path).map_err(|e| VwError::FileSystem { - message: format!("Failed to read file {path:?}: {e}"), + match self.dependencies.entry(path.to_path_buf()) { + Entry::Occupied(e) => Ok(e.into_mut()), + Entry::Vacant(e) => { + let content = fs::read_to_string(path).map_err(|e| { + VwError::FileSystem { + message: format!("Failed to read file {path:?}: {e}"), + } })?; - let deps = parse_file_dependencies(&content)?; - self.dependencies.insert(path.to_path_buf(), deps); + let deps = parse_file_dependencies(&content)?; + Ok(e.insert(deps)) + } } - Ok(self.dependencies.get(path).unwrap()) } /// Get cached provided symbols (packages and entities), reading and parsing if not cached. @@ -915,28 +918,34 @@ impl FileCache { &mut self, path: &Path, ) -> Result<&Vec> { - if !self.provided_symbols.contains_key(path) { - let content = - fs::read_to_string(path).map_err(|e| VwError::FileSystem { - message: format!("Failed to read file {path:?}: {e}"), + match self.provided_symbols.entry(path.to_path_buf()) { + Entry::Occupied(e) => Ok(e.into_mut()), + Entry::Vacant(e) => { + let content = fs::read_to_string(path).map_err(|e| { + VwError::FileSystem { + message: format!("Failed to read file {path:?}: {e}"), + } })?; - let symbols = parse_provided_symbols(&content)?; - self.provided_symbols.insert(path.to_path_buf(), symbols); + let symbols = parse_provided_symbols(&content)?; + Ok(e.insert(symbols)) + } } - Ok(self.provided_symbols.get(path).unwrap()) } /// Get cached entities in file, reading and parsing if not cached. pub fn get_entities(&mut self, path: &Path) -> Result<&Vec> { - if !self.entities.contains_key(path) { - let content = - fs::read_to_string(path).map_err(|e| VwError::FileSystem { - message: format!("Failed to read file {path:?}: {e}"), + match self.entities.entry(path.to_path_buf()) { + Entry::Occupied(e) => Ok(e.into_mut()), + Entry::Vacant(e) => { + let content = fs::read_to_string(path).map_err(|e| { + VwError::FileSystem { + message: format!("Failed to read file {path:?}: {e}"), + } })?; - let entities = parse_entities(&content)?; - self.entities.insert(path.to_path_buf(), entities); + let entities = parse_entities(&content)?; + Ok(e.insert(entities)) + } } - Ok(self.entities.get(path).unwrap()) } /// Get mutable access to the entities cache for functions that only need entity lookups. @@ -1546,8 +1555,22 @@ fn topological_sort( for (file, deps) in &dependencies { for dep in deps { if files.contains(dep) { - adj_list.get_mut(dep).unwrap().push(file.clone()); - *in_degree.get_mut(file).unwrap() += 1; + adj_list + .get_mut(dep) + .ok_or(VwError::Dependency { + message: format!( + "Somehow adj list didn't contain dep {:?}", + dep + ), + })? + .push(file.clone()); + *in_degree.get_mut(file).ok_or(VwError::Dependency { + message: format!( + "Somehow in_degree didn't + contain {:?}", + file + ), + })? += 1; } } } @@ -1569,7 +1592,13 @@ fn topological_sort( // For each neighbor of current if let Some(neighbors) = adj_list.get(¤t) { for neighbor in neighbors { - *in_degree.get_mut(neighbor).unwrap() -= 1; + *in_degree.get_mut(neighbor).ok_or(VwError::Dependency { + message: format!( + "Somehow in_degree doesn't have neighbor {:?}", + neighbor + ), + })? -= 1; + if in_degree[neighbor] == 0 { queue.push_back(neighbor.clone()); } @@ -1671,15 +1700,17 @@ fn get_cached_entities<'a>( path: &Path, entities_cache: &'a mut HashMap>, ) -> Result<&'a Vec> { - if !entities_cache.contains_key(path) { - let content = - fs::read_to_string(path).map_err(|e| VwError::FileSystem { - message: format!("Failed to read file {path:?}: {e}"), - })?; - let entities = parse_entities(&content)?; - entities_cache.insert(path.to_path_buf(), entities); + match entities_cache.entry(path.to_path_buf()) { + Entry::Occupied(e) => Ok(e.into_mut()), + Entry::Vacant(e) => { + let content = + fs::read_to_string(path).map_err(|e| VwError::FileSystem { + message: format!("Failed to read file {path:?}: {e}"), + })?; + let entities = parse_entities(&content)?; + Ok(e.insert(entities)) + } } - Ok(entities_cache.get(path).unwrap()) } fn make_path_portable(path: PathBuf) -> PathBuf { From f75d39a6287ec57e9bccb15aefaf0a268b92ecb8 Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Thu, 19 Mar 2026 00:00:19 -0700 Subject: [PATCH 20/22] Also add package instantiations --- vw-lib/src/mapping.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/vw-lib/src/mapping.rs b/vw-lib/src/mapping.rs index fea6eb6..63a3644 100644 --- a/vw-lib/src/mapping.rs +++ b/vw-lib/src/mapping.rs @@ -1,8 +1,8 @@ use vhdl_lang::ast::{ AnyDesignUnit, AnyPrimaryUnit, AttributeSpecification, Designator, DiscreteRange, ElementDeclaration, EntityClass, EntityDeclaration, - EntityName, Name, PackageDeclaration, Range, RangeConstraint, - SubtypeConstraint, TypeDeclaration, TypeDefinition, + EntityName, Name, PackageDeclaration, PackageInstantiation, Range, + RangeConstraint, SubtypeConstraint, TypeDeclaration, TypeDefinition, }; use crate::visitor::{Visitor, VisitorResult}; @@ -232,6 +232,15 @@ impl Visitor for VwSymbolFinder { self.symbols.push(VwSymbol::Package(name)); VisitorResult::Continue } + + fn visit_package_instance( + &mut self, + instance: &PackageInstantiation, + ) -> VisitorResult { + let name = instance.ident.tree.item.name_utf8(); + self.symbols.push(VwSymbol::Package(name)); + VisitorResult::Continue + } } fn get_fields(elements: &Vec) -> Vec { From 06476f0c64accbd40d580d08cc6589732520cdec Mon Sep 17 00:00:00 2001 From: HelloKayT Date: Thu, 19 Mar 2026 00:00:41 -0700 Subject: [PATCH 21/22] Use petgraph topological sort --- Cargo.lock | 36 ++++++++++++++++- Cargo.toml | 1 + vw-lib/Cargo.toml | 1 + vw-lib/src/lib.rs | 100 +++++++++++++++++----------------------------- 4 files changed, 74 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f657fe9..cbe3f2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -320,12 +320,24 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -385,6 +397,15 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -511,7 +532,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", ] [[package]] @@ -761,6 +782,18 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", + "serde", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1305,6 +1338,7 @@ dependencies = [ "git2", "glob", "netrc", + "petgraph", "prettyplease", "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 22bdc64..128c96f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,4 @@ camino = "1.1" netrc = "0.4" url = "2.5" glob = "0.3" +petgraph = "0.8.3" diff --git a/vw-lib/Cargo.toml b/vw-lib/Cargo.toml index bcb66ed..b96e057 100644 --- a/vw-lib/Cargo.toml +++ b/vw-lib/Cargo.toml @@ -21,6 +21,7 @@ camino.workspace = true netrc.workspace = true url.workspace = true glob.workspace = true +petgraph.workspace = true git2 = "0.18" vhdl_lang = "0.86" quote = "1" diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index 80694d2..b2e437c 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -36,6 +36,11 @@ use camino::{Utf8Path, Utf8PathBuf}; use serde::{Deserialize, Serialize}; use vhdl_lang::{VHDLParser, VHDLStandard}; +use petgraph::{ + algo::toposort, + graph::{DiGraph, NodeIndex}, +}; + use crate::mapping::{FileData, VwSymbol, VwSymbolFinder}; use crate::nvc_helpers::{run_nvc_analysis, run_nvc_elab, run_nvc_sim}; use crate::visitor::walk_design_file; @@ -1434,7 +1439,7 @@ pub fn sort_files_by_dependencies( } // Topological sort using Kahn's algorithm - let sorted = topological_sort(files.clone(), dependencies)?; + let sorted = topological_sort_files(files.clone(), dependencies)?; *files = sorted; Ok(()) @@ -1538,81 +1543,50 @@ fn analyze_file( Ok(file_finder.get_symbols().clone()) } -fn topological_sort( +fn topological_sort_files( files: Vec, dependencies: HashMap>, ) -> Result> { - let mut in_degree: HashMap = HashMap::new(); - let mut adj_list: HashMap> = HashMap::new(); + let mut dep_graph: DiGraph = DiGraph::default(); + let mut index_map: HashMap = HashMap::new(); - // Initialize in-degree and adjacency list + // initialize the nodes for file in &files { - in_degree.insert(file.clone(), 0); - adj_list.insert(file.clone(), Vec::new()); + let index = dep_graph.add_node(file.clone()); + index_map.insert(file.clone(), index); } - // Build the graph + // now add edges from files to their dependencies for (file, deps) in &dependencies { + let source_node = index_map.get(file).ok_or(VwError::Dependency { + message: format!( + "Index map somehow didn't contain file {:?}", + file + ), + })?; + // file depends on every dep in deps for dep in deps { - if files.contains(dep) { - adj_list - .get_mut(dep) - .ok_or(VwError::Dependency { - message: format!( - "Somehow adj list didn't contain dep {:?}", - dep - ), - })? - .push(file.clone()); - *in_degree.get_mut(file).ok_or(VwError::Dependency { - message: format!( - "Somehow in_degree didn't - contain {:?}", - file - ), - })? += 1; - } - } - } - - // Kahn's algorithm - let mut queue = VecDeque::new(); - let mut result = Vec::new(); - - // Add all nodes with in-degree 0 to queue - for (file, °ree) in &in_degree { - if degree == 0 { - queue.push_back(file.clone()); - } - } - - while let Some(current) = queue.pop_front() { - result.push(current.clone()); - - // For each neighbor of current - if let Some(neighbors) = adj_list.get(¤t) { - for neighbor in neighbors { - *in_degree.get_mut(neighbor).ok_or(VwError::Dependency { - message: format!( - "Somehow in_degree doesn't have neighbor {:?}", - neighbor - ), - })? -= 1; - - if in_degree[neighbor] == 0 { - queue.push_back(neighbor.clone()); - } - } + let dst_node = index_map.get(dep).ok_or(VwError::Dependency { + message: format!( + "Index map somehow didn't contain dep {:?}", + dep + ), + })?; + dep_graph.add_edge(*source_node, *dst_node, ()); } } - // Check for cycles - if result.len() != files.len() { - return Err(VwError::Dependency { - message: "Circular dependency detected in VHDL files".to_string(), - }); - } + // ok now topological sort + let ordered_files = + toposort(&dep_graph, None).map_err(|_| VwError::Dependency { + message: "Got circular dependency".to_string(), + })?; + let result: Vec = ordered_files + .iter() + .map(|&idx| dep_graph[idx].clone()) + .rev() + .collect(); Ok(result) } From 31ee5e999ebb40c79b4215b07281597c08beb34f Mon Sep 17 00:00:00 2001 From: Ryan Goodfellow Date: Fri, 20 Mar 2026 13:22:06 -0700 Subject: [PATCH 22/22] macos support --- vw-lib/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vw-lib/src/lib.rs b/vw-lib/src/lib.rs index b2e437c..a5b5291 100644 --- a/vw-lib/src/lib.rs +++ b/vw-lib/src/lib.rs @@ -2400,7 +2400,8 @@ async fn build_rust_library( })??; // Find the .so file in the workspace target directory (parent of testbench dir) - let lib_name = format!("lib{}.so", package_name.replace('-', "_")); + let ext = if cfg!(target_os = "macos") { "dylib" } else { "so" }; + let lib_name = format!("lib{}.{ext}", package_name.replace('-', "_")); let workspace_target = bench_dir.join("target").join("debug"); let lib_path = workspace_target.join(&lib_name);