diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..4e0c9e095 --- /dev/null +++ b/.clang-format @@ -0,0 +1,18 @@ +--- +Language: Cpp +BasedOnStyle: LLVM + +# 4 spaces everywhere +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +ContinuationIndentWidth: 4 + +# Modern C++ style +Standard: c++20 +ColumnLimit: 120 +PointerAlignment: Left + +# Organize includes +SortIncludes: true +IncludeBlocks: Regroup diff --git a/.clangd b/.clangd new file mode 100644 index 000000000..e1f5bf28a --- /dev/null +++ b/.clangd @@ -0,0 +1,15 @@ +If: + PathMatch: (^|.*/)crates/bender-slang/cpp/.*\.(h|hpp|hh|c|cc|cpp|cxx)$ +CompileFlags: + Add: + - -std=c++20 + - -fno-cxx-modules + - -I. + - -I../../../crates + - -I../vendor/slang/include + - -I../vendor/slang/external + - -I../../../target/slang-generated-include + - -I../../../target/cxxbridge + - -DSLANG_USE_MIMALLOC=1 + - -DSLANG_USE_THREADS=1 + - -DSLANG_BOOST_SINGLE_HEADER=1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4420a4f4e..f438eb44a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: [master] pull_request: - branches: [master] workflow_dispatch: jobs: diff --git a/.gitignore b/.gitignore index 796da3f24..eeeddced9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ -.* -!/.ci/ -!.git* -!.travis.yml -/target -/tests/tmp +# Cargo build files +target + +# Temporary test files +tests/**/tmp +tests/**/.bender + +# clangd +.cache/clangd diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 4244eb209..1064edd84 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -1,6 +1,37 @@ // Copyright (c) 2025 ETH Zurich // Tim Fischer +#[cfg(unix)] +// We create a symlink from the generated include directory to a stable location in the target directory +// so that tools like clangd can find the headers without needing to know the exact OUT_DIR path. +// This is purely for improving the development experience and is not necessary for the build itself. +fn refresh_include_symlink(generated_include_dir: &std::path::Path) { + use std::ffi::OsStr; + use std::fs; + use std::os::unix::fs::symlink; + use std::path::PathBuf; + + let Ok(out_dir) = std::env::var("OUT_DIR") else { + return; + }; + let out_dir = PathBuf::from(out_dir); + + let Some(target_root) = out_dir + .ancestors() + .find(|path| path.file_name() == Some(OsStr::new("target"))) + else { + return; + }; + + let stable_link = target_root.join("slang-generated-include"); + let _ = fs::remove_file(&stable_link); + let _ = fs::remove_dir_all(&stable_link); + let _ = symlink(generated_include_dir, &stable_link); +} + +#[cfg(not(unix))] +fn refresh_include_symlink(_generated_include_dir: &std::path::Path) {} + fn main() { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap(); @@ -64,6 +95,11 @@ fn main() { let dst = slang_lib.build(); let lib_dir = dst.join("lib"); + // Create a symlink for the generated include directory + if target_os == "linux" || target_os == "macos" { + refresh_include_symlink(&dst.join("include")); + } + // Configure Linker to find Slang static library println!("cargo:rustc-link-search=native={}", lib_dir.display()); println!("cargo:rustc-link-lib=static=svlang"); diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index 240eff336..ac4233636 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -7,8 +7,9 @@ #include "rust/cxx.h" #include "slang/diagnostics/DiagnosticEngine.h" #include "slang/diagnostics/TextDiagnosticClient.h" -#include "slang/driver/Driver.h" +#include "slang/parsing/Preprocessor.h" #include "slang/syntax/SyntaxTree.h" +#include "slang/text/SourceManager.h" #include #include diff --git a/crates/bender-slang/tests/basic.rs b/crates/bender-slang/tests/basic.rs new file mode 100644 index 000000000..5b405f26e --- /dev/null +++ b/crates/bender-slang/tests/basic.rs @@ -0,0 +1,96 @@ +use std::path::PathBuf; + +fn fixture_path(rel: &str) -> String { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("tests/pickle") + .join(rel) + .canonicalize() + .expect("valid fixture path") + .to_string_lossy() + .into_owned() +} + +#[test] +fn parse_valid_file_succeeds() { + let mut session = bender_slang::SlangSession::new(); + let files = vec![fixture_path("src/top.sv")]; + let includes = vec![fixture_path("include")]; + let defines = vec![]; + assert!(session.parse_group(&files, &includes, &defines).is_ok()); + assert_eq!(session.tree_count(), 1); +} + +#[test] +fn parse_invalid_file_returns_parse_error() { + let mut session = bender_slang::SlangSession::new(); + let files = vec![fixture_path("src/broken.sv")]; + let includes = vec![]; + let defines = vec![]; + let result = session.parse_group(&files, &includes, &defines); + + match result { + Err(bender_slang::SlangError::ParseGroup { .. }) => {} + Err(other) => panic!("expected SlangError::ParseGroup, got {other}"), + Ok(_) => panic!("expected parse to fail"), + } +} + +#[test] +fn rewriter_build_from_trees_is_repeatable() { + let mut session = bender_slang::SlangSession::new(); + let files = vec![fixture_path("src/top.sv")]; + let includes = vec![fixture_path("include")]; + let defines = vec![]; + session + .parse_group(&files, &includes, &defines) + .expect("parse should succeed"); + + let trees = session.all_trees().expect("tree collection should succeed"); + let mut rewriter_once = bender_slang::SyntaxTreeRewriter::new(); + rewriter_once.set_prefix("p_"); + rewriter_once.set_suffix("_s"); + let first_pass_trees: Vec<_> = trees + .iter() + .map(|t| rewriter_once.rewrite_declarations(t)) + .collect(); + let renamed_once = rewriter_once.rewrite_references( + first_pass_trees + .first() + .expect("one first-pass tree expected"), + ); + assert!( + renamed_once + .display(bender_slang::SlangPrintOpts { + expand_macros: false, + include_directives: true, + include_comments: true, + squash_newlines: false, + }) + .contains("module p_top_s (") + ); + + // Rebuilding with the same trees should remain stable. + let mut rewriter_twice = bender_slang::SyntaxTreeRewriter::new(); + rewriter_twice.set_prefix("p_"); + rewriter_twice.set_suffix("_s"); + let first_pass_trees: Vec<_> = trees + .iter() + .map(|t| rewriter_twice.rewrite_declarations(t)) + .collect(); + let renamed_twice = rewriter_twice.rewrite_references( + first_pass_trees + .first() + .expect("one first-pass tree expected"), + ); + assert!( + renamed_twice + .display(bender_slang::SlangPrintOpts { + expand_macros: false, + include_directives: true, + include_comments: true, + squash_newlines: false, + }) + .contains("module p_top_s (") + ); +} diff --git a/tests/cli_regression.rs b/tests/cli_regression.rs index c75b37bfe..66f652eeb 100644 --- a/tests/cli_regression.rs +++ b/tests/cli_regression.rs @@ -161,5 +161,8 @@ regression_tests! { packages: &["packages"], packages_graph: &["packages", "--graph"], packages_flat: &["packages", "--flat"], + // Enable once the golden binary is built with `slang` support. + // pickle_basic: &["pickle", "--target", "top"], + // pickle_top_trim: &["pickle", "--target", "top", "--top", "top"], } diff --git a/tests/pickle.rs b/tests/pickle.rs new file mode 100644 index 000000000..912e2cdd7 --- /dev/null +++ b/tests/pickle.rs @@ -0,0 +1,185 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +#[cfg(feature = "slang")] +mod tests { + use assert_cmd::cargo; + fn run_pickle_output(args: &[&str]) -> std::process::Output { + let mut full_args = vec!["-d", "tests/pickle", "pickle"]; + full_args.extend(args); + + let out = cargo::cargo_bin_cmd!() + .args(&full_args) + .output() + .expect("Failed to execute bender binary"); + + assert!( + out.status.success(), + "pickle command failed.\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + + out + } + + fn run_pickle(args: &[&str]) -> String { + let out = run_pickle_output(args); + String::from_utf8(out.stdout).expect("stdout must be utf-8") + } + + fn run_pickle_fail(args: &[&str]) -> std::process::Output { + let mut full_args = vec!["-d", "tests/pickle", "pickle"]; + full_args.extend(args); + + let out = cargo::cargo_bin_cmd!() + .args(&full_args) + .output() + .expect("Failed to execute bender binary"); + + assert!( + !out.status.success(), + "pickle command unexpectedly succeeded.\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + + out + } + + #[test] + fn pickle_top_trim_filters_unreachable_modules() { + let full = run_pickle(&["--target", "top"]); + assert!(full.contains("module unused_top;")); + assert!(full.contains("module unused_leaf;")); + + let trimmed = run_pickle(&["--target", "top", "--top", "top"]); + assert!(trimmed.contains("module top (")); + assert!(trimmed.contains("module core #(")); + assert!(trimmed.contains("module leaf;")); + assert!(!trimmed.contains("module unused_top;")); + assert!(!trimmed.contains("module unused_leaf;")); + } + + #[test] + fn pickle_rename_applies_prefix_and_suffix() { + let renamed = run_pickle(&[ + "--target", + "top", + "--top", + "top", + "--prefix", + "p_", + "--suffix", + "_s", + "--expand-macros", + ]); + + assert!(renamed.contains("module p_top_s (")); + assert!(renamed.contains("module p_core_s #(")); + assert!(renamed.contains("module p_leaf_s;")); + assert!(renamed.contains("package p_common_pkg_s;")); + assert!(renamed.contains("interface p_bus_intf_s #(")); + assert!(renamed.contains("virtual p_bus_intf_s v_if_handle;")); + assert!(renamed.contains("p_common_pkg_s::is_error(current_state);")); + assert!(!renamed.contains("`define PKG_IS_ERROR")); + assert!(!renamed.contains("`define LOG")); + } + + #[test] + fn pickle_exclude_rename_keeps_selected_names() { + let renamed = run_pickle(&[ + "--target", + "top", + "--top", + "top", + "--prefix", + "p_", + "--suffix", + "_s", + "--expand-macros", + "--exclude-rename", + "top", + "--exclude-rename", + "core", + ]); + + assert!(renamed.contains("module top (")); + assert!(renamed.contains("module core #(")); + assert!(renamed.contains("module p_leaf_s;")); + assert!(!renamed.contains("module p_top_s (")); + assert!(!renamed.contains("module p_core_s")); + } + + #[test] + fn pickle_rename_keeps_undefined_references() { + let renamed = run_pickle(&[ + "--target", + "top", + "--prefix", + "p_", + "--suffix", + "_s", + "--expand-macros", + ]); + + assert!(renamed.contains("module p_top_s (")); + assert!(renamed.contains("undefined_mod u_ext_mod();")); + assert!(renamed.contains("virtual undefined_intf ext_if;")); + assert!(renamed.contains("undefined_pkg::undefined_t ext_state;")); + assert!(!renamed.contains("p_undefined_mod_s")); + assert!(!renamed.contains("p_undefined_intf_s")); + assert!(!renamed.contains("p_undefined_pkg_s")); + } + + #[test] + fn pickle_rename_renames_scoped_params() { + let renamed = run_pickle(&["--prefix", "p_", "--suffix", "_s", "--expand-macros"]); + + // Both the scoped type and the scoped default value in parameters must be renamed. + assert!( + renamed + .contains("parameter p_common_pkg_s::state_t DefaultState = p_common_pkg_s::Idle") + ); + // The original (un-renamed) package name must not appear in any parameter. + assert!(!renamed.contains("parameter common_pkg::state_t")); + assert!(!renamed.contains("common_pkg::Idle")); + } + + #[test] + fn pickle_rename_renames_scoped_instantiation_params() { + let renamed = run_pickle(&[ + "--target", + "top", + "--prefix", + "p_", + "--suffix", + "_s", + "--expand-macros", + ]); + + // Scoped values used in module parameter overrides must also be renamed. + assert!(renamed.contains(".DefaultState(p_common_pkg_s::Error)")); + assert!(!renamed.contains(".DefaultState(common_pkg::Error)")); + } + + #[test] + fn pickle_rename_renames_named_end_label() { + let renamed = run_pickle(&["--prefix", "p_", "--suffix", "_s", "--expand-macros"]); + + // Both the module declaration and the named end label must be renamed. + assert!(renamed.contains("module p_named_end_s;")); + assert!(renamed.contains("endmodule : p_named_end_s")); + // The original name must not appear in any end label. + assert!(!renamed.contains("endmodule : named_end")); + } + + #[test] + fn pickle_rename_requires_expand_macros() { + let out = run_pickle_fail(&["--target", "top", "--prefix", "p_", "--suffix", "_s"]); + let stderr = String::from_utf8(out.stderr).expect("stderr must be utf-8"); + + assert!(stderr.contains("--expand-macros")); + assert!(stderr.contains("--prefix")); + } +} diff --git a/tests/pickle/Bender.lock b/tests/pickle/Bender.lock new file mode 100644 index 000000000..c33c0b6df --- /dev/null +++ b/tests/pickle/Bender.lock @@ -0,0 +1 @@ +packages: {} diff --git a/tests/pickle/Bender.yml b/tests/pickle/Bender.yml new file mode 100644 index 000000000..7373ef379 --- /dev/null +++ b/tests/pickle/Bender.yml @@ -0,0 +1,20 @@ +package: + name: pickle_repo + +sources: + - defines: + ENABLE_LOGGING: 1 + files: + - src/common_pkg.sv + - src/bus_intf.sv + - src/leaf.sv + - src/core.sv + - src/unused_leaf.sv + - src/unused_top.sv + - src/named_end.sv + + - target: top + include_dirs: + - include + files: + - src/top.sv diff --git a/tests/pickle/include/macros.svh b/tests/pickle/include/macros.svh new file mode 100644 index 000000000..925be4fd7 --- /dev/null +++ b/tests/pickle/include/macros.svh @@ -0,0 +1,10 @@ +// Simple macro to test if includes are resolved correctly +`define LOG(msg) \ + $display("[LOG]: %s", msg); + +// Macro that references a package symbol; pickle renaming should update this. +`define PKG_IS_ERROR(sig) \ + common_pkg::is_error(sig) + +// A constant used in the RTL +localparam int unsigned DataWidth = 32; diff --git a/tests/pickle/src/broken.sv b/tests/pickle/src/broken.sv new file mode 100644 index 000000000..0fbcdfa50 --- /dev/null +++ b/tests/pickle/src/broken.sv @@ -0,0 +1,2 @@ +module broken(; +endmodule diff --git a/tests/pickle/src/bus_intf.sv b/tests/pickle/src/bus_intf.sv new file mode 100644 index 000000000..bcd581028 --- /dev/null +++ b/tests/pickle/src/bus_intf.sv @@ -0,0 +1,21 @@ +interface bus_intf #( + parameter int Width = 32 +) ( + input logic clk +); + logic [Width-1:0] addr; + logic [Width-1:0] data; + logic valid; + logic ready; + + modport master ( + output addr, data, valid, + input ready + ); + + modport slave ( + input addr, data, valid, + output ready + ); + +endinterface diff --git a/tests/pickle/src/common_pkg.sv b/tests/pickle/src/common_pkg.sv new file mode 100644 index 000000000..7a2d02d59 --- /dev/null +++ b/tests/pickle/src/common_pkg.sv @@ -0,0 +1,13 @@ +package common_pkg; + + typedef enum logic [1:0] { + Idle = 2'b00, + Busy = 2'b01, + Error = 2'b11 + } state_t; + + function automatic logic is_error(state_t s); + return s == Error; + endfunction + +endpackage diff --git a/tests/pickle/src/core.sv b/tests/pickle/src/core.sv new file mode 100644 index 000000000..30c0baa4e --- /dev/null +++ b/tests/pickle/src/core.sv @@ -0,0 +1,5 @@ +module core #( + parameter common_pkg::state_t DefaultState = common_pkg::Idle +) (); + leaf u_leaf(); +endmodule diff --git a/tests/pickle/src/leaf.sv b/tests/pickle/src/leaf.sv new file mode 100644 index 000000000..5a7a547a2 --- /dev/null +++ b/tests/pickle/src/leaf.sv @@ -0,0 +1,2 @@ +module leaf; +endmodule diff --git a/tests/pickle/src/named_end.sv b/tests/pickle/src/named_end.sv new file mode 100644 index 000000000..0cf1144d3 --- /dev/null +++ b/tests/pickle/src/named_end.sv @@ -0,0 +1,3 @@ +// Module with a named end label to test that `endmodule : name` is also renamed. +module named_end; +endmodule : named_end diff --git a/tests/pickle/src/top.sv b/tests/pickle/src/top.sv new file mode 100644 index 000000000..24b5f7cae --- /dev/null +++ b/tests/pickle/src/top.sv @@ -0,0 +1,49 @@ +`include "macros.svh" + +import common_pkg::*; + +module top ( + input logic clk, + input logic rst_n +); + + core #( + .DefaultState(common_pkg::Error) + ) u_core(); + + // Interface Instantiation + bus_intf #(.WIDTH(DATA_WIDTH)) axi_bus ( + .clk(clk) + ); + + // Virtual Interface Type + virtual bus_intf v_if_handle; + + initial begin + v_if_handle = axi_bus; + +`ifdef ENABLE_LOGGING + `LOG("TopModule started successfully!") +`endif + end + + // Type Usage from Package (state_t) + common_pkg::state_t current_state; + logic macro_error; + + // Undefined dependency references must not be renamed. + undefined_pkg::undefined_t ext_state; + undefined_mod u_ext_mod(); + virtual undefined_intf ext_if; + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + current_state <= Idle; + macro_error <= 1'b0; + end else begin + current_state <= Busy; + macro_error <= `PKG_IS_ERROR(current_state); + end + end + +endmodule diff --git a/tests/pickle/src/unused_leaf.sv b/tests/pickle/src/unused_leaf.sv new file mode 100644 index 000000000..f7d261d00 --- /dev/null +++ b/tests/pickle/src/unused_leaf.sv @@ -0,0 +1,2 @@ +module unused_leaf; +endmodule diff --git a/tests/pickle/src/unused_top.sv b/tests/pickle/src/unused_top.sv new file mode 100644 index 000000000..a62e36504 --- /dev/null +++ b/tests/pickle/src/unused_top.sv @@ -0,0 +1,3 @@ +module unused_top; + unused_leaf u_unused_leaf(); +endmodule