From bb81295c656e357470407db0e3820909ecc1d4a1 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 28 Jan 2026 21:50:16 +0100 Subject: [PATCH 01/33] 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 1459bcd84..b7c34edb2 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.10.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.31" @@ -801,6 +898,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.11.0" @@ -1240,6 +1346,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" @@ -1467,6 +1579,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 91eb08861..87e507060 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 16a6080d85ac40f1dba8237f3d12283d90e00e8e Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 00:04:40 +0100 Subject: [PATCH 02/33] 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 b7c34edb2..b20a20cef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,7 @@ version = "0.30.0" dependencies = [ "assert_cmd", "async-recursion", + "bender-slang", "blake2", "clap", "clap_complete", diff --git a/Cargo.toml b/Cargo.toml index 87e507060..d435b7a37 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 59782f0624138b24d9a611ba965d4818e959d291 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 10:13:28 +0100 Subject: [PATCH 03/33] 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 ba6b4ea434cfaf4def3388572a94353c029d4680 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 10:50:08 +0100 Subject: [PATCH 04/33] 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 87c41ade90486e45e2ae7aa594e6726801de93e4 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 12:29:04 +0100 Subject: [PATCH 05/33] 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 defdead898daa4a72a782554ae626e9bac250a9b Mon Sep 17 00:00:00 2001 From: Michael Rogenmoser Date: Thu, 29 Jan 2026 16:48:01 +0100 Subject: [PATCH 06/33] 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 d435b7a37..eeca629ff 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 a34070fd82eea8d5e263453db598521ceceb6d19 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 23:45:08 +0100 Subject: [PATCH 07/33] 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 a842cf34af4334a9e1138b5c05c283624b01a3ef Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 23:46:41 +0100 Subject: [PATCH 08/33] 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 500bf29227bc294407aaeedf1778d851a68f6e9b Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 31 Jan 2026 22:14:11 +0100 Subject: [PATCH 09/33] 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 2cf2a7c07417a551f56c73cf682ea266d2c9159f Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 31 Jan 2026 22:53:45 +0100 Subject: [PATCH 10/33] 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 863c2f84b537141793fb9f37b65fc706370717c1 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 31 Jan 2026 23:41:37 +0100 Subject: [PATCH 11/33] 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 58e209fecad21d86368a97ad791c6e2b4411e3d6 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 31 Jan 2026 22:54:07 +0100 Subject: [PATCH 12/33] 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 efce5e63a525f36476f2522e31b7f70884678859 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 1 Feb 2026 00:46:32 +0100 Subject: [PATCH 13/33] 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 5c81446f725474be832e6f84af6d19a628598568 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 1 Feb 2026 13:52:00 +0100 Subject: [PATCH 14/33] 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 170351c8b99b91f98cc7b756d55fc12ae182f34a Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 2 Feb 2026 20:00:33 +0100 Subject: [PATCH 15/33] 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 1e02ddb4cfea6d802b53558bbb5f14a4b5690d9e Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 4 Feb 2026 12:40:26 +0100 Subject: [PATCH 16/33] 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 28167be85494abe422b55297f7fdc26f228c49bf Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 4 Feb 2026 14:25:26 +0100 Subject: [PATCH 17/33] 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 e89cc89eaaae06992b5f5688c45ee8120cb7a25f Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 5 Feb 2026 23:51:45 +0100 Subject: [PATCH 18/33] 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 eac7efe9b1eda4e5dc1396d1b74e734d25adb5ac Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 11 Feb 2026 22:55:48 +0100 Subject: [PATCH 19/33] 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 4cdf2a0a6..cc0e46f2f 100644 --- a/src/src.rs +++ b/src/src.rs @@ -342,8 +342,6 @@ impl<'ctx> SourceGroup<'ctx> { pub enum SourceType { /// A Verilog file. Verilog, - // /// A SystemVerilog file. - // SystemVerilog, /// A VHDL file. Vhdl, /// Unknown file type From 2443ae4d9efecab15e56109a81f8714402d8f6fd Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 11:32:25 +0100 Subject: [PATCH 20/33] pickle: Filter non-verilog files and emit warnings --- src/cmd/pickle.rs | 17 +++++++++++------ src/cmd/script.rs | 24 ++++++------------------ src/diagnostic.rs | 4 ++++ src/sess.rs | 17 +++++++++++++---- src/src.rs | 13 +++---------- 5 files changed, 37 insertions(+), 38 deletions(-) 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 847563059..566dcedb4 100644 --- a/src/cmd/script.rs +++ b/src/cmd/script.rs @@ -494,15 +494,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| { @@ -540,21 +532,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()); } _ => {} @@ -601,5 +589,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 6210d5212..a93736dc9 100644 --- a/src/diagnostic.rs +++ b/src/diagnostic.rs @@ -361,6 +361,10 @@ pub enum Warnings { )] LfsDisabled(String), + #[error("File {} is not a Verilog file and will be ignored in the pickle output.", fmt_path!(.0.display()))] + #[diagnostic(code(W28))] + PickleNonVerilogFile(PathBuf), + #[error("File not added, ignoring: {cause}")] #[diagnostic(code(W30))] IgnoredPath { cause: String }, diff --git a/src/sess.rs b/src/sess.rs index e2c1bd747..e8abfb98c 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -414,17 +414,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 cc0e46f2f..c0a62c7ec 100644 --- a/src/src.rs +++ b/src/src.rs @@ -338,14 +338,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. @@ -354,7 +353,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>), } @@ -381,12 +380,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 22a251e398779998e355819f6ddf68947e723f05 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 15:40:05 +0100 Subject: [PATCH 21/33] 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 d199efe6deeeb9be93713208fadfa511343ef82b Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 17:46:55 +0100 Subject: [PATCH 22/33] 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 6f0e1dfe4b6eb23eaa8a19d7d3ff6f6bd69ab29c Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 18:42:29 +0100 Subject: [PATCH 23/33] 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 17dde79841cff960cbe3bfdb64876f5b0c11fc56 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 18:57:33 +0100 Subject: [PATCH 24/33] 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 6497117fe288cfb03c4a97928c72f82650edc4a3 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 14:09:51 +0100 Subject: [PATCH 25/33] 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 77773dbeb320cdf163e77d65032059781f823f0b Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 15:00:09 +0100 Subject: [PATCH 26/33] 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 69a3d40810746786578b51d427c028a9f601b9ec Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 18:10:37 +0100 Subject: [PATCH 27/33] 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 a9a557fb93c724a1003bffa43fcfc1f3076416c2 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 18:49:48 +0100 Subject: [PATCH 28/33] bender-slang: Use typed errors with `thiserror` --- Cargo.lock | 5 +- 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, 77 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b20a20cef..68d036af7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,7 @@ dependencies = [ "cmake", "cxx", "cxx-build", + "thiserror", ] [[package]] @@ -398,7 +399,7 @@ dependencies = [ "cxxbridge-cmd", "cxxbridge-flags", "cxxbridge-macro", - "foldhash", + "foldhash 0.2.0", "link-cplusplus", ] @@ -716,7 +717,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] 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 b67ad615d..7c7aa745c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -147,3 +147,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 0ec606377158f8db3ed9543e507ee0b5cfef9c83 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 22:18:07 +0100 Subject: [PATCH 29/33] 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 331e09b54d24fc4e79595c322a89aafa80669e88 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 00:17:00 +0100 Subject: [PATCH 30/33] 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 df878c2d6..8a73c4436 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.30.0 - 2026-02-12 ### Added diff --git a/README.md b/README.md index 052d831d1..f4ae5aecc 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] @@ -518,6 +524,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 9553d41f6df0808400cf19fb4fda18ac27be18ae Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 00:32:16 +0100 Subject: [PATCH 31/33] 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 6477e7810ec9cdb3186f37889ff080962a90df2a Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 00:37:23 +0100 Subject: [PATCH 32/33] 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 038b5fa9fd5f03b86e138e971e6e7435c3dd8b94 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 15:11:07 +0100 Subject: [PATCH 33/33] 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