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..825a89cba 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: @@ -26,13 +25,10 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust}} - components: rustfmt - name: Build run: cargo build --all-features - name: Cargo Test run: cargo test --workspace --all-features - - name: Format (fix with `cargo fmt`) - run: cargo fmt -- --check - name: Run unit-tests run: tests/run_all.sh shell: bash diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml new file mode 100644 index 000000000..f81cf6392 --- /dev/null +++ b/.github/workflows/formatting.yml @@ -0,0 +1,35 @@ +name: formatting + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +jobs: + rustfmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: rustfmt + - name: Check Rust formatting + run: cargo fmt -- --check + + clang-format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + - name: Check C/C++ formatting + uses: DoozyX/clang-format-lint-action@v0.18 + with: + source: "." + extensions: "h,hpp,c,cc,cpp,cxx" + exclude: "./crates/bender-slang/vendor" 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 7db0396f7..faf39b8f3 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"); @@ -97,7 +133,7 @@ fn main() { let compiler = std::env::var("CXX").unwrap_or_else(|_| "g++".to_string()); // We search for the static libstdc++ file using g++ let output = std::process::Command::new(&compiler) - .args(&["-print-file-name=libstdc++.a"]) + .args(["-print-file-name=libstdc++.a"]) .output() .expect("Failed to run g++"); diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 755690992..7e7c02eb7 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -4,8 +4,6 @@ #include "slang_bridge.h" #include "bender-slang/src/lib.rs.h" -#include "slang/diagnostics/DiagnosticEngine.h" -#include "slang/diagnostics/TextDiagnosticClient.h" #include "slang/syntax/CSTSerializer.h" #include "slang/syntax/SyntaxPrinter.h" #include "slang/syntax/SyntaxVisitor.h" @@ -17,7 +15,6 @@ #include using namespace slang; -using namespace slang::driver; using namespace slang::syntax; using namespace slang::parsing; diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index a309b5a95..faa4431d1 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -7,13 +7,13 @@ #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 #include -#include #include struct SlangPrintOpts; diff --git a/crates/bender-slang/tests/basic.rs b/crates/bender-slang/tests/basic.rs new file mode 100644 index 000000000..03d45538f --- /dev/null +++ b/crates/bender-slang/tests/basic.rs @@ -0,0 +1,37 @@ +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"), + } +} 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..307b8bf2b --- /dev/null +++ b/tests/pickle.rs @@ -0,0 +1,75 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +#[cfg(feature = "slang")] +mod tests { + use assert_cmd::cargo; + + fn run_pickle(args: &[&str]) -> String { + 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) + ); + + String::from_utf8(out.stdout).expect("stdout must be utf-8") + } + + #[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", + ]); + + assert!(renamed.contains("module p_top_s (")); + assert!(renamed.contains("module p_core_s;")); + assert!(renamed.contains("module p_leaf_s;")); + } + + #[test] + fn pickle_exclude_rename_keeps_selected_names() { + let renamed = run_pickle(&[ + "--target", + "top", + "--top", + "top", + "--prefix", + "p_", + "--suffix", + "_s", + "--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;")); + } +} 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..c5724f952 --- /dev/null +++ b/tests/pickle/Bender.yml @@ -0,0 +1,19 @@ +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 + + - 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..a041281aa --- /dev/null +++ b/tests/pickle/include/macros.svh @@ -0,0 +1,6 @@ +// Simple macro to test if includes are resolved correctly +`define LOG(msg) \ + $display("[LOG]: %s", msg); + +// 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..ce4f14c49 --- /dev/null +++ b/tests/pickle/src/core.sv @@ -0,0 +1,3 @@ +module core; + 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/top.sv b/tests/pickle/src/top.sv new file mode 100644 index 000000000..2365ab895 --- /dev/null +++ b/tests/pickle/src/top.sv @@ -0,0 +1,39 @@ +`include "macros.svh" + +import common_pkg::*; + +module top ( + input logic clk, + input logic rst_n +); + + core 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; + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + current_state <= Idle; + end else begin + current_state <= Busy; + 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