diff --git a/.cargo/config.toml.iis b/.cargo/config.toml.iis new file mode 100644 index 000000000..4b427418a --- /dev/null +++ b/.cargo/config.toml.iis @@ -0,0 +1,7 @@ +[target.x86_64-unknown-linux-gnu] +linker = "/usr/pack/gcc-14.2.0-af/bin/gcc" +rustflags = ["-C", "link-arg=-Wl,-rpath,/usr/pack/gcc-14.2.0-af/lib64"] + +[env] +CC = "/usr/pack/gcc-14.2.0-af/bin/gcc" +CXX = "/usr/pack/gcc-14.2.0-af/bin/g++" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84d2fa64e..4420a4f4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,15 +20,17 @@ jobs: - 1.87.0 # minimum supported version continue-on-error: ${{ matrix.rust == 'nightly' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust}} components: rustfmt - name: Build - run: cargo build + run: cargo build --all-features - name: Cargo Test - run: cargo test --all + run: cargo test --workspace --all-features - name: Format (fix with `cargo fmt`) run: cargo fmt -- --check - name: Run unit-tests @@ -38,14 +40,16 @@ jobs: test-windows: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable with: toolchain: stable - name: Build - run: cargo build + run: cargo build --all-features - name: Cargo Test - run: cargo test --all + run: cargo test --workspace --all-features - name: Run unit-tests run: tests/run_all.sh shell: bash @@ -53,14 +57,16 @@ jobs: test-macos: runs-on: macos-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable with: toolchain: stable - name: Build - run: cargo build + run: cargo build --all-features - name: Cargo Test - run: cargo test --all + run: cargo test --workspace --all-features - name: Run unit-tests run: tests/run_all.sh shell: bash @@ -69,7 +75,9 @@ jobs: name: Clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable with: toolchain: stable @@ -80,7 +88,7 @@ jobs: name: Unused Dependencies runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: toolchain: stable diff --git a/.github/workflows/cli_regression.yml b/.github/workflows/cli_regression.yml index a03e8f02e..bfdcf9bd7 100644 --- a/.github/workflows/cli_regression.yml +++ b/.github/workflows/cli_regression.yml @@ -8,35 +8,41 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable with: toolchain: stable - name: Run CLI Regression - run: cargo test --test cli_regression -- --ignored + run: cargo test --all-features --test cli_regression -- --ignored env: BENDER_TEST_GOLDEN_BRANCH: ${{ github.base_ref }} test-windows: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable with: toolchain: stable - name: Run CLI Regression - run: cargo test --test cli_regression -- --ignored + run: cargo test --all-features --test cli_regression -- --ignored env: BENDER_TEST_GOLDEN_BRANCH: ${{ github.base_ref }} test-macos: runs-on: macos-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable with: toolchain: stable - name: Run CLI Regression - run: cargo test --test cli_regression -- --ignored + run: cargo test --all-features --test cli_regression -- --ignored env: BENDER_TEST_GOLDEN_BRANCH: ${{ github.base_ref }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f7d81632e..2df026bd0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -76,7 +76,7 @@ jobs: -v "$GITHUB_WORKSPACE/target/$platform/$tgtname:/source/target" \ --platform $full_platform \ $tgtname-$platform \ - cargo build --release; + cargo build --release --all-features; shell: bash - name: OS Create Package run: | @@ -121,7 +121,7 @@ jobs: -v "$GITHUB_WORKSPACE/target/$platform/$tgtname:/source/target" \ --platform $full_platform \ $tgtname-$platform \ - cargo build --release; + cargo build --release --all-features; shell: bash - name: OS Create Package run: | @@ -170,7 +170,7 @@ jobs: -v "$GITHUB_WORKSPACE/target/amd64:/source/target" \ --platform linux/amd64 \ manylinux-amd64 \ - cargo build --release; + cargo build --release --all-features; - name: GNU Create Package run: .github/scripts/package.sh amd64 shell: bash @@ -215,7 +215,7 @@ jobs: -v "$GITHUB_WORKSPACE/target/arm64:/source/target" \ --platform linux/arm64 \ manylinux-arm64 \ - cargo build --release; + cargo build --release --all-features; - name: GNU Create Package run: .github/scripts/package.sh arm64 shell: bash @@ -240,7 +240,7 @@ jobs: rustup target add aarch64-apple-darwin cargo install universal2 - name: MacOS Build - run: cargo-universal2 --release + run: cargo-universal2 --release --all-features - name: Get Artifact Name run: | if [[ "$GITHUB_REF" =~ ^refs/tags/v.*$ ]]; then \ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..cccf606d2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "crates/bender-slang/vendor/slang"] + path = crates/bender-slang/vendor/slang + url = https://github.com/MikePopoloski/slang.git diff --git a/CHANGELOG.md b/CHANGELOG.md index e0af3692c..d4ae77144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## Unreleased +### Added +- Add new `crates/bender-slang` crate that integrates the vendored Slang parser via a Rust/C++ bridge. +- Add new `pickle` command (behind feature `slang`) to parse and re-emit SystemVerilog sources. ## 0.31.0 - 2026-03-03 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 0c5a06ab3..718605220 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,7 @@ version = "0.31.0" dependencies = [ "assert_cmd", "async-recursion", + "bender-slang", "blake2", "clap", "clap_complete", @@ -143,6 +144,17 @@ dependencies = [ "walkdir", ] +[[package]] +name = "bender-slang" +version = "0.1.0" +dependencies = [ + "cmake", + "cxx", + "cxx-build", + "dunce", + "thiserror", +] + [[package]] name = "bitflags" version = "2.11.0" @@ -288,6 +300,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width 0.2.2", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -357,6 +389,68 @@ dependencies = [ "typenum", ] +[[package]] +name = "cxx" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e" +dependencies = [ + "cc", + "cxx-build", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash 0.2.0", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.194" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf" +dependencies = [ + "indexmap", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "deunicode" version = "1.6.2" @@ -459,6 +553,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "futures" version = "0.3.32" @@ -617,7 +717,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -799,6 +899,15 @@ dependencies = [ "libc", ] +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1232,6 +1341,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + [[package]] name = "semver" version = "1.0.27" @@ -1459,6 +1574,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "termtree" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index fb15ecdb6..08930c63f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,12 @@ license = "Apache-2.0 OR MIT" edition = "2024" rust-version = "1.87.0" +[workspace] +members = ["crates/bender-slang"] + [dependencies] +bender-slang = { path = "crates/bender-slang", optional = true} + serde = { version = "1", features = ["derive"] } serde_yaml_ng = "0.10" serde_json = "1" @@ -49,3 +54,6 @@ dunce = "1.0.4" [dev-dependencies] assert_cmd = "2.1.1" pretty_assertions = "1.4" + +[features] +slang = ["dep:bender-slang"] diff --git a/README.md b/README.md index 41ed841fd..044f641fa 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,12 @@ cargo install bender ``` If you need a specific version of Bender (e.g., `0.21.0`), append ` --version 0.21.0` to that command. +To enable optional features (including the Slang-backed `pickle` command), install with: +```sh +cargo install bender --all-features +``` +This may increase build time and additional build dependencies. + To install Bender system-wide, you can simply copy the binary you have obtained from one of the above methods to one of the system directories on your `PATH`. Even better, some Linux distributions have Bender in their repositories. We are currently aware of: ### [ArchLinux ![aur-shield](https://img.shields.io/aur/version/bender)][aur-bender] @@ -553,6 +559,37 @@ Supported formats: Furthermore, similar flags to the `sources` command exist. +### `pickle` --- Parse and rewrite SystemVerilog sources with Slang + +The `bender pickle` command parses SystemVerilog sources with Slang and prints the resulting source again. It supports optional renaming and trimming of unreachable files for specified top modules. + +This command is only available when Bender is built with Slang support (for example via `cargo install bender --all-features`). + +Useful options: +- `--top `: Trim output to files reachable from one or more top modules. +- `--prefix ` / `--suffix `: Add a prefix and/or suffix to renamed symbols. +- `--exclude-rename `: Exclude specific symbols from renaming. +- `--ast-json`: Emit AST JSON instead of source code. +- `--expand-macros`, `--strip-comments`, `--squash-newlines`: Control output formatting. +- `-I `, `-D `: Add extra include directories and preprocessor defines. + +Furthermore, similar flags to the `sources` and `script` command exist: + +- `-t`/`--target`: Enable specific targets. +- `-p`/`--package`: Specify package to show sources for. +- `-e`/`--exclude`: Specify package to exclude from sources. +- `-n`/`--no-deps`: Exclude all dependencies, i.e. only top level or specified package(s). + +Examples: + +```sh +# Keep only files reachable from top module `top`. +bender pickle --top my_top + +# Rename symbols, but keep selected names unchanged. +bender pickle --top my_top --prefix p_ --suffix _s --exclude-rename my_top +``` + ### `update` --- Re-resolve dependencies diff --git a/crates/bender-slang/Cargo.toml b/crates/bender-slang/Cargo.toml new file mode 100644 index 000000000..bdf157918 --- /dev/null +++ b/crates/bender-slang/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "bender-slang" +version = "0.1.0" +edition = "2024" + +[dependencies] +cxx = "1.0.194" +thiserror = "2.0.12" + +[target.'cfg(windows)'.dependencies] +dunce = "1.0.4" + +[build-dependencies] +cmake = "0.1.57" +cxx-build = "1.0.194" diff --git a/crates/bender-slang/README.md b/crates/bender-slang/README.md new file mode 100644 index 000000000..a873debfa --- /dev/null +++ b/crates/bender-slang/README.md @@ -0,0 +1,19 @@ +# bender-slang + +`bender-slang` provides the C++ bridge between `bender` and the [Slang](https://github.com/MikePopoloski/slang) parser infrastructure, included as a submodule. + +It is used by Bender's optional Slang-backed features, most notably the `pickle` command. + +## IIS Environment Setup + +In the IIS environment on Linux, a newer GCC toolchain is required to build `bender-slang`. Simply copy the provided Cargo configuration file to use the appropriate toolchain: + +```sh +cp .cargo/config.toml.iis .cargo/config.toml +``` + +Then, you can build or install bender with the usual Cargo command: + +```sh +cargo install --path . --features slang +``` diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs new file mode 100644 index 000000000..4244eb209 --- /dev/null +++ b/crates/bender-slang/build.rs @@ -0,0 +1,113 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +fn main() { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap(); + let build_profile = std::env::var("PROFILE").unwrap(); + let cmake_profile = match (target_env.as_str(), build_profile.as_str()) { + // Rust MSVC links against the release CRT; + // using C++ Debug CRT (/MDd) causes LNK2038 mismatches. + ("msvc", _) => "RelWithDebInfo", + (_, "debug") => "Debug", + _ => "Release", + }; + + // Create the configuration builder + let mut slang_lib = cmake::Config::new("vendor/slang"); + + // Common defines to give to both Slang and the Bridge + // Note: It is very important to provide the same defines and flags + // to both the Slang library build and the C++ bridge build to avoid + // ABI incompatibilities. Otherwise, this will cause segfaults at runtime. + let mut common_cxx_defines = vec![ + ("SLANG_USE_MIMALLOC", "1"), + ("SLANG_USE_THREADS", "1"), + ("SLANG_BOOST_SINGLE_HEADER", "1"), + ]; + + // Add debug define if in debug build + if build_profile == "debug" && (target_env != "msvc") { + common_cxx_defines.push(("SLANG_DEBUG", "1")); + common_cxx_defines.push(("SLANG_ASSERT_ENABLED", "1")); + }; + + // Common compiler flags + let common_cxx_flags = if target_env == "msvc" { + vec!["/std:c++20", "/EHsc", "/utf-8"] + } else { + vec!["-std=c++20"] + }; + + // Apply cmake configuration for Slang library + slang_lib + .define("SLANG_INCLUDE_TESTS", "OFF") + .define("SLANG_INCLUDE_TOOLS", "OFF") + // Forces installation into 'lib' instead of 'lib64' on some systems. + .define("CMAKE_INSTALL_LIBDIR", "lib") + // Disable finding system-installed packages, we want to fetch and build them from source. + .define("CMAKE_DISABLE_FIND_PACKAGE_fmt", "ON") + .define("CMAKE_DISABLE_FIND_PACKAGE_mimalloc", "ON") + .define("CMAKE_DISABLE_FIND_PACKAGE_Boost", "ON") + .profile(cmake_profile); + + // Apply common defines and flags + for (def, value) in common_cxx_defines.iter() { + slang_lib.define(def, *value); + slang_lib.cxxflag(format!("-D{}={}", def, value)); + } + for flag in common_cxx_flags.iter() { + slang_lib.cxxflag(flag); + } + + // Build the slang library + let dst = slang_lib.build(); + let lib_dir = dst.join("lib"); + + // Configure Linker to find Slang static library + println!("cargo:rustc-link-search=native={}", lib_dir.display()); + println!("cargo:rustc-link-lib=static=svlang"); + + // Link the additional libraries based on build profile. + let (fmt_lib, mimalloc_lib) = match (target_env.as_str(), build_profile.as_str()) { + ("msvc", _) => ("fmt", "mimalloc"), + (_, "debug") => ("fmtd", "mimalloc-debug"), + _ => ("fmt", "mimalloc"), + }; + + println!("cargo:rustc-link-lib=static={fmt_lib}"); + println!("cargo:rustc-link-lib=static={mimalloc_lib}"); + + if target_os == "windows" { + println!("cargo:rustc-link-lib=advapi32"); + } + + // Compile the C++ Bridge + let mut bridge_build = cxx_build::bridge("src/lib.rs"); + bridge_build + .file("cpp/session.cpp") + .file("cpp/rewriter.cpp") + .file("cpp/print.cpp") + .file("cpp/analysis.cpp") + .flag_if_supported("-std=c++20") + .include("vendor/slang/include") + .include("vendor/slang/external") + .include(dst.join("include")); + + // Apply common defines and flags to the bridge build as well + for (def, value) in common_cxx_defines.iter() { + bridge_build.define(def, *value); + } + for flag in common_cxx_flags.iter() { + bridge_build.flag(flag); + } + + bridge_build.compile("slang-bridge"); + + println!("cargo:rerun-if-changed=src/lib.rs"); + println!("cargo:rerun-if-changed=cpp/slang_bridge.h"); + println!("cargo:rerun-if-changed=cpp/session.cpp"); + println!("cargo:rerun-if-changed=cpp/rewriter.cpp"); + println!("cargo:rerun-if-changed=cpp/print.cpp"); + println!("cargo:rerun-if-changed=cpp/analysis.cpp"); +} diff --git a/crates/bender-slang/cpp/analysis.cpp b/crates/bender-slang/cpp/analysis.cpp new file mode 100644 index 000000000..c1989b5d9 --- /dev/null +++ b/crates/bender-slang/cpp/analysis.cpp @@ -0,0 +1,77 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +#include "slang_bridge.h" + +#include +#include +#include +#include + +using namespace slang; + +rust::Vec reachable_tree_indices(const SlangSession& session, const rust::Vec& tops) { + const auto& treeVec = session.trees(); + + // Build a mapping from declared symbol names to the index of the tree that + // declares them. + std::unordered_map nameToTreeIndex; + for (size_t i = 0; i < treeVec.size(); ++i) { + const auto& metadata = treeVec[i]->getMetadata(); + for (auto name : metadata.getDeclaredSymbols()) { + nameToTreeIndex.emplace(name, i); + } + } + + // Build a dependency graph where each tree points to the trees that declare + // symbols it references. + std::vector> deps(treeVec.size()); + for (size_t i = 0; i < treeVec.size(); ++i) { + const auto& metadata = treeVec[i]->getMetadata(); + std::unordered_set seen; + for (auto ref : metadata.getReferencedSymbols()) { + auto it = nameToTreeIndex.find(ref); + // Avoid duplicate dependencies in case of multiple references to the same + // symbol. + if (it != nameToTreeIndex.end() && seen.insert(it->second).second) { + deps[i].push_back(it->second); + } + } + } + + // Map the top module names to their corresponding tree indices. + std::vector startIndices; + startIndices.reserve(tops.size()); + for (const auto& top : tops) { + std::string_view name(top.data(), top.size()); + auto it = nameToTreeIndex.find(name); + if (it == nameToTreeIndex.end()) { + throw std::runtime_error("Top module not found in any parsed source file: " + std::string(name)); + } + startIndices.push_back(it->second); + } + + // Perform a DFS from the top modules to find all reachable trees. + std::vector reachable(treeVec.size(), false); + std::function dfs = [&](size_t index) { + if (reachable[index]) { + return; + } + reachable[index] = true; + for (auto dep : deps[index]) { + dfs(dep); + } + }; + + for (auto start : startIndices) { + dfs(start); + } + + rust::Vec result; + for (size_t i = 0; i < reachable.size(); ++i) { + if (reachable[i]) { + result.push_back(static_cast(i)); + } + } + return result; +} diff --git a/crates/bender-slang/cpp/print.cpp b/crates/bender-slang/cpp/print.cpp new file mode 100644 index 000000000..d50f2e5af --- /dev/null +++ b/crates/bender-slang/cpp/print.cpp @@ -0,0 +1,39 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +#include "bender-slang/src/lib.rs.h" +#include "slang/syntax/CSTSerializer.h" +#include "slang/syntax/SyntaxPrinter.h" +#include "slang/text/Json.h" +#include "slang_bridge.h" + +using namespace slang; +using namespace slang::syntax; + +using std::shared_ptr; + +// Prints the given syntax tree back to SystemVerilog source code, +// with options to control the printing behavior +rust::String print_tree(const shared_ptr tree, SlangPrintOpts options) { + SyntaxPrinter printer(tree->sourceManager()); + + printer.setIncludeDirectives(options.include_directives); + printer.setExpandIncludes(true); + printer.setExpandMacros(options.expand_macros); + printer.setSquashNewlines(options.squash_newlines); + printer.setIncludeComments(options.include_comments); + + printer.print(tree->root()); + return rust::String(printer.str()); +} + +// Dumps the given syntax tree to a JSON string for debugging/analysis purposes +rust::String dump_tree_json(std::shared_ptr tree) { + JsonWriter writer; + writer.setPrettyPrint(true); + + CSTSerializer serializer(writer); + serializer.serialize(*tree); + + return rust::String(std::string(writer.view())); +} diff --git a/crates/bender-slang/cpp/rewriter.cpp b/crates/bender-slang/cpp/rewriter.cpp new file mode 100644 index 000000000..042ea2111 --- /dev/null +++ b/crates/bender-slang/cpp/rewriter.cpp @@ -0,0 +1,275 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +#include "slang/syntax/SyntaxVisitor.h" +#include "slang_bridge.h" + +#include +#include + +using namespace slang; +using namespace slang::syntax; +using namespace slang::parsing; + +using std::string_view; + +namespace { +// Returns true for language-defined scoped roots that must never be renamed. +bool is_reserved_scope_root(string_view name) { + return name == "$unit" || name == "local" || name == "super" || name == "this"; +} +} // namespace + +std::unique_ptr new_syntax_tree_rewriter() { return std::make_unique(); } + +// Pass 1: collects declarations and renames declaration sites. +class DeclarationRewriter : public SyntaxRewriter { + public: + DeclarationRewriter(std::unordered_map& renameMap, const std::string& prefix, + const std::string& suffix, const std::unordered_set& excludes, + std::uint64_t& declRenamed) + : renameMap(renameMap), prefix(prefix), suffix(suffix), excludes(excludes), declRenamed(declRenamed) {} + + string_view declaration_name(string_view name) { + if (prefix.empty() && suffix.empty()) { + return {}; + } + if (excludes.count(std::string(name))) { + return {}; + } + + auto [it, inserted] = renameMap.try_emplace(std::string(name), prefix + std::string(name) + suffix); + (void)inserted; + return string_view(it->second); + } + + // e.g.: "module top;" -> "module p_top_s;" and "endmodule : top" -> "endmodule : p_top_s". + void handle(const ModuleDeclarationSyntax& node) { + if (node.header->name.isMissing()) { + visitDefault(node); + return; + } + + auto newName = declaration_name(node.header->name.valueText()); + if (newName.empty()) { + visitDefault(node); + return; + } + + auto newNameToken = node.header->name.withRawText(alloc, newName); + + ModuleHeaderSyntax* newHeader = deepClone(*node.header, alloc); + newHeader->name = newNameToken; + + replace(*node.header, *newHeader); + declRenamed++; + + // Also rename the end label if present (e.g., `endmodule : module_name`). + if (node.blockName && !node.blockName->name.isMissing()) { + auto newBlockNameToken = node.blockName->name.withRawText(alloc, newName); + NamedBlockClauseSyntax* newBlockName = deepClone(*node.blockName, alloc); + newBlockName->name = newBlockNameToken; + replace(*node.blockName, *newBlockName); + } + + visitDefault(node); + } + + private: + std::unordered_map& renameMap; + const std::string& prefix; + const std::string& suffix; + const std::unordered_set& excludes; + std::uint64_t& declRenamed; +}; + +// Pass 2: rewrites references based on the map built in pass 1. +// Internally this is split into: +// - 2a structural references (instantiations / imports / virtual interfaces) +// - 2b scoped-name references +class ReferenceRewriter : public SyntaxRewriter { + public: + ReferenceRewriter(const std::unordered_map& renameMap, std::uint64_t& refRenamed) + : renameMap(renameMap), refRenamed(refRenamed) {} + + string_view mapped_name(string_view name) const { + auto it = renameMap.find(std::string(name)); + if (it == renameMap.end()) { + return {}; + } + return string_view(it->second); + } + + // Returns the mapped replacement for the left side of a scoped name + // (e.g. common_pkg in common_pkg::state_t), or empty if not renamable. + string_view mapped_scoped_left_name(const ScopedNameSyntax& node) const { + if (node.left->kind != SyntaxKind::IdentifierName) { + return {}; + } + + auto& leftNode = node.left->as(); + auto name = leftNode.identifier.valueText(); + if (is_reserved_scope_root(name)) { + return {}; + } + return mapped_name(name); + } + + // e.g.: "core u_core();" -> "p_core_s u_core();". + void handle(const HierarchyInstantiationSyntax& node) { + if (node.type.kind != TokenKind::Identifier) { + visitDefault(node); + return; + } + + auto newName = mapped_name(node.type.valueText()); + if (newName.empty()) { + visitDefault(node); + return; + } + + auto newNameToken = node.type.withRawText(alloc, newName); + HierarchyInstantiationSyntax* newNode = deepClone(node, alloc); + newNode->type = newNameToken; + + // Preserve scoped renames in overridden parameters of this + // instantiation, which would otherwise be shadowed by replacing + // the whole instantiation node. + rewrite_scoped_names_inplace(*newNode); + + replace(node, *newNode); + refRenamed++; + } + + // e.g.: "import common_pkg::*;" -> "import p_common_pkg_s::*;". + void handle(const PackageImportItemSyntax& node) { + if (node.package.isMissing()) { + return; + } + + auto newName = mapped_name(node.package.valueText()); + if (newName.empty()) { + visitDefault(node); + return; + } + auto newNameToken = node.package.withRawText(alloc, newName); + + PackageImportItemSyntax* newNode = deepClone(node, alloc); + newNode->package = newNameToken; + + replace(node, *newNode); + refRenamed++; + } + + // e.g.: "virtual bus_intf v_if;" -> "virtual p_bus_intf_s v_if;". + void handle(const VirtualInterfaceTypeSyntax& node) { + if (node.name.isMissing()) { + return; + } + + auto newName = mapped_name(node.name.valueText()); + if (newName.empty()) { + visitDefault(node); + return; + } + auto newNameToken = node.name.withRawText(alloc, newName); + + VirtualInterfaceTypeSyntax* newNode = deepClone(node, alloc); + newNode->name = newNameToken; + + replace(node, *newNode); + refRenamed++; + } + + // e.g.: "common_pkg::state_t" -> "p_common_pkg_s::state_t". + void handle(const ScopedNameSyntax& node) { + auto newName = mapped_scoped_left_name(node); + if (newName.empty()) { + visitDefault(node); + return; + } + + auto& leftNode = node.left->as(); + auto newNameToken = leftNode.identifier.withRawText(alloc, newName); + + IdentifierNameSyntax* newLeft = deepClone(leftNode, alloc); + newLeft->identifier = newNameToken; + + ScopedNameSyntax* newNode = deepClone(node, alloc); + newNode->left = newLeft; + + replace(node, *newNode); + refRenamed++; + } + + private: + // Rewrites only the left identifier of a scoped name in-place if mapped. + void rewrite_scoped_name_left(ScopedNameSyntax& node) { + auto newName = mapped_scoped_left_name(node); + if (newName.empty()) { + return; + } + + auto& leftNode = node.left->as(); + leftNode.identifier = leftNode.identifier.withRawText(alloc, newName); + refRenamed++; + } + + // Walks a subtree and rewrites all scoped-name left identifiers in-place. + // Used on cloned instantiation subtrees before replacing the parent node. + void rewrite_scoped_names_inplace(SyntaxNode& root) { + if (auto* scoped = root.as_if()) { + rewrite_scoped_name_left(*scoped); + } + + for (size_t i = 0; i < root.getChildCount(); i++) { + if (auto* child = root.childNode(i)) { + rewrite_scoped_names_inplace(*child); + } + } + } + + const std::unordered_map& renameMap; + std::uint64_t& refRenamed; +}; + +void SyntaxTreeRewriter::set_prefix(rust::Str value) { prefix = std::string(value.data(), value.size()); } + +void SyntaxTreeRewriter::set_suffix(rust::Str value) { suffix = std::string(value.data(), value.size()); } + +void SyntaxTreeRewriter::set_excludes(const rust::Vec values) { + excludes.clear(); + for (const auto& value : values) { + excludes.insert(std::string(value)); + } +} + +// Pass 1: collect declaration names and rename declaration sites. +std::shared_ptr SyntaxTreeRewriter::rewrite_declarations(std::shared_ptr tree) { + if (prefix.empty() && suffix.empty()) { + return tree; + } + + std::uint64_t declRenamed = 0; + DeclarationRewriter rewriter(renameMap, prefix, suffix, excludes, declRenamed); + auto transformed = rewriter.transform(tree); + renamedDeclarations += declRenamed; + return transformed; +} + +// Pass 2: rename references using the map built in pass 1. +std::shared_ptr SyntaxTreeRewriter::rewrite_references(std::shared_ptr tree) { + if (renameMap.empty()) { + return tree; + } + + std::uint64_t refRenamed = 0; + ReferenceRewriter rewriter(renameMap, refRenamed); + auto transformed = rewriter.transform(tree); + renamedReferences += refRenamed; + return transformed; +} + +std::uint64_t renamed_declarations(const SyntaxTreeRewriter& rewriter) { return rewriter.renamed_declarations(); } + +std::uint64_t renamed_references(const SyntaxTreeRewriter& rewriter) { return rewriter.renamed_references(); } diff --git a/crates/bender-slang/cpp/session.cpp b/crates/bender-slang/cpp/session.cpp new file mode 100644 index 000000000..783e4bd8c --- /dev/null +++ b/crates/bender-slang/cpp/session.cpp @@ -0,0 +1,108 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +#include "slang_bridge.h" + +#include + +using namespace slang; +using namespace slang::syntax; + +using std::shared_ptr; +using std::string; +using std::string_view; + +std::unique_ptr new_slang_session() { return std::make_unique(); } + +SlangContext::SlangContext() : diagEngine(sourceManager), diagClient(std::make_shared()) { + diagEngine.addClient(diagClient); +} + +void SlangContext::set_includes(const rust::Vec& incs) { + for (const auto& inc : incs) { + std::string incStr(inc.data(), inc.size()); + if (auto ec = sourceManager.addUserDirectories(incStr); ec) { + throw std::runtime_error("Failed to add include directory '" + incStr + "': " + ec.message()); + } + } +} + +void SlangContext::set_defines(const rust::Vec& defs) { + ppOptions.predefines.reserve(defs.size()); + for (const auto& def : defs) { + ppOptions.predefines.emplace_back(def.data(), def.size()); + } +} + +// Parses a list of source files and returns the resulting syntax trees as a vector (of shared pointers). +// If any file fails to parse, an exception is thrown with the error message(s) from the diagnostic engine. +std::vector> SlangContext::parse_files(const rust::Vec& paths) { + Bag options; + options.set(ppOptions); + + std::vector> out; + out.reserve(paths.size()); + + for (const auto& path : paths) { + string_view pathView(path.data(), path.size()); + auto result = SyntaxTree::fromFile(pathView, sourceManager, options); + + if (!result) { + auto& err = result.error(); + std::string msg = "System Error loading '" + std::string(err.second) + "': " + err.first.message(); + throw std::runtime_error(msg); + } + + auto tree = *result; + diagClient->clear(); + diagEngine.clearIncludeStack(); + + bool hasErrors = false; + for (const auto& diag : tree->diagnostics()) { + hasErrors |= diag.isError(); + diagEngine.issue(diag); + } + + if (hasErrors) { + std::string rendered = diagClient->getString(); + if (rendered.empty()) { + rendered = "Failed to parse '" + std::string(pathView) + "'."; + } + throw std::runtime_error(rendered); + } + + out.push_back(tree); + } + + return out; +} + +// Parses a group of files with the given include paths and preprocessor defines. +// Stores the resulting syntax trees and contexts in the session for later retrieval and analysis. +void SlangSession::parse_group(const rust::Vec& files, const rust::Vec& includes, + const rust::Vec& defines) { + // Create a new context for this group of files. + auto ctx = std::make_unique(); + ctx->set_includes(includes); + ctx->set_defines(defines); + + // Parse the files and store the resulting syntax trees in the session. + auto parsed = ctx->parse_files(files); + allTrees.reserve(allTrees.size() + parsed.size()); + for (const auto& tree : parsed) { + allTrees.push_back(tree); + } + + contexts.push_back(std::move(ctx)); +} + +// Returns the number of syntax trees currently stored in the session. +std::size_t tree_count(const SlangSession& session) { return session.trees().size(); } + +// Returns the syntax tree at the given index in the session. +std::shared_ptr tree_at(const SlangSession& session, std::size_t index) { + if (index >= session.trees().size()) { + throw std::runtime_error("Tree index out of bounds."); + } + return session.trees()[index]; +} diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h new file mode 100644 index 000000000..240eff336 --- /dev/null +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -0,0 +1,85 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +#ifndef BENDER_SLANG_BRIDGE_H +#define BENDER_SLANG_BRIDGE_H + +#include "rust/cxx.h" +#include "slang/diagnostics/DiagnosticEngine.h" +#include "slang/diagnostics/TextDiagnosticClient.h" +#include "slang/driver/Driver.h" +#include "slang/syntax/SyntaxTree.h" + +#include +#include +#include +#include +#include +#include +#include + +struct SlangPrintOpts; + +class SlangContext { + public: + SlangContext(); + + void set_includes(const rust::Vec& includes); + void set_defines(const rust::Vec& defines); + + std::vector> parse_files(const rust::Vec& paths); + + private: + slang::SourceManager sourceManager; + slang::parsing::PreprocessorOptions ppOptions; + slang::DiagnosticEngine diagEngine; + std::shared_ptr diagClient; +}; + +class SlangSession { + public: + void parse_group(const rust::Vec& files, const rust::Vec& includes, + const rust::Vec& defines); + + const std::vector>& trees() const { return allTrees; } + + private: + std::vector> contexts; + std::vector> allTrees; +}; + +class SyntaxTreeRewriter { + public: + void set_prefix(rust::Str prefix); + void set_suffix(rust::Str suffix); + void set_excludes(const rust::Vec excludes); + + std::shared_ptr rewrite_declarations(std::shared_ptr tree); + std::shared_ptr rewrite_references(std::shared_ptr tree); + + std::uint64_t renamed_declarations() const { return renamedDeclarations; } + std::uint64_t renamed_references() const { return renamedReferences; } + + private: + std::string prefix; + std::string suffix; + std::unordered_set excludes; + std::unordered_map renameMap; + std::uint64_t renamedDeclarations = 0; + std::uint64_t renamedReferences = 0; +}; + +std::unique_ptr new_slang_session(); +std::unique_ptr new_syntax_tree_rewriter(); + +rust::String print_tree(std::shared_ptr tree, SlangPrintOpts options); + +rust::String dump_tree_json(std::shared_ptr tree); + +rust::Vec reachable_tree_indices(const SlangSession& session, const rust::Vec& tops); +std::size_t tree_count(const SlangSession& session); +std::shared_ptr tree_at(const SlangSession& session, std::size_t index); +std::uint64_t renamed_declarations(const SyntaxTreeRewriter& rewriter); +std::uint64_t renamed_references(const SyntaxTreeRewriter& rewriter); + +#endif // BENDER_SLANG_BRIDGE_H diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs new file mode 100644 index 000000000..6930abb50 --- /dev/null +++ b/crates/bender-slang/src/lib.rs @@ -0,0 +1,301 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +use std::marker::PhantomData; + +use cxx::{SharedPtr, UniquePtr}; +use thiserror::Error; + +pub use ffi::SlangPrintOpts; + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum SlangError { + #[error("Failed to parse source group: {message}")] + ParseGroup { message: String }, + #[error("Failed to trim files by top modules: {message}")] + TrimByTop { message: String }, + #[error("Failed to access parsed syntax tree: {message}")] + TreeAccess { message: String }, + #[error("Failed to rewrite syntax trees: {message}")] + Rewrite { message: String }, +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct RenameStats { + pub renamed_declarations: u64, + pub renamed_references: u64, +} + +#[cxx::bridge] +mod ffi { + /// Options for the syntax printer + #[derive(Clone, Copy)] + struct SlangPrintOpts { + expand_macros: bool, + include_directives: bool, + include_comments: bool, + squash_newlines: bool, + } + + unsafe extern "C++" { + include!("bender-slang/cpp/slang_bridge.h"); + include!("slang/syntax/SyntaxTree.h"); + + /// Opaque session that owns parse contexts and syntax trees. + type SlangSession; + + /// Opaque type for the Slang syntax tree. + #[namespace = "slang::syntax"] + type SyntaxTree; + type SyntaxTreeRewriter; + + fn new_slang_session() -> UniquePtr; + + fn parse_group( + self: Pin<&mut SlangSession>, + files: &Vec, + includes: &Vec, + defines: &Vec, + ) -> Result<()>; + + fn reachable_tree_indices(session: &SlangSession, tops: &Vec) -> Result>; + + fn tree_count(session: &SlangSession) -> usize; + + fn tree_at(session: &SlangSession, index: usize) -> Result>; + + fn new_syntax_tree_rewriter() -> UniquePtr; + fn set_suffix(self: Pin<&mut SyntaxTreeRewriter>, suffix: &str); + fn set_excludes(self: Pin<&mut SyntaxTreeRewriter>, excludes: Vec); + fn rewrite_declarations( + self: Pin<&mut SyntaxTreeRewriter>, + tree: SharedPtr, + ) -> SharedPtr; + fn set_prefix(self: Pin<&mut SyntaxTreeRewriter>, prefix: &str); + fn rewrite_references( + self: Pin<&mut SyntaxTreeRewriter>, + tree: SharedPtr, + ) -> SharedPtr; + fn renamed_declarations(rewriter: &SyntaxTreeRewriter) -> u64; + fn renamed_references(rewriter: &SyntaxTreeRewriter) -> u64; + + fn print_tree(tree: SharedPtr, options: SlangPrintOpts) -> String; + + fn dump_tree_json(tree: SharedPtr) -> String; + } +} + +/// Public owner for all parsed trees and parse contexts. +pub struct SlangSession { + inner: UniquePtr, +} + +/// Borrowed syntax-tree handle tied to the owning session lifetime. +pub struct SyntaxTree<'a> { + inner: SharedPtr, + _session: PhantomData<&'a SlangSession>, +} + +pub struct SyntaxTreeRewriter { + inner: UniquePtr, +} + +impl<'a> Clone for SyntaxTree<'a> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + _session: PhantomData, + } + } +} + +impl<'a> SyntaxTree<'a> { + /// Displays the syntax tree as a string with the given options. + pub fn display(&self, options: SlangPrintOpts) -> String { + ffi::print_tree(self.inner.clone(), options) + } + + /// Dumps the syntax tree as JSON for debugging purposes. + pub fn as_debug(&self) -> String { + ffi::dump_tree_json(self.inner.clone()) + } +} + +impl std::fmt::Display for SyntaxTree<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let options = SlangPrintOpts { + expand_macros: false, + include_directives: true, + include_comments: true, + squash_newlines: false, + }; + f.write_str(&self.display(options)) + } +} + +impl std::fmt::Debug for SyntaxTree<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.as_debug()) + } +} + +impl SlangSession { + pub fn new() -> Self { + Self { + inner: ffi::new_slang_session(), + } + } + + /// Parses one source group with scoped include directories and defines. + pub fn parse_group( + &mut self, + files: &[String], + includes: &[String], + defines: &[String], + ) -> Result> { + let files_vec = files.to_vec(); + let includes_vec = normalize_include_dirs(includes)?; + let defines_vec = defines.to_vec(); + + let start = self.tree_count(); + self.inner + .pin_mut() + .parse_group(&files_vec, &includes_vec, &defines_vec) + .map_err(|cause| SlangError::ParseGroup { + message: cause.to_string(), + })?; + + let end = self.tree_count(); + Ok((start..end).collect()) + } + + /// Returns the total number of parsed syntax trees in the session. + pub fn tree_count(&self) -> usize { + ffi::tree_count(self.inner.as_ref().unwrap()) + } + + /// Returns all parsed syntax trees in the session. + pub fn all_trees(&self) -> Result>> { + let count = self.tree_count(); + let mut out = Vec::with_capacity(count); + for idx in 0..count { + out.push(self.tree(idx)?); + } + Ok(out) + } + + /// Returns the indices of syntax trees reachable from the given top modules. + pub fn reachable_indices(&self, tops: &[String]) -> Result> { + let tops = tops.to_vec(); + let indices = + ffi::reachable_tree_indices(self.inner.as_ref().unwrap(), &tops).map_err(|cause| { + SlangError::TrimByTop { + message: cause.to_string(), + } + })?; + Ok(indices.into_iter().map(|i| i as usize).collect()) + } + + /// Returns syntax trees reachable from the given top modules. + pub fn reachable_trees(&self, tops: &[String]) -> Result>> { + let indices = self.reachable_indices(tops)?; + let mut out = Vec::with_capacity(indices.len()); + for idx in indices { + out.push(self.tree(idx)?); + } + Ok(out) + } + + /// Returns a handle to the syntax tree at the given index. + pub fn tree(&self, index: usize) -> Result> { + Ok(SyntaxTree { + inner: ffi::tree_at(self.inner.as_ref().unwrap(), index).map_err(|cause| { + SlangError::TreeAccess { + message: cause.to_string(), + } + })?, + _session: PhantomData, + }) + } +} + +impl Default for SlangSession { + fn default() -> Self { + Self::new() + } +} + +impl SyntaxTreeRewriter { + pub fn new() -> Self { + Self { + inner: ffi::new_syntax_tree_rewriter(), + } + } + + pub fn set_prefix(&mut self, prefix: impl Into) { + let prefix = prefix.into(); + self.inner.pin_mut().set_prefix(&prefix); + } + + pub fn set_suffix(&mut self, suffix: impl Into) { + let suffix = suffix.into(); + self.inner.pin_mut().set_suffix(&suffix); + } + + pub fn set_excludes(&mut self, excludes: Vec) { + self.inner.pin_mut().set_excludes(excludes); + } + + pub fn rewrite_declarations<'a>(&mut self, tree: &SyntaxTree<'a>) -> SyntaxTree<'a> { + SyntaxTree { + inner: self + .inner + .pin_mut() + .rewrite_declarations(tree.inner.clone()), + _session: PhantomData, + } + } + + pub fn rewrite_references<'a>(&mut self, tree: &SyntaxTree<'a>) -> SyntaxTree<'a> { + SyntaxTree { + inner: self.inner.pin_mut().rewrite_references(tree.inner.clone()), + _session: PhantomData, + } + } + + pub fn stats(&self) -> RenameStats { + let rewriter = self.inner.as_ref().unwrap(); + RenameStats { + renamed_declarations: ffi::renamed_declarations(rewriter), + renamed_references: ffi::renamed_references(rewriter), + } + } +} + +impl Default for SyntaxTreeRewriter { + fn default() -> Self { + Self::new() + } +} + +#[cfg(windows)] +fn normalize_include_dirs(includes: &[String]) -> Result> { + let mut out = Vec::with_capacity(includes.len()); + for include in includes { + let canonical = dunce::canonicalize(include).map_err(|cause| SlangError::ParseGroup { + message: format!( + "Failed to canonicalize include directory '{}': {}", + include, cause + ), + })?; + out.push(canonical.to_string_lossy().into_owned()); + } + Ok(out) +} + +#[cfg(unix)] +fn normalize_include_dirs(includes: &[String]) -> Result> { + Ok(includes.to_vec()) +} diff --git a/crates/bender-slang/vendor/slang b/crates/bender-slang/vendor/slang new file mode 160000 index 000000000..ace09c5d7 --- /dev/null +++ b/crates/bender-slang/vendor/slang @@ -0,0 +1 @@ +Subproject commit ace09c5d7c9f4e28eed654d2f353c6dc792ebf67 diff --git a/src/cli.rs b/src/cli.rs index 6f8ee5938..f14eda4bd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -106,6 +106,8 @@ enum Commands { Init, Snapshot(cmd::snapshot::SnapshotArgs), Audit(cmd::audit::AuditArgs), + #[cfg(feature = "slang")] + Pickle(cmd::pickle::PickleArgs), #[command(external_subcommand)] Plugin(Vec), } @@ -329,6 +331,8 @@ pub fn main() -> Result<()> { Commands::Fusesoc(args) => cmd::fusesoc::run(&sess, &args), Commands::Snapshot(args) => cmd::snapshot::run(&sess, &args), Commands::Audit(args) => cmd::audit::run(&sess, &args), + #[cfg(feature = "slang")] + Commands::Pickle(args) => cmd::pickle::run(&sess, args), Commands::Plugin(args) => { let (plugin_name, plugin_args) = args .split_first() diff --git a/src/cmd.rs b/src/cmd.rs index 8399f03b6..bbae6227d 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -19,6 +19,8 @@ pub mod init; pub mod packages; pub mod parents; pub mod path; +#[cfg(feature = "slang")] +pub mod pickle; pub mod script; pub mod snapshot; pub mod sources; diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs new file mode 100644 index 000000000..71c1298aa --- /dev/null +++ b/src/cmd/pickle.rs @@ -0,0 +1,274 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +//! The `pickle` subcommand. + +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::path::Path; + +use clap::Args; +use indexmap::{IndexMap, IndexSet}; +use tokio::runtime::Runtime; + +use crate::cmd::sources::get_passed_targets; +use crate::config::{Validate, ValidationContext}; +use crate::diagnostic::Warnings; +use crate::error::*; +use crate::sess::{Session, SessionIo}; +use crate::src::{SourceFile, SourceGroup, SourceType}; +use crate::target::{TargetSet, TargetSpec}; + +use bender_slang::{SlangPrintOpts, SlangSession, SyntaxTreeRewriter}; + +/// Pickle files +#[derive(Args, Debug)] +pub struct PickleArgs { + /// Additional source files to pickle, which are not part of the manifest. + files: Vec, + + /// The output file (defaults to stdout) + #[arg(short, long)] + output: Option, + + /// Only include sources that match the given target + #[arg(short, long)] + pub target: Vec, + + /// Specify package to show sources for + #[arg(short, long)] + pub package: Vec, + + /// Specify package to exclude from sources + #[arg(long)] + pub exclude: Vec, + + /// Exclude all dependencies, i.e. only top level or specified package(s) + #[arg(long)] + pub no_deps: bool, + + /// Additional include directory, which are not part of the manifest. + #[arg(short = 'I')] + include_dir: Vec, + + /// Additional preprocessor definition, which are not part of the manifest. + #[arg(short = 'D')] + define: Vec, + + /// One or more top-level modules used to trim unreachable parsed files. + #[arg(long, help_heading = "Slang Options")] + top: Vec, + + /// A prefix to add to all names (modules, packages, interfaces) + #[arg(long, help_heading = "Slang Options", requires = "expand_macros")] + prefix: Option, + + /// A suffix to add to all names (modules, packages, interfaces) + #[arg(long, help_heading = "Slang Options", requires = "expand_macros")] + suffix: Option, + + /// Names to exclude from renaming (modules, packages, interfaces) + #[arg(long, help_heading = "Slang Options")] + exclude_rename: Vec, + + /// Expand macros in the output + #[arg(long, help_heading = "Slang Options")] + expand_macros: bool, + + /// Strip comments from the output + #[arg(long, help_heading = "Slang Options")] + strip_comments: bool, + + /// Squash newlines in the output + #[arg(long, help_heading = "Slang Options")] + squash_newlines: bool, + + /// Dump the syntax trees as JSON instead of the source code + #[arg(long, help_heading = "Slang Options")] + ast_json: bool, +} + +/// Execute the `pickle` subcommand. +pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { + // Load the source files + let rt = Runtime::new()?; + let io = SessionIo::new(sess); + let srcs = rt.block_on(io.sources(false, &[]))?; + + // Filter the sources by target. + let targets = TargetSet::new(args.target.iter().map(|s| s.as_str())); + + // Convert vector to sets for packages and excluded packages. + let package_set = IndexSet::from_iter(args.package); + let exclude_set = IndexSet::from_iter(args.exclude); + + // Filter the sources by specified packages. + let packages = &srcs.get_package_list( + sess.manifest.package.name.to_string(), + &package_set, + &exclude_set, + args.no_deps, + ); + + let (targets, packages) = get_passed_targets(sess, &rt, &io, &targets, packages, &package_set)?; + + // Filter the sources by target and package. + let srcs = srcs + .filter_targets(&targets) + .unwrap_or_default() + .filter_packages(&packages) + .unwrap_or_default(); + + // Flatten and validate the sources. + let mut srcs = srcs + .flatten() + .into_iter() + .map(|f| f.validate(&ValidationContext::default())) + .collect::>>()?; + + if !args.files.is_empty() { + let include_dirs = args + .include_dir + .iter() + .map(|d| (TargetSpec::Wildcard, sess.intern_path(Path::new(d)))) + .collect(); + let defines = args + .define + .iter() + .map(|d| { + let mut parts = d.splitn(2, '='); + let name = parts.next().unwrap_or_default().trim().to_string(); + let value = parts + .next() + .map(|v| sess.intern_string(v.trim().to_string())); + (name, value) + }) + .collect::>(); + let files = args + .files + .iter() + .map(|f| SourceFile::File(sess.intern_path(Path::new(f)), Some(SourceType::Verilog))) + .collect::>(); + + srcs.push(SourceGroup { + include_dirs, + defines, + files, + ..SourceGroup::default() + }); + } + + let print_opts = SlangPrintOpts { + expand_macros: args.expand_macros, + include_directives: !args.expand_macros, + include_comments: !args.strip_comments, + squash_newlines: args.squash_newlines, + }; + + let mut session = SlangSession::new(); + for src_group in srcs { + // Collect include directories from the source group and command line arguments. + let include_dirs: Vec = src_group + .include_dirs + .iter() + .chain(src_group.export_incdirs.values().flatten()) + .map(|(_, path)| path.to_string_lossy().into_owned()) + .chain(args.include_dir.iter().cloned()) + .collect::>() + .into_iter() + .collect(); + + // Collect defines from the source group and command line arguments. + let defines: Vec = src_group + .defines + .iter() + .map(|(def, value)| match value { + Some(v) => format!("{def}={v}"), + None => def.to_string(), + }) + .chain(args.define.iter().cloned()) + .collect::>() + .into_iter() + .collect(); + + // Collect file paths from the source group. + let file_paths: Vec = src_group + .files + .iter() + .filter_map(|source| match source { + SourceFile::File(path, Some(SourceType::Verilog)) => { + Some(path.to_string_lossy().into_owned()) + } + // Vhdl or unknown file types are not supported by Slang, so we emit a warning and skip them. + SourceFile::File(path, _) => { + Warnings::PickleNonVerilogFile(path.to_path_buf()).emit(); + None + } + // Groups should not exist at this point, + // as we have already flattened the sources. + _ => unreachable!(), + }) + .collect(); + + session.parse_group(&file_paths, &include_dirs, &defines)?; + } + + let trees = if args.top.is_empty() { + session.all_trees()? + } else { + session.reachable_trees(&args.top)? + }; + + let mut rewriter = SyntaxTreeRewriter::new(); + rewriter.set_prefix(args.prefix.unwrap_or_default()); + rewriter.set_suffix(args.suffix.unwrap_or_default()); + rewriter.set_excludes(args.exclude_rename); + + // Pass 1: build rename map across all trees. + let trees: Vec<_> = trees + .iter() + .map(|tree| rewriter.rewrite_declarations(tree)) + .collect(); + + // Pass 2: rewrite declarations and references using the complete map. + let trees: Vec<_> = trees + .iter() + .map(|tree| rewriter.rewrite_references(tree)) + .collect(); + + // Setup Output Writer, either to file or stdout + let raw_writer: Box = match &args.output { + Some(path) => Box::new( + File::create(path) + .map_err(|e| Error::new(format!("Cannot create output file: {}", e)))?, + ), + None => Box::new(std::io::stdout()), + }; + let mut writer = BufWriter::new(raw_writer); + + // Start JSON Array if needed + if args.ast_json { + write!(writer, "[")?; + } + + let mut first_item = true; + for tree in trees { + if args.ast_json { + // JSON Array Logic: Prepend comma if not the first item + if !first_item { + write!(writer, ",")?; + } + write!(writer, "{:?}", tree)?; + first_item = false; + } else { + write!(writer, "{}", tree.display(print_opts))?; + } + } + + // Close JSON Array + if args.ast_json { + writeln!(writer, "]")?; + } + + Ok(()) +} diff --git a/src/cmd/script.rs b/src/cmd/script.rs index a06b1b95e..40eb4ea6c 100644 --- a/src/cmd/script.rs +++ b/src/cmd/script.rs @@ -546,15 +546,7 @@ fn emit_template( separate_files_in_group( src, |f| match f { - SourceFile::File(p, fmt) => match fmt { - Some(SourceType::Verilog) => Some(SourceType::Verilog), - Some(SourceType::Vhdl) => Some(SourceType::Vhdl), - _ => match p.extension().and_then(std::ffi::OsStr::to_str) { - Some("sv") | Some("v") | Some("vp") => Some(SourceType::Verilog), - Some("vhd") | Some("vhdl") => Some(SourceType::Vhdl), - _ => Some(SourceType::Unknown), - }, - }, + SourceFile::File(_, fmt) => *fmt, _ => None, }, |src, ty, files| { @@ -611,21 +603,17 @@ fn emit_template( SourceFile::Group(_) => unreachable!(), }) .collect(), - file_type: match ty { - SourceType::Verilog => "verilog".to_string(), - SourceType::Vhdl => "vhdl".to_string(), - SourceType::Unknown => "".to_string(), - }, + file_type: Some(ty), }); }, ); } for src in &split_srcs { - match src.file_type.as_str() { - "verilog" => { + match src.file_type { + Some(SourceType::Verilog) => { all_verilog.append(&mut src.files.clone().into_iter().collect()); } - "vhdl" => { + Some(SourceType::Vhdl) => { all_vhdl.append(&mut src.files.clone().into_iter().collect()); } _ => { @@ -683,5 +671,5 @@ struct TplSrcStruct { defines: IndexSet<(String, Option)>, incdirs: IndexSet, files: IndexSet, - file_type: String, + file_type: Option, } diff --git a/src/diagnostic.rs b/src/diagnostic.rs index 112cd0a3c..7a0bfde57 100644 --- a/src/diagnostic.rs +++ b/src/diagnostic.rs @@ -394,6 +394,10 @@ pub enum Warnings { #[error("Override files in {} does not support additional fields like include_dirs, defines, etc.", fmt_pkg!(.0))] #[diagnostic(code(W33))] OverrideFilesWithExtras(String), + + #[error("File {} is not a Verilog file and will be ignored in the pickle output.", fmt_path!(.0.display()))] + #[diagnostic(code(W34))] + PickleNonVerilogFile(PathBuf), } #[cfg(test)] diff --git a/src/error.rs b/src/error.rs index 02a1b9cc9..0b0f42580 100644 --- a/src/error.rs +++ b/src/error.rs @@ -145,3 +145,10 @@ impl From for Error { Error::chain("Cannot startup runtime.".to_string(), err) } } + +#[cfg(feature = "slang")] +impl From for Error { + fn from(err: bender_slang::SlangError) -> Error { + Error::chain("Slang error:", err) + } +} diff --git a/src/sess.rs b/src/sess.rs index 490c0df8c..5ca579e2c 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -416,17 +416,26 @@ impl<'ctx> Session<'ctx> { .files .iter() .map(|file| match *file { - config::SourceFile::File(ref path) => (path as &Path).into(), + config::SourceFile::File(ref path) => { + let ty = match path.extension().and_then(std::ffi::OsStr::to_str) { + Some("sv") | Some("v") | Some("vp") | Some("svh") => { + Some(crate::src::SourceType::Verilog) + } + Some("vhd") | Some("vhdl") => Some(crate::src::SourceType::Vhdl), + _ => None, + }; + crate::src::SourceFile::File(path as &Path, ty) + } config::SourceFile::SvFile(ref path) => crate::src::SourceFile::File( path as &Path, - &Some(crate::src::SourceType::Verilog), + Some(crate::src::SourceType::Verilog), ), config::SourceFile::VerilogFile(ref path) => crate::src::SourceFile::File( path as &Path, - &Some(crate::src::SourceType::Verilog), + Some(crate::src::SourceType::Verilog), ), config::SourceFile::VhdlFile(ref path) => { - crate::src::SourceFile::File(path as &Path, &Some(crate::src::SourceType::Vhdl)) + crate::src::SourceFile::File(path as &Path, Some(crate::src::SourceType::Vhdl)) } config::SourceFile::Group(ref group) => self .load_sources( diff --git a/src/src.rs b/src/src.rs index 6cb37feb5..e3f2a31f0 100644 --- a/src/src.rs +++ b/src/src.rs @@ -382,16 +382,13 @@ impl<'ctx> SourceGroup<'ctx> { } /// File types for a source file. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)] +#[serde(rename_all = "lowercase")] pub enum SourceType { /// A Verilog file. Verilog, - // /// A SystemVerilog file. - // SystemVerilog, /// A VHDL file. Vhdl, - /// Unknown file type - Unknown, } /// A source file. @@ -400,7 +397,7 @@ pub enum SourceType { #[derive(Clone)] pub enum SourceFile<'ctx> { /// A file. - File(&'ctx Path, &'ctx Option), + File(&'ctx Path, Option), /// A group of files. Group(Box>), } @@ -427,12 +424,6 @@ impl<'ctx> From> for SourceFile<'ctx> { } } -impl<'ctx> From<&'ctx Path> for SourceFile<'ctx> { - fn from(path: &'ctx Path) -> SourceFile<'ctx> { - SourceFile::File(path, &None) - } -} - impl<'ctx> Validate for SourceFile<'ctx> { type Output = SourceFile<'ctx>; type Error = Error;