From 2aeedda4e0df80928be4664fdc5ed30285e1dda4 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 28 Jan 2026 21:50:16 +0100 Subject: [PATCH 01/46] bender-slang: Initial `slang` bindings --- .gitmodules | 3 + Cargo.lock | 121 +++++++++++++++++++++++ Cargo.toml | 3 + crates/bender-slang/Cargo.toml | 11 +++ crates/bender-slang/build.rs | 51 ++++++++++ crates/bender-slang/cpp/slang_bridge.cpp | 80 +++++++++++++++ crates/bender-slang/cpp/slang_bridge.h | 10 ++ crates/bender-slang/src/lib.rs | 118 ++++++++++++++++++++++ crates/bender-slang/vendor/slang | 1 + 9 files changed, 398 insertions(+) create mode 100644 .gitmodules create mode 100644 crates/bender-slang/Cargo.toml create mode 100644 crates/bender-slang/build.rs create mode 100644 crates/bender-slang/cpp/slang_bridge.cpp create mode 100644 crates/bender-slang/cpp/slang_bridge.h create mode 100644 crates/bender-slang/src/lib.rs create mode 160000 crates/bender-slang/vendor/slang 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/Cargo.lock b/Cargo.lock index 0c5a06ab3..36b93a3d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "bender-slang" +version = "0.1.0" +dependencies = [ + "cmake", + "cxx", + "cxx-build", +] + [[package]] name = "bitflags" version = "2.11.0" @@ -288,6 +297,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 +386,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", + "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 +550,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" @@ -799,6 +896,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 +1338,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 +1571,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..1f26be211 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,9 @@ license = "Apache-2.0 OR MIT" edition = "2024" rust-version = "1.87.0" +[workspace] +members = ["crates/bender-slang"] + [dependencies] serde = { version = "1", features = ["derive"] } serde_yaml_ng = "0.10" diff --git a/crates/bender-slang/Cargo.toml b/crates/bender-slang/Cargo.toml new file mode 100644 index 000000000..92e14714b --- /dev/null +++ b/crates/bender-slang/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "bender-slang" +version = "0.1.0" +edition = "2024" + +[dependencies] +cxx = "1.0.194" + +[build-dependencies] +cmake = "0.1.57" +cxx-build = "1.0.194" diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs new file mode 100644 index 000000000..9252567e5 --- /dev/null +++ b/crates/bender-slang/build.rs @@ -0,0 +1,51 @@ +fn main() { + // Build Slang with CMake into a static library + let dst = cmake::Config::new("vendor/slang") + .define("SLANG_INCLUDE_TESTS", "OFF") + .define("SLANG_INCLUDE_TOOLS", "OFF") + .define("SLANG_INCLUDE_PYSLANG", "OFF") + .define("BUILD_SHARED_LIBS", "OFF") + // TODO(fischeti): Check whether mimalloc can/should be enabled again. + .define("SLANG_USE_MIMALLOC", "OFF") + // TODO(fischeti): `fmt` currently causes issues on my machine since there is a system-wide installation. + .define("CMAKE_DISABLE_FIND_PACKAGE_fmt", "ON") + // TODO(fischeti): Investigate how boost should be handled properly. + .cxxflag("-DSLANG_BOOST_SINGLE_HEADER=1") + .build(); + + // Configure Linker to find Slang static library + println!("cargo:rustc-link-search=native={}/lib", dst.display()); + println!("cargo:rustc-link-lib=static=svLang"); + println!("cargo:rustc-link-lib=static=fmtd"); + + // Compile the C++ Bridge + let mut bridge_build = cxx_build::bridge("src/lib.rs"); + bridge_build + .file("cpp/slang_bridge.cpp") + .flag_if_supported("-std=c++20") + // Static Linking Definition + // Tells Slang headers not to look for DLL import/export symbols. + .define("SLANG_STATIC_DEFINE", "1") + // Boost Vendored Mode + // Tells Slang to use the local 'external/boost_*.hpp' files instead of system Boost. + // TODO(fischeti): Investigate how boost should be handled properly. + .define("SLANG_BOOST_SINGLE_HEADER", "1") + // Include Paths + // 1. Slang source headers + .include("vendor/slang/include") + // 2. Slang external headers (where boost_unordered.hpp lives) + .include("vendor/slang/external") + // 3. CMake build output (where slang_export.h and fmt headers live) + .include(dst.join("include")); + + // TODO(fischeti): Check whether debug definitions are necessary. + if std::env::var("PROFILE").unwrap() == "debug" { + bridge_build.define("SLANG_DEBUG", "1"); + } + + bridge_build.compile("slang-bridge"); + + println!("cargo:rerun-if-changed=src/lib.rs"); + println!("cargo:rerun-if-changed=cpp/slang_bridge.cpp"); + println!("cargo:rerun-if-changed=cpp/slang_bridge.h"); +} diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp new file mode 100644 index 000000000..885595225 --- /dev/null +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -0,0 +1,80 @@ +#include "slang_bridge.h" +#include "bender-slang/src/lib.rs.h" // Import the generated C++ definition of the structs + +#include "slang/driver/Driver.h" +#include "slang/syntax/SyntaxPrinter.h" +#include "slang/syntax/SyntaxTree.h" + +#include +#include +#include + +using namespace slang; +using namespace slang::driver; +using namespace slang::syntax; + +rust::String pickle(rust::Vec sources, + rust::Vec include_dirs, + rust::Vec defines, + SlangPrintOpts options) { + Driver driver; + driver.addStandardArgs(); + + // 1. Construct Arguments from SlangFiles + std::vector arg_strings; + arg_strings.push_back("slang_tool"); + + for (const auto& source : sources) { + arg_strings.push_back(std::string(source)); + } + for (const auto& path : include_dirs) { + arg_strings.push_back("-I"); + arg_strings.push_back(std::string(path)); + } + + for (const auto& def : defines) { + arg_strings.push_back("-D"); + arg_strings.push_back(std::string(def)); + } + + // Convert to C-style argv + std::vector c_args; + c_args.reserve(arg_strings.size()); + for (const auto& s : arg_strings) c_args.push_back(s.c_str()); + + // 2. Run Compilation + if (!driver.parseCommandLine(c_args.size(), c_args.data())) { + throw std::runtime_error("Failed to parse command line arguments."); + } + + if (!driver.processOptions()) { + throw std::runtime_error("Failed to process options."); + } + + bool parseSuccess = driver.parseAllSources(); + bool diagSuccess = driver.reportDiagnostics(false); + + if (!parseSuccess || !diagSuccess) { + throw std::runtime_error("Parsing failed. Check stderr for details."); + } + + auto& syntaxTrees = driver.syntaxTrees; + if (syntaxTrees.empty()) { + return ""; + } + + // 3. Configure Printer from SlangPrinterOptions + SyntaxPrinter printer(driver.sourceManager); + + printer.setIncludeDirectives(options.include_directives); + printer.setExpandIncludes(options.expand_includes); + printer.setExpandMacros(options.expand_macros); + printer.setSquashNewlines(options.squash_newlines); + printer.setIncludeComments(options.include_comments); + + for (auto& tree : syntaxTrees) { + printer.print(*tree); + } + + return rust::String(printer.str()); +} diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h new file mode 100644 index 000000000..1d4174138 --- /dev/null +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -0,0 +1,10 @@ +#pragma once +#include "rust/cxx.h" + +// Forward declare the structs generated by CXX +struct SlangPrintOpts; + +rust::String pickle(rust::Vec sources, + rust::Vec include_dirs, + rust::Vec defines, + SlangPrintOpts options); diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs new file mode 100644 index 000000000..a78d08928 --- /dev/null +++ b/crates/bender-slang/src/lib.rs @@ -0,0 +1,118 @@ +pub use ffi::SlangPrintOpts; + +#[cxx::bridge] +mod ffi { + + /// Options for the syntax printer + #[derive(Clone)] + struct SlangPrintOpts { + /// Whether to include preprocessor directives + include_directives: bool, + /// Whether to expand include directives + expand_includes: bool, + /// Whether to expand macros + expand_macros: bool, + /// Whether to print comments + include_comments: bool, + /// Whether to squash newlines + squash_newlines: bool, + } + + unsafe extern "C++" { + include!("bender-slang/cpp/slang_bridge.h"); + + fn pickle( + sources: Vec, + include_dirs: Vec, + defines: Vec, + options: SlangPrintOpts, + ) -> Result; + } +} + +/// Main interface for Slang bindings +pub struct Slang { + /// Source files to be pickled + sources: Vec, + /// Include directories + include_dirs: Vec, + /// Defines + defines: Vec, + /// Print options + print_opts: ffi::SlangPrintOpts, +} + +/// Main interface for interfacing with Slang +impl Slang { + pub fn new() -> Self { + Slang { + sources: Vec::new(), + include_dirs: Vec::new(), + defines: Vec::new(), + print_opts: ffi::SlangPrintOpts { + include_directives: true, + expand_includes: true, + expand_macros: true, + include_comments: true, + squash_newlines: true, + }, + } + } + + /// Adds source files to be pickled. + pub fn add_sources(&mut self, sources: Vec) { + self.sources.extend(sources); + } + + /// Adds source sources to be pickled, returning self for chaining. + pub fn with_sources(mut self, sources: Vec) -> Self { + self.sources.extend(sources); + self + } + + /// Adds include directories. + pub fn add_include_dirs(&mut self, dirs: Vec) { + self.include_dirs.extend(dirs); + } + + /// Adds include directories, returning self for chaining. + pub fn with_include_dirs(mut self, dirs: Vec) -> Self { + self.include_dirs.extend(dirs); + self + } + + /// Adds defines. + pub fn add_defines(&mut self, defines: Vec) { + self.defines.extend(defines); + } + + /// Adds defines, returning self for chaining. + pub fn with_defines(mut self, defines: Vec) -> Self { + self.defines.extend(defines); + self + } + + /// Sets print options. + pub fn set_print_options(&mut self, print_opts: ffi::SlangPrintOpts) { + self.print_opts = print_opts; + } + + /// Sets print options, returning self for chaining. + pub fn with_print_options(mut self, print_opts: ffi::SlangPrintOpts) -> Self { + self.print_opts = print_opts; + self + } + + /// Pickles files based on the provided configuration. + /// Returns the pickled content or an error if parsing/processing failed. + pub fn pickle(&self) -> Result> { + // call the C++ function; errors are propagated as Rust Results + let result = ffi::pickle( + self.sources.clone(), + self.include_dirs.clone(), + self.defines.clone(), + self.print_opts.clone(), + )?; + Ok(result) + } +} 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 From 44f9b85f0041c14016e04b7a04517733c11835f1 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 00:04:40 +0100 Subject: [PATCH 02/46] pickle: Add initial command --- Cargo.lock | 1 + Cargo.toml | 2 ++ src/cli.rs | 2 ++ src/cmd.rs | 1 + src/cmd/pickle.rs | 79 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 src/cmd/pickle.rs diff --git a/Cargo.lock b/Cargo.lock index 36b93a3d3..4f3978152 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", diff --git a/Cargo.toml b/Cargo.toml index 1f26be211..ee6959dde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ rust-version = "1.87.0" members = ["crates/bender-slang"] [dependencies] +bender-slang = { path = "crates/bender-slang" } + serde = { version = "1", features = ["derive"] } serde_yaml_ng = "0.10" serde_json = "1" diff --git a/src/cli.rs b/src/cli.rs index 6f8ee5938..0e23e34f8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -106,6 +106,7 @@ enum Commands { Init, Snapshot(cmd::snapshot::SnapshotArgs), Audit(cmd::audit::AuditArgs), + Pickle(cmd::pickle::PickleArgs), #[command(external_subcommand)] Plugin(Vec), } @@ -329,6 +330,7 @@ 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), + Commands::Pickle(args) => cmd::pickle::run(args), Commands::Plugin(args) => { let (plugin_name, plugin_args) = args .split_first() diff --git a/src/cmd.rs b/src/cmd.rs index 8399f03b6..689b148dd 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -19,6 +19,7 @@ pub mod init; pub mod packages; pub mod parents; pub mod path; +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..3a77b50a1 --- /dev/null +++ b/src/cmd/pickle.rs @@ -0,0 +1,79 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +//! The `pickle` subcommand. + +use clap::{ArgAction, Args}; + +use bender_slang::{Slang, SlangPrintOpts}; + +use crate::error::*; + +// TODO(fischeti): Clean up the arguments and options. +// At the moment, they are just directly mirroring the Slang API. +// for debugging purposes. +/// Pickle files +#[derive(Args, Debug)] +pub struct PickleArgs { + /// Source files to pickle + #[arg(required = true)] + files: Vec, + + /// The output file (defaults to stdout) + #[arg(short, long)] + output: Option, + + /// Add an include directory + #[arg(short = 'I', long, action = ArgAction::Append)] + include_dirs: Vec, + + /// Add defines + #[arg(short = 'D', long, action = ArgAction::Append)] + defines: Vec, + + /// Whether to include preprocessor directives + #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Print Options")] + include_directives: bool, + + /// Whether to expand include directives + #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Print Options")] + expand_includes: bool, + + /// Whether to expand macros + #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Print Options")] + expand_macros: bool, + + /// Whether to strip comments + #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Print Options")] + strip_comments: bool, + + /// Whether to strip newlines + #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Print Options")] + strip_newlines: bool, +} + +/// Execute the `pickle` subcommand. +pub fn run(args: PickleArgs) -> Result<()> { + let slang = Slang::new() + .with_sources(args.files) + .with_include_dirs(args.include_dirs) + .with_defines(args.defines) + .with_print_options(SlangPrintOpts { + include_directives: args.include_directives, + expand_includes: args.expand_includes, + expand_macros: args.expand_macros, + include_comments: !args.strip_comments, + squash_newlines: args.strip_newlines, + }); + match slang.pickle() { + Ok(pickled) => { + if let Some(output) = args.output { + std::fs::write(output, pickled).expect("Failed to write output file"); + } else { + println!("{}", pickled); + }; + } + Err(cause) => return Err(Error::new(format!("Cannot pickle files: {}", cause))), + } + Ok(()) +} From 5d779e6456686d0e51bd6da657be86b2a3e7497d Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 10:13:28 +0100 Subject: [PATCH 03/46] ci: Clone slang submodule and bump checkout action --- .github/workflows/ci.yml | 18 +++++++++++++----- .github/workflows/cli_regression.yml | 12 +++++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84d2fa64e..90427d67c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,9 @@ 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}} @@ -38,7 +40,9 @@ 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 @@ -53,7 +57,9 @@ 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 @@ -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..76d8cdc88 100644 --- a/.github/workflows/cli_regression.yml +++ b/.github/workflows/cli_regression.yml @@ -8,7 +8,9 @@ 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 @@ -20,7 +22,9 @@ 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 @@ -32,7 +36,9 @@ 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 From d55c50bba2b62885b7bdd8d62985e86fd20ab0c8 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 10:50:08 +0100 Subject: [PATCH 04/46] bender-slang(build): Fix Linux builds --- crates/bender-slang/build.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 9252567e5..10ea4e633 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -5,6 +5,8 @@ fn main() { .define("SLANG_INCLUDE_TOOLS", "OFF") .define("SLANG_INCLUDE_PYSLANG", "OFF") .define("BUILD_SHARED_LIBS", "OFF") + // Forces installation into 'lib' instead of 'lib64' on some systems. + .define("CMAKE_INSTALL_LIBDIR", "lib") // TODO(fischeti): Check whether mimalloc can/should be enabled again. .define("SLANG_USE_MIMALLOC", "OFF") // TODO(fischeti): `fmt` currently causes issues on my machine since there is a system-wide installation. @@ -15,7 +17,9 @@ fn main() { // Configure Linker to find Slang static library println!("cargo:rustc-link-search=native={}/lib", dst.display()); - println!("cargo:rustc-link-lib=static=svLang"); + // Note: Linux is case-sensitive, so we use lowercase here. + // On macOS, the library is called `svLang`, but the linker is case-insensitive there. + println!("cargo:rustc-link-lib=static=svlang"); println!("cargo:rustc-link-lib=static=fmtd"); // Compile the C++ Bridge From 396ddd013752ff983dad9072646881ae8349f573 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 12:29:04 +0100 Subject: [PATCH 05/46] bender-slang(build): Provide config template for IIS env --- .cargo/config.toml.iis | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .cargo/config.toml.iis diff --git a/.cargo/config.toml.iis b/.cargo/config.toml.iis new file mode 100644 index 000000000..168231c97 --- /dev/null +++ b/.cargo/config.toml.iis @@ -0,0 +1,6 @@ +[target.x86_64-unknown-linux-gnu] +linker = "/usr/pack/gcc-14.2.0-af/bin/gcc" + +[env] +CC = "/usr/pack/gcc-14.2.0-af/bin/gcc" +CXX = "/usr/pack/gcc-14.2.0-af/bin/g++" From 3b16102e35be9689494301a0245f4a32128a9335 Mon Sep 17 00:00:00 2001 From: Michael Rogenmoser Date: Thu, 29 Jan 2026 16:48:01 +0100 Subject: [PATCH 06/46] Add slang feature to disable slang build --- .github/workflows/ci.yml | 10 +++++----- .github/workflows/cli_regression.yml | 4 ++-- .github/workflows/release.yaml | 10 +++++----- Cargo.toml | 5 ++++- src/cli.rs | 2 ++ src/cmd.rs | 1 + 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90427d67c..4b6eb2ecb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,9 +28,9 @@ jobs: 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 @@ -49,7 +49,7 @@ jobs: - name: Build run: cargo build - name: Cargo Test - run: cargo test --all + run: cargo test - name: Run unit-tests run: tests/run_all.sh shell: bash @@ -64,9 +64,9 @@ jobs: 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 diff --git a/.github/workflows/cli_regression.yml b/.github/workflows/cli_regression.yml index 76d8cdc88..e9c93dee4 100644 --- a/.github/workflows/cli_regression.yml +++ b/.github/workflows/cli_regression.yml @@ -15,7 +15,7 @@ jobs: 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 }} @@ -43,6 +43,6 @@ jobs: 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/Cargo.toml b/Cargo.toml index ee6959dde..08930c63f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ rust-version = "1.87.0" members = ["crates/bender-slang"] [dependencies] -bender-slang = { path = "crates/bender-slang" } +bender-slang = { path = "crates/bender-slang", optional = true} serde = { version = "1", features = ["derive"] } serde_yaml_ng = "0.10" @@ -54,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/src/cli.rs b/src/cli.rs index 0e23e34f8..714e34914 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -106,6 +106,7 @@ enum Commands { Init, Snapshot(cmd::snapshot::SnapshotArgs), Audit(cmd::audit::AuditArgs), + #[cfg(feature = "slang")] Pickle(cmd::pickle::PickleArgs), #[command(external_subcommand)] Plugin(Vec), @@ -330,6 +331,7 @@ 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(args), Commands::Plugin(args) => { let (plugin_name, plugin_args) = args diff --git a/src/cmd.rs b/src/cmd.rs index 689b148dd..bbae6227d 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -19,6 +19,7 @@ pub mod init; pub mod packages; pub mod parents; pub mod path; +#[cfg(feature = "slang")] pub mod pickle; pub mod script; pub mod snapshot; From a800abf11040a7cae05dce8ab0f815f5c4799074 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 23:45:08 +0100 Subject: [PATCH 07/46] bender-slang(build): Link libc++ statically on linux and windows --- crates/bender-slang/build.rs | 39 +++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 10ea4e633..983d9fcba 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -13,6 +13,7 @@ fn main() { .define("CMAKE_DISABLE_FIND_PACKAGE_fmt", "ON") // TODO(fischeti): Investigate how boost should be handled properly. .cxxflag("-DSLANG_BOOST_SINGLE_HEADER=1") + .static_crt(true) .build(); // Configure Linker to find Slang static library @@ -27,6 +28,7 @@ fn main() { bridge_build .file("cpp/slang_bridge.cpp") .flag_if_supported("-std=c++20") + .flag_if_supported("/std:c++20") // Static Linking Definition // Tells Slang headers not to look for DLL import/export symbols. .define("SLANG_STATIC_DEFINE", "1") @@ -35,11 +37,8 @@ fn main() { // TODO(fischeti): Investigate how boost should be handled properly. .define("SLANG_BOOST_SINGLE_HEADER", "1") // Include Paths - // 1. Slang source headers .include("vendor/slang/include") - // 2. Slang external headers (where boost_unordered.hpp lives) .include("vendor/slang/external") - // 3. CMake build output (where slang_export.h and fmt headers live) .include(dst.join("include")); // TODO(fischeti): Check whether debug definitions are necessary. @@ -47,6 +46,40 @@ fn main() { bridge_build.define("SLANG_DEBUG", "1"); } + // Linux: we try static linking of libstdc++ to avoid issues on older distros. + if std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "linux" { + // Determine the C++ compiler to use. Respect the CXX environment variable if set. + 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"]) + .output() + .expect("Failed to run g++"); + + if output.status.success() { + let path_str = std::str::from_utf8(&output.stdout).unwrap().trim(); + let path = std::path::Path::new(path_str); + + if path.is_absolute() && path.exists() { + if let Some(parent) = path.parent() { + // Add the directory containing libstdc++.a to the link search path + println!("cargo:rustc-link-search=native={}", parent.display()); + } + + bridge_build.cpp_set_stdlib(None); + println!("cargo:rustc-link-lib=static=stdc++"); + } else { + println!( + "cargo:warning=Could not find static libstdc++.a, falling back to dynamic linking" + ); + } + } + // Windows / MSVC: we force static linking of the CRT to avoid missing DLL issues + } else if std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "msvc" { + bridge_build.static_crt(true); + } + // macOS: we leave the default dynamic linking of libc++ as is. + bridge_build.compile("slang-bridge"); println!("cargo:rerun-if-changed=src/lib.rs"); From d1813e6211f8f3852d6b9d8660dc4744b8767ea4 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 23:46:41 +0100 Subject: [PATCH 08/46] ci: Enable `slang` for Windows again --- .github/workflows/ci.yml | 4 ++-- .github/workflows/cli_regression.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b6eb2ecb..4420a4f4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,9 +47,9 @@ jobs: with: toolchain: stable - name: Build - run: cargo build + run: cargo build --all-features - name: Cargo Test - run: cargo test + run: cargo test --workspace --all-features - name: Run unit-tests run: tests/run_all.sh shell: bash diff --git a/.github/workflows/cli_regression.yml b/.github/workflows/cli_regression.yml index e9c93dee4..bfdcf9bd7 100644 --- a/.github/workflows/cli_regression.yml +++ b/.github/workflows/cli_regression.yml @@ -29,7 +29,7 @@ jobs: 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 }} From 3a27cc36c36259722a4c7dd73be0bfcb0d84e847 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 31 Jan 2026 22:14:11 +0100 Subject: [PATCH 09/46] bender-slang(build): Fix `fmt` library in release builds --- crates/bender-slang/build.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 983d9fcba..cfb364dff 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -21,7 +21,12 @@ fn main() { // Note: Linux is case-sensitive, so we use lowercase here. // On macOS, the library is called `svLang`, but the linker is case-insensitive there. println!("cargo:rustc-link-lib=static=svlang"); - println!("cargo:rustc-link-lib=static=fmtd"); + + if std::env::var("PROFILE").unwrap() == "debug" { + println!("cargo:rustc-link-lib=static=fmtd"); + } else { + println!("cargo:rustc-link-lib=static=fmt"); + } // Compile the C++ Bridge let mut bridge_build = cxx_build::bridge("src/lib.rs"); From 0e04cec4356582f116721e265763faba1a5e278a Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 31 Jan 2026 22:53:45 +0100 Subject: [PATCH 10/46] bender-slang(build): Clean up --- crates/bender-slang/build.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index cfb364dff..796020e76 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -1,6 +1,13 @@ fn main() { - // Build Slang with CMake into a static library - let dst = cmake::Config::new("vendor/slang") + 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(); + + // Create the configuration builder + let mut slang_lib = cmake::Config::new("vendor/slang"); + + // Apply common settings + slang_lib .define("SLANG_INCLUDE_TESTS", "OFF") .define("SLANG_INCLUDE_TOOLS", "OFF") .define("SLANG_INCLUDE_PYSLANG", "OFF") @@ -18,14 +25,13 @@ fn main() { // Configure Linker to find Slang static library println!("cargo:rustc-link-search=native={}/lib", dst.display()); - // Note: Linux is case-sensitive, so we use lowercase here. - // On macOS, the library is called `svLang`, but the linker is case-insensitive there. println!("cargo:rustc-link-lib=static=svlang"); - if std::env::var("PROFILE").unwrap() == "debug" { - println!("cargo:rustc-link-lib=static=fmtd"); - } else { - println!("cargo:rustc-link-lib=static=fmt"); + // Link the appropriate fmt library based on build profile + match build_profile.as_str() { + "debug" => println!("cargo:rustc-link-lib=static=fmtd"), + "release" => println!("cargo:rustc-link-lib=static=fmt"), + _ => unreachable!(), } // Compile the C++ Bridge @@ -52,7 +58,7 @@ fn main() { } // Linux: we try static linking of libstdc++ to avoid issues on older distros. - if std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "linux" { + if target_os == "linux" { // Determine the C++ compiler to use. Respect the CXX environment variable if set. let compiler = std::env::var("CXX").unwrap_or_else(|_| "g++".to_string()); // We search for the static libstdc++ file using g++ From c575becbc7360c3446264327ef1fc0047c298c46 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 31 Jan 2026 23:41:37 +0100 Subject: [PATCH 11/46] bender-slang(build): Enable `mimalloc` library again --- crates/bender-slang/build.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 796020e76..400886721 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -14,8 +14,6 @@ fn main() { .define("BUILD_SHARED_LIBS", "OFF") // Forces installation into 'lib' instead of 'lib64' on some systems. .define("CMAKE_INSTALL_LIBDIR", "lib") - // TODO(fischeti): Check whether mimalloc can/should be enabled again. - .define("SLANG_USE_MIMALLOC", "OFF") // TODO(fischeti): `fmt` currently causes issues on my machine since there is a system-wide installation. .define("CMAKE_DISABLE_FIND_PACKAGE_fmt", "ON") // TODO(fischeti): Investigate how boost should be handled properly. @@ -27,11 +25,13 @@ fn main() { println!("cargo:rustc-link-search=native={}/lib", dst.display()); println!("cargo:rustc-link-lib=static=svlang"); - // Link the appropriate fmt library based on build profile - match build_profile.as_str() { - "debug" => println!("cargo:rustc-link-lib=static=fmtd"), - "release" => println!("cargo:rustc-link-lib=static=fmt"), - _ => unreachable!(), + // Link the additional libraries based on build profile + if build_profile == "debug" { + println!("cargo:rustc-link-lib=static=fmtd"); + println!("cargo:rustc-link-lib=static=mimalloc-debug") + } else { + println!("cargo:rustc-link-lib=static=fmt"); + println!("cargo:rustc-link-lib=static=mimalloc") } // Compile the C++ Bridge @@ -52,11 +52,6 @@ fn main() { .include("vendor/slang/external") .include(dst.join("include")); - // TODO(fischeti): Check whether debug definitions are necessary. - if std::env::var("PROFILE").unwrap() == "debug" { - bridge_build.define("SLANG_DEBUG", "1"); - } - // Linux: we try static linking of libstdc++ to avoid issues on older distros. if target_os == "linux" { // Determine the C++ compiler to use. Respect the CXX environment variable if set. From 812bde70178dc08fb79437dc516de658478724a1 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 31 Jan 2026 22:54:07 +0100 Subject: [PATCH 12/46] bender-slang(build): Fix windows build --- .github/workflows/ci.yml | 4 +-- .github/workflows/cli_regression.yml | 2 +- crates/bender-slang/build.rs | 42 ++++++++++++++++++---------- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4420a4f4e..d8b13df41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,9 +47,9 @@ jobs: with: toolchain: stable - name: Build - run: cargo build --all-features + run: cargo build --all-features --release - name: Cargo Test - run: cargo test --workspace --all-features + run: cargo test --workspace --all-features --release - name: Run unit-tests run: tests/run_all.sh shell: bash diff --git a/.github/workflows/cli_regression.yml b/.github/workflows/cli_regression.yml index bfdcf9bd7..91069aefe 100644 --- a/.github/workflows/cli_regression.yml +++ b/.github/workflows/cli_regression.yml @@ -29,7 +29,7 @@ jobs: with: toolchain: stable - name: Run CLI Regression - run: cargo test --all-features --test cli_regression -- --ignored + run: cargo test --all-features --test cli_regression --release -- --ignored env: BENDER_TEST_GOLDEN_BRANCH: ${{ github.base_ref }} diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 400886721..8132e1a78 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -17,21 +17,31 @@ fn main() { // TODO(fischeti): `fmt` currently causes issues on my machine since there is a system-wide installation. .define("CMAKE_DISABLE_FIND_PACKAGE_fmt", "ON") // TODO(fischeti): Investigate how boost should be handled properly. - .cxxflag("-DSLANG_BOOST_SINGLE_HEADER=1") - .static_crt(true) - .build(); + .cxxflag("-DSLANG_BOOST_SINGLE_HEADER=1"); + + // Windows / MSVC specific flags + if target_env == "msvc" { + slang_lib.cxxflag("/EHsc").cxxflag("/utf-8"); + } + + // Build the slang library + let dst = slang_lib.build(); // Configure Linker to find Slang static library println!("cargo:rustc-link-search=native={}/lib", dst.display()); println!("cargo:rustc-link-lib=static=svlang"); - // Link the additional libraries based on build profile - if build_profile == "debug" { - println!("cargo:rustc-link-lib=static=fmtd"); - println!("cargo:rustc-link-lib=static=mimalloc-debug") - } else { - println!("cargo:rustc-link-lib=static=fmt"); - println!("cargo:rustc-link-lib=static=mimalloc") + // Link the additional libraries based on build profile and OS + match (build_profile.as_str(), target_env.as_str()) { + ("release", _) | (_, "msvc") => { + println!("cargo:rustc-link-lib=static=fmt"); + println!("cargo:rustc-link-lib=static=mimalloc"); + } + ("debug", _) => { + println!("cargo:rustc-link-lib=static=fmtd"); + println!("cargo:rustc-link-lib=static=mimalloc-debug"); + } + _ => unreachable!(), } // Compile the C++ Bridge @@ -39,7 +49,6 @@ fn main() { bridge_build .file("cpp/slang_bridge.cpp") .flag_if_supported("-std=c++20") - .flag_if_supported("/std:c++20") // Static Linking Definition // Tells Slang headers not to look for DLL import/export symbols. .define("SLANG_STATIC_DEFINE", "1") @@ -80,10 +89,13 @@ fn main() { ); } } - // Windows / MSVC: we force static linking of the CRT to avoid missing DLL issues - } else if std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "msvc" { - bridge_build.static_crt(true); - } + // Windows / MSVC: we set the appropriate flags for C++20 and exception handling. + } else if target_env == "msvc" { + bridge_build + .flag_if_supported("/std:c++20") + .flag("/EHsc") + .flag("/utf-8"); + }; // macOS: we leave the default dynamic linking of libc++ as is. bridge_build.compile("slang-bridge"); From fc5d51942dca1d4b7f9a1ee76a2e17c392c42df3 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 1 Feb 2026 00:46:32 +0100 Subject: [PATCH 13/46] bender-slang(build): Don't use system-installed slang dependencies --- crates/bender-slang/build.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 8132e1a78..e253fc806 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -14,10 +14,10 @@ fn main() { .define("BUILD_SHARED_LIBS", "OFF") // Forces installation into 'lib' instead of 'lib64' on some systems. .define("CMAKE_INSTALL_LIBDIR", "lib") - // TODO(fischeti): `fmt` currently causes issues on my machine since there is a system-wide installation. + // Disable finding system-installed packages, we want to fetch and build them from source. .define("CMAKE_DISABLE_FIND_PACKAGE_fmt", "ON") - // TODO(fischeti): Investigate how boost should be handled properly. - .cxxflag("-DSLANG_BOOST_SINGLE_HEADER=1"); + .define("CMAKE_DISABLE_FIND_PACKAGE_mimalloc", "ON") + .define("CMAKE_DISABLE_FIND_PACKAGE_Boost", "ON"); // Windows / MSVC specific flags if target_env == "msvc" { @@ -49,14 +49,10 @@ fn main() { bridge_build .file("cpp/slang_bridge.cpp") .flag_if_supported("-std=c++20") - // Static Linking Definition // Tells Slang headers not to look for DLL import/export symbols. .define("SLANG_STATIC_DEFINE", "1") - // Boost Vendored Mode - // Tells Slang to use the local 'external/boost_*.hpp' files instead of system Boost. - // TODO(fischeti): Investigate how boost should be handled properly. + // Tells Slang to use vendor-provided instead of system-installed Boost header files. .define("SLANG_BOOST_SINGLE_HEADER", "1") - // Include Paths .include("vendor/slang/include") .include("vendor/slang/external") .include(dst.join("include")); From fda45747c64d8b6fc4f19e4c53d3062f1fc89e7e Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 1 Feb 2026 13:52:00 +0100 Subject: [PATCH 14/46] bender-slang(ffi): Refactor interface --- crates/bender-slang/build.rs | 7 ++ crates/bender-slang/cpp/slang_bridge.cpp | 93 ++++++++------- crates/bender-slang/cpp/slang_bridge.h | 41 ++++++- crates/bender-slang/src/lib.rs | 146 +++++++++++------------ src/cmd/pickle.rs | 51 ++++---- 5 files changed, 188 insertions(+), 150 deletions(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index e253fc806..2d946c404 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -1,3 +1,6 @@ +// 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(); @@ -12,6 +15,7 @@ fn main() { .define("SLANG_INCLUDE_TOOLS", "OFF") .define("SLANG_INCLUDE_PYSLANG", "OFF") .define("BUILD_SHARED_LIBS", "OFF") + .define("SLANG_USE_MIMALLOC", "ON") // 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. @@ -53,6 +57,9 @@ fn main() { .define("SLANG_STATIC_DEFINE", "1") // Tells Slang to use vendor-provided instead of system-installed Boost header files. .define("SLANG_BOOST_SINGLE_HEADER", "1") + .define("SLANG_DEBUG", "") + .define("SLANG_USE_THREADS", "1") + .define("SLANG_USE_MIMALLOC", "1") .include("vendor/slang/include") .include("vendor/slang/external") .include(dst.join("include")); diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 885595225..d0aed9786 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -1,69 +1,73 @@ -#include "slang_bridge.h" -#include "bender-slang/src/lib.rs.h" // Import the generated C++ definition of the structs +// Copyright (c) 2025 ETH Zurich +// Tim Fischer -#include "slang/driver/Driver.h" +#include "slang_bridge.h" +#include "bender-slang/src/lib.rs.h" #include "slang/syntax/SyntaxPrinter.h" -#include "slang/syntax/SyntaxTree.h" - -#include -#include -#include +#include using namespace slang; using namespace slang::driver; using namespace slang::syntax; -rust::String pickle(rust::Vec sources, - rust::Vec include_dirs, - rust::Vec defines, - SlangPrintOpts options) { - Driver driver; +SlangContext::SlangContext() { driver.addStandardArgs(); +} + +void SlangContext::add_source(rust::Str path) { + sources.emplace_back(std::string(path)); +} - // 1. Construct Arguments from SlangFiles +void SlangContext::add_include(rust::Str path) { + includes.emplace_back(std::string(path)); +} + +void SlangContext::add_define(rust::Str def) { + defines.emplace_back(std::string(def)); +} + +bool SlangContext::parse() { + // Construct argv for the driver std::vector arg_strings; arg_strings.push_back("slang_tool"); - for (const auto& source : sources) { - arg_strings.push_back(std::string(source)); - } - for (const auto& path : include_dirs) { - arg_strings.push_back("-I"); - arg_strings.push_back(std::string(path)); - } + for (const auto& s : sources) arg_strings.push_back(s); + for (const auto& i : includes) { arg_strings.push_back("-I"); arg_strings.push_back(i); } + for (const auto& d : defines) { arg_strings.push_back("-D"); arg_strings.push_back(d); } - for (const auto& def : defines) { - arg_strings.push_back("-D"); - arg_strings.push_back(std::string(def)); - } - - // Convert to C-style argv std::vector c_args; - c_args.reserve(arg_strings.size()); for (const auto& s : arg_strings) c_args.push_back(s.c_str()); - // 2. Run Compilation if (!driver.parseCommandLine(c_args.size(), c_args.data())) { - throw std::runtime_error("Failed to parse command line arguments."); + // You might want to capture stderr here or throw a clearer error + throw std::runtime_error("Failed to parse command line args"); } if (!driver.processOptions()) { - throw std::runtime_error("Failed to process options."); + throw std::runtime_error("Failed to process options"); } - bool parseSuccess = driver.parseAllSources(); - bool diagSuccess = driver.reportDiagnostics(false); + bool ok = driver.parseAllSources(); + // reportDiagnostics returns true if issues found, so we invert logic or check strictness + bool hasErrors = driver.reportDiagnostics(false); - if (!parseSuccess || !diagSuccess) { - throw std::runtime_error("Parsing failed. Check stderr for details."); - } + return ok && !hasErrors; +} + +size_t SlangContext::get_tree_count() const { + return driver.syntaxTrees.size(); +} - auto& syntaxTrees = driver.syntaxTrees; - if (syntaxTrees.empty()) { - return ""; +std::shared_ptr SlangContext::get_tree(size_t index) const { + if (index >= driver.syntaxTrees.size()) { + // Rust's loop bounds prevent this, but good for safety + throw std::out_of_range("Syntax tree index out of range"); } + return driver.syntaxTrees[index]; +} - // 3. Configure Printer from SlangPrinterOptions +rust::String SlangContext::print_tree(const SyntaxTree& tree, SlangPrintOpts options) const { + // Use the SourceManager from the driver (this context) SyntaxPrinter printer(driver.sourceManager); printer.setIncludeDirectives(options.include_directives); @@ -72,9 +76,10 @@ rust::String pickle(rust::Vec sources, printer.setSquashNewlines(options.squash_newlines); printer.setIncludeComments(options.include_comments); - for (auto& tree : syntaxTrees) { - printer.print(*tree); - } - + printer.print(tree); return rust::String(printer.str()); } + +std::unique_ptr new_slang_context() { + return std::make_unique(); +} diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index 1d4174138..4153e29eb 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -1,10 +1,39 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + #pragma once #include "rust/cxx.h" +#include "slang/driver/Driver.h" +#include "slang/syntax/SyntaxTree.h" +#include +#include +#include + +struct SlangPrintOpts; // Forward decl + +// The wrapper class exposed as "SlangContext" to Rust +class SlangContext { +public: + SlangContext(); + + void add_source(rust::Str path); + void add_include(rust::Str path); + void add_define(rust::Str def); + + bool parse(); + + size_t get_tree_count() const; + std::shared_ptr get_tree(size_t index) const; + + rust::String print_tree(const slang::syntax::SyntaxTree& tree, SlangPrintOpts options) const; + +private: + slang::driver::Driver driver; -// Forward declare the structs generated by CXX -struct SlangPrintOpts; + // We buffer args to pass to driver.parseCommandLine later + std::vector sources; + std::vector includes; + std::vector defines; +}; -rust::String pickle(rust::Vec sources, - rust::Vec include_dirs, - rust::Vec defines, - SlangPrintOpts options); +std::unique_ptr new_slang_context(); diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index a78d08928..214912729 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -1,118 +1,106 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +use cxx::{SharedPtr, UniquePtr}; + pub use ffi::SlangPrintOpts; #[cxx::bridge] mod ffi { - /// Options for the syntax printer - #[derive(Clone)] + #[derive(Clone, Copy)] struct SlangPrintOpts { - /// Whether to include preprocessor directives include_directives: bool, - /// Whether to expand include directives expand_includes: bool, - /// Whether to expand macros expand_macros: bool, - /// Whether to print comments include_comments: bool, - /// Whether to squash newlines squash_newlines: bool, } unsafe extern "C++" { include!("bender-slang/cpp/slang_bridge.h"); + // Include Slang header to define SyntaxTree type for CXX + include!("slang/syntax/SyntaxTree.h"); + + /// Opaque type for the Slang Driver wrapper + type SlangContext; + + /// Opaque type for the Slang SyntaxTree + #[namespace = "slang::syntax"] + type SyntaxTree; + + /// Create a new persistent context (owns the Driver) + fn new_slang_context() -> UniquePtr; + + // Methods on SlangContext + fn add_source(self: Pin<&mut SlangContext>, path: &str); + fn add_include(self: Pin<&mut SlangContext>, path: &str); + fn add_define(self: Pin<&mut SlangContext>, def: &str); + + /// Parse all added sources. Returns true on success. + fn parse(self: Pin<&mut SlangContext>) -> Result; - fn pickle( - sources: Vec, - include_dirs: Vec, - defines: Vec, - options: SlangPrintOpts, - ) -> Result; + /// Retrieves the number of parsed syntax trees + fn get_tree_count(self: &SlangContext) -> usize; + + /// Retrieves a shared pointer to a specific syntax tree by index + fn get_tree(self: &SlangContext, index: usize) -> SharedPtr; + + /// Print a specific tree using the context's SourceManager + fn print_tree(self: &SlangContext, tree: &SyntaxTree, options: SlangPrintOpts) -> String; } } -/// Main interface for Slang bindings -pub struct Slang { - /// Source files to be pickled - sources: Vec, - /// Include directories - include_dirs: Vec, - /// Defines - defines: Vec, - /// Print options - print_opts: ffi::SlangPrintOpts, +/// A persistent Slang session +pub struct SlangSession { + ctx: UniquePtr, } -/// Main interface for interfacing with Slang -impl Slang { +impl SlangSession { + /// Creates a new Slang session pub fn new() -> Self { - Slang { - sources: Vec::new(), - include_dirs: Vec::new(), - defines: Vec::new(), - print_opts: ffi::SlangPrintOpts { - include_directives: true, - expand_includes: true, - expand_macros: true, - include_comments: true, - squash_newlines: true, - }, + Self { + ctx: ffi::new_slang_context(), } } - /// Adds source files to be pickled. - pub fn add_sources(&mut self, sources: Vec) { - self.sources.extend(sources); - } - - /// Adds source sources to be pickled, returning self for chaining. - pub fn with_sources(mut self, sources: Vec) -> Self { - self.sources.extend(sources); - self + /// Adds a source file to be parsed + pub fn add_source(&mut self, path: &str) { + self.ctx.pin_mut().add_source(path); } - /// Adds include directories. - pub fn add_include_dirs(&mut self, dirs: Vec) { - self.include_dirs.extend(dirs); + /// Adds an include directory + pub fn add_include(&mut self, path: &str) { + self.ctx.pin_mut().add_include(path); } - /// Adds include directories, returning self for chaining. - pub fn with_include_dirs(mut self, dirs: Vec) -> Self { - self.include_dirs.extend(dirs); - self + /// Adds a preprocessor define + pub fn add_define(&mut self, define: &str) { + self.ctx.pin_mut().add_define(define); } - /// Adds defines. - pub fn add_defines(&mut self, defines: Vec) { - self.defines.extend(defines); + /// Parses all added source files into syntax trees + pub fn parse(&mut self) -> Result> { + Ok(self.ctx.pin_mut().parse()?) } - /// Adds defines, returning self for chaining. - pub fn with_defines(mut self, defines: Vec) -> Self { - self.defines.extend(defines); - self - } - - /// Sets print options. - pub fn set_print_options(&mut self, print_opts: ffi::SlangPrintOpts) { - self.print_opts = print_opts; + /// Returns the parsed syntax trees as a Rust vector + pub fn get_trees(&self) -> Vec> { + let count = self.ctx.get_tree_count(); + let mut trees = Vec::with_capacity(count); + for i in 0..count { + trees.push(self.ctx.get_tree(i)); + } + trees } - /// Sets print options, returning self for chaining. - pub fn with_print_options(mut self, print_opts: ffi::SlangPrintOpts) -> Self { - self.print_opts = print_opts; - self + /// Returns an iterator over the parsed syntax trees + pub fn trees_iter(&self) -> impl Iterator> + '_ { + (0..self.ctx.get_tree_count()).map(|i| self.ctx.get_tree(i)) } - /// Pickles files based on the provided configuration. - /// Returns the pickled content or an error if parsing/processing failed. - pub fn pickle(&self) -> Result> { - // call the C++ function; errors are propagated as Rust Results - let result = ffi::pickle( - self.sources.clone(), - self.include_dirs.clone(), - self.defines.clone(), - self.print_opts.clone(), - )?; - Ok(result) + /// Prints a syntax tree with given printing options + pub fn print_tree(&self, tree: &ffi::SyntaxTree, opts: ffi::SlangPrintOpts) -> String { + self.ctx.print_tree(tree, opts) } } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 3a77b50a1..dec881c02 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -5,7 +5,7 @@ use clap::{ArgAction, Args}; -use bender_slang::{Slang, SlangPrintOpts}; +use bender_slang::{SlangPrintOpts, SlangSession}; use crate::error::*; @@ -54,26 +54,35 @@ pub struct PickleArgs { /// Execute the `pickle` subcommand. pub fn run(args: PickleArgs) -> Result<()> { - let slang = Slang::new() - .with_sources(args.files) - .with_include_dirs(args.include_dirs) - .with_defines(args.defines) - .with_print_options(SlangPrintOpts { - include_directives: args.include_directives, - expand_includes: args.expand_includes, - expand_macros: args.expand_macros, - include_comments: !args.strip_comments, - squash_newlines: args.strip_newlines, - }); - match slang.pickle() { - Ok(pickled) => { - if let Some(output) = args.output { - std::fs::write(output, pickled).expect("Failed to write output file"); - } else { - println!("{}", pickled); - }; - } - Err(cause) => return Err(Error::new(format!("Cannot pickle files: {}", cause))), + let mut slang = SlangSession::new(); + + for file in args.files.iter() { + slang.add_source(file); + } + + for include in args.include_dirs.iter() { + slang.add_include(include); + } + + for define in args.defines.iter() { + slang.add_define(define); + } + + slang + .parse() + .map_err(|cause| Error::new(format!("Cannot parse files: {}", cause)))?; + + let print_opts = SlangPrintOpts { + include_directives: args.include_directives, + expand_includes: args.expand_includes, + expand_macros: args.expand_macros, + include_comments: !args.strip_comments, + squash_newlines: args.strip_newlines, + }; + + for tree in slang.trees_iter() { + let pickled = slang.print_tree(&tree, print_opts); + println!("{}", pickled); } Ok(()) } From 4fa9b3a97f6f41e3fef4d26444038e8f3b4c76d1 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 2 Feb 2026 20:00:33 +0100 Subject: [PATCH 15/46] bender-slang(build): Align defines and flags in library and bridge build Will result in ABI mismatches i.e. segfaults otherwise --- crates/bender-slang/build.rs | 62 +++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 2d946c404..4a9e46a6e 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -9,13 +9,33 @@ fn main() { // Create the configuration builder let mut slang_lib = cmake::Config::new("vendor/slang"); - // Apply common settings + // 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" { + 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") - .define("SLANG_INCLUDE_PYSLANG", "OFF") - .define("BUILD_SHARED_LIBS", "OFF") - .define("SLANG_USE_MIMALLOC", "ON") // 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. @@ -23,9 +43,13 @@ fn main() { .define("CMAKE_DISABLE_FIND_PACKAGE_mimalloc", "ON") .define("CMAKE_DISABLE_FIND_PACKAGE_Boost", "ON"); - // Windows / MSVC specific flags - if target_env == "msvc" { - slang_lib.cxxflag("/EHsc").cxxflag("/utf-8"); + // 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 @@ -53,13 +77,6 @@ fn main() { bridge_build .file("cpp/slang_bridge.cpp") .flag_if_supported("-std=c++20") - // Tells Slang headers not to look for DLL import/export symbols. - .define("SLANG_STATIC_DEFINE", "1") - // Tells Slang to use vendor-provided instead of system-installed Boost header files. - .define("SLANG_BOOST_SINGLE_HEADER", "1") - .define("SLANG_DEBUG", "") - .define("SLANG_USE_THREADS", "1") - .define("SLANG_USE_MIMALLOC", "1") .include("vendor/slang/include") .include("vendor/slang/external") .include(dst.join("include")); @@ -92,14 +109,15 @@ fn main() { ); } } - // Windows / MSVC: we set the appropriate flags for C++20 and exception handling. - } else if target_env == "msvc" { - bridge_build - .flag_if_supported("/std:c++20") - .flag("/EHsc") - .flag("/utf-8"); - }; - // macOS: we leave the default dynamic linking of libc++ as is. + } + + // 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"); From 4d7134212eba56333b6d57ca195cca293d56e4b9 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 4 Feb 2026 12:40:26 +0100 Subject: [PATCH 16/46] bender-slang(bridge): Add SyntaxTree rewriter for module name prefixes/suffixes --- crates/bender-slang/cpp/slang_bridge.cpp | 152 +++++++++++++++++------ crates/bender-slang/cpp/slang_bridge.h | 12 +- crates/bender-slang/src/lib.rs | 35 +++++- src/cmd/pickle.rs | 11 +- 4 files changed, 167 insertions(+), 43 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index d0aed9786..b5af401fb 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -2,44 +2,56 @@ // Tim Fischer #include "slang_bridge.h" + #include "bender-slang/src/lib.rs.h" #include "slang/syntax/SyntaxPrinter.h" -#include +#include "slang/syntax/SyntaxVisitor.h" using namespace slang; using namespace slang::driver; using namespace slang::syntax; -SlangContext::SlangContext() { - driver.addStandardArgs(); -} +using std::memcpy; +using std::shared_ptr; +using std::string; +using std::string_view; +using std::vector; -void SlangContext::add_source(rust::Str path) { - sources.emplace_back(std::string(path)); -} +// Create a new SlangContext instance +std::unique_ptr new_slang_context() { return std::make_unique(); } -void SlangContext::add_include(rust::Str path) { - includes.emplace_back(std::string(path)); -} +// Constructor: initialize driver with standard args +SlangContext::SlangContext() { driver.addStandardArgs(); } -void SlangContext::add_define(rust::Str def) { - defines.emplace_back(std::string(def)); -} +// Add a source file path to the context +void SlangContext::add_source(rust::Str path) { sources.emplace_back(std::string(path)); } + +// Add an include path to the context +void SlangContext::add_include(rust::Str path) { includes.emplace_back(std::string(path)); } + +// Add a define to the context +void SlangContext::add_define(rust::Str def) { defines.emplace_back(std::string(def)); } bool SlangContext::parse() { - // Construct argv for the driver - std::vector arg_strings; + vector arg_strings; arg_strings.push_back("slang_tool"); - for (const auto& s : sources) arg_strings.push_back(s); - for (const auto& i : includes) { arg_strings.push_back("-I"); arg_strings.push_back(i); } - for (const auto& d : defines) { arg_strings.push_back("-D"); arg_strings.push_back(d); } + for (const auto& s : sources) + arg_strings.push_back(s); + for (const auto& i : includes) { + arg_strings.push_back("-I"); + arg_strings.push_back(i); + } + for (const auto& d : defines) { + arg_strings.push_back("-D"); + arg_strings.push_back(d); + } - std::vector c_args; - for (const auto& s : arg_strings) c_args.push_back(s.c_str()); + vector c_args; + for (const auto& s : arg_strings) + c_args.push_back(s.c_str()); if (!driver.parseCommandLine(c_args.size(), c_args.data())) { - // You might want to capture stderr here or throw a clearer error throw std::runtime_error("Failed to parse command line args"); } @@ -54,20 +66,91 @@ bool SlangContext::parse() { return ok && !hasErrors; } -size_t SlangContext::get_tree_count() const { - return driver.syntaxTrees.size(); -} +// Get the number of syntax trees parsed by the driver +size_t SlangContext::get_tree_count() const { return driver.syntaxTrees.size(); } + +// Get the syntax tree at the specified index +shared_ptr SlangContext::get_tree(size_t index) const { return driver.syntaxTrees[index]; } + +// Rewriter that adds prefix/suffix to module and instantiated hierarchy names +class SuffixPrefixRewriter : public SyntaxRewriter { + public: + SuffixPrefixRewriter(string_view prefix, string_view suffix) : prefix(prefix), suffix(suffix) {} + + // Helper to allocate and build renamed string with prefix/suffix + string_view rename(string_view name) { + size_t len = prefix.size() + name.size() + suffix.size(); + char* mem = (char*)alloc.allocate(len, 1); + memcpy(mem, prefix.data(), prefix.size()); + memcpy(mem + prefix.size(), name.data(), name.size()); + memcpy(mem + prefix.size() + name.size(), suffix.data(), suffix.size()); + return string_view(mem, len); + } + + // Renames "module foo;" -> "module foo;" + void handle(const ModuleDeclarationSyntax& node) { + if (node.header->name.isMissing()) + return; + + // Create a new name token + auto newName = rename(node.header->name.valueText()); + auto newNameToken = makeId(newName, node.header->name.trivia()); + + // Clone the header and update the name + ModuleHeaderSyntax* newHeader = deepClone(*node.header, alloc); + newHeader->name = newNameToken; -std::shared_ptr SlangContext::get_tree(size_t index) const { - if (index >= driver.syntaxTrees.size()) { - // Rust's loop bounds prevent this, but good for safety - throw std::out_of_range("Syntax tree index out of range"); + // Replace the old header with the new one + replace(*node.header, *newHeader); + + // Continue visiting child nodes + visitDefault(node); + } + + // Renames "foo i_foo();" -> "foo i_foo();" + void handle(const HierarchyInstantiationSyntax& node) { + // Check to make sure we are dealing with an identifier + // and not a built-in type e.g. `initial foo();` + if (node.type.kind == parsing::TokenKind::Identifier) { + + // Create a new name token + auto newName = rename(node.type.valueText()); + auto newNameToken = makeId(newName); + + // Clone the node and update the type token + HierarchyInstantiationSyntax* newNode = deepClone(node, alloc); + newNode->type = newNameToken; + + // Replace the old node with the new one + replace(node, *newNode, true); + } + + // Continue visiting child nodes + visitDefault(node); } - return driver.syntaxTrees[index]; + + private: + string_view prefix; + string_view suffix; +}; + +// Rename modules and instantiated hierarchy names in the given syntax tree +shared_ptr SlangContext::rename_tree(const shared_ptr tree, rust::Str prefix, + rust::Str suffix) const { + + // Convert rust::Str to string_view and instantiate rewriter + string_view prefix_str(prefix.data(), prefix.size()); + string_view suffix_str(suffix.data(), suffix.size()); + SuffixPrefixRewriter rewriter(prefix_str, suffix_str); + + // Apply the rewriter to the tree and return the transformed tree + return rewriter.transform(tree); } -rust::String SlangContext::print_tree(const SyntaxTree& tree, SlangPrintOpts options) const { - // Use the SourceManager from the driver (this context) +// Print the given syntax tree with specified options +rust::String SlangContext::print_tree(const shared_ptr tree, SlangPrintOpts options) const { + + // Set up the printer with options SyntaxPrinter printer(driver.sourceManager); printer.setIncludeDirectives(options.include_directives); @@ -76,10 +159,7 @@ rust::String SlangContext::print_tree(const SyntaxTree& tree, SlangPrintOpts opt printer.setSquashNewlines(options.squash_newlines); printer.setIncludeComments(options.include_comments); - printer.print(tree); + // Print the tree root and return as rust::String + printer.print(tree->root()); return rust::String(printer.str()); } - -std::unique_ptr new_slang_context() { - return std::make_unique(); -} diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index 4153e29eb..9d9e600bb 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -5,15 +5,16 @@ #include "rust/cxx.h" #include "slang/driver/Driver.h" #include "slang/syntax/SyntaxTree.h" + #include -#include #include +#include struct SlangPrintOpts; // Forward decl // The wrapper class exposed as "SlangContext" to Rust class SlangContext { -public: + public: SlangContext(); void add_source(rust::Str path); @@ -25,9 +26,12 @@ class SlangContext { size_t get_tree_count() const; std::shared_ptr get_tree(size_t index) const; - rust::String print_tree(const slang::syntax::SyntaxTree& tree, SlangPrintOpts options) const; + std::shared_ptr rename_tree(const std::shared_ptr, + rust::Str prefix, rust::Str suffix) const; + + rust::String print_tree(const std::shared_ptr, SlangPrintOpts options) const; -private: + private: slang::driver::Driver driver; // We buffer args to pass to driver.parseCommandLine later diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index 214912729..0d25c6b23 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -46,8 +46,20 @@ mod ffi { /// Retrieves a shared pointer to a specific syntax tree by index fn get_tree(self: &SlangContext, index: usize) -> SharedPtr; + /// Rename names in the syntax tree with a given prefix and suffix + fn rename_tree( + self: &SlangContext, + tree: SharedPtr, + prefix: &str, + suffix: &str, + ) -> SharedPtr; + /// Print a specific tree using the context's SourceManager - fn print_tree(self: &SlangContext, tree: &SyntaxTree, options: SlangPrintOpts) -> String; + fn print_tree( + self: &SlangContext, + tree: SharedPtr, + options: SlangPrintOpts, + ) -> String; } } @@ -99,8 +111,27 @@ impl SlangSession { (0..self.ctx.get_tree_count()).map(|i| self.ctx.get_tree(i)) } + /// Renames names in the syntax tree with a given prefix and suffix + pub fn rename_tree( + &self, + tree: SharedPtr, + prefix: Option<&str>, + suffix: Option<&str>, + ) -> SharedPtr { + if prefix.is_none() && suffix.is_none() { + return tree; + } + let prefix = prefix.unwrap_or(""); + let suffix = suffix.unwrap_or(""); + self.ctx.rename_tree(tree, prefix, suffix) + } + /// Prints a syntax tree with given printing options - pub fn print_tree(&self, tree: &ffi::SyntaxTree, opts: ffi::SlangPrintOpts) -> String { + pub fn print_tree( + &self, + tree: SharedPtr, + opts: ffi::SlangPrintOpts, + ) -> String { self.ctx.print_tree(tree, opts) } } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index dec881c02..6218116d4 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -31,6 +31,14 @@ pub struct PickleArgs { #[arg(short = 'D', long, action = ArgAction::Append)] defines: Vec, + /// The prefix to add to all names + #[arg(long)] + prefix: Option, + + /// The suffix to add to all names + #[arg(long)] + suffix: Option, + /// Whether to include preprocessor directives #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Print Options")] include_directives: bool, @@ -81,7 +89,8 @@ pub fn run(args: PickleArgs) -> Result<()> { }; for tree in slang.trees_iter() { - let pickled = slang.print_tree(&tree, print_opts); + let renamed_tree = slang.rename_tree(tree, args.prefix.as_deref(), args.suffix.as_deref()); + let pickled = slang.print_tree(renamed_tree, print_opts); println!("{}", pickled); } Ok(()) From 73fcf102d98b48cc914f3194ff8ec9c4e9087249 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 4 Feb 2026 14:25:26 +0100 Subject: [PATCH 17/46] bender-slang(build): Add include guard to slang_bridge.h Prevent multiple inclusion of the header by adding a traditional #ifndef/define/endif include guard (BENDER_SLANG_BRIDGE_H). This replaces the lone #pragma once for better portability and ensures the header can be safely included multiple times across translation units. --- crates/bender-slang/cpp/slang_bridge.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index 9d9e600bb..c060e7646 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -1,7 +1,9 @@ // Copyright (c) 2025 ETH Zurich // Tim Fischer -#pragma once +#ifndef BENDER_SLANG_BRIDGE_H +#define BENDER_SLANG_BRIDGE_H + #include "rust/cxx.h" #include "slang/driver/Driver.h" #include "slang/syntax/SyntaxTree.h" @@ -41,3 +43,5 @@ class SlangContext { }; std::unique_ptr new_slang_context(); + +#endif // BENDER_SLANG_BRIDGE_H From f823dffc087cedad5b4c81302ead8dd68d371f8b Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 5 Feb 2026 23:51:45 +0100 Subject: [PATCH 18/46] bender-slang(ffi): Refactor interface (once again) --- crates/bender-slang/cpp/slang_bridge.cpp | 86 ++++++-------- crates/bender-slang/cpp/slang_bridge.h | 31 ++--- crates/bender-slang/src/lib.rs | 139 +++++++++-------------- src/cmd/pickle.rs | 35 ++---- 4 files changed, 111 insertions(+), 180 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index b5af401fb..999d3f12e 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -10,6 +10,7 @@ using namespace slang; using namespace slang::driver; using namespace slang::syntax; +using namespace slang::parsing; using std::memcpy; using std::shared_ptr; @@ -20,58 +21,40 @@ using std::vector; // Create a new SlangContext instance std::unique_ptr new_slang_context() { return std::make_unique(); } -// Constructor: initialize driver with standard args -SlangContext::SlangContext() { driver.addStandardArgs(); } +SlangContext::SlangContext() {} -// Add a source file path to the context -void SlangContext::add_source(rust::Str path) { sources.emplace_back(std::string(path)); } - -// Add an include path to the context -void SlangContext::add_include(rust::Str path) { includes.emplace_back(std::string(path)); } - -// Add a define to the context -void SlangContext::add_define(rust::Str def) { defines.emplace_back(std::string(def)); } - -bool SlangContext::parse() { - vector arg_strings; - arg_strings.push_back("slang_tool"); - - for (const auto& s : sources) - arg_strings.push_back(s); - for (const auto& i : includes) { - arg_strings.push_back("-I"); - arg_strings.push_back(i); +// Set the include paths for the preprocessor +void SlangContext::set_includes(const rust::Vec& incs) { + ppOptions.additionalIncludePaths.clear(); + for (const auto& inc : incs) { + ppOptions.additionalIncludePaths.emplace_back(std::string(inc)); } - for (const auto& d : defines) { - arg_strings.push_back("-D"); - arg_strings.push_back(d); +} + +// Sets the preprocessor defines +void SlangContext::set_defines(const rust::Vec& defs) { + ppOptions.predefines.clear(); + for (const auto& def : defs) { + ppOptions.predefines.emplace_back(std::string(def)); } +} - vector c_args; - for (const auto& s : arg_strings) - c_args.push_back(s.c_str()); +// Parses the given file and returns a syntax tree, if successful +std::shared_ptr SlangContext::parse_file(rust::Str path) { + Bag options; + options.set(ppOptions); - if (!driver.parseCommandLine(c_args.size(), c_args.data())) { - throw std::runtime_error("Failed to parse command line args"); - } + auto result = SyntaxTree::fromFile(string_view(path.data(), path.size()), sourceManager, options); - if (!driver.processOptions()) { - throw std::runtime_error("Failed to process 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); } - bool ok = driver.parseAllSources(); - // reportDiagnostics returns true if issues found, so we invert logic or check strictness - bool hasErrors = driver.reportDiagnostics(false); - - return ok && !hasErrors; + return *result; } -// Get the number of syntax trees parsed by the driver -size_t SlangContext::get_tree_count() const { return driver.syntaxTrees.size(); } - -// Get the syntax tree at the specified index -shared_ptr SlangContext::get_tree(size_t index) const { return driver.syntaxTrees[index]; } - // Rewriter that adds prefix/suffix to module and instantiated hierarchy names class SuffixPrefixRewriter : public SyntaxRewriter { public: @@ -134,24 +117,21 @@ class SuffixPrefixRewriter : public SyntaxRewriter { string_view suffix; }; -// Rename modules and instantiated hierarchy names in the given syntax tree -shared_ptr SlangContext::rename_tree(const shared_ptr tree, rust::Str prefix, - rust::Str suffix) const { - - // Convert rust::Str to string_view and instantiate rewriter - string_view prefix_str(prefix.data(), prefix.size()); - string_view suffix_str(suffix.data(), suffix.size()); - SuffixPrefixRewriter rewriter(prefix_str, suffix_str); +// Transform the given syntax tree by renaming modules and instantiated hierarchy names with the specified prefix/suffix +std::shared_ptr rename(std::shared_ptr tree, rust::Str prefix, rust::Str suffix) { + std::string_view p(prefix.data(), prefix.size()); + std::string_view s(suffix.data(), suffix.size()); - // Apply the rewriter to the tree and return the transformed tree + // SuffixPrefixRewriter is defined in the .cpp file as before + SuffixPrefixRewriter rewriter(p, s); return rewriter.transform(tree); } // Print the given syntax tree with specified options -rust::String SlangContext::print_tree(const shared_ptr tree, SlangPrintOpts options) const { +rust::String print_tree(const shared_ptr tree, SlangPrintOpts options) { // Set up the printer with options - SyntaxPrinter printer(driver.sourceManager); + SyntaxPrinter printer(tree->sourceManager()); printer.setIncludeDirectives(options.include_directives); printer.setExpandIncludes(options.expand_includes); diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index c060e7646..d599660f2 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -12,36 +12,27 @@ #include #include -struct SlangPrintOpts; // Forward decl +struct SlangPrintOpts; -// The wrapper class exposed as "SlangContext" to Rust class SlangContext { public: SlangContext(); - void add_source(rust::Str path); - void add_include(rust::Str path); - void add_define(rust::Str def); + void set_includes(const rust::Vec& includes); + void set_defines(const rust::Vec& defines); - bool parse(); - - size_t get_tree_count() const; - std::shared_ptr get_tree(size_t index) const; - - std::shared_ptr rename_tree(const std::shared_ptr, - rust::Str prefix, rust::Str suffix) const; - - rust::String print_tree(const std::shared_ptr, SlangPrintOpts options) const; + std::shared_ptr parse_file(rust::Str path); private: - slang::driver::Driver driver; - - // We buffer args to pass to driver.parseCommandLine later - std::vector sources; - std::vector includes; - std::vector defines; + slang::SourceManager sourceManager; + slang::parsing::PreprocessorOptions ppOptions; }; std::unique_ptr new_slang_context(); +std::shared_ptr rename(std::shared_ptr tree, rust::Str prefix, + rust::Str suffix); + +rust::String print_tree(std::shared_ptr tree, SlangPrintOpts options); + #endif // BENDER_SLANG_BRIDGE_H diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index 0d25c6b23..64f6811b6 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -22,116 +22,87 @@ mod ffi { // Include Slang header to define SyntaxTree type for CXX include!("slang/syntax/SyntaxTree.h"); - /// Opaque type for the Slang Driver wrapper + /// Opaque type for the Slang Context type SlangContext; /// Opaque type for the Slang SyntaxTree #[namespace = "slang::syntax"] type SyntaxTree; - /// Create a new persistent context (owns the Driver) + /// Create a new persistent context fn new_slang_context() -> UniquePtr; - // Methods on SlangContext - fn add_source(self: Pin<&mut SlangContext>, path: &str); - fn add_include(self: Pin<&mut SlangContext>, path: &str); - fn add_define(self: Pin<&mut SlangContext>, def: &str); + /// Set the include directories + fn set_includes(self: Pin<&mut SlangContext>, includes: &Vec); + /// Set the preprocessor defines + fn set_defines(self: Pin<&mut SlangContext>, defines: &Vec); - /// Parse all added sources. Returns true on success. - fn parse(self: Pin<&mut SlangContext>) -> Result; - - /// Retrieves the number of parsed syntax trees - fn get_tree_count(self: &SlangContext) -> usize; - - /// Retrieves a shared pointer to a specific syntax tree by index - fn get_tree(self: &SlangContext, index: usize) -> SharedPtr; + /// Parse all added sources. Returns a syntax tree on success, or an error message on failure. + fn parse_file(self: Pin<&mut SlangContext>, path: &str) -> Result>; /// Rename names in the syntax tree with a given prefix and suffix - fn rename_tree( - self: &SlangContext, - tree: SharedPtr, - prefix: &str, - suffix: &str, - ) -> SharedPtr; - - /// Print a specific tree using the context's SourceManager - fn print_tree( - self: &SlangContext, - tree: SharedPtr, - options: SlangPrintOpts, - ) -> String; + fn rename(tree: SharedPtr, prefix: &str, suffix: &str) + -> SharedPtr; + + /// Print a specific tree + fn print_tree(tree: SharedPtr, options: SlangPrintOpts) -> String; } } -/// A persistent Slang session -pub struct SlangSession { - ctx: UniquePtr, +/// Extension trait for SyntaxTree +pub trait SyntaxTreeExt { + fn rename(&self, prefix: Option<&str>, suffix: Option<&str>) -> Self; + fn display(&self, options: SlangPrintOpts) -> String; } -impl SlangSession { - /// Creates a new Slang session - pub fn new() -> Self { - Self { - ctx: ffi::new_slang_context(), +impl SyntaxTreeExt for SharedPtr { + /// Renames all names in the syntax tree with the given prefix and suffix + fn rename(&self, prefix: Option<&str>, suffix: Option<&str>) -> Self { + if prefix.is_none() && suffix.is_none() { + return self.clone(); } + ffi::rename(self.clone(), prefix.unwrap_or(""), suffix.unwrap_or("")) } - /// Adds a source file to be parsed - pub fn add_source(&mut self, path: &str) { - self.ctx.pin_mut().add_source(path); - } - - /// Adds an include directory - pub fn add_include(&mut self, path: &str) { - self.ctx.pin_mut().add_include(path); - } - - /// Adds a preprocessor define - pub fn add_define(&mut self, define: &str) { - self.ctx.pin_mut().add_define(define); + /// Displays the syntax tree as a string with the given options + fn display(&self, options: SlangPrintOpts) -> String { + ffi::print_tree(self.clone(), options) } +} - /// Parses all added source files into syntax trees - pub fn parse(&mut self) -> Result> { - Ok(self.ctx.pin_mut().parse()?) - } +/// Extension trait for SlangContext +pub trait SlangContextExt { + fn set_includes(self, includes: &Vec) -> Self; + fn set_defines(self, defines: &Vec) -> Self; + fn parse( + &mut self, + path: &str, + ) -> Result, Box>; +} - /// Returns the parsed syntax trees as a Rust vector - pub fn get_trees(&self) -> Vec> { - let count = self.ctx.get_tree_count(); - let mut trees = Vec::with_capacity(count); - for i in 0..count { - trees.push(self.ctx.get_tree(i)); - } - trees +impl SlangContextExt for UniquePtr { + /// Sets the include directories + fn set_includes(mut self, includes: &Vec) -> Self { + self.pin_mut().set_includes(&includes); + self } - /// Returns an iterator over the parsed syntax trees - pub fn trees_iter(&self) -> impl Iterator> + '_ { - (0..self.ctx.get_tree_count()).map(|i| self.ctx.get_tree(i)) + /// Sets the preprocessor defines + fn set_defines(mut self, defines: &Vec) -> Self { + self.pin_mut().set_defines(&defines); + self } - /// Renames names in the syntax tree with a given prefix and suffix - pub fn rename_tree( - &self, - tree: SharedPtr, - prefix: Option<&str>, - suffix: Option<&str>, - ) -> SharedPtr { - if prefix.is_none() && suffix.is_none() { - return tree; - } - let prefix = prefix.unwrap_or(""); - let suffix = suffix.unwrap_or(""); - self.ctx.rename_tree(tree, prefix, suffix) + /// Parses a source file and returns the syntax tree + fn parse( + &mut self, + path: &str, + ) -> Result, Box> { + Ok(self.pin_mut().parse_file(path)?) } +} - /// Prints a syntax tree with given printing options - pub fn print_tree( - &self, - tree: SharedPtr, - opts: ffi::SlangPrintOpts, - ) -> String { - self.ctx.print_tree(tree, opts) - } +/// Creates a new Slang session +pub fn new_session() -> UniquePtr { + ffi::new_slang_context() } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 6218116d4..62c89f429 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -5,7 +5,7 @@ use clap::{ArgAction, Args}; -use bender_slang::{SlangPrintOpts, SlangSession}; +use bender_slang::{SlangContextExt, SlangPrintOpts, SyntaxTreeExt}; use crate::error::*; @@ -62,24 +62,6 @@ pub struct PickleArgs { /// Execute the `pickle` subcommand. pub fn run(args: PickleArgs) -> Result<()> { - let mut slang = SlangSession::new(); - - for file in args.files.iter() { - slang.add_source(file); - } - - for include in args.include_dirs.iter() { - slang.add_include(include); - } - - for define in args.defines.iter() { - slang.add_define(define); - } - - slang - .parse() - .map_err(|cause| Error::new(format!("Cannot parse files: {}", cause)))?; - let print_opts = SlangPrintOpts { include_directives: args.include_directives, expand_includes: args.expand_includes, @@ -88,10 +70,17 @@ pub fn run(args: PickleArgs) -> Result<()> { squash_newlines: args.strip_newlines, }; - for tree in slang.trees_iter() { - let renamed_tree = slang.rename_tree(tree, args.prefix.as_deref(), args.suffix.as_deref()); - let pickled = slang.print_tree(renamed_tree, print_opts); - println!("{}", pickled); + let mut slang = bender_slang::new_session() + .set_includes(&args.include_dirs) + .set_defines(&args.defines); + + for source in &args.files { + let tree = slang + .parse(source) + .map_err(|cause| Error::new(format!("Cannot parse file {}: {}", source, cause)))?; + let renamed_tree = tree.rename(args.prefix.as_deref(), args.suffix.as_deref()); + println!("{}", renamed_tree.display(print_opts)); } + Ok(()) } From 2e5067da342dc29faeb91c664114af29cc9f8bc1 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 11 Feb 2026 22:55:48 +0100 Subject: [PATCH 19/46] pickle: Bender integration --- crates/bender-slang/src/lib.rs | 8 +- src/cli.rs | 2 +- src/cmd/pickle.rs | 147 ++++++++++++++++++++++++++------- src/src.rs | 2 - 4 files changed, 124 insertions(+), 35 deletions(-) diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index 64f6811b6..d0467d4ea 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -72,8 +72,8 @@ impl SyntaxTreeExt for SharedPtr { /// Extension trait for SlangContext pub trait SlangContextExt { - fn set_includes(self, includes: &Vec) -> Self; - fn set_defines(self, defines: &Vec) -> Self; + fn set_includes(&mut self, includes: &Vec) -> &mut Self; + fn set_defines(&mut self, defines: &Vec) -> &mut Self; fn parse( &mut self, path: &str, @@ -82,13 +82,13 @@ pub trait SlangContextExt { impl SlangContextExt for UniquePtr { /// Sets the include directories - fn set_includes(mut self, includes: &Vec) -> Self { + fn set_includes(&mut self, includes: &Vec) -> &mut Self { self.pin_mut().set_includes(&includes); self } /// Sets the preprocessor defines - fn set_defines(mut self, defines: &Vec) -> Self { + fn set_defines(&mut self, defines: &Vec) -> &mut Self { self.pin_mut().set_defines(&defines); self } diff --git a/src/cli.rs b/src/cli.rs index 714e34914..f14eda4bd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -332,7 +332,7 @@ pub fn main() -> Result<()> { 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(args), + Commands::Pickle(args) => cmd::pickle::run(&sess, args), Commands::Plugin(args) => { let (plugin_name, plugin_args) = args .split_first() diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 62c89f429..2ca7b12af 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -4,10 +4,17 @@ //! The `pickle` subcommand. use clap::{ArgAction, Args}; +use indexmap::IndexSet; +use tokio::runtime::Runtime; -use bender_slang::{SlangContextExt, SlangPrintOpts, SyntaxTreeExt}; - +use crate::cmd::sources::get_passed_targets; +use crate::config::{Validate, ValidationContext}; use crate::error::*; +use crate::sess::{Session, SessionIo}; +use crate::src::SourceFile; +use crate::target::TargetSet; + +use bender_slang::{SlangContextExt, SlangPrintOpts, SyntaxTreeExt}; // TODO(fischeti): Clean up the arguments and options. // At the moment, they are just directly mirroring the Slang API. @@ -15,53 +22,105 @@ use crate::error::*; /// Pickle files #[derive(Args, Debug)] pub struct PickleArgs { - /// Source files to pickle - #[arg(required = true)] + /// Additional source files to pickle files: Vec, /// The output file (defaults to stdout) + // TODO(fischeti): Actually implement this. #[arg(short, long)] output: Option, - /// Add an include directory - #[arg(short = 'I', long, action = ArgAction::Append)] - include_dirs: Vec, + /// Only include sources that match the given target + #[arg(short, long, action = ArgAction::Append, global = true)] + pub target: Vec, + + /// Specify package to show sources for + #[arg(short, long, action = ArgAction::Append, global = true)] + pub package: Vec, - /// Add defines - #[arg(short = 'D', long, action = ArgAction::Append)] - defines: Vec, + /// Specify package to exclude from sources + #[arg(long, action = ArgAction::Append, global = true)] + pub exclude: Vec, + + /// Exclude all dependencies, i.e. only top level or specified package(s) + #[arg(long, global = true)] + pub no_deps: bool, + + /// Additional include directory + #[arg(short = 'I', action = ArgAction::Append)] + include_dir: Vec, + + /// Additional preprocessor definition + #[arg(short = 'D', action = ArgAction::Append)] + define: Vec, /// The prefix to add to all names - #[arg(long)] + #[arg(long, help_heading = "Slang Options")] prefix: Option, /// The suffix to add to all names - #[arg(long)] + #[arg(long, help_heading = "Slang Options")] suffix: Option, /// Whether to include preprocessor directives - #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Print Options")] + #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Slang Options")] include_directives: bool, /// Whether to expand include directives - #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Print Options")] + #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Slang Options")] expand_includes: bool, /// Whether to expand macros - #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Print Options")] + #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Slang Options")] expand_macros: bool, /// Whether to strip comments - #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Print Options")] + #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Slang Options")] strip_comments: bool, /// Whether to strip newlines - #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Print Options")] + #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Slang Options")] strip_newlines: bool, } /// Execute the `pickle` subcommand. -pub fn run(args: PickleArgs) -> Result<()> { +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 srcs = srcs + .flatten() + .into_iter() + .map(|f| f.validate(&ValidationContext::default())) + .collect::>>()?; + let print_opts = SlangPrintOpts { include_directives: args.include_directives, expand_includes: args.expand_includes, @@ -70,16 +129,48 @@ pub fn run(args: PickleArgs) -> Result<()> { squash_newlines: args.strip_newlines, }; - let mut slang = bender_slang::new_session() - .set_includes(&args.include_dirs) - .set_defines(&args.defines); - - for source in &args.files { - let tree = slang - .parse(source) - .map_err(|cause| Error::new(format!("Cannot parse file {}: {}", source, cause)))?; - let renamed_tree = tree.rename(args.prefix.as_deref(), args.suffix.as_deref()); - println!("{}", renamed_tree.display(print_opts)); + for src_group in srcs { + let mut slang = bender_slang::new_session(); + + // Collect include directories and defines 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(); + + // 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(); + + // Set the include directories and defines in the Slang session. + slang.set_includes(&include_dirs).set_defines(&defines); + + // Collect file paths from the source group. + let file_paths = src_group.files.iter().filter_map(|source| { + match source { + // TODO(fischeti): Emit warnings for VHDL sources. + SourceFile::File(path, _) => path.to_str(), + _ => None, // Skip Group/Box/etc. + } + }); + + for file_path in file_paths { + let tree = slang.parse(file_path).map_err(|cause| { + Error::new(format!("Cannot parse file {}: {}", file_path, cause)) + })?; + let renamed_tree = tree.rename(args.prefix.as_deref(), args.suffix.as_deref()); + println!("{}", renamed_tree.display(print_opts)); + } } Ok(()) diff --git a/src/src.rs b/src/src.rs index 6cb37feb5..3ac7833d7 100644 --- a/src/src.rs +++ b/src/src.rs @@ -386,8 +386,6 @@ impl<'ctx> SourceGroup<'ctx> { pub enum SourceType { /// A Verilog file. Verilog, - // /// A SystemVerilog file. - // SystemVerilog, /// A VHDL file. Vhdl, /// Unknown file type From 2673472c4a4bef426bcfa19ac1bbbdc424619ef4 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 11:32:25 +0100 Subject: [PATCH 20/46] pickle: Filter non-verilog files and emit warnings --- Cargo.lock | 4 ++-- src/cmd/pickle.rs | 17 +++++++++++------ src/cmd/script.rs | 24 ++++++------------------ src/diagnostic.rs | 4 ++++ src/sess.rs | 17 +++++++++++++---- src/src.rs | 13 +++---------- 6 files changed, 39 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f3978152..34ff1cf95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -398,7 +398,7 @@ dependencies = [ "cxxbridge-cmd", "cxxbridge-flags", "cxxbridge-macro", - "foldhash", + "foldhash 0.2.0", "link-cplusplus", ] @@ -715,7 +715,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 2ca7b12af..8e6ee9503 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -9,9 +9,10 @@ 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; +use crate::src::{SourceFile, SourceType}; use crate::target::TargetSet; use bender_slang::{SlangContextExt, SlangPrintOpts, SyntaxTreeExt}; @@ -156,12 +157,16 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { slang.set_includes(&include_dirs).set_defines(&defines); // Collect file paths from the source group. - let file_paths = src_group.files.iter().filter_map(|source| { - match source { - // TODO(fischeti): Emit warnings for VHDL sources. - SourceFile::File(path, _) => path.to_str(), - _ => None, // Skip Group/Box/etc. + let file_paths = src_group.files.iter().filter_map(|source| match source { + SourceFile::File(path, Some(SourceType::Verilog)) => path.to_str(), + // 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. + _ => None, }); for file_path in file_paths { 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/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 3ac7833d7..e3f2a31f0 100644 --- a/src/src.rs +++ b/src/src.rs @@ -382,14 +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 VHDL file. Vhdl, - /// Unknown file type - Unknown, } /// A source file. @@ -398,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>), } @@ -425,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; From f18f2cc45fcb55e65387b01010a4309c038dab60 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 15:40:05 +0100 Subject: [PATCH 21/46] bender-slang: Allow dumping AST as JSON --- crates/bender-slang/cpp/slang_bridge.cpp | 17 ++++++++++ crates/bender-slang/cpp/slang_bridge.h | 2 ++ crates/bender-slang/src/lib.rs | 10 ++++++ src/cmd/pickle.rs | 40 +++++++++++++++++++++++- 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 999d3f12e..bc68d1012 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -4,8 +4,10 @@ #include "slang_bridge.h" #include "bender-slang/src/lib.rs.h" +#include "slang/syntax/CSTSerializer.h" #include "slang/syntax/SyntaxPrinter.h" #include "slang/syntax/SyntaxVisitor.h" +#include "slang/text/Json.h" using namespace slang; using namespace slang::driver; @@ -143,3 +145,18 @@ rust::String print_tree(const shared_ptr tree, SlangPrintOpts option printer.print(tree->root()); return rust::String(printer.str()); } + +// Dumps the AST/CST to a JSON string +rust::String dump_tree_json(std::shared_ptr tree) { + JsonWriter writer; + writer.setPrettyPrint(true); + + // CSTSerializer is the class Slang uses to convert AST -> JSON + CSTSerializer serializer(writer); + + // Serialize the specific tree root + serializer.serialize(*tree); + + // Convert string_view to rust::String + return rust::String(std::string(writer.view())); +} diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index d599660f2..f479ec578 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -35,4 +35,6 @@ std::shared_ptr rename(std::shared_ptr tree, SlangPrintOpts options); +rust::String dump_tree_json(std::shared_ptr tree); + #endif // BENDER_SLANG_BRIDGE_H diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index d0467d4ea..e24fd67ea 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -46,13 +46,19 @@ mod ffi { /// Print a specific tree fn print_tree(tree: SharedPtr, options: SlangPrintOpts) -> String; + + /// Dump the syntax tree as JSON for debugging purposes + fn dump_tree_json(tree: SharedPtr) -> String; } } /// Extension trait for SyntaxTree +// TODO(fischeti): Consider using a wrapper to implement traits like Debug and Display +// instead of an extension trait. This would be more idiomatic in Rust. pub trait SyntaxTreeExt { fn rename(&self, prefix: Option<&str>, suffix: Option<&str>) -> Self; fn display(&self, options: SlangPrintOpts) -> String; + fn as_debug(&self) -> String; } impl SyntaxTreeExt for SharedPtr { @@ -68,6 +74,10 @@ impl SyntaxTreeExt for SharedPtr { fn display(&self, options: SlangPrintOpts) -> String { ffi::print_tree(self.clone(), options) } + + fn as_debug(&self) -> String { + ffi::dump_tree_json(self.clone()) + } } /// Extension trait for SlangContext diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 8e6ee9503..2ca03b590 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -3,6 +3,9 @@ //! The `pickle` subcommand. +use std::fs::File; +use std::io::{BufWriter, Write}; + use clap::{ArgAction, Args}; use indexmap::IndexSet; use tokio::runtime::Runtime; @@ -82,6 +85,10 @@ pub struct PickleArgs { /// Whether to strip newlines #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Slang Options")] strip_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. @@ -130,6 +137,23 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { squash_newlines: args.strip_newlines, }; + // 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 src_group in srcs { let mut slang = bender_slang::new_session(); @@ -174,9 +198,23 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { Error::new(format!("Cannot parse file {}: {}", file_path, cause)) })?; let renamed_tree = tree.rename(args.prefix.as_deref(), args.suffix.as_deref()); - println!("{}", renamed_tree.display(print_opts)); + if args.ast_json { + // JSON Array Logic: Prepend comma if not the first item + if !first_item { + write!(writer, ",")?; + } + write!(writer, "{}", renamed_tree.as_debug())?; + first_item = false; + } else { + write!(writer, "{}", renamed_tree.display(print_opts))?; + } } } + // Close JSON Array + if args.ast_json { + writeln!(writer, "]")?; + } + Ok(()) } From a48e0719bc2510ab0d204ba7e1b8f5c132f02008 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 17:46:55 +0100 Subject: [PATCH 22/46] bender-slang(rewriter): Handle package imports, virtual interfaces and scoped names --- crates/bender-slang/cpp/slang_bridge.cpp | 62 ++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index bc68d1012..50449004e 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -73,6 +73,7 @@ class SuffixPrefixRewriter : public SyntaxRewriter { } // Renames "module foo;" -> "module foo;" + // Note: Handles packages and interfaces too. void handle(const ModuleDeclarationSyntax& node) { if (node.header->name.isMissing()) return; @@ -93,6 +94,7 @@ class SuffixPrefixRewriter : public SyntaxRewriter { } // Renames "foo i_foo();" -> "foo i_foo();" + // Note: Handles modules and interfaces. void handle(const HierarchyInstantiationSyntax& node) { // Check to make sure we are dealing with an identifier // and not a built-in type e.g. `initial foo();` @@ -114,6 +116,66 @@ class SuffixPrefixRewriter : public SyntaxRewriter { visitDefault(node); } + // Renames "import foo;" -> "import foo;" + void handle(const PackageImportItemSyntax& node) { + if (node.package.isMissing()) + return; + + auto newName = rename(node.package.valueText()); + auto newNameToken = makeId(newName, node.package.trivia()); + + PackageImportItemSyntax* newNode = deepClone(node, alloc); + newNode->package = newNameToken; + + replace(node, *newNode); + visitDefault(node); + } + + // Renames "virtual MyIntf foo;" -> "virtual MyIntf foo;" + void handle(const VirtualInterfaceTypeSyntax& node) { + if (node.name.isMissing()) + return; + + auto newName = rename(node.name.valueText()); + auto newNameToken = makeId(newName, node.name.trivia()); + + VirtualInterfaceTypeSyntax* newNode = deepClone(node, alloc); + newNode->name = newNameToken; + + replace(node, *newNode); + visitDefault(node); + } + + // Renames "foo::bar" -> "foo::bar" + void handle(const ScopedNameSyntax& node) { + // Only rename if the left side is a simple identifier (e.g., a package name) + // We ignore nested calls or parameterized classes for now. + if (node.left->kind == SyntaxKind::IdentifierName) { + auto& leftNode = node.left->as(); + auto name = leftNode.identifier.valueText(); + + // Skip built-in keywords that look like scopes + if (name != "$unit" && name != "local" && name != "super" && name != "this") { + auto newName = rename(name); + auto newNameToken = makeId(newName, leftNode.identifier.trivia()); + + // Clone the left node and update identifier + IdentifierNameSyntax* newLeft = deepClone(leftNode, alloc); + newLeft->identifier = newNameToken; + + // Clone the scoped node and attach new left + ScopedNameSyntax* newNode = deepClone(node, alloc); + newNode->left = newLeft; + + replace(node, *newNode); + } + } + + // Visit children to handle recursive scopes + // e.g., OuterPkg::InnerPkg::Item + visitDefault(node); + } + private: string_view prefix; string_view suffix; From 0325dcbd76ed66aee5035943e3b608cadd5bd911 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 18:42:29 +0100 Subject: [PATCH 23/46] pickle: Allow to exclude names from renaming --- crates/bender-slang/cpp/slang_bridge.cpp | 19 ++++++++++++++++--- crates/bender-slang/cpp/slang_bridge.h | 2 +- crates/bender-slang/src/lib.rs | 19 ++++++++++++++----- src/cmd/pickle.rs | 10 +++++++++- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 50449004e..f2cd4bbad 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -9,6 +9,8 @@ #include "slang/syntax/SyntaxVisitor.h" #include "slang/text/Json.h" +#include + using namespace slang; using namespace slang::driver; using namespace slang::syntax; @@ -60,10 +62,14 @@ std::shared_ptr SlangContext::parse_file(rust::Str path) { // Rewriter that adds prefix/suffix to module and instantiated hierarchy names class SuffixPrefixRewriter : public SyntaxRewriter { public: - SuffixPrefixRewriter(string_view prefix, string_view suffix) : prefix(prefix), suffix(suffix) {} + SuffixPrefixRewriter(string_view prefix, string_view suffix, const std::unordered_set& excludes) + : prefix(prefix), suffix(suffix), excludes(excludes) {} // Helper to allocate and build renamed string with prefix/suffix string_view rename(string_view name) { + if (excludes.count(std::string(name))) { + return name; + } size_t len = prefix.size() + name.size() + suffix.size(); char* mem = (char*)alloc.allocate(len, 1); memcpy(mem, prefix.data(), prefix.size()); @@ -179,15 +185,22 @@ class SuffixPrefixRewriter : public SyntaxRewriter { private: string_view prefix; string_view suffix; + const std::unordered_set& excludes; }; // Transform the given syntax tree by renaming modules and instantiated hierarchy names with the specified prefix/suffix -std::shared_ptr rename(std::shared_ptr tree, rust::Str prefix, rust::Str suffix) { +std::shared_ptr rename(std::shared_ptr tree, rust::Str prefix, rust::Str suffix, + const rust::Vec& excludes) { std::string_view p(prefix.data(), prefix.size()); std::string_view s(suffix.data(), suffix.size()); + std::unordered_set excludeSet; + for (const auto& e : excludes) { + excludeSet.insert(std::string(e)); + } + // SuffixPrefixRewriter is defined in the .cpp file as before - SuffixPrefixRewriter rewriter(p, s); + SuffixPrefixRewriter rewriter(p, s, excludeSet); return rewriter.transform(tree); } diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index f479ec578..64f8232c7 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -31,7 +31,7 @@ class SlangContext { std::unique_ptr new_slang_context(); std::shared_ptr rename(std::shared_ptr tree, rust::Str prefix, - rust::Str suffix); + rust::Str suffix, const rust::Vec& excludes); rust::String print_tree(std::shared_ptr tree, SlangPrintOpts options); diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index e24fd67ea..23d0280e6 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -41,8 +41,12 @@ mod ffi { fn parse_file(self: Pin<&mut SlangContext>, path: &str) -> Result>; /// Rename names in the syntax tree with a given prefix and suffix - fn rename(tree: SharedPtr, prefix: &str, suffix: &str) - -> SharedPtr; + fn rename( + tree: SharedPtr, + prefix: &str, + suffix: &str, + excludes: &Vec, + ) -> SharedPtr; /// Print a specific tree fn print_tree(tree: SharedPtr, options: SlangPrintOpts) -> String; @@ -56,18 +60,23 @@ mod ffi { // TODO(fischeti): Consider using a wrapper to implement traits like Debug and Display // instead of an extension trait. This would be more idiomatic in Rust. pub trait SyntaxTreeExt { - fn rename(&self, prefix: Option<&str>, suffix: Option<&str>) -> Self; + fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &Vec) -> Self; fn display(&self, options: SlangPrintOpts) -> String; fn as_debug(&self) -> String; } impl SyntaxTreeExt for SharedPtr { /// Renames all names in the syntax tree with the given prefix and suffix - fn rename(&self, prefix: Option<&str>, suffix: Option<&str>) -> Self { + fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &Vec) -> Self { if prefix.is_none() && suffix.is_none() { return self.clone(); } - ffi::rename(self.clone(), prefix.unwrap_or(""), suffix.unwrap_or("")) + ffi::rename( + self.clone(), + prefix.unwrap_or(""), + suffix.unwrap_or(""), + excludes, + ) } /// Displays the syntax tree as a string with the given options diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 2ca03b590..a7b8baa45 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -66,6 +66,10 @@ pub struct PickleArgs { #[arg(long, help_heading = "Slang Options")] suffix: Option, + /// Names to exclude from renaming (modules, packages, interfaces) + #[arg(long, help_heading = "Slang Options")] + exclude_rename: Vec, + /// Whether to include preprocessor directives #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Slang Options")] include_directives: bool, @@ -197,7 +201,11 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { let tree = slang.parse(file_path).map_err(|cause| { Error::new(format!("Cannot parse file {}: {}", file_path, cause)) })?; - let renamed_tree = tree.rename(args.prefix.as_deref(), args.suffix.as_deref()); + let renamed_tree = tree.rename( + args.prefix.as_deref(), + args.suffix.as_deref(), + &args.exclude_rename, + ); if args.ast_json { // JSON Array Logic: Prepend comma if not the first item if !first_item { From 9c8d4fbe7067c6556dea7bb3c5f0df4d98c11b32 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 18:57:33 +0100 Subject: [PATCH 24/46] pickle: Clean up CLI --- crates/bender-slang/cpp/slang_bridge.cpp | 4 +- crates/bender-slang/src/lib.rs | 2 - src/cmd/pickle.rs | 54 +++++++++--------------- 3 files changed, 22 insertions(+), 38 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index f2cd4bbad..315c16759 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -210,8 +210,8 @@ rust::String print_tree(const shared_ptr tree, SlangPrintOpts option // Set up the printer with options SyntaxPrinter printer(tree->sourceManager()); - printer.setIncludeDirectives(options.include_directives); - printer.setExpandIncludes(options.expand_includes); + printer.setIncludeDirectives(true); + printer.setExpandIncludes(true); printer.setExpandMacros(options.expand_macros); printer.setSquashNewlines(options.squash_newlines); printer.setIncludeComments(options.include_comments); diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index 23d0280e6..b56da9268 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -10,8 +10,6 @@ mod ffi { /// Options for the syntax printer #[derive(Clone, Copy)] struct SlangPrintOpts { - include_directives: bool, - expand_includes: bool, expand_macros: bool, include_comments: bool, squash_newlines: bool, diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index a7b8baa45..e9c043d98 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -6,7 +6,7 @@ use std::fs::File; use std::io::{BufWriter, Write}; -use clap::{ArgAction, Args}; +use clap::Args; use indexmap::IndexSet; use tokio::runtime::Runtime; @@ -20,49 +20,45 @@ use crate::target::TargetSet; use bender_slang::{SlangContextExt, SlangPrintOpts, SyntaxTreeExt}; -// TODO(fischeti): Clean up the arguments and options. -// At the moment, they are just directly mirroring the Slang API. -// for debugging purposes. /// Pickle files #[derive(Args, Debug)] pub struct PickleArgs { - /// Additional source files to pickle + /// Additional source files to pickle, which are not part of the manifest. files: Vec, /// The output file (defaults to stdout) - // TODO(fischeti): Actually implement this. #[arg(short, long)] output: Option, /// Only include sources that match the given target - #[arg(short, long, action = ArgAction::Append, global = true)] + #[arg(short, long)] pub target: Vec, /// Specify package to show sources for - #[arg(short, long, action = ArgAction::Append, global = true)] + #[arg(short, long)] pub package: Vec, /// Specify package to exclude from sources - #[arg(long, action = ArgAction::Append, global = true)] + #[arg(long)] pub exclude: Vec, /// Exclude all dependencies, i.e. only top level or specified package(s) - #[arg(long, global = true)] + #[arg(long)] pub no_deps: bool, - /// Additional include directory - #[arg(short = 'I', action = ArgAction::Append)] + /// Additional include directory, which are not part of the manifest. + #[arg(short = 'I')] include_dir: Vec, - /// Additional preprocessor definition - #[arg(short = 'D', action = ArgAction::Append)] + /// Additional preprocessor definition, which are not part of the manifest. + #[arg(short = 'D')] define: Vec, - /// The prefix to add to all names + /// A prefix to add to all names (modules, packages, interfaces) #[arg(long, help_heading = "Slang Options")] prefix: Option, - /// The suffix to add to all names + /// A suffix to add to all names (modules, packages, interfaces) #[arg(long, help_heading = "Slang Options")] suffix: Option, @@ -70,25 +66,17 @@ pub struct PickleArgs { #[arg(long, help_heading = "Slang Options")] exclude_rename: Vec, - /// Whether to include preprocessor directives - #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Slang Options")] - include_directives: bool, - - /// Whether to expand include directives - #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Slang Options")] - expand_includes: bool, - - /// Whether to expand macros - #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Slang Options")] + /// Expand macros in the output + #[arg(long, help_heading = "Slang Options")] expand_macros: bool, - /// Whether to strip comments - #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Slang Options")] + /// Strip comments from the output + #[arg(long, help_heading = "Slang Options")] strip_comments: bool, - /// Whether to strip newlines - #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Slang Options")] - strip_newlines: 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")] @@ -134,11 +122,9 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { .collect::>>()?; let print_opts = SlangPrintOpts { - include_directives: args.include_directives, - expand_includes: args.expand_includes, expand_macros: args.expand_macros, include_comments: !args.strip_comments, - squash_newlines: args.strip_newlines, + squash_newlines: args.squash_newlines, }; // Setup Output Writer, either to file or stdout From 54a0eba7d4a2fbe33a3eebe6e1bf30edadd16ef3 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 14:09:51 +0100 Subject: [PATCH 25/46] bender-slang: Emit error when parsing fails --- crates/bender-slang/cpp/slang_bridge.cpp | 30 +++++++++++++++++++++--- crates/bender-slang/cpp/slang_bridge.h | 4 ++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 315c16759..2beeac7a3 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -4,11 +4,14 @@ #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" #include "slang/text/Json.h" +#include #include using namespace slang; @@ -25,7 +28,9 @@ using std::vector; // Create a new SlangContext instance std::unique_ptr new_slang_context() { return std::make_unique(); } -SlangContext::SlangContext() {} +SlangContext::SlangContext() : diagEngine(sourceManager), diagClient(std::make_shared()) { + diagEngine.addClient(diagClient); +} // Set the include paths for the preprocessor void SlangContext::set_includes(const rust::Vec& incs) { @@ -45,10 +50,11 @@ void SlangContext::set_defines(const rust::Vec& defs) { // Parses the given file and returns a syntax tree, if successful std::shared_ptr SlangContext::parse_file(rust::Str path) { + string_view pathView(path.data(), path.size()); Bag options; options.set(ppOptions); - auto result = SyntaxTree::fromFile(string_view(path.data(), path.size()), sourceManager, options); + auto result = SyntaxTree::fromFile(pathView, sourceManager, options); if (!result) { auto& err = result.error(); @@ -56,7 +62,25 @@ std::shared_ptr SlangContext::parse_file(rust::Str path) { throw std::runtime_error(msg); } - return *result; + 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); + } + + return tree; } // Rewriter that adds prefix/suffix to module and instantiated hierarchy names diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index 64f8232c7..b10b157a6 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -5,6 +5,8 @@ #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" @@ -26,6 +28,8 @@ class SlangContext { private: slang::SourceManager sourceManager; slang::parsing::PreprocessorOptions ppOptions; + slang::DiagnosticEngine diagEngine; + std::shared_ptr diagClient; }; std::unique_ptr new_slang_context(); From 1e3f1255f8b23d0963e2d8fb3d0d636fefcd0b1d Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 15:00:09 +0100 Subject: [PATCH 26/46] Wrap FFI types with safe wrappers Introduce safe wrapper types for the opaque FFI objects and move the extension methods into proper Rust structs to provide a clearer, idiomatic API and safer ownership semantics. - Add SyntaxTree wrapper around SharedPtr with Clone, display, as_debug, rename, and fmt impls (Display/Debug). - Add SlangContext wrapper around UniquePtr with new, set_includes, set_defines, and parse returning a SyntaxTree. - Replace new_session() to return SlangContext instead of raw pointer. - Update callers: remove use of extension traits and change formatting at pickling to use Debug/Display impls (write!("{:?}", renamed_tree)). --- crates/bender-slang/src/lib.rs | 107 +++++++++++++++++++++------------ src/cmd/pickle.rs | 4 +- 2 files changed, 69 insertions(+), 42 deletions(-) diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index b56da9268..e05683a63 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -54,72 +54,99 @@ mod ffi { } } -/// Extension trait for SyntaxTree -// TODO(fischeti): Consider using a wrapper to implement traits like Debug and Display -// instead of an extension trait. This would be more idiomatic in Rust. -pub trait SyntaxTreeExt { - fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &Vec) -> Self; - fn display(&self, options: SlangPrintOpts) -> String; - fn as_debug(&self) -> String; +/// Wrapper around an opaque Slang syntax tree. +pub struct SyntaxTree { + inner: SharedPtr, } -impl SyntaxTreeExt for SharedPtr { +impl Clone for SyntaxTree { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl SyntaxTree { /// Renames all names in the syntax tree with the given prefix and suffix - fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &Vec) -> Self { + pub fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &Vec) -> Self { if prefix.is_none() && suffix.is_none() { return self.clone(); } - ffi::rename( - self.clone(), - prefix.unwrap_or(""), - suffix.unwrap_or(""), - excludes, - ) + Self { + inner: ffi::rename( + self.inner.clone(), + prefix.unwrap_or(""), + suffix.unwrap_or(""), + excludes, + ), + } } /// Displays the syntax tree as a string with the given options - fn display(&self, options: SlangPrintOpts) -> String { - ffi::print_tree(self.clone(), options) + pub fn display(&self, options: SlangPrintOpts) -> String { + ffi::print_tree(self.inner.clone(), options) } - fn as_debug(&self) -> String { - ffi::dump_tree_json(self.clone()) + pub fn as_debug(&self) -> String { + ffi::dump_tree_json(self.inner.clone()) } } -/// Extension trait for SlangContext -pub trait SlangContextExt { - fn set_includes(&mut self, includes: &Vec) -> &mut Self; - fn set_defines(&mut self, defines: &Vec) -> &mut Self; - fn parse( - &mut self, - path: &str, - ) -> Result, Box>; +impl std::fmt::Display for SyntaxTree { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let options = SlangPrintOpts { + expand_macros: false, + include_comments: true, + squash_newlines: false, + }; + f.write_str(&self.display(options)) + } } -impl SlangContextExt for UniquePtr { - /// Sets the include directories - fn set_includes(&mut self, includes: &Vec) -> &mut Self { - self.pin_mut().set_includes(&includes); +impl std::fmt::Debug for SyntaxTree { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.as_debug()) + } +} + +/// Wrapper around an opaque Slang context. +pub struct SlangContext { + inner: UniquePtr, +} + +impl SlangContext { + /// Creates a new Slang session. + pub fn new() -> Self { + Self { + inner: ffi::new_slang_context(), + } + } + + /// Sets the include directories. + pub fn set_includes(&mut self, includes: &Vec) -> &mut Self { + self.inner.pin_mut().set_includes(includes); self } - /// Sets the preprocessor defines - fn set_defines(&mut self, defines: &Vec) -> &mut Self { - self.pin_mut().set_defines(&defines); + /// Sets the preprocessor defines. + pub fn set_defines(&mut self, defines: &Vec) -> &mut Self { + self.inner.pin_mut().set_defines(defines); self } - /// Parses a source file and returns the syntax tree - fn parse( + /// Parses a source file and returns the syntax tree. + pub fn parse( &mut self, path: &str, - ) -> Result, Box> { - Ok(self.pin_mut().parse_file(path)?) + ) -> Result> { + Ok(SyntaxTree { + inner: self.inner.pin_mut().parse_file(path)?, + }) } } /// Creates a new Slang session -pub fn new_session() -> UniquePtr { - ffi::new_slang_context() +pub fn new_session() -> SlangContext { + SlangContext::new() } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index e9c043d98..d1c2e6fe0 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -18,7 +18,7 @@ use crate::sess::{Session, SessionIo}; use crate::src::{SourceFile, SourceType}; use crate::target::TargetSet; -use bender_slang::{SlangContextExt, SlangPrintOpts, SyntaxTreeExt}; +use bender_slang::SlangPrintOpts; /// Pickle files #[derive(Args, Debug)] @@ -197,7 +197,7 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { if !first_item { write!(writer, ",")?; } - write!(writer, "{}", renamed_tree.as_debug())?; + write!(writer, "{:?}", renamed_tree)?; first_item = false; } else { write!(writer, "{}", renamed_tree.display(print_opts))?; From 930f240589522f14b78e72c285917d527483d528 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 18:10:37 +0100 Subject: [PATCH 27/46] pickle: Filter out unreachable SyntaxTrees --- crates/bender-slang/cpp/slang_bridge.cpp | 94 ++++++++++++++++++++++++ crates/bender-slang/cpp/slang_bridge.h | 14 ++++ crates/bender-slang/src/lib.rs | 78 ++++++++++++++++++++ src/cmd/pickle.rs | 91 ++++++++++++++--------- 4 files changed, 244 insertions(+), 33 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 2beeac7a3..029548552 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -11,7 +11,9 @@ #include "slang/syntax/SyntaxVisitor.h" #include "slang/text/Json.h" +#include #include +#include #include using namespace slang; @@ -83,6 +85,15 @@ std::shared_ptr SlangContext::parse_file(rust::Str path) { return tree; } +std::unique_ptr SlangContext::parse_files(const rust::Vec& paths) { + auto out = std::make_unique(); + out->trees.reserve(paths.size()); + for (const auto& path : paths) { + out->trees.push_back(parse_file(path)); + } + return out; +} + // Rewriter that adds prefix/suffix to module and instantiated hierarchy names class SuffixPrefixRewriter : public SyntaxRewriter { public: @@ -259,3 +270,86 @@ rust::String dump_tree_json(std::shared_ptr tree) { // Convert string_view to rust::String return rust::String(std::string(writer.view())); } + +std::unique_ptr new_syntax_trees() { return std::make_unique(); } + +void append_trees(SyntaxTrees& dst, const SyntaxTrees& src) { + dst.trees.reserve(dst.trees.size() + src.trees.size()); + for (const auto& tree : src.trees) { + dst.trees.push_back(tree); + } +} + +rust::Vec reachable_tree_indices(const SyntaxTrees& trees, const rust::Vec& tops) { + const auto& treeVec = trees.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)); + } else { + 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); + } + + // Collect the indices of reachable trees and return as rust::Vec + rust::Vec result; + for (size_t i = 0; i < reachable.size(); ++i) { + if (reachable[i]) { + result.push_back(static_cast(i)); + } + } + return result; +} + +std::size_t tree_count(const SyntaxTrees& trees) { return trees.trees.size(); } + +std::shared_ptr tree_at(const SyntaxTrees& trees, std::size_t index) { + if (index >= trees.trees.size()) { + throw std::runtime_error("Tree index out of bounds."); + } + return trees.trees[index]; +} diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index b10b157a6..9e2ff59d9 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -10,11 +10,14 @@ #include "slang/driver/Driver.h" #include "slang/syntax/SyntaxTree.h" +#include +#include #include #include #include struct SlangPrintOpts; +struct SyntaxTrees; class SlangContext { public: @@ -24,6 +27,7 @@ class SlangContext { void set_defines(const rust::Vec& defines); std::shared_ptr parse_file(rust::Str path); + std::unique_ptr parse_files(const rust::Vec& paths); private: slang::SourceManager sourceManager; @@ -41,4 +45,14 @@ rust::String print_tree(std::shared_ptr tree, SlangPr rust::String dump_tree_json(std::shared_ptr tree); +struct SyntaxTrees { + std::vector> trees; +}; + +std::unique_ptr new_syntax_trees(); +void append_trees(SyntaxTrees& dst, const SyntaxTrees& src); +rust::Vec reachable_tree_indices(const SyntaxTrees& trees, const rust::Vec& tops); +std::size_t tree_count(const SyntaxTrees& trees); +std::shared_ptr tree_at(const SyntaxTrees& trees, std::size_t index); + #endif // BENDER_SLANG_BRIDGE_H diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index e05683a63..a23326792 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -26,6 +26,8 @@ mod ffi { /// Opaque type for the Slang SyntaxTree #[namespace = "slang::syntax"] type SyntaxTree; + /// Opaque type for a batch of parsed syntax trees. + type SyntaxTrees; /// Create a new persistent context fn new_slang_context() -> UniquePtr; @@ -37,6 +39,18 @@ mod ffi { /// Parse all added sources. Returns a syntax tree on success, or an error message on failure. fn parse_file(self: Pin<&mut SlangContext>, path: &str) -> Result>; + /// Parse multiple source files and return a batch of syntax trees. + fn parse_files(self: Pin<&mut SlangContext>, paths: &Vec) -> Result>; + /// Create an empty syntax-tree batch. + fn new_syntax_trees() -> UniquePtr; + /// Appends trees from src into dst. + fn append_trees(dst: Pin<&mut SyntaxTrees>, src: &SyntaxTrees); + /// Computes reachable tree indices from the provided top names. + fn reachable_tree_indices(trees: &SyntaxTrees, tops: &Vec) -> Result>; + /// Returns the number of trees in the batch. + fn tree_count(trees: &SyntaxTrees) -> usize; + /// Returns tree at index from the batch. + fn tree_at(trees: &SyntaxTrees, index: usize) -> Result>; /// Rename names in the syntax tree with a given prefix and suffix fn rename( @@ -115,6 +129,60 @@ pub struct SlangContext { inner: UniquePtr, } +/// Wrapper around an opaque batch of syntax trees. +pub struct SyntaxTrees { + inner: UniquePtr, +} + +impl SyntaxTrees { + /// Creates an empty syntax-tree batch. + pub fn new() -> Self { + Self { + inner: ffi::new_syntax_trees(), + } + } + + /// Appends all trees from src into self. + pub fn append_trees(&mut self, src: &SyntaxTrees) { + ffi::append_trees( + self.inner.pin_mut(), + src.inner.as_ref().expect("syntax trees pointer must be valid"), + ); + } + + /// Returns tree count in this batch. + pub fn len(&self) -> usize { + ffi::tree_count(self.inner.as_ref().expect("syntax trees pointer must be valid")) + } + + /// Returns true if the batch contains no trees. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns indices reachable from top names. + pub fn reachable_indices( + &self, + tops: &Vec, + ) -> Result, Box> { + let indices = ffi::reachable_tree_indices( + self.inner.as_ref().expect("syntax trees pointer must be valid"), + tops, + )?; + Ok(indices.into_iter().map(|i| i as usize).collect()) + } + + /// Returns a tree at the provided index. + pub fn tree_at(&self, index: usize) -> Result> { + Ok(SyntaxTree { + inner: ffi::tree_at( + self.inner.as_ref().expect("syntax trees pointer must be valid"), + index, + )?, + }) + } +} + impl SlangContext { /// Creates a new Slang session. pub fn new() -> Self { @@ -144,6 +212,16 @@ impl SlangContext { inner: self.inner.pin_mut().parse_file(path)?, }) } + + /// Parses multiple source files and returns a batch of syntax trees. + pub fn parse_files( + &mut self, + paths: &Vec, + ) -> Result> { + Ok(SyntaxTrees { + inner: self.inner.pin_mut().parse_files(paths)?, + }) + } } /// Creates a new Slang session diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index d1c2e6fe0..f753b8e80 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -54,6 +54,10 @@ pub struct PickleArgs { #[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")] prefix: Option, @@ -142,11 +146,9 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { write!(writer, "[")?; } - let mut first_item = true; - + let mut parsed_trees = bender_slang::SyntaxTrees::new(); + let mut slang = bender_slang::new_session(); for src_group in srcs { - let mut slang = bender_slang::new_session(); - // Collect include directories and defines from the source group and command line arguments. let include_dirs: Vec = src_group .include_dirs @@ -171,37 +173,60 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { slang.set_includes(&include_dirs).set_defines(&defines); // Collect file paths from the source group. - let file_paths = src_group.files.iter().filter_map(|source| match source { - SourceFile::File(path, Some(SourceType::Verilog)) => path.to_str(), - // 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. - _ => None, - }); - - for file_path in file_paths { - let tree = slang.parse(file_path).map_err(|cause| { - Error::new(format!("Cannot parse file {}: {}", file_path, cause)) - })?; - let renamed_tree = tree.rename( - args.prefix.as_deref(), - args.suffix.as_deref(), - &args.exclude_rename, - ); - if args.ast_json { - // JSON Array Logic: Prepend comma if not the first item - if !first_item { - write!(writer, ",")?; + 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 } - write!(writer, "{:?}", renamed_tree)?; - first_item = false; - } else { - write!(writer, "{}", renamed_tree.display(print_opts))?; + // Groups should not exist at this point, + // as we have already flattened the sources. + _ => None, + }) + .collect(); + + let group_trees = slang + .parse_files(&file_paths) + .map_err(|cause| Error::new(format!("Cannot parse source file set: {}", cause)))?; + parsed_trees.append_trees(&group_trees); + } + + let reachable = if args.top.is_empty() { + (0..parsed_trees.len()).collect::>() + } else { + parsed_trees + .reachable_indices(&args.top) + .map_err(|cause| Error::new(format!("Cannot trim parsed trees by --top: {}", cause)))? + }; + + let mut first_item = true; + for idx in reachable { + let tree = parsed_trees.tree_at(idx).map_err(|cause| { + Error::new(format!( + "Cannot access parsed tree at index {}: {}", + idx, cause + )) + })?; + let renamed_tree = tree.rename( + args.prefix.as_deref(), + args.suffix.as_deref(), + &args.exclude_rename, + ); + if args.ast_json { + // JSON Array Logic: Prepend comma if not the first item + if !first_item { + write!(writer, ",")?; } + write!(writer, "{:?}", renamed_tree)?; + first_item = false; + } else { + write!(writer, "{}", renamed_tree.display(print_opts))?; } } From 49d48de7577045cd20784aac73d74b41420e94fa Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 18:49:48 +0100 Subject: [PATCH 28/46] bender-slang: Use typed errors with `thiserror` --- Cargo.lock | 1 + crates/bender-slang/Cargo.toml | 1 + crates/bender-slang/src/lib.rs | 86 +++++++++++++++++++++++++--------- src/cmd/pickle.rs | 15 ++---- src/error.rs | 7 +++ 5 files changed, 75 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 34ff1cf95..c129d79e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,7 @@ dependencies = [ "cmake", "cxx", "cxx-build", + "thiserror", ] [[package]] diff --git a/crates/bender-slang/Cargo.toml b/crates/bender-slang/Cargo.toml index 92e14714b..b660dcca0 100644 --- a/crates/bender-slang/Cargo.toml +++ b/crates/bender-slang/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] cxx = "1.0.194" +thiserror = "2.0.12" [build-dependencies] cmake = "0.1.57" diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index a23326792..3e6e1c438 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -2,9 +2,24 @@ // Tim Fischer 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 file: {message}")] + Parse { message: String }, + #[error("Failed to parse files: {message}")] + ParseFiles { 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 }, +} + #[cxx::bridge] mod ffi { /// Options for the syntax printer @@ -40,7 +55,10 @@ mod ffi { /// Parse all added sources. Returns a syntax tree on success, or an error message on failure. fn parse_file(self: Pin<&mut SlangContext>, path: &str) -> Result>; /// Parse multiple source files and return a batch of syntax trees. - fn parse_files(self: Pin<&mut SlangContext>, paths: &Vec) -> Result>; + fn parse_files( + self: Pin<&mut SlangContext>, + paths: &Vec, + ) -> Result>; /// Create an empty syntax-tree batch. fn new_syntax_trees() -> UniquePtr; /// Appends trees from src into dst. @@ -83,7 +101,12 @@ impl Clone for SyntaxTree { impl SyntaxTree { /// Renames all names in the syntax tree with the given prefix and suffix - pub fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &Vec) -> Self { + pub fn rename( + &self, + prefix: Option<&str>, + suffix: Option<&str>, + excludes: &Vec, + ) -> Self { if prefix.is_none() && suffix.is_none() { return self.clone(); } @@ -146,13 +169,19 @@ impl SyntaxTrees { pub fn append_trees(&mut self, src: &SyntaxTrees) { ffi::append_trees( self.inner.pin_mut(), - src.inner.as_ref().expect("syntax trees pointer must be valid"), + src.inner + .as_ref() + .expect("syntax trees pointer must be valid"), ); } /// Returns tree count in this batch. pub fn len(&self) -> usize { - ffi::tree_count(self.inner.as_ref().expect("syntax trees pointer must be valid")) + ffi::tree_count( + self.inner + .as_ref() + .expect("syntax trees pointer must be valid"), + ) } /// Returns true if the batch contains no trees. @@ -161,24 +190,31 @@ impl SyntaxTrees { } /// Returns indices reachable from top names. - pub fn reachable_indices( - &self, - tops: &Vec, - ) -> Result, Box> { + pub fn reachable_indices(&self, tops: &Vec) -> Result> { let indices = ffi::reachable_tree_indices( - self.inner.as_ref().expect("syntax trees pointer must be valid"), + self.inner + .as_ref() + .expect("syntax trees pointer must be valid"), tops, - )?; + ) + .map_err(|cause| SlangError::TrimByTop { + message: cause.to_string(), + })?; Ok(indices.into_iter().map(|i| i as usize).collect()) } /// Returns a tree at the provided index. - pub fn tree_at(&self, index: usize) -> Result> { + pub fn tree_at(&self, index: usize) -> Result { Ok(SyntaxTree { inner: ffi::tree_at( - self.inner.as_ref().expect("syntax trees pointer must be valid"), + self.inner + .as_ref() + .expect("syntax trees pointer must be valid"), index, - )?, + ) + .map_err(|cause| SlangError::TreeAccess { + message: cause.to_string(), + })?, }) } } @@ -204,22 +240,26 @@ impl SlangContext { } /// Parses a source file and returns the syntax tree. - pub fn parse( - &mut self, - path: &str, - ) -> Result> { + pub fn parse(&mut self, path: &str) -> Result { Ok(SyntaxTree { - inner: self.inner.pin_mut().parse_file(path)?, + inner: self + .inner + .pin_mut() + .parse_file(path) + .map_err(|cause| SlangError::Parse { + message: cause.to_string(), + })?, }) } /// Parses multiple source files and returns a batch of syntax trees. - pub fn parse_files( - &mut self, - paths: &Vec, - ) -> Result> { + pub fn parse_files(&mut self, paths: &Vec) -> Result { Ok(SyntaxTrees { - inner: self.inner.pin_mut().parse_files(paths)?, + inner: self.inner.pin_mut().parse_files(paths).map_err(|cause| { + SlangError::ParseFiles { + message: cause.to_string(), + } + })?, }) } } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index f753b8e80..6e703cab8 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -191,28 +191,19 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { }) .collect(); - let group_trees = slang - .parse_files(&file_paths) - .map_err(|cause| Error::new(format!("Cannot parse source file set: {}", cause)))?; + let group_trees = slang.parse_files(&file_paths)?; parsed_trees.append_trees(&group_trees); } let reachable = if args.top.is_empty() { (0..parsed_trees.len()).collect::>() } else { - parsed_trees - .reachable_indices(&args.top) - .map_err(|cause| Error::new(format!("Cannot trim parsed trees by --top: {}", cause)))? + parsed_trees.reachable_indices(&args.top)? }; let mut first_item = true; for idx in reachable { - let tree = parsed_trees.tree_at(idx).map_err(|cause| { - Error::new(format!( - "Cannot access parsed tree at index {}: {}", - idx, cause - )) - })?; + let tree = parsed_trees.tree_at(idx)?; let renamed_tree = tree.rename( args.prefix.as_deref(), args.suffix.as_deref(), 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) + } +} From 278e76c9e0637c639209b9f7141fe58bf4708d84 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 22:18:07 +0100 Subject: [PATCH 29/46] bender-slang: Unwrap instead of expect --- crates/bender-slang/src/lib.rs | 40 ++++++++++------------------------ 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index 3e6e1c438..aec79e0a2 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -167,21 +167,12 @@ impl SyntaxTrees { /// Appends all trees from src into self. pub fn append_trees(&mut self, src: &SyntaxTrees) { - ffi::append_trees( - self.inner.pin_mut(), - src.inner - .as_ref() - .expect("syntax trees pointer must be valid"), - ); + ffi::append_trees(self.inner.pin_mut(), src.inner.as_ref().unwrap()); } /// Returns tree count in this batch. pub fn len(&self) -> usize { - ffi::tree_count( - self.inner - .as_ref() - .expect("syntax trees pointer must be valid"), - ) + ffi::tree_count(self.inner.as_ref().unwrap()) } /// Returns true if the batch contains no trees. @@ -191,29 +182,22 @@ impl SyntaxTrees { /// Returns indices reachable from top names. pub fn reachable_indices(&self, tops: &Vec) -> Result> { - let indices = ffi::reachable_tree_indices( - self.inner - .as_ref() - .expect("syntax trees pointer must be valid"), - tops, - ) - .map_err(|cause| SlangError::TrimByTop { - message: cause.to_string(), - })?; + 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 a tree at the provided index. pub fn tree_at(&self, index: usize) -> Result { Ok(SyntaxTree { - inner: ffi::tree_at( - self.inner - .as_ref() - .expect("syntax trees pointer must be valid"), - index, - ) - .map_err(|cause| SlangError::TreeAccess { - message: cause.to_string(), + inner: ffi::tree_at(self.inner.as_ref().unwrap(), index).map_err(|cause| { + SlangError::TreeAccess { + message: cause.to_string(), + } })?, }) } From 24b4e43468ed9517386284a3219bbbe65162d75d Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 00:17:00 +0100 Subject: [PATCH 30/46] bender-slang: Add documentation --- CHANGELOG.md | 3 +++ README.md | 30 ++++++++++++++++++++++++++++++ crates/bender-slang/README.md | 19 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 crates/bender-slang/README.md 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/README.md b/README.md index 41ed841fd..9170a8e2b 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,30 @@ 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. + +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/README.md b/crates/bender-slang/README.md new file mode 100644 index 000000000..a105eb6de --- /dev/null +++ b/crates/bender-slang/README.md @@ -0,0 +1,19 @@ +# bender-slang + +`bender-slang` provides the C++ bridge between `bender` and the vendored [Slang](https://github.com/MikePopoloski/slang) parser infrastructure. + +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 +``` From 367d919e5af4abe400e9af1db9b96f276d12b602 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 00:32:16 +0100 Subject: [PATCH 31/46] pickle: Actually include additional sourcefiles --- src/cmd/pickle.rs | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 6e703cab8..ad0d88929 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -5,9 +5,10 @@ use std::fs::File; use std::io::{BufWriter, Write}; +use std::path::Path; use clap::Args; -use indexmap::IndexSet; +use indexmap::{IndexMap, IndexSet}; use tokio::runtime::Runtime; use crate::cmd::sources::get_passed_targets; @@ -15,7 +16,7 @@ use crate::config::{Validate, ValidationContext}; use crate::diagnostic::Warnings; use crate::error::*; use crate::sess::{Session, SessionIo}; -use crate::src::{SourceFile, SourceType}; +use crate::src::{SourceFile, SourceGroup, SourceType}; use crate::target::TargetSet; use bender_slang::SlangPrintOpts; @@ -119,12 +120,44 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { .unwrap_or_default(); // Flatten and validate the sources. - let srcs = srcs + 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| 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_comments: !args.strip_comments, From 8f0e4f79dccdf05795301f846543346b2004eecb Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 00:37:23 +0100 Subject: [PATCH 32/46] bender-slang: Fix windows build bender-slang: Fix windows build 2 --- .github/workflows/ci.yml | 4 +-- .github/workflows/cli_regression.yml | 2 +- crates/bender-slang/build.rs | 38 ++++++++++++++++++---------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8b13df41..4420a4f4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,9 +47,9 @@ jobs: with: toolchain: stable - name: Build - run: cargo build --all-features --release + run: cargo build --all-features - name: Cargo Test - run: cargo test --workspace --all-features --release + run: cargo test --workspace --all-features - name: Run unit-tests run: tests/run_all.sh shell: bash diff --git a/.github/workflows/cli_regression.yml b/.github/workflows/cli_regression.yml index 91069aefe..bfdcf9bd7 100644 --- a/.github/workflows/cli_regression.yml +++ b/.github/workflows/cli_regression.yml @@ -29,7 +29,7 @@ jobs: with: toolchain: stable - name: Run CLI Regression - run: cargo test --all-features --test cli_regression --release -- --ignored + run: cargo test --all-features --test cli_regression -- --ignored env: BENDER_TEST_GOLDEN_BRANCH: ${{ github.base_ref }} diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 4a9e46a6e..ac946b9a9 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -5,6 +5,13 @@ 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"); @@ -20,7 +27,7 @@ fn main() { ]; // Add debug define if in debug build - if build_profile == "debug" { + if build_profile == "debug" && !(target_env == "msvc") { common_cxx_defines.push(("SLANG_DEBUG", "1")); common_cxx_defines.push(("SLANG_ASSERT_ENABLED", "1")); }; @@ -41,7 +48,8 @@ fn main() { // 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"); + .define("CMAKE_DISABLE_FIND_PACKAGE_Boost", "ON") + .profile(cmake_profile); // Apply common defines and flags for (def, value) in common_cxx_defines.iter() { @@ -54,22 +62,24 @@ fn main() { // 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", dst.display()); + println!("cargo:rustc-link-search=native={}", lib_dir.display()); println!("cargo:rustc-link-lib=static=svlang"); - // Link the additional libraries based on build profile and OS - match (build_profile.as_str(), target_env.as_str()) { - ("release", _) | (_, "msvc") => { - println!("cargo:rustc-link-lib=static=fmt"); - println!("cargo:rustc-link-lib=static=mimalloc"); - } - ("debug", _) => { - println!("cargo:rustc-link-lib=static=fmtd"); - println!("cargo:rustc-link-lib=static=mimalloc-debug"); - } - _ => unreachable!(), + // 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 From 604dfc98ef6011ac5e5aae2c523c57d5d58000b4 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 15:11:07 +0100 Subject: [PATCH 33/46] bender-slang: Clippy fixes and clean up --- crates/bender-slang/build.rs | 2 +- crates/bender-slang/src/lib.rs | 13 ++++++++++--- src/cmd/pickle.rs | 6 +++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index ac946b9a9..7db0396f7 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -27,7 +27,7 @@ fn main() { ]; // Add debug define if in debug build - if build_profile == "debug" && !(target_env == "msvc") { + if build_profile == "debug" && (target_env != "msvc") { common_cxx_defines.push(("SLANG_DEBUG", "1")); common_cxx_defines.push(("SLANG_ASSERT_ENABLED", "1")); }; diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index aec79e0a2..e5e070be4 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -203,6 +203,12 @@ impl SyntaxTrees { } } +impl Default for SyntaxTrees { + fn default() -> Self { + Self::new() + } +} + impl SlangContext { /// Creates a new Slang session. pub fn new() -> Self { @@ -248,7 +254,8 @@ impl SlangContext { } } -/// Creates a new Slang session -pub fn new_session() -> SlangContext { - SlangContext::new() +impl Default for SlangContext { + fn default() -> Self { + Self::new() + } } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index ad0d88929..0bdd5890e 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -19,7 +19,7 @@ use crate::sess::{Session, SessionIo}; use crate::src::{SourceFile, SourceGroup, SourceType}; use crate::target::TargetSet; -use bender_slang::SlangPrintOpts; +use bender_slang::{SlangContext, SlangPrintOpts, SyntaxTrees}; /// Pickle files #[derive(Args, Debug)] @@ -179,8 +179,8 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { write!(writer, "[")?; } - let mut parsed_trees = bender_slang::SyntaxTrees::new(); - let mut slang = bender_slang::new_session(); + let mut parsed_trees = SyntaxTrees::new(); + let mut slang = SlangContext::new(); for src_group in srcs { // Collect include directories and defines from the source group and command line arguments. let include_dirs: Vec = src_group From 094547fd8ddbdbacb6c8cafc410bdaf95b96754c Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 18 Feb 2026 21:20:57 +0100 Subject: [PATCH 34/46] bender-slang(lib): Refactor to respect lifetime of C++ objects --- crates/bender-slang/cpp/slang_bridge.cpp | 104 ++++++----- crates/bender-slang/cpp/slang_bridge.h | 30 +-- crates/bender-slang/src/lib.rs | 225 ++++++++++------------- src/cmd/pickle.rs | 26 ++- 4 files changed, 176 insertions(+), 209 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 029548552..755690992 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -27,8 +27,7 @@ using std::string; using std::string_view; using std::vector; -// Create a new SlangContext instance -std::unique_ptr new_slang_context() { return std::make_unique(); } +std::unique_ptr new_slang_session() { return std::make_unique(); } SlangContext::SlangContext() : diagEngine(sourceManager), diagClient(std::make_shared()) { diagEngine.addClient(diagClient); @@ -36,62 +35,76 @@ SlangContext::SlangContext() : diagEngine(sourceManager), diagClient(std::make_s // Set the include paths for the preprocessor void SlangContext::set_includes(const rust::Vec& incs) { - ppOptions.additionalIncludePaths.clear(); for (const auto& inc : incs) { - ppOptions.additionalIncludePaths.emplace_back(std::string(inc)); + 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()); + } } } // Sets the preprocessor defines void SlangContext::set_defines(const rust::Vec& defs) { - ppOptions.predefines.clear(); + ppOptions.predefines.reserve(defs.size()); for (const auto& def : defs) { - ppOptions.predefines.emplace_back(std::string(def)); + ppOptions.predefines.emplace_back(def.data(), def.size()); } } -// Parses the given file and returns a syntax tree, if successful -std::shared_ptr SlangContext::parse_file(rust::Str path) { - string_view pathView(path.data(), path.size()); +std::vector> SlangContext::parse_files(const rust::Vec& paths) { Bag options; options.set(ppOptions); - auto result = SyntaxTree::fromFile(pathView, sourceManager, options); + std::vector> out; + out.reserve(paths.size()); - if (!result) { - auto& err = result.error(); - std::string msg = "System Error loading '" + std::string(err.second) + "': " + err.first.message(); - throw std::runtime_error(msg); - } + for (const auto& path : paths) { + string_view pathView(path.data(), path.size()); + auto result = SyntaxTree::fromFile(pathView, sourceManager, options); - auto tree = *result; - diagClient->clear(); - diagEngine.clearIncludeStack(); + if (!result) { + auto& err = result.error(); + std::string msg = "System Error loading '" + std::string(err.second) + "': " + err.first.message(); + throw std::runtime_error(msg); + } - bool hasErrors = false; - for (const auto& diag : tree->diagnostics()) { - hasErrors |= diag.isError(); - diagEngine.issue(diag); - } + auto tree = *result; + diagClient->clear(); + diagEngine.clearIncludeStack(); - if (hasErrors) { - std::string rendered = diagClient->getString(); - if (rendered.empty()) { - rendered = "Failed to parse '" + std::string(pathView) + "'."; + bool hasErrors = false; + for (const auto& diag : tree->diagnostics()) { + hasErrors |= diag.isError(); + diagEngine.issue(diag); } - throw std::runtime_error(rendered); + + 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 tree; + return out; } -std::unique_ptr SlangContext::parse_files(const rust::Vec& paths) { - auto out = std::make_unique(); - out->trees.reserve(paths.size()); - for (const auto& path : paths) { - out->trees.push_back(parse_file(path)); +void SlangSession::parse_group(const rust::Vec& files, const rust::Vec& includes, + const rust::Vec& defines) { + auto ctx = std::make_unique(); + ctx->set_includes(includes); + ctx->set_defines(defines); + + auto parsed = ctx->parse_files(files); + allTrees.reserve(allTrees.size() + parsed.size()); + for (const auto& tree : parsed) { + allTrees.push_back(tree); } - return out; + + contexts.push_back(std::move(ctx)); } // Rewriter that adds prefix/suffix to module and instantiated hierarchy names @@ -271,17 +284,8 @@ rust::String dump_tree_json(std::shared_ptr tree) { return rust::String(std::string(writer.view())); } -std::unique_ptr new_syntax_trees() { return std::make_unique(); } - -void append_trees(SyntaxTrees& dst, const SyntaxTrees& src) { - dst.trees.reserve(dst.trees.size() + src.trees.size()); - for (const auto& tree : src.trees) { - dst.trees.push_back(tree); - } -} - -rust::Vec reachable_tree_indices(const SyntaxTrees& trees, const rust::Vec& tops) { - const auto& treeVec = trees.trees; +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; @@ -345,11 +349,11 @@ rust::Vec reachable_tree_indices(const SyntaxTrees& trees, const return result; } -std::size_t tree_count(const SyntaxTrees& trees) { return trees.trees.size(); } +std::size_t tree_count(const SlangSession& session) { return session.trees().size(); } -std::shared_ptr tree_at(const SyntaxTrees& trees, std::size_t index) { - if (index >= trees.trees.size()) { +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 trees.trees[index]; + return session.trees()[index]; } diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index 9e2ff59d9..a309b5a95 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -17,7 +17,6 @@ #include struct SlangPrintOpts; -struct SyntaxTrees; class SlangContext { public: @@ -26,8 +25,7 @@ class SlangContext { void set_includes(const rust::Vec& includes); void set_defines(const rust::Vec& defines); - std::shared_ptr parse_file(rust::Str path); - std::unique_ptr parse_files(const rust::Vec& paths); + std::vector> parse_files(const rust::Vec& paths); private: slang::SourceManager sourceManager; @@ -36,7 +34,19 @@ class SlangContext { std::shared_ptr diagClient; }; -std::unique_ptr new_slang_context(); +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; +}; + +std::unique_ptr new_slang_session(); std::shared_ptr rename(std::shared_ptr tree, rust::Str prefix, rust::Str suffix, const rust::Vec& excludes); @@ -45,14 +55,8 @@ rust::String print_tree(std::shared_ptr tree, SlangPr rust::String dump_tree_json(std::shared_ptr tree); -struct SyntaxTrees { - std::vector> trees; -}; - -std::unique_ptr new_syntax_trees(); -void append_trees(SyntaxTrees& dst, const SyntaxTrees& src); -rust::Vec reachable_tree_indices(const SyntaxTrees& trees, const rust::Vec& tops); -std::size_t tree_count(const SyntaxTrees& trees); -std::shared_ptr tree_at(const SyntaxTrees& trees, std::size_t index); +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); #endif // BENDER_SLANG_BRIDGE_H diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index e5e070be4..715c96f0c 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -1,6 +1,8 @@ // Copyright (c) 2025 ETH Zurich // Tim Fischer +use std::marker::PhantomData; + use cxx::{SharedPtr, UniquePtr}; use thiserror::Error; @@ -10,10 +12,8 @@ pub type Result = std::result::Result; #[derive(Debug, Error)] pub enum SlangError { - #[error("Failed to parse file: {message}")] - Parse { message: String }, - #[error("Failed to parse files: {message}")] - ParseFiles { message: String }, + #[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}")] @@ -32,45 +32,30 @@ mod ffi { unsafe extern "C++" { include!("bender-slang/cpp/slang_bridge.h"); - // Include Slang header to define SyntaxTree type for CXX include!("slang/syntax/SyntaxTree.h"); - /// Opaque type for the Slang Context - type SlangContext; + /// Opaque session that owns parse contexts and syntax trees. + type SlangSession; - /// Opaque type for the Slang SyntaxTree + /// Opaque type for the Slang syntax tree. #[namespace = "slang::syntax"] type SyntaxTree; - /// Opaque type for a batch of parsed syntax trees. - type SyntaxTrees; - - /// Create a new persistent context - fn new_slang_context() -> UniquePtr; - - /// Set the include directories - fn set_includes(self: Pin<&mut SlangContext>, includes: &Vec); - /// Set the preprocessor defines - fn set_defines(self: Pin<&mut SlangContext>, defines: &Vec); - - /// Parse all added sources. Returns a syntax tree on success, or an error message on failure. - fn parse_file(self: Pin<&mut SlangContext>, path: &str) -> Result>; - /// Parse multiple source files and return a batch of syntax trees. - fn parse_files( - self: Pin<&mut SlangContext>, - paths: &Vec, - ) -> Result>; - /// Create an empty syntax-tree batch. - fn new_syntax_trees() -> UniquePtr; - /// Appends trees from src into dst. - fn append_trees(dst: Pin<&mut SyntaxTrees>, src: &SyntaxTrees); - /// Computes reachable tree indices from the provided top names. - fn reachable_tree_indices(trees: &SyntaxTrees, tops: &Vec) -> Result>; - /// Returns the number of trees in the batch. - fn tree_count(trees: &SyntaxTrees) -> usize; - /// Returns tree at index from the batch. - fn tree_at(trees: &SyntaxTrees, index: usize) -> Result>; - - /// Rename names in the syntax tree with a given prefix and suffix + + 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 rename( tree: SharedPtr, prefix: &str, @@ -78,59 +63,62 @@ mod ffi { excludes: &Vec, ) -> SharedPtr; - /// Print a specific tree fn print_tree(tree: SharedPtr, options: SlangPrintOpts) -> String; - /// Dump the syntax tree as JSON for debugging purposes fn dump_tree_json(tree: SharedPtr) -> String; } } -/// Wrapper around an opaque Slang syntax tree. -pub struct SyntaxTree { +/// 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>, } -impl Clone for SyntaxTree { +impl<'a> Clone for SyntaxTree<'a> { fn clone(&self) -> Self { Self { inner: self.inner.clone(), + _session: PhantomData, } } } -impl SyntaxTree { - /// Renames all names in the syntax tree with the given prefix and suffix - pub fn rename( - &self, - prefix: Option<&str>, - suffix: Option<&str>, - excludes: &Vec, - ) -> Self { +impl<'a> SyntaxTree<'a> { + /// Renames all names in the syntax tree with the given prefix and suffix. + pub fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &[String]) -> Self { if prefix.is_none() && suffix.is_none() { return self.clone(); } + let excludes = excludes.to_vec(); Self { inner: ffi::rename( self.inner.clone(), prefix.unwrap_or(""), suffix.unwrap_or(""), - excludes, + &excludes, ), + _session: PhantomData, } } - /// Displays the syntax tree as a string with the given options + /// 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 { +impl std::fmt::Display for SyntaxTree<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let options = SlangPrintOpts { expand_macros: false, @@ -141,49 +129,62 @@ impl std::fmt::Display for SyntaxTree { } } -impl std::fmt::Debug for SyntaxTree { +impl std::fmt::Debug for SyntaxTree<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.as_debug()) } } -/// Wrapper around an opaque Slang context. -pub struct SlangContext { - inner: UniquePtr, -} - -/// Wrapper around an opaque batch of syntax trees. -pub struct SyntaxTrees { - inner: UniquePtr, -} - -impl SyntaxTrees { - /// Creates an empty syntax-tree batch. +impl SlangSession { pub fn new() -> Self { Self { - inner: ffi::new_syntax_trees(), + inner: ffi::new_slang_session(), } } - /// Appends all trees from src into self. - pub fn append_trees(&mut self, src: &SyntaxTrees) { - ffi::append_trees(self.inner.pin_mut(), src.inner.as_ref().unwrap()); + /// 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 = includes.to_vec(); + 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 tree count in this batch. - pub fn len(&self) -> usize { + /// 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 true if the batch contains no trees. - pub fn is_empty(&self) -> bool { - self.len() == 0 + /// 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 indices reachable from top names. - pub fn reachable_indices(&self, tops: &Vec) -> Result> { + /// 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| { + ffi::reachable_tree_indices(self.inner.as_ref().unwrap(), &tops).map_err(|cause| { SlangError::TrimByTop { message: cause.to_string(), } @@ -191,70 +192,30 @@ impl SyntaxTrees { Ok(indices.into_iter().map(|i| i as usize).collect()) } - /// Returns a tree at the provided index. - pub fn tree_at(&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(), - } - })?, - }) - } -} - -impl Default for SyntaxTrees { - fn default() -> Self { - Self::new() - } -} - -impl SlangContext { - /// Creates a new Slang session. - pub fn new() -> Self { - Self { - inner: ffi::new_slang_context(), + /// 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) } - /// Sets the include directories. - pub fn set_includes(&mut self, includes: &Vec) -> &mut Self { - self.inner.pin_mut().set_includes(includes); - self - } - - /// Sets the preprocessor defines. - pub fn set_defines(&mut self, defines: &Vec) -> &mut Self { - self.inner.pin_mut().set_defines(defines); - self - } - - /// Parses a source file and returns the syntax tree. - pub fn parse(&mut self, path: &str) -> Result { + /// Returns a handle to the syntax tree at the given index. + pub fn tree(&self, index: usize) -> Result> { Ok(SyntaxTree { - inner: self - .inner - .pin_mut() - .parse_file(path) - .map_err(|cause| SlangError::Parse { - message: cause.to_string(), - })?, - }) - } - - /// Parses multiple source files and returns a batch of syntax trees. - pub fn parse_files(&mut self, paths: &Vec) -> Result { - Ok(SyntaxTrees { - inner: self.inner.pin_mut().parse_files(paths).map_err(|cause| { - SlangError::ParseFiles { + inner: ffi::tree_at(self.inner.as_ref().unwrap(), index).map_err(|cause| { + SlangError::TreeAccess { message: cause.to_string(), } })?, + _session: PhantomData, }) } } -impl Default for SlangContext { +impl Default for SlangSession { fn default() -> Self { Self::new() } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 0bdd5890e..50e210a48 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -19,7 +19,7 @@ use crate::sess::{Session, SessionIo}; use crate::src::{SourceFile, SourceGroup, SourceType}; use crate::target::TargetSet; -use bender_slang::{SlangContext, SlangPrintOpts, SyntaxTrees}; +use bender_slang::{SlangPrintOpts, SlangSession}; /// Pickle files #[derive(Args, Debug)] @@ -179,16 +179,17 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { write!(writer, "[")?; } - let mut parsed_trees = SyntaxTrees::new(); - let mut slang = SlangContext::new(); + let mut session = SlangSession::new(); for src_group in srcs { - // Collect include directories and defines from the source group and command line arguments. + // 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. @@ -200,11 +201,10 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { None => def.to_string(), }) .chain(args.define.iter().cloned()) + .collect::>() + .into_iter() .collect(); - // Set the include directories and defines in the Slang session. - slang.set_includes(&include_dirs).set_defines(&defines); - // Collect file paths from the source group. let file_paths: Vec = src_group .files @@ -224,19 +224,17 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { }) .collect(); - let group_trees = slang.parse_files(&file_paths)?; - parsed_trees.append_trees(&group_trees); + session.parse_group(&file_paths, &include_dirs, &defines)?; } - let reachable = if args.top.is_empty() { - (0..parsed_trees.len()).collect::>() + let trees = if args.top.is_empty() { + session.all_trees()? } else { - parsed_trees.reachable_indices(&args.top)? + session.reachable_trees(&args.top)? }; let mut first_item = true; - for idx in reachable { - let tree = parsed_trees.tree_at(idx)?; + for tree in trees { let renamed_tree = tree.rename( args.prefix.as_deref(), args.suffix.as_deref(), From f94af870737028323584b587a71420424670d59f Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 22 Feb 2026 18:27:37 +0100 Subject: [PATCH 35/46] bender-slang: Cannonicalize include paths on windows --- Cargo.lock | 1 + crates/bender-slang/Cargo.toml | 3 +++ crates/bender-slang/src/lib.rs | 22 +++++++++++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index c129d79e7..718605220 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,7 @@ dependencies = [ "cmake", "cxx", "cxx-build", + "dunce", "thiserror", ] diff --git a/crates/bender-slang/Cargo.toml b/crates/bender-slang/Cargo.toml index b660dcca0..bdf157918 100644 --- a/crates/bender-slang/Cargo.toml +++ b/crates/bender-slang/Cargo.toml @@ -7,6 +7,9 @@ edition = "2024" 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/src/lib.rs b/crates/bender-slang/src/lib.rs index 715c96f0c..9372e0499 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -150,7 +150,7 @@ impl SlangSession { defines: &[String], ) -> Result> { let files_vec = files.to_vec(); - let includes_vec = includes.to_vec(); + let includes_vec = normalize_include_dirs(includes)?; let defines_vec = defines.to_vec(); let start = self.tree_count(); @@ -220,3 +220,23 @@ impl Default for SlangSession { 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(not(windows))] +fn normalize_include_dirs(includes: &[String]) -> Result> { + Ok(includes.to_vec()) +} From 7635cb73aaaca33045f29e91a4cf1c10f5c79e1d Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 24 Feb 2026 21:32:01 +0100 Subject: [PATCH 36/46] Update crates/bender-slang/README.md Co-authored-by: Michael Rogenmoser --- crates/bender-slang/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bender-slang/README.md b/crates/bender-slang/README.md index a105eb6de..a873debfa 100644 --- a/crates/bender-slang/README.md +++ b/crates/bender-slang/README.md @@ -1,6 +1,6 @@ # bender-slang -`bender-slang` provides the C++ bridge between `bender` and the vendored [Slang](https://github.com/MikePopoloski/slang) parser infrastructure. +`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. From e18f8e3c5d7fb71f0da23cf756891b9bd0ad07ad Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 24 Feb 2026 21:38:16 +0100 Subject: [PATCH 37/46] bender-slang(lib): Use `cfg(unix)` --- crates/bender-slang/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index 9372e0499..9fd1ebc37 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -236,7 +236,7 @@ fn normalize_include_dirs(includes: &[String]) -> Result> { Ok(out) } -#[cfg(not(windows))] +#[cfg(unix)] fn normalize_include_dirs(includes: &[String]) -> Result> { Ok(includes.to_vec()) } From 49c1a9c9c3a2e3988de48aa5b8b2cfc4953b0070 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 24 Feb 2026 21:47:11 +0100 Subject: [PATCH 38/46] Update Readme with `script` flags --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 9170a8e2b..044f641fa 100644 --- a/README.md +++ b/README.md @@ -573,6 +573,13 @@ Useful options: - `--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 From b516a069da68f187af1c94f18b1d5bce9da582da Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 25 Feb 2026 17:45:57 +0100 Subject: [PATCH 39/46] pickle: Rename only defined references --- crates/bender-slang/cpp/slang_bridge.cpp | 184 ++++++++++++++--------- crates/bender-slang/cpp/slang_bridge.h | 30 +++- crates/bender-slang/src/lib.rs | 96 +++++++++--- src/cmd/pickle.rs | 14 +- 4 files changed, 224 insertions(+), 100 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 755690992..5807aa7c8 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -21,13 +21,13 @@ using namespace slang::driver; using namespace slang::syntax; using namespace slang::parsing; -using std::memcpy; using std::shared_ptr; using std::string; using std::string_view; using std::vector; std::unique_ptr new_slang_session() { return std::make_unique(); } +std::unique_ptr new_syntax_tree_rewriter() { return std::make_unique(); } SlangContext::SlangContext() : diagEngine(sourceManager), diagClient(std::make_shared()) { diagEngine.addClient(diagClient); @@ -107,149 +107,189 @@ void SlangSession::parse_group(const rust::Vec& files, const rust: contexts.push_back(std::move(ctx)); } -// Rewriter that adds prefix/suffix to module and instantiated hierarchy names -class SuffixPrefixRewriter : public SyntaxRewriter { +class DeclarationCollector : public SyntaxVisitor { public: - SuffixPrefixRewriter(string_view prefix, string_view suffix, const std::unordered_set& excludes) - : prefix(prefix), suffix(suffix), excludes(excludes) {} + explicit DeclarationCollector(std::unordered_set& names) : names(names) {} - // Helper to allocate and build renamed string with prefix/suffix - string_view rename(string_view name) { - if (excludes.count(std::string(name))) { - return name; + void handle(const ModuleDeclarationSyntax& node) { + if (!node.header->name.isMissing()) { + names.insert(std::string(node.header->name.valueText())); + } + visitDefault(node); + } + + private: + std::unordered_set& names; +}; + +// Rewriter that renames declarations and references only if their declaration exists +// in the precomputed renameMap. +class MappedRewriter : public SyntaxRewriter { + public: + MappedRewriter(const std::unordered_map& renameMap, std::uint64_t& declRenamed, + std::uint64_t& refRenamed) + : renameMap(renameMap), declRenamed(declRenamed), refRenamed(refRenamed) {} + + string_view mapped_name(string_view name) { + auto it = renameMap.find(std::string(name)); + if (it == renameMap.end()) { + return {}; } - size_t len = prefix.size() + name.size() + suffix.size(); - char* mem = (char*)alloc.allocate(len, 1); - memcpy(mem, prefix.data(), prefix.size()); - memcpy(mem + prefix.size(), name.data(), name.size()); - memcpy(mem + prefix.size() + name.size(), suffix.data(), suffix.size()); - return string_view(mem, len); + return string_view(it->second); } - // Renames "module foo;" -> "module foo;" - // Note: Handles packages and interfaces too. void handle(const ModuleDeclarationSyntax& node) { if (node.header->name.isMissing()) return; - // Create a new name token - auto newName = rename(node.header->name.valueText()); + auto newName = mapped_name(node.header->name.valueText()); + if (newName.empty()) { + visitDefault(node); + return; + } + auto newNameToken = makeId(newName, node.header->name.trivia()); - // Clone the header and update the name ModuleHeaderSyntax* newHeader = deepClone(*node.header, alloc); newHeader->name = newNameToken; - // Replace the old header with the new one replace(*node.header, *newHeader); - - // Continue visiting child nodes + declRenamed++; visitDefault(node); } - // Renames "foo i_foo();" -> "foo i_foo();" - // Note: Handles modules and interfaces. void handle(const HierarchyInstantiationSyntax& node) { - // Check to make sure we are dealing with an identifier - // and not a built-in type e.g. `initial foo();` if (node.type.kind == parsing::TokenKind::Identifier) { + auto newName = mapped_name(node.type.valueText()); + if (!newName.empty()) { + auto newNameToken = makeId(newName, node.type.trivia()); - // Create a new name token - auto newName = rename(node.type.valueText()); - auto newNameToken = makeId(newName); + HierarchyInstantiationSyntax* newNode = deepClone(node, alloc); + newNode->type = newNameToken; - // Clone the node and update the type token - HierarchyInstantiationSyntax* newNode = deepClone(node, alloc); - newNode->type = newNameToken; - - // Replace the old node with the new one - replace(node, *newNode, true); + replace(node, *newNode, true); + refRenamed++; + } } - // Continue visiting child nodes visitDefault(node); } - // Renames "import foo;" -> "import foo;" void handle(const PackageImportItemSyntax& node) { if (node.package.isMissing()) return; - auto newName = rename(node.package.valueText()); + auto newName = mapped_name(node.package.valueText()); + if (newName.empty()) { + visitDefault(node); + return; + } auto newNameToken = makeId(newName, node.package.trivia()); PackageImportItemSyntax* newNode = deepClone(node, alloc); newNode->package = newNameToken; replace(node, *newNode); + refRenamed++; visitDefault(node); } - // Renames "virtual MyIntf foo;" -> "virtual MyIntf foo;" void handle(const VirtualInterfaceTypeSyntax& node) { if (node.name.isMissing()) return; - auto newName = rename(node.name.valueText()); + auto newName = mapped_name(node.name.valueText()); + if (newName.empty()) { + visitDefault(node); + return; + } auto newNameToken = makeId(newName, node.name.trivia()); VirtualInterfaceTypeSyntax* newNode = deepClone(node, alloc); newNode->name = newNameToken; replace(node, *newNode); + refRenamed++; visitDefault(node); } - // Renames "foo::bar" -> "foo::bar" void handle(const ScopedNameSyntax& node) { - // Only rename if the left side is a simple identifier (e.g., a package name) - // We ignore nested calls or parameterized classes for now. if (node.left->kind == SyntaxKind::IdentifierName) { auto& leftNode = node.left->as(); auto name = leftNode.identifier.valueText(); - // Skip built-in keywords that look like scopes if (name != "$unit" && name != "local" && name != "super" && name != "this") { - auto newName = rename(name); - auto newNameToken = makeId(newName, leftNode.identifier.trivia()); + auto newName = mapped_name(name); + if (!newName.empty()) { + auto newNameToken = makeId(newName, leftNode.identifier.trivia()); - // Clone the left node and update identifier - IdentifierNameSyntax* newLeft = deepClone(leftNode, alloc); - newLeft->identifier = newNameToken; + IdentifierNameSyntax* newLeft = deepClone(leftNode, alloc); + newLeft->identifier = newNameToken; - // Clone the scoped node and attach new left - ScopedNameSyntax* newNode = deepClone(node, alloc); - newNode->left = newLeft; + ScopedNameSyntax* newNode = deepClone(node, alloc); + newNode->left = newLeft; - replace(node, *newNode); + replace(node, *newNode); + refRenamed++; + } } } - // Visit children to handle recursive scopes - // e.g., OuterPkg::InnerPkg::Item visitDefault(node); } private: - string_view prefix; - string_view suffix; - const std::unordered_set& excludes; + const std::unordered_map& renameMap; + std::uint64_t& declRenamed; + std::uint64_t& refRenamed; }; -// Transform the given syntax tree by renaming modules and instantiated hierarchy names with the specified prefix/suffix -std::shared_ptr rename(std::shared_ptr tree, rust::Str prefix, rust::Str suffix, - const rust::Vec& excludes) { - std::string_view p(prefix.data(), prefix.size()); - std::string_view s(suffix.data(), suffix.size()); +void SyntaxTreeRewriter::reset_rename_map() { + renameMap.clear(); + renamedDeclarations = 0; + renamedReferences = 0; +} + +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()); } - std::unordered_set excludeSet; - for (const auto& e : excludes) { - excludeSet.insert(std::string(e)); +void SyntaxTreeRewriter::set_excludes(const rust::Vec values) { + excludes.clear(); + for (const auto& value : values) { + excludes.insert(std::string(value)); } +} + +void SyntaxTreeRewriter::register_declarations(std::shared_ptr tree) { + if (prefix.empty() && suffix.empty()) { + return; + } + + std::unordered_set declaredNames; + DeclarationCollector collector(declaredNames); + collector.visit(tree->root()); - // SuffixPrefixRewriter is defined in the .cpp file as before - SuffixPrefixRewriter rewriter(p, s, excludeSet); - return rewriter.transform(tree); + for (const auto& name : declaredNames) { + if (excludes.count(name)) { + continue; + } + renameMap.insert_or_assign(name, prefix + name + suffix); + } +} + +std::shared_ptr SyntaxTreeRewriter::rewrite_tree(std::shared_ptr tree) { + if (renameMap.empty()) { + return tree; + } + + std::uint64_t declRenamed = 0; + std::uint64_t refRenamed = 0; + MappedRewriter rewriter(renameMap, declRenamed, refRenamed); + auto transformed = rewriter.transform(tree); + renamedDeclarations += declRenamed; + renamedReferences += refRenamed; + return transformed; } // Print the given syntax tree with specified options @@ -357,3 +397,7 @@ std::shared_ptr tree_at(const SlangSession& session, std::size_t ind } return session.trees()[index]; } + +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/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index a309b5a95..b1ca94279 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include struct SlangPrintOpts; @@ -46,10 +48,30 @@ class SlangSession { std::vector> allTrees; }; -std::unique_ptr new_slang_session(); +class SyntaxTreeRewriter { + public: + void reset_rename_map(); + void register_declarations(std::shared_ptr tree); + void set_prefix(rust::Str prefix); + void set_suffix(rust::Str suffix); + void set_excludes(const rust::Vec excludes); + + std::shared_ptr rewrite_tree(std::shared_ptr tree); + + std::uint64_t renamed_declarations() const { return renamedDeclarations; } + std::uint64_t renamed_references() const { return renamedReferences; } -std::shared_ptr rename(std::shared_ptr tree, rust::Str prefix, - rust::Str suffix, const rust::Vec& excludes); + 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); @@ -58,5 +80,7 @@ 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 index 9fd1ebc37..9e0224bce 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -18,6 +18,14 @@ pub enum SlangError { 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] @@ -40,6 +48,7 @@ mod ffi { /// Opaque type for the Slang syntax tree. #[namespace = "slang::syntax"] type SyntaxTree; + type SyntaxTreeRewriter; fn new_slang_session() -> UniquePtr; @@ -56,12 +65,18 @@ mod ffi { fn tree_at(session: &SlangSession, index: usize) -> Result>; - fn rename( + fn new_syntax_tree_rewriter() -> UniquePtr; + fn reset_rename_map(self: Pin<&mut SyntaxTreeRewriter>); + fn register_declarations(self: Pin<&mut SyntaxTreeRewriter>, tree: SharedPtr); + fn set_prefix(self: Pin<&mut SyntaxTreeRewriter>, prefix: &str); + fn set_suffix(self: Pin<&mut SyntaxTreeRewriter>, suffix: &str); + fn set_excludes(self: Pin<&mut SyntaxTreeRewriter>, excludes: Vec); + fn rewrite_tree( + self: Pin<&mut SyntaxTreeRewriter>, tree: SharedPtr, - prefix: &str, - suffix: &str, - excludes: &Vec, ) -> SharedPtr; + fn renamed_declarations(rewriter: &SyntaxTreeRewriter) -> u64; + fn renamed_references(rewriter: &SyntaxTreeRewriter) -> u64; fn print_tree(tree: SharedPtr, options: SlangPrintOpts) -> String; @@ -80,6 +95,10 @@ pub struct SyntaxTree<'a> { _session: PhantomData<&'a SlangSession>, } +pub struct SyntaxTreeRewriter { + inner: UniquePtr, +} + impl<'a> Clone for SyntaxTree<'a> { fn clone(&self) -> Self { Self { @@ -90,23 +109,6 @@ impl<'a> Clone for SyntaxTree<'a> { } impl<'a> SyntaxTree<'a> { - /// Renames all names in the syntax tree with the given prefix and suffix. - pub fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &[String]) -> Self { - if prefix.is_none() && suffix.is_none() { - return self.clone(); - } - let excludes = excludes.to_vec(); - Self { - inner: ffi::rename( - self.inner.clone(), - prefix.unwrap_or(""), - suffix.unwrap_or(""), - &excludes, - ), - _session: PhantomData, - } - } - /// 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) @@ -221,6 +223,58 @@ impl Default for SlangSession { } } +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 build_rename_map(&mut self, trees: &[SyntaxTree<'_>]) { + self.inner.pin_mut().reset_rename_map(); + for tree in trees { + self.inner + .pin_mut() + .register_declarations(tree.inner.clone()); + } + } + + pub fn rewrite_tree<'a>(&mut self, tree: &SyntaxTree<'a>) -> SyntaxTree<'a> { + SyntaxTree { + inner: self.inner.pin_mut().rewrite_tree(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()); diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 50e210a48..34c58b929 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -19,7 +19,7 @@ use crate::sess::{Session, SessionIo}; use crate::src::{SourceFile, SourceGroup, SourceType}; use crate::target::TargetSet; -use bender_slang::{SlangPrintOpts, SlangSession}; +use bender_slang::{SlangPrintOpts, SlangSession, SyntaxTreeRewriter}; /// Pickle files #[derive(Args, Debug)] @@ -233,13 +233,15 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { 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); + rewriter.build_rename_map(&trees); + let mut first_item = true; for tree in trees { - let renamed_tree = tree.rename( - args.prefix.as_deref(), - args.suffix.as_deref(), - &args.exclude_rename, - ); + let renamed_tree = rewriter.rewrite_tree(&tree); if args.ast_json { // JSON Array Logic: Prepend comma if not the first item if !first_item { From 90cc648c0c01655e30092397a298b6cdcc5e60ee Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 26 Feb 2026 16:01:16 +0100 Subject: [PATCH 40/46] pickle: Fix and require macro expansions --- crates/bender-slang/cpp/slang_bridge.cpp | 25 +++++++++++++----------- crates/bender-slang/src/lib.rs | 2 ++ src/cmd/pickle.rs | 5 +++-- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 5807aa7c8..0ec85c4b5 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -122,8 +122,8 @@ class DeclarationCollector : public SyntaxVisitor { std::unordered_set& names; }; -// Rewriter that renames declarations and references only if their declaration exists -// in the precomputed renameMap. +// Rewriter that renames declarations and references only if their declaration +// exists in the precomputed renameMap. class MappedRewriter : public SyntaxRewriter { public: MappedRewriter(const std::unordered_map& renameMap, std::uint64_t& declRenamed, @@ -148,7 +148,7 @@ class MappedRewriter : public SyntaxRewriter { return; } - auto newNameToken = makeId(newName, node.header->name.trivia()); + auto newNameToken = node.header->name.withRawText(alloc, newName); ModuleHeaderSyntax* newHeader = deepClone(*node.header, alloc); newHeader->name = newNameToken; @@ -162,7 +162,7 @@ class MappedRewriter : public SyntaxRewriter { if (node.type.kind == parsing::TokenKind::Identifier) { auto newName = mapped_name(node.type.valueText()); if (!newName.empty()) { - auto newNameToken = makeId(newName, node.type.trivia()); + auto newNameToken = node.type.withRawText(alloc, newName); HierarchyInstantiationSyntax* newNode = deepClone(node, alloc); newNode->type = newNameToken; @@ -184,7 +184,7 @@ class MappedRewriter : public SyntaxRewriter { visitDefault(node); return; } - auto newNameToken = makeId(newName, node.package.trivia()); + auto newNameToken = node.package.withRawText(alloc, newName); PackageImportItemSyntax* newNode = deepClone(node, alloc); newNode->package = newNameToken; @@ -203,7 +203,7 @@ class MappedRewriter : public SyntaxRewriter { visitDefault(node); return; } - auto newNameToken = makeId(newName, node.name.trivia()); + auto newNameToken = node.name.withRawText(alloc, newName); VirtualInterfaceTypeSyntax* newNode = deepClone(node, alloc); newNode->name = newNameToken; @@ -221,7 +221,7 @@ class MappedRewriter : public SyntaxRewriter { if (name != "$unit" && name != "local" && name != "super" && name != "this") { auto newName = mapped_name(name); if (!newName.empty()) { - auto newNameToken = makeId(newName, leftNode.identifier.trivia()); + auto newNameToken = leftNode.identifier.withRawText(alloc, newName); IdentifierNameSyntax* newLeft = deepClone(leftNode, alloc); newLeft->identifier = newNameToken; @@ -298,7 +298,7 @@ rust::String print_tree(const shared_ptr tree, SlangPrintOpts option // Set up the printer with options SyntaxPrinter printer(tree->sourceManager()); - printer.setIncludeDirectives(true); + printer.setIncludeDirectives(options.include_directives); printer.setExpandIncludes(true); printer.setExpandMacros(options.expand_macros); printer.setSquashNewlines(options.squash_newlines); @@ -327,7 +327,8 @@ rust::String dump_tree_json(std::shared_ptr tree) { 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 + // 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(); @@ -336,14 +337,16 @@ rust::Vec reachable_tree_indices(const SlangSession& session, con } } - // Build a dependency graph where each tree points to the trees that declare symbols it references + // 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 + // 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); } diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index 9e0224bce..b06f4a785 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -34,6 +34,7 @@ mod ffi { #[derive(Clone, Copy)] struct SlangPrintOpts { expand_macros: bool, + include_directives: bool, include_comments: bool, squash_newlines: bool, } @@ -124,6 +125,7 @@ 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, }; diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 34c58b929..f631acc3a 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -60,11 +60,11 @@ pub struct PickleArgs { top: Vec, /// A prefix to add to all names (modules, packages, interfaces) - #[arg(long, help_heading = "Slang Options")] + #[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")] + #[arg(long, help_heading = "Slang Options", requires = "expand_macros")] suffix: Option, /// Names to exclude from renaming (modules, packages, interfaces) @@ -160,6 +160,7 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { let print_opts = SlangPrintOpts { expand_macros: args.expand_macros, + include_directives: !args.expand_macros, include_comments: !args.strip_comments, squash_newlines: args.squash_newlines, }; From 833115db04d388473e2d679e5dd821f1dc12a5dd Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 26 Feb 2026 17:22:35 +0100 Subject: [PATCH 41/46] bender-slang: Split up the library --- crates/bender-slang/build.rs | 10 +- crates/bender-slang/cpp/analysis.cpp | 77 +++++ crates/bender-slang/cpp/print.cpp | 39 +++ crates/bender-slang/cpp/rewriter.cpp | 218 ++++++++++++ crates/bender-slang/cpp/session.cpp | 108 ++++++ crates/bender-slang/cpp/slang_bridge.cpp | 406 ----------------------- 6 files changed, 450 insertions(+), 408 deletions(-) create mode 100644 crates/bender-slang/cpp/analysis.cpp create mode 100644 crates/bender-slang/cpp/print.cpp create mode 100644 crates/bender-slang/cpp/rewriter.cpp create mode 100644 crates/bender-slang/cpp/session.cpp delete mode 100644 crates/bender-slang/cpp/slang_bridge.cpp diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 7db0396f7..1cb9c4db4 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -85,7 +85,10 @@ fn main() { // Compile the C++ Bridge let mut bridge_build = cxx_build::bridge("src/lib.rs"); bridge_build - .file("cpp/slang_bridge.cpp") + .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") @@ -132,6 +135,9 @@ fn main() { bridge_build.compile("slang-bridge"); println!("cargo:rerun-if-changed=src/lib.rs"); - println!("cargo:rerun-if-changed=cpp/slang_bridge.cpp"); 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..4e4b6f7ba --- /dev/null +++ b/crates/bender-slang/cpp/rewriter.cpp @@ -0,0 +1,218 @@ +// 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; + +std::unique_ptr new_syntax_tree_rewriter() { return std::make_unique(); } + +// A syntax visitor that collects the names of all declared modules/interfaces/packages in a syntax tree. +class DeclarationCollector : public SyntaxVisitor { + public: + explicit DeclarationCollector(std::unordered_set& names) : names(names) {} + + void handle(const ModuleDeclarationSyntax& node) { + if (!node.header->name.isMissing()) { + names.insert(std::string(node.header->name.valueText())); + } + visitDefault(node); + } + + private: + std::unordered_set& names; +}; + +// Rewriter that renames declarations and references only if their declaration +// exists in the precomputed renameMap. +class MappedRewriter : public SyntaxRewriter { + public: + MappedRewriter(const std::unordered_map& renameMap, std::uint64_t& declRenamed, + std::uint64_t& refRenamed) + : renameMap(renameMap), declRenamed(declRenamed), refRenamed(refRenamed) {} + + // Returns the mapped name for the given name if it exists in the renameMap, + // or an empty string_view otherwise. + string_view mapped_name(string_view name) { + auto it = renameMap.find(std::string(name)); + if (it == renameMap.end()) { + return {}; + } + return string_view(it->second); + } + + // e.g.: "module top;" -> "module p_top_s;". + void handle(const ModuleDeclarationSyntax& node) { + if (node.header->name.isMissing()) + return; + + auto newName = mapped_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++; + visitDefault(node); + } + + // e.g.: "core u_core();" -> "p_core_s u_core();". + void handle(const HierarchyInstantiationSyntax& node) { + if (node.type.kind == TokenKind::Identifier) { + auto newName = mapped_name(node.type.valueText()); + if (!newName.empty()) { + auto newNameToken = node.type.withRawText(alloc, newName); + + HierarchyInstantiationSyntax* newNode = deepClone(node, alloc); + newNode->type = newNameToken; + + replace(node, *newNode, true); + refRenamed++; + } + } + + visitDefault(node); + } + + // 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++; + visitDefault(node); + } + + // 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++; + visitDefault(node); + } + + // e.g.: "common_pkg::state_t" -> "p_common_pkg_s::state_t". + void handle(const ScopedNameSyntax& node) { + if (node.left->kind == SyntaxKind::IdentifierName) { + auto& leftNode = node.left->as(); + auto name = leftNode.identifier.valueText(); + + if (name != "$unit" && name != "local" && name != "super" && name != "this") { + auto newName = mapped_name(name); + if (!newName.empty()) { + 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++; + } + } + } + + visitDefault(node); + } + + private: + const std::unordered_map& renameMap; + std::uint64_t& declRenamed; + std::uint64_t& refRenamed; +}; + +void SyntaxTreeRewriter::reset_rename_map() { + renameMap.clear(); + renamedDeclarations = 0; + renamedReferences = 0; +} + +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)); + } +} + +// Registers all declarations in the given syntax tree by adding entries to the renameMap. +void SyntaxTreeRewriter::register_declarations(std::shared_ptr tree) { + if (prefix.empty() && suffix.empty()) { + return; + } + + // Collect all declared symbol names in the tree. + std::unordered_set declaredNames; + DeclarationCollector collector(declaredNames); + collector.visit(tree->root()); + + // Populate the renameMap with new names for all collected declarations, + // except those in the excludes set. + for (const auto& name : declaredNames) { + if (excludes.count(name)) { + continue; + } + renameMap.insert_or_assign(name, prefix + name + suffix); + } +} + +// Rewrites the given syntax tree by renaming declarations and references according to the renameMap. +std::shared_ptr SyntaxTreeRewriter::rewrite_tree(std::shared_ptr tree) { + if (renameMap.empty()) { + return tree; + } + + std::uint64_t declRenamed = 0; + std::uint64_t refRenamed = 0; + MappedRewriter rewriter(renameMap, declRenamed, refRenamed); + auto transformed = rewriter.transform(tree); + renamedDeclarations += declRenamed; + 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.cpp b/crates/bender-slang/cpp/slang_bridge.cpp deleted file mode 100644 index 0ec85c4b5..000000000 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ /dev/null @@ -1,406 +0,0 @@ -// Copyright (c) 2025 ETH Zurich -// Tim Fischer - -#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" -#include "slang/text/Json.h" - -#include -#include -#include -#include - -using namespace slang; -using namespace slang::driver; -using namespace slang::syntax; -using namespace slang::parsing; - -using std::shared_ptr; -using std::string; -using std::string_view; -using std::vector; - -std::unique_ptr new_slang_session() { return std::make_unique(); } -std::unique_ptr new_syntax_tree_rewriter() { return std::make_unique(); } - -SlangContext::SlangContext() : diagEngine(sourceManager), diagClient(std::make_shared()) { - diagEngine.addClient(diagClient); -} - -// Set the include paths for the preprocessor -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()); - } - } -} - -// Sets the preprocessor defines -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()); - } -} - -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; -} - -void SlangSession::parse_group(const rust::Vec& files, const rust::Vec& includes, - const rust::Vec& defines) { - auto ctx = std::make_unique(); - ctx->set_includes(includes); - ctx->set_defines(defines); - - 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)); -} - -class DeclarationCollector : public SyntaxVisitor { - public: - explicit DeclarationCollector(std::unordered_set& names) : names(names) {} - - void handle(const ModuleDeclarationSyntax& node) { - if (!node.header->name.isMissing()) { - names.insert(std::string(node.header->name.valueText())); - } - visitDefault(node); - } - - private: - std::unordered_set& names; -}; - -// Rewriter that renames declarations and references only if their declaration -// exists in the precomputed renameMap. -class MappedRewriter : public SyntaxRewriter { - public: - MappedRewriter(const std::unordered_map& renameMap, std::uint64_t& declRenamed, - std::uint64_t& refRenamed) - : renameMap(renameMap), declRenamed(declRenamed), refRenamed(refRenamed) {} - - string_view mapped_name(string_view name) { - auto it = renameMap.find(std::string(name)); - if (it == renameMap.end()) { - return {}; - } - return string_view(it->second); - } - - void handle(const ModuleDeclarationSyntax& node) { - if (node.header->name.isMissing()) - return; - - auto newName = mapped_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++; - visitDefault(node); - } - - void handle(const HierarchyInstantiationSyntax& node) { - if (node.type.kind == parsing::TokenKind::Identifier) { - auto newName = mapped_name(node.type.valueText()); - if (!newName.empty()) { - auto newNameToken = node.type.withRawText(alloc, newName); - - HierarchyInstantiationSyntax* newNode = deepClone(node, alloc); - newNode->type = newNameToken; - - replace(node, *newNode, true); - refRenamed++; - } - } - - visitDefault(node); - } - - 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++; - visitDefault(node); - } - - 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++; - visitDefault(node); - } - - void handle(const ScopedNameSyntax& node) { - if (node.left->kind == SyntaxKind::IdentifierName) { - auto& leftNode = node.left->as(); - auto name = leftNode.identifier.valueText(); - - if (name != "$unit" && name != "local" && name != "super" && name != "this") { - auto newName = mapped_name(name); - if (!newName.empty()) { - 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++; - } - } - } - - visitDefault(node); - } - - private: - const std::unordered_map& renameMap; - std::uint64_t& declRenamed; - std::uint64_t& refRenamed; -}; - -void SyntaxTreeRewriter::reset_rename_map() { - renameMap.clear(); - renamedDeclarations = 0; - renamedReferences = 0; -} - -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)); - } -} - -void SyntaxTreeRewriter::register_declarations(std::shared_ptr tree) { - if (prefix.empty() && suffix.empty()) { - return; - } - - std::unordered_set declaredNames; - DeclarationCollector collector(declaredNames); - collector.visit(tree->root()); - - for (const auto& name : declaredNames) { - if (excludes.count(name)) { - continue; - } - renameMap.insert_or_assign(name, prefix + name + suffix); - } -} - -std::shared_ptr SyntaxTreeRewriter::rewrite_tree(std::shared_ptr tree) { - if (renameMap.empty()) { - return tree; - } - - std::uint64_t declRenamed = 0; - std::uint64_t refRenamed = 0; - MappedRewriter rewriter(renameMap, declRenamed, refRenamed); - auto transformed = rewriter.transform(tree); - renamedDeclarations += declRenamed; - renamedReferences += refRenamed; - return transformed; -} - -// Print the given syntax tree with specified options -rust::String print_tree(const shared_ptr tree, SlangPrintOpts options) { - - // Set up the printer with 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); - - // Print the tree root and return as rust::String - printer.print(tree->root()); - return rust::String(printer.str()); -} - -// Dumps the AST/CST to a JSON string -rust::String dump_tree_json(std::shared_ptr tree) { - JsonWriter writer; - writer.setPrettyPrint(true); - - // CSTSerializer is the class Slang uses to convert AST -> JSON - CSTSerializer serializer(writer); - - // Serialize the specific tree root - serializer.serialize(*tree); - - // Convert string_view to rust::String - return rust::String(std::string(writer.view())); -} - -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)); - } else { - 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); - } - - // Collect the indices of reachable trees and return as rust::Vec - rust::Vec result; - for (size_t i = 0; i < reachable.size(); ++i) { - if (reachable[i]) { - result.push_back(static_cast(i)); - } - } - return result; -} - -std::size_t tree_count(const SlangSession& session) { return session.trees().size(); } - -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]; -} - -std::uint64_t renamed_declarations(const SyntaxTreeRewriter& rewriter) { return rewriter.renamed_declarations(); } - -std::uint64_t renamed_references(const SyntaxTreeRewriter& rewriter) { return rewriter.renamed_references(); } From 570c13bf75bb9e7194e81a7466ebf621ee9baf2e Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 27 Feb 2026 15:29:01 +0100 Subject: [PATCH 42/46] pickle: Use `unreachable` for groups Co-authored-by: Michael Rogenmoser --- src/cmd/pickle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index f631acc3a..e8269c1ef 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -221,7 +221,7 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { } // Groups should not exist at this point, // as we have already flattened the sources. - _ => None, + _ => unreachable!(), }) .collect(); From 1ae9c4a2b678abbb725d821d3bfc5bab8c896e9b Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 1 Mar 2026 19:04:01 +0100 Subject: [PATCH 43/46] bender-slang(rewriter): Rename block names --- crates/bender-slang/cpp/rewriter.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/bender-slang/cpp/rewriter.cpp b/crates/bender-slang/cpp/rewriter.cpp index 4e4b6f7ba..29ab943ea 100644 --- a/crates/bender-slang/cpp/rewriter.cpp +++ b/crates/bender-slang/cpp/rewriter.cpp @@ -49,7 +49,7 @@ class MappedRewriter : public SyntaxRewriter { return string_view(it->second); } - // e.g.: "module top;" -> "module p_top_s;". + // 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()) return; @@ -67,6 +67,15 @@ class MappedRewriter : public SyntaxRewriter { 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); } From a25fc40f798963f6d51c67fc775dc933040c5128 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 3 Mar 2026 09:22:25 +0100 Subject: [PATCH 44/46] bender-slang: Two-pass renaming scheme --- crates/bender-slang/cpp/rewriter.cpp | 224 +++++++++++++++---------- crates/bender-slang/cpp/slang_bridge.h | 5 +- crates/bender-slang/src/lib.rs | 25 +-- src/cmd/pickle.rs | 48 +++--- 4 files changed, 181 insertions(+), 121 deletions(-) diff --git a/crates/bender-slang/cpp/rewriter.cpp b/crates/bender-slang/cpp/rewriter.cpp index 29ab943ea..042ea2111 100644 --- a/crates/bender-slang/cpp/rewriter.cpp +++ b/crates/bender-slang/cpp/rewriter.cpp @@ -13,48 +13,44 @@ 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(); } -// A syntax visitor that collects the names of all declared modules/interfaces/packages in a syntax tree. -class DeclarationCollector : public SyntaxVisitor { +// Pass 1: collects declarations and renames declaration sites. +class DeclarationRewriter : public SyntaxRewriter { public: - explicit DeclarationCollector(std::unordered_set& names) : names(names) {} + 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) {} - void handle(const ModuleDeclarationSyntax& node) { - if (!node.header->name.isMissing()) { - names.insert(std::string(node.header->name.valueText())); + string_view declaration_name(string_view name) { + if (prefix.empty() && suffix.empty()) { + return {}; } - visitDefault(node); - } - - private: - std::unordered_set& names; -}; - -// Rewriter that renames declarations and references only if their declaration -// exists in the precomputed renameMap. -class MappedRewriter : public SyntaxRewriter { - public: - MappedRewriter(const std::unordered_map& renameMap, std::uint64_t& declRenamed, - std::uint64_t& refRenamed) - : renameMap(renameMap), declRenamed(declRenamed), refRenamed(refRenamed) {} - - // Returns the mapped name for the given name if it exists in the renameMap, - // or an empty string_view otherwise. - string_view mapped_name(string_view name) { - auto it = renameMap.find(std::string(name)); - if (it == renameMap.end()) { + 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()) + if (node.header->name.isMissing()) { + visitDefault(node); return; + } - auto newName = mapped_name(node.header->name.valueText()); + auto newName = declaration_name(node.header->name.valueText()); if (newName.empty()) { visitDefault(node); return; @@ -79,28 +75,77 @@ class MappedRewriter : public SyntaxRewriter { 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) { - auto newName = mapped_name(node.type.valueText()); - if (!newName.empty()) { - auto newNameToken = node.type.withRawText(alloc, newName); - - HierarchyInstantiationSyntax* newNode = deepClone(node, alloc); - newNode->type = newNameToken; + if (node.type.kind != TokenKind::Identifier) { + visitDefault(node); + return; + } - replace(node, *newNode, true); - refRenamed++; - } + auto newName = mapped_name(node.type.valueText()); + if (newName.empty()) { + visitDefault(node); + return; } - visitDefault(node); + 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()) + if (node.package.isMissing()) { return; + } auto newName = mapped_name(node.package.valueText()); if (newName.empty()) { @@ -114,13 +159,13 @@ class MappedRewriter : public SyntaxRewriter { replace(node, *newNode); refRenamed++; - visitDefault(node); } // e.g.: "virtual bus_intf v_if;" -> "virtual p_bus_intf_s v_if;". void handle(const VirtualInterfaceTypeSyntax& node) { - if (node.name.isMissing()) + if (node.name.isMissing()) { return; + } auto newName = mapped_name(node.name.valueText()); if (newName.empty()) { @@ -134,47 +179,60 @@ class MappedRewriter : public SyntaxRewriter { replace(node, *newNode); refRenamed++; - visitDefault(node); } // e.g.: "common_pkg::state_t" -> "p_common_pkg_s::state_t". void handle(const ScopedNameSyntax& node) { - if (node.left->kind == SyntaxKind::IdentifierName) { - auto& leftNode = node.left->as(); - auto name = leftNode.identifier.valueText(); + auto newName = mapped_scoped_left_name(node); + if (newName.empty()) { + visitDefault(node); + return; + } - if (name != "$unit" && name != "local" && name != "super" && name != "this") { - auto newName = mapped_name(name); - if (!newName.empty()) { - auto newNameToken = leftNode.identifier.withRawText(alloc, newName); + auto& leftNode = node.left->as(); + auto newNameToken = leftNode.identifier.withRawText(alloc, newName); - IdentifierNameSyntax* newLeft = deepClone(leftNode, alloc); - newLeft->identifier = newNameToken; + IdentifierNameSyntax* newLeft = deepClone(leftNode, alloc); + newLeft->identifier = newNameToken; - ScopedNameSyntax* newNode = deepClone(node, alloc); - newNode->left = newLeft; + ScopedNameSyntax* newNode = deepClone(node, alloc); + newNode->left = newLeft; - replace(node, *newNode); - refRenamed++; - } - } + 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; } - visitDefault(node); + 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); + } + } } - private: const std::unordered_map& renameMap; - std::uint64_t& declRenamed; std::uint64_t& refRenamed; }; -void SyntaxTreeRewriter::reset_rename_map() { - renameMap.clear(); - renamedDeclarations = 0; - renamedReferences = 0; -} - 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()); } @@ -186,38 +244,28 @@ void SyntaxTreeRewriter::set_excludes(const rust::Vec values) { } } -// Registers all declarations in the given syntax tree by adding entries to the renameMap. -void SyntaxTreeRewriter::register_declarations(std::shared_ptr tree) { +// 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; + return tree; } - // Collect all declared symbol names in the tree. - std::unordered_set declaredNames; - DeclarationCollector collector(declaredNames); - collector.visit(tree->root()); - - // Populate the renameMap with new names for all collected declarations, - // except those in the excludes set. - for (const auto& name : declaredNames) { - if (excludes.count(name)) { - continue; - } - renameMap.insert_or_assign(name, prefix + name + suffix); - } + std::uint64_t declRenamed = 0; + DeclarationRewriter rewriter(renameMap, prefix, suffix, excludes, declRenamed); + auto transformed = rewriter.transform(tree); + renamedDeclarations += declRenamed; + return transformed; } -// Rewrites the given syntax tree by renaming declarations and references according to the renameMap. -std::shared_ptr SyntaxTreeRewriter::rewrite_tree(std::shared_ptr tree) { +// 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 declRenamed = 0; std::uint64_t refRenamed = 0; - MappedRewriter rewriter(renameMap, declRenamed, refRenamed); + ReferenceRewriter rewriter(renameMap, refRenamed); auto transformed = rewriter.transform(tree); - renamedDeclarations += declRenamed; renamedReferences += refRenamed; return transformed; } diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index b1ca94279..240eff336 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -50,13 +50,12 @@ class SlangSession { class SyntaxTreeRewriter { public: - void reset_rename_map(); - void register_declarations(std::shared_ptr tree); void set_prefix(rust::Str prefix); void set_suffix(rust::Str suffix); void set_excludes(const rust::Vec excludes); - std::shared_ptr rewrite_tree(std::shared_ptr tree); + 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; } diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index b06f4a785..6930abb50 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -67,12 +67,14 @@ mod ffi { fn tree_at(session: &SlangSession, index: usize) -> Result>; fn new_syntax_tree_rewriter() -> UniquePtr; - fn reset_rename_map(self: Pin<&mut SyntaxTreeRewriter>); - fn register_declarations(self: Pin<&mut SyntaxTreeRewriter>, tree: SharedPtr); - fn set_prefix(self: Pin<&mut SyntaxTreeRewriter>, prefix: &str); fn set_suffix(self: Pin<&mut SyntaxTreeRewriter>, suffix: &str); fn set_excludes(self: Pin<&mut SyntaxTreeRewriter>, excludes: Vec); - fn rewrite_tree( + 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; @@ -246,18 +248,19 @@ impl SyntaxTreeRewriter { self.inner.pin_mut().set_excludes(excludes); } - pub fn build_rename_map(&mut self, trees: &[SyntaxTree<'_>]) { - self.inner.pin_mut().reset_rename_map(); - for tree in trees { - self.inner + pub fn rewrite_declarations<'a>(&mut self, tree: &SyntaxTree<'a>) -> SyntaxTree<'a> { + SyntaxTree { + inner: self + .inner .pin_mut() - .register_declarations(tree.inner.clone()); + .rewrite_declarations(tree.inner.clone()), + _session: PhantomData, } } - pub fn rewrite_tree<'a>(&mut self, tree: &SyntaxTree<'a>) -> SyntaxTree<'a> { + pub fn rewrite_references<'a>(&mut self, tree: &SyntaxTree<'a>) -> SyntaxTree<'a> { SyntaxTree { - inner: self.inner.pin_mut().rewrite_tree(tree.inner.clone()), + inner: self.inner.pin_mut().rewrite_references(tree.inner.clone()), _session: PhantomData, } } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index e8269c1ef..bf01e4ab1 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -165,21 +165,6 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { squash_newlines: args.squash_newlines, }; - // 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 session = SlangSession::new(); for src_group in srcs { // Collect include directories from the source group and command line arguments. @@ -238,20 +223,45 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { rewriter.set_prefix(args.prefix.unwrap_or_default()); rewriter.set_suffix(args.suffix.unwrap_or_default()); rewriter.set_excludes(args.exclude_rename); - rewriter.build_rename_map(&trees); + + // 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 { - let renamed_tree = rewriter.rewrite_tree(&tree); if args.ast_json { // JSON Array Logic: Prepend comma if not the first item if !first_item { write!(writer, ",")?; } - write!(writer, "{:?}", renamed_tree)?; + write!(writer, "{:?}", tree)?; first_item = false; } else { - write!(writer, "{}", renamed_tree.display(print_opts))?; + write!(writer, "{}", tree.display(print_opts))?; } } From 49135156b3166840cc0bac26641f270e5a2dd3de Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 3 Mar 2026 14:17:31 +0100 Subject: [PATCH 45/46] bender-slang(build): Link stdlibc++ dynamically by default --- .cargo/config.toml.iis | 1 + crates/bender-slang/build.rs | 30 ------------------------------ 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/.cargo/config.toml.iis b/.cargo/config.toml.iis index 168231c97..4b427418a 100644 --- a/.cargo/config.toml.iis +++ b/.cargo/config.toml.iis @@ -1,5 +1,6 @@ [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" diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 1cb9c4db4..4244eb209 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -94,36 +94,6 @@ fn main() { .include("vendor/slang/external") .include(dst.join("include")); - // Linux: we try static linking of libstdc++ to avoid issues on older distros. - if target_os == "linux" { - // Determine the C++ compiler to use. Respect the CXX environment variable if set. - 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"]) - .output() - .expect("Failed to run g++"); - - if output.status.success() { - let path_str = std::str::from_utf8(&output.stdout).unwrap().trim(); - let path = std::path::Path::new(path_str); - - if path.is_absolute() && path.exists() { - if let Some(parent) = path.parent() { - // Add the directory containing libstdc++.a to the link search path - println!("cargo:rustc-link-search=native={}", parent.display()); - } - - bridge_build.cpp_set_stdlib(None); - println!("cargo:rustc-link-lib=static=stdc++"); - } else { - println!( - "cargo:warning=Could not find static libstdc++.a, falling back to dynamic linking" - ); - } - } - } - // Apply common defines and flags to the bridge build as well for (def, value) in common_cxx_defines.iter() { bridge_build.define(def, *value); From f68b52a8f2cee6b721a4046e348879954fca9915 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 3 Mar 2026 19:29:35 +0100 Subject: [PATCH 46/46] pickle: Align `include_dirs` --- src/cmd/pickle.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index bf01e4ab1..71c1298aa 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -17,7 +17,7 @@ use crate::diagnostic::Warnings; use crate::error::*; use crate::sess::{Session, SessionIo}; use crate::src::{SourceFile, SourceGroup, SourceType}; -use crate::target::TargetSet; +use crate::target::{TargetSet, TargetSpec}; use bender_slang::{SlangPrintOpts, SlangSession, SyntaxTreeRewriter}; @@ -130,8 +130,8 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { let include_dirs = args .include_dir .iter() - .map(|d| sess.intern_path(Path::new(d))) - .collect::>(); + .map(|d| (TargetSpec::Wildcard, sess.intern_path(Path::new(d)))) + .collect(); let defines = args .define .iter() @@ -172,7 +172,7 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { .include_dirs .iter() .chain(src_group.export_incdirs.values().flatten()) - .map(|path| path.to_string_lossy().into_owned()) + .map(|(_, path)| path.to_string_lossy().into_owned()) .chain(args.include_dir.iter().cloned()) .collect::>() .into_iter()