From 6d2e62047784da899911871952220b50e6bddd46 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 28 Jan 2026 21:50:16 +0100 Subject: [PATCH 01/46] bender-slang: Initial `slang` bindings --- .gitmodules | 3 + Cargo.lock | 121 +++++++++++++++++++++++ Cargo.toml | 3 + crates/bender-slang/Cargo.toml | 11 +++ crates/bender-slang/build.rs | 51 ++++++++++ crates/bender-slang/cpp/slang_bridge.cpp | 80 +++++++++++++++ crates/bender-slang/cpp/slang_bridge.h | 10 ++ crates/bender-slang/src/lib.rs | 118 ++++++++++++++++++++++ crates/bender-slang/vendor/slang | 1 + 9 files changed, 398 insertions(+) create mode 100644 .gitmodules create mode 100644 crates/bender-slang/Cargo.toml create mode 100644 crates/bender-slang/build.rs create mode 100644 crates/bender-slang/cpp/slang_bridge.cpp create mode 100644 crates/bender-slang/cpp/slang_bridge.h create mode 100644 crates/bender-slang/src/lib.rs create mode 160000 crates/bender-slang/vendor/slang diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..cccf606d2 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "crates/bender-slang/vendor/slang"] + path = crates/bender-slang/vendor/slang + url = https://github.com/MikePopoloski/slang.git diff --git a/Cargo.lock b/Cargo.lock index 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 a6d3f63cb00b3164f51ad85c31ff5e46d8aed898 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 00:04:40 +0100 Subject: [PATCH 02/46] pickle: Add initial command --- Cargo.lock | 1 + Cargo.toml | 2 ++ src/cli.rs | 2 ++ src/cmd.rs | 1 + src/cmd/pickle.rs | 79 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 src/cmd/pickle.rs diff --git a/Cargo.lock b/Cargo.lock index 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 fe68c78dc7305137e2adf2bc960f557733665bd7 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 10:13:28 +0100 Subject: [PATCH 03/46] ci: Clone slang submodule and bump checkout action --- .github/workflows/ci.yml | 18 +++++++++++++----- .github/workflows/cli_regression.yml | 12 +++++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84d2fa64e..90427d67c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,9 @@ jobs: - 1.87.0 # minimum supported version continue-on-error: ${{ matrix.rust == 'nightly' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust}} @@ -38,7 +40,9 @@ jobs: test-windows: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable with: toolchain: stable @@ -53,7 +57,9 @@ jobs: test-macos: runs-on: macos-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable with: toolchain: stable @@ -69,7 +75,9 @@ jobs: name: Clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable with: toolchain: stable @@ -80,7 +88,7 @@ jobs: name: Unused Dependencies runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: toolchain: stable diff --git a/.github/workflows/cli_regression.yml b/.github/workflows/cli_regression.yml index a03e8f02e..76d8cdc88 100644 --- a/.github/workflows/cli_regression.yml +++ b/.github/workflows/cli_regression.yml @@ -8,7 +8,9 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable with: toolchain: stable @@ -20,7 +22,9 @@ jobs: test-windows: runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable with: toolchain: stable @@ -32,7 +36,9 @@ jobs: test-macos: runs-on: macos-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 + with: + submodules: recursive - uses: dtolnay/rust-toolchain@stable with: toolchain: stable From baf8a899a71d52b519988a4792c35fd906d8fb2f Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 10:50:08 +0100 Subject: [PATCH 04/46] bender-slang(build): Fix Linux builds --- crates/bender-slang/build.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 9252567e5..10ea4e633 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -5,6 +5,8 @@ fn main() { .define("SLANG_INCLUDE_TOOLS", "OFF") .define("SLANG_INCLUDE_PYSLANG", "OFF") .define("BUILD_SHARED_LIBS", "OFF") + // Forces installation into 'lib' instead of 'lib64' on some systems. + .define("CMAKE_INSTALL_LIBDIR", "lib") // TODO(fischeti): Check whether mimalloc can/should be enabled again. .define("SLANG_USE_MIMALLOC", "OFF") // TODO(fischeti): `fmt` currently causes issues on my machine since there is a system-wide installation. @@ -15,7 +17,9 @@ fn main() { // Configure Linker to find Slang static library println!("cargo:rustc-link-search=native={}/lib", dst.display()); - println!("cargo:rustc-link-lib=static=svLang"); + // Note: Linux is case-sensitive, so we use lowercase here. + // On macOS, the library is called `svLang`, but the linker is case-insensitive there. + println!("cargo:rustc-link-lib=static=svlang"); println!("cargo:rustc-link-lib=static=fmtd"); // Compile the C++ Bridge From 9b13ca7f28fc62c7d024f426008b364a770e78ac Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 12:29:04 +0100 Subject: [PATCH 05/46] bender-slang(build): Provide config template for IIS env --- .cargo/config.toml.iis | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .cargo/config.toml.iis diff --git a/.cargo/config.toml.iis b/.cargo/config.toml.iis new file mode 100644 index 000000000..168231c97 --- /dev/null +++ b/.cargo/config.toml.iis @@ -0,0 +1,6 @@ +[target.x86_64-unknown-linux-gnu] +linker = "/usr/pack/gcc-14.2.0-af/bin/gcc" + +[env] +CC = "/usr/pack/gcc-14.2.0-af/bin/gcc" +CXX = "/usr/pack/gcc-14.2.0-af/bin/g++" From c784c3acd5b5c464645ed407cfed33fbc4350d07 Mon Sep 17 00:00:00 2001 From: Michael Rogenmoser Date: Thu, 29 Jan 2026 16:48:01 +0100 Subject: [PATCH 06/46] Add slang feature to disable slang build --- .github/workflows/ci.yml | 10 +++++----- .github/workflows/cli_regression.yml | 4 ++-- .github/workflows/release.yaml | 10 +++++----- Cargo.toml | 5 ++++- src/cli.rs | 2 ++ src/cmd.rs | 1 + 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90427d67c..4b6eb2ecb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,9 +28,9 @@ jobs: toolchain: ${{ matrix.rust}} components: rustfmt - name: Build - run: cargo build + run: cargo build --all-features - name: Cargo Test - run: cargo test --all + run: cargo test --workspace --all-features - name: Format (fix with `cargo fmt`) run: cargo fmt -- --check - name: Run unit-tests @@ -49,7 +49,7 @@ jobs: - name: Build run: cargo build - name: Cargo Test - run: cargo test --all + run: cargo test - name: Run unit-tests run: tests/run_all.sh shell: bash @@ -64,9 +64,9 @@ jobs: with: toolchain: stable - name: Build - run: cargo build + run: cargo build --all-features - name: Cargo Test - run: cargo test --all + run: cargo test --workspace --all-features - name: Run unit-tests run: tests/run_all.sh shell: bash diff --git a/.github/workflows/cli_regression.yml b/.github/workflows/cli_regression.yml index 76d8cdc88..e9c93dee4 100644 --- a/.github/workflows/cli_regression.yml +++ b/.github/workflows/cli_regression.yml @@ -15,7 +15,7 @@ jobs: with: toolchain: stable - name: Run CLI Regression - run: cargo test --test cli_regression -- --ignored + run: cargo test --all-features --test cli_regression -- --ignored env: BENDER_TEST_GOLDEN_BRANCH: ${{ github.base_ref }} @@ -43,6 +43,6 @@ jobs: with: toolchain: stable - name: Run CLI Regression - run: cargo test --test cli_regression -- --ignored + run: cargo test --all-features --test cli_regression -- --ignored env: BENDER_TEST_GOLDEN_BRANCH: ${{ github.base_ref }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f7d81632e..2df026bd0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -76,7 +76,7 @@ jobs: -v "$GITHUB_WORKSPACE/target/$platform/$tgtname:/source/target" \ --platform $full_platform \ $tgtname-$platform \ - cargo build --release; + cargo build --release --all-features; shell: bash - name: OS Create Package run: | @@ -121,7 +121,7 @@ jobs: -v "$GITHUB_WORKSPACE/target/$platform/$tgtname:/source/target" \ --platform $full_platform \ $tgtname-$platform \ - cargo build --release; + cargo build --release --all-features; shell: bash - name: OS Create Package run: | @@ -170,7 +170,7 @@ jobs: -v "$GITHUB_WORKSPACE/target/amd64:/source/target" \ --platform linux/amd64 \ manylinux-amd64 \ - cargo build --release; + cargo build --release --all-features; - name: GNU Create Package run: .github/scripts/package.sh amd64 shell: bash @@ -215,7 +215,7 @@ jobs: -v "$GITHUB_WORKSPACE/target/arm64:/source/target" \ --platform linux/arm64 \ manylinux-arm64 \ - cargo build --release; + cargo build --release --all-features; - name: GNU Create Package run: .github/scripts/package.sh arm64 shell: bash @@ -240,7 +240,7 @@ jobs: rustup target add aarch64-apple-darwin cargo install universal2 - name: MacOS Build - run: cargo-universal2 --release + run: cargo-universal2 --release --all-features - name: Get Artifact Name run: | if [[ "$GITHUB_REF" =~ ^refs/tags/v.*$ ]]; then \ diff --git a/Cargo.toml b/Cargo.toml index 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 a0d785c7b0b6638d96888f34223ff5fde0a88385 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 23:45:08 +0100 Subject: [PATCH 07/46] bender-slang(build): Link libc++ statically on linux and windows --- crates/bender-slang/build.rs | 39 +++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 10ea4e633..983d9fcba 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -13,6 +13,7 @@ fn main() { .define("CMAKE_DISABLE_FIND_PACKAGE_fmt", "ON") // TODO(fischeti): Investigate how boost should be handled properly. .cxxflag("-DSLANG_BOOST_SINGLE_HEADER=1") + .static_crt(true) .build(); // Configure Linker to find Slang static library @@ -27,6 +28,7 @@ fn main() { bridge_build .file("cpp/slang_bridge.cpp") .flag_if_supported("-std=c++20") + .flag_if_supported("/std:c++20") // Static Linking Definition // Tells Slang headers not to look for DLL import/export symbols. .define("SLANG_STATIC_DEFINE", "1") @@ -35,11 +37,8 @@ fn main() { // TODO(fischeti): Investigate how boost should be handled properly. .define("SLANG_BOOST_SINGLE_HEADER", "1") // Include Paths - // 1. Slang source headers .include("vendor/slang/include") - // 2. Slang external headers (where boost_unordered.hpp lives) .include("vendor/slang/external") - // 3. CMake build output (where slang_export.h and fmt headers live) .include(dst.join("include")); // TODO(fischeti): Check whether debug definitions are necessary. @@ -47,6 +46,40 @@ fn main() { bridge_build.define("SLANG_DEBUG", "1"); } + // Linux: we try static linking of libstdc++ to avoid issues on older distros. + if std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "linux" { + // Determine the C++ compiler to use. Respect the CXX environment variable if set. + let compiler = std::env::var("CXX").unwrap_or_else(|_| "g++".to_string()); + // We search for the static libstdc++ file using g++ + let output = std::process::Command::new(&compiler) + .args(&["-print-file-name=libstdc++.a"]) + .output() + .expect("Failed to run g++"); + + if output.status.success() { + let path_str = std::str::from_utf8(&output.stdout).unwrap().trim(); + let path = std::path::Path::new(path_str); + + if path.is_absolute() && path.exists() { + if let Some(parent) = path.parent() { + // Add the directory containing libstdc++.a to the link search path + println!("cargo:rustc-link-search=native={}", parent.display()); + } + + bridge_build.cpp_set_stdlib(None); + println!("cargo:rustc-link-lib=static=stdc++"); + } else { + println!( + "cargo:warning=Could not find static libstdc++.a, falling back to dynamic linking" + ); + } + } + // Windows / MSVC: we force static linking of the CRT to avoid missing DLL issues + } else if std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "msvc" { + bridge_build.static_crt(true); + } + // macOS: we leave the default dynamic linking of libc++ as is. + bridge_build.compile("slang-bridge"); println!("cargo:rerun-if-changed=src/lib.rs"); From 32fae34f3cb20b4ebb578c2d54e7e4441d675b55 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 29 Jan 2026 23:46:41 +0100 Subject: [PATCH 08/46] ci: Enable `slang` for Windows again --- .github/workflows/ci.yml | 4 ++-- .github/workflows/cli_regression.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b6eb2ecb..4420a4f4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,9 +47,9 @@ jobs: with: toolchain: stable - name: Build - run: cargo build + run: cargo build --all-features - name: Cargo Test - run: cargo test + run: cargo test --workspace --all-features - name: Run unit-tests run: tests/run_all.sh shell: bash diff --git a/.github/workflows/cli_regression.yml b/.github/workflows/cli_regression.yml index e9c93dee4..bfdcf9bd7 100644 --- a/.github/workflows/cli_regression.yml +++ b/.github/workflows/cli_regression.yml @@ -29,7 +29,7 @@ jobs: with: toolchain: stable - name: Run CLI Regression - run: cargo test --test cli_regression -- --ignored + run: cargo test --all-features --test cli_regression -- --ignored env: BENDER_TEST_GOLDEN_BRANCH: ${{ github.base_ref }} From 3bb17777a46496db150e4ff7555fc4018d961600 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 31 Jan 2026 22:14:11 +0100 Subject: [PATCH 09/46] bender-slang(build): Fix `fmt` library in release builds --- crates/bender-slang/build.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 983d9fcba..cfb364dff 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -21,7 +21,12 @@ fn main() { // Note: Linux is case-sensitive, so we use lowercase here. // On macOS, the library is called `svLang`, but the linker is case-insensitive there. println!("cargo:rustc-link-lib=static=svlang"); - println!("cargo:rustc-link-lib=static=fmtd"); + + if std::env::var("PROFILE").unwrap() == "debug" { + println!("cargo:rustc-link-lib=static=fmtd"); + } else { + println!("cargo:rustc-link-lib=static=fmt"); + } // Compile the C++ Bridge let mut bridge_build = cxx_build::bridge("src/lib.rs"); From f065f4f1de25b499520e5636d4130e93df2c2ffb Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 31 Jan 2026 22:53:45 +0100 Subject: [PATCH 10/46] bender-slang(build): Clean up --- crates/bender-slang/build.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index cfb364dff..796020e76 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -1,6 +1,13 @@ fn main() { - // Build Slang with CMake into a static library - let dst = cmake::Config::new("vendor/slang") + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap(); + let build_profile = std::env::var("PROFILE").unwrap(); + + // Create the configuration builder + let mut slang_lib = cmake::Config::new("vendor/slang"); + + // Apply common settings + slang_lib .define("SLANG_INCLUDE_TESTS", "OFF") .define("SLANG_INCLUDE_TOOLS", "OFF") .define("SLANG_INCLUDE_PYSLANG", "OFF") @@ -18,14 +25,13 @@ fn main() { // Configure Linker to find Slang static library println!("cargo:rustc-link-search=native={}/lib", dst.display()); - // Note: Linux is case-sensitive, so we use lowercase here. - // On macOS, the library is called `svLang`, but the linker is case-insensitive there. println!("cargo:rustc-link-lib=static=svlang"); - if std::env::var("PROFILE").unwrap() == "debug" { - println!("cargo:rustc-link-lib=static=fmtd"); - } else { - println!("cargo:rustc-link-lib=static=fmt"); + // Link the appropriate fmt library based on build profile + match build_profile.as_str() { + "debug" => println!("cargo:rustc-link-lib=static=fmtd"), + "release" => println!("cargo:rustc-link-lib=static=fmt"), + _ => unreachable!(), } // Compile the C++ Bridge @@ -52,7 +58,7 @@ fn main() { } // Linux: we try static linking of libstdc++ to avoid issues on older distros. - if std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "linux" { + if target_os == "linux" { // Determine the C++ compiler to use. Respect the CXX environment variable if set. let compiler = std::env::var("CXX").unwrap_or_else(|_| "g++".to_string()); // We search for the static libstdc++ file using g++ From 4af95ea5d7520a0227a8bba93009b2d54b612375 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 31 Jan 2026 23:41:37 +0100 Subject: [PATCH 11/46] bender-slang(build): Enable `mimalloc` library again --- crates/bender-slang/build.rs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 796020e76..400886721 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -14,8 +14,6 @@ fn main() { .define("BUILD_SHARED_LIBS", "OFF") // Forces installation into 'lib' instead of 'lib64' on some systems. .define("CMAKE_INSTALL_LIBDIR", "lib") - // TODO(fischeti): Check whether mimalloc can/should be enabled again. - .define("SLANG_USE_MIMALLOC", "OFF") // TODO(fischeti): `fmt` currently causes issues on my machine since there is a system-wide installation. .define("CMAKE_DISABLE_FIND_PACKAGE_fmt", "ON") // TODO(fischeti): Investigate how boost should be handled properly. @@ -27,11 +25,13 @@ fn main() { println!("cargo:rustc-link-search=native={}/lib", dst.display()); println!("cargo:rustc-link-lib=static=svlang"); - // Link the appropriate fmt library based on build profile - match build_profile.as_str() { - "debug" => println!("cargo:rustc-link-lib=static=fmtd"), - "release" => println!("cargo:rustc-link-lib=static=fmt"), - _ => unreachable!(), + // Link the additional libraries based on build profile + if build_profile == "debug" { + println!("cargo:rustc-link-lib=static=fmtd"); + println!("cargo:rustc-link-lib=static=mimalloc-debug") + } else { + println!("cargo:rustc-link-lib=static=fmt"); + println!("cargo:rustc-link-lib=static=mimalloc") } // Compile the C++ Bridge @@ -52,11 +52,6 @@ fn main() { .include("vendor/slang/external") .include(dst.join("include")); - // TODO(fischeti): Check whether debug definitions are necessary. - if std::env::var("PROFILE").unwrap() == "debug" { - bridge_build.define("SLANG_DEBUG", "1"); - } - // Linux: we try static linking of libstdc++ to avoid issues on older distros. if target_os == "linux" { // Determine the C++ compiler to use. Respect the CXX environment variable if set. From 3b56691eb3f9be306b8c99905c67ef229be9c13a Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 31 Jan 2026 22:54:07 +0100 Subject: [PATCH 12/46] bender-slang(build): Fix windows build --- .github/workflows/ci.yml | 4 +-- .github/workflows/cli_regression.yml | 2 +- crates/bender-slang/build.rs | 42 ++++++++++++++++++---------- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4420a4f4e..d8b13df41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,9 +47,9 @@ jobs: with: toolchain: stable - name: Build - run: cargo build --all-features + run: cargo build --all-features --release - name: Cargo Test - run: cargo test --workspace --all-features + run: cargo test --workspace --all-features --release - name: Run unit-tests run: tests/run_all.sh shell: bash diff --git a/.github/workflows/cli_regression.yml b/.github/workflows/cli_regression.yml index bfdcf9bd7..91069aefe 100644 --- a/.github/workflows/cli_regression.yml +++ b/.github/workflows/cli_regression.yml @@ -29,7 +29,7 @@ jobs: with: toolchain: stable - name: Run CLI Regression - run: cargo test --all-features --test cli_regression -- --ignored + run: cargo test --all-features --test cli_regression --release -- --ignored env: BENDER_TEST_GOLDEN_BRANCH: ${{ github.base_ref }} diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 400886721..8132e1a78 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -17,21 +17,31 @@ fn main() { // TODO(fischeti): `fmt` currently causes issues on my machine since there is a system-wide installation. .define("CMAKE_DISABLE_FIND_PACKAGE_fmt", "ON") // TODO(fischeti): Investigate how boost should be handled properly. - .cxxflag("-DSLANG_BOOST_SINGLE_HEADER=1") - .static_crt(true) - .build(); + .cxxflag("-DSLANG_BOOST_SINGLE_HEADER=1"); + + // Windows / MSVC specific flags + if target_env == "msvc" { + slang_lib.cxxflag("/EHsc").cxxflag("/utf-8"); + } + + // Build the slang library + let dst = slang_lib.build(); // Configure Linker to find Slang static library println!("cargo:rustc-link-search=native={}/lib", dst.display()); println!("cargo:rustc-link-lib=static=svlang"); - // Link the additional libraries based on build profile - if build_profile == "debug" { - println!("cargo:rustc-link-lib=static=fmtd"); - println!("cargo:rustc-link-lib=static=mimalloc-debug") - } else { - println!("cargo:rustc-link-lib=static=fmt"); - println!("cargo:rustc-link-lib=static=mimalloc") + // Link the additional libraries based on build profile and OS + match (build_profile.as_str(), target_env.as_str()) { + ("release", _) | (_, "msvc") => { + println!("cargo:rustc-link-lib=static=fmt"); + println!("cargo:rustc-link-lib=static=mimalloc"); + } + ("debug", _) => { + println!("cargo:rustc-link-lib=static=fmtd"); + println!("cargo:rustc-link-lib=static=mimalloc-debug"); + } + _ => unreachable!(), } // Compile the C++ Bridge @@ -39,7 +49,6 @@ fn main() { bridge_build .file("cpp/slang_bridge.cpp") .flag_if_supported("-std=c++20") - .flag_if_supported("/std:c++20") // Static Linking Definition // Tells Slang headers not to look for DLL import/export symbols. .define("SLANG_STATIC_DEFINE", "1") @@ -80,10 +89,13 @@ fn main() { ); } } - // Windows / MSVC: we force static linking of the CRT to avoid missing DLL issues - } else if std::env::var("CARGO_CFG_TARGET_ENV").unwrap() == "msvc" { - bridge_build.static_crt(true); - } + // Windows / MSVC: we set the appropriate flags for C++20 and exception handling. + } else if target_env == "msvc" { + bridge_build + .flag_if_supported("/std:c++20") + .flag("/EHsc") + .flag("/utf-8"); + }; // macOS: we leave the default dynamic linking of libc++ as is. bridge_build.compile("slang-bridge"); From d1541c5da22097b74aa3a8e3f789c3bcaf6861ad Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 1 Feb 2026 00:46:32 +0100 Subject: [PATCH 13/46] bender-slang(build): Don't use system-installed slang dependencies --- crates/bender-slang/build.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 8132e1a78..e253fc806 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -14,10 +14,10 @@ fn main() { .define("BUILD_SHARED_LIBS", "OFF") // Forces installation into 'lib' instead of 'lib64' on some systems. .define("CMAKE_INSTALL_LIBDIR", "lib") - // TODO(fischeti): `fmt` currently causes issues on my machine since there is a system-wide installation. + // Disable finding system-installed packages, we want to fetch and build them from source. .define("CMAKE_DISABLE_FIND_PACKAGE_fmt", "ON") - // TODO(fischeti): Investigate how boost should be handled properly. - .cxxflag("-DSLANG_BOOST_SINGLE_HEADER=1"); + .define("CMAKE_DISABLE_FIND_PACKAGE_mimalloc", "ON") + .define("CMAKE_DISABLE_FIND_PACKAGE_Boost", "ON"); // Windows / MSVC specific flags if target_env == "msvc" { @@ -49,14 +49,10 @@ fn main() { bridge_build .file("cpp/slang_bridge.cpp") .flag_if_supported("-std=c++20") - // Static Linking Definition // Tells Slang headers not to look for DLL import/export symbols. .define("SLANG_STATIC_DEFINE", "1") - // Boost Vendored Mode - // Tells Slang to use the local 'external/boost_*.hpp' files instead of system Boost. - // TODO(fischeti): Investigate how boost should be handled properly. + // Tells Slang to use vendor-provided instead of system-installed Boost header files. .define("SLANG_BOOST_SINGLE_HEADER", "1") - // Include Paths .include("vendor/slang/include") .include("vendor/slang/external") .include(dst.join("include")); From 0e8e0df07d6e42bd90b714b9c2e4cacf7b853442 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 1 Feb 2026 13:52:00 +0100 Subject: [PATCH 14/46] bender-slang(ffi): Refactor interface --- crates/bender-slang/build.rs | 7 ++ crates/bender-slang/cpp/slang_bridge.cpp | 93 ++++++++------- crates/bender-slang/cpp/slang_bridge.h | 41 ++++++- crates/bender-slang/src/lib.rs | 146 +++++++++++------------ src/cmd/pickle.rs | 51 ++++---- 5 files changed, 188 insertions(+), 150 deletions(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index e253fc806..2d946c404 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -1,3 +1,6 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + fn main() { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap(); @@ -12,6 +15,7 @@ fn main() { .define("SLANG_INCLUDE_TOOLS", "OFF") .define("SLANG_INCLUDE_PYSLANG", "OFF") .define("BUILD_SHARED_LIBS", "OFF") + .define("SLANG_USE_MIMALLOC", "ON") // Forces installation into 'lib' instead of 'lib64' on some systems. .define("CMAKE_INSTALL_LIBDIR", "lib") // Disable finding system-installed packages, we want to fetch and build them from source. @@ -53,6 +57,9 @@ fn main() { .define("SLANG_STATIC_DEFINE", "1") // Tells Slang to use vendor-provided instead of system-installed Boost header files. .define("SLANG_BOOST_SINGLE_HEADER", "1") + .define("SLANG_DEBUG", "") + .define("SLANG_USE_THREADS", "1") + .define("SLANG_USE_MIMALLOC", "1") .include("vendor/slang/include") .include("vendor/slang/external") .include(dst.join("include")); diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 885595225..d0aed9786 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -1,69 +1,73 @@ -#include "slang_bridge.h" -#include "bender-slang/src/lib.rs.h" // Import the generated C++ definition of the structs +// Copyright (c) 2025 ETH Zurich +// Tim Fischer -#include "slang/driver/Driver.h" +#include "slang_bridge.h" +#include "bender-slang/src/lib.rs.h" #include "slang/syntax/SyntaxPrinter.h" -#include "slang/syntax/SyntaxTree.h" - -#include -#include -#include +#include using namespace slang; using namespace slang::driver; using namespace slang::syntax; -rust::String pickle(rust::Vec sources, - rust::Vec include_dirs, - rust::Vec defines, - SlangPrintOpts options) { - Driver driver; +SlangContext::SlangContext() { driver.addStandardArgs(); +} + +void SlangContext::add_source(rust::Str path) { + sources.emplace_back(std::string(path)); +} - // 1. Construct Arguments from SlangFiles +void SlangContext::add_include(rust::Str path) { + includes.emplace_back(std::string(path)); +} + +void SlangContext::add_define(rust::Str def) { + defines.emplace_back(std::string(def)); +} + +bool SlangContext::parse() { + // Construct argv for the driver std::vector arg_strings; arg_strings.push_back("slang_tool"); - for (const auto& source : sources) { - arg_strings.push_back(std::string(source)); - } - for (const auto& path : include_dirs) { - arg_strings.push_back("-I"); - arg_strings.push_back(std::string(path)); - } + for (const auto& s : sources) arg_strings.push_back(s); + for (const auto& i : includes) { arg_strings.push_back("-I"); arg_strings.push_back(i); } + for (const auto& d : defines) { arg_strings.push_back("-D"); arg_strings.push_back(d); } - for (const auto& def : defines) { - arg_strings.push_back("-D"); - arg_strings.push_back(std::string(def)); - } - - // Convert to C-style argv std::vector c_args; - c_args.reserve(arg_strings.size()); for (const auto& s : arg_strings) c_args.push_back(s.c_str()); - // 2. Run Compilation if (!driver.parseCommandLine(c_args.size(), c_args.data())) { - throw std::runtime_error("Failed to parse command line arguments."); + // You might want to capture stderr here or throw a clearer error + throw std::runtime_error("Failed to parse command line args"); } if (!driver.processOptions()) { - throw std::runtime_error("Failed to process options."); + throw std::runtime_error("Failed to process options"); } - bool parseSuccess = driver.parseAllSources(); - bool diagSuccess = driver.reportDiagnostics(false); + bool ok = driver.parseAllSources(); + // reportDiagnostics returns true if issues found, so we invert logic or check strictness + bool hasErrors = driver.reportDiagnostics(false); - if (!parseSuccess || !diagSuccess) { - throw std::runtime_error("Parsing failed. Check stderr for details."); - } + return ok && !hasErrors; +} + +size_t SlangContext::get_tree_count() const { + return driver.syntaxTrees.size(); +} - auto& syntaxTrees = driver.syntaxTrees; - if (syntaxTrees.empty()) { - return ""; +std::shared_ptr SlangContext::get_tree(size_t index) const { + if (index >= driver.syntaxTrees.size()) { + // Rust's loop bounds prevent this, but good for safety + throw std::out_of_range("Syntax tree index out of range"); } + return driver.syntaxTrees[index]; +} - // 3. Configure Printer from SlangPrinterOptions +rust::String SlangContext::print_tree(const SyntaxTree& tree, SlangPrintOpts options) const { + // Use the SourceManager from the driver (this context) SyntaxPrinter printer(driver.sourceManager); printer.setIncludeDirectives(options.include_directives); @@ -72,9 +76,10 @@ rust::String pickle(rust::Vec sources, printer.setSquashNewlines(options.squash_newlines); printer.setIncludeComments(options.include_comments); - for (auto& tree : syntaxTrees) { - printer.print(*tree); - } - + printer.print(tree); return rust::String(printer.str()); } + +std::unique_ptr new_slang_context() { + return std::make_unique(); +} diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index 1d4174138..4153e29eb 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -1,10 +1,39 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + #pragma once #include "rust/cxx.h" +#include "slang/driver/Driver.h" +#include "slang/syntax/SyntaxTree.h" +#include +#include +#include + +struct SlangPrintOpts; // Forward decl + +// The wrapper class exposed as "SlangContext" to Rust +class SlangContext { +public: + SlangContext(); + + void add_source(rust::Str path); + void add_include(rust::Str path); + void add_define(rust::Str def); + + bool parse(); + + size_t get_tree_count() const; + std::shared_ptr get_tree(size_t index) const; + + rust::String print_tree(const slang::syntax::SyntaxTree& tree, SlangPrintOpts options) const; + +private: + slang::driver::Driver driver; -// Forward declare the structs generated by CXX -struct SlangPrintOpts; + // We buffer args to pass to driver.parseCommandLine later + std::vector sources; + std::vector includes; + std::vector defines; +}; -rust::String pickle(rust::Vec sources, - rust::Vec include_dirs, - rust::Vec defines, - SlangPrintOpts options); +std::unique_ptr new_slang_context(); diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index a78d08928..214912729 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -1,118 +1,106 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +use cxx::{SharedPtr, UniquePtr}; + pub use ffi::SlangPrintOpts; #[cxx::bridge] mod ffi { - /// Options for the syntax printer - #[derive(Clone)] + #[derive(Clone, Copy)] struct SlangPrintOpts { - /// Whether to include preprocessor directives include_directives: bool, - /// Whether to expand include directives expand_includes: bool, - /// Whether to expand macros expand_macros: bool, - /// Whether to print comments include_comments: bool, - /// Whether to squash newlines squash_newlines: bool, } unsafe extern "C++" { include!("bender-slang/cpp/slang_bridge.h"); + // Include Slang header to define SyntaxTree type for CXX + include!("slang/syntax/SyntaxTree.h"); + + /// Opaque type for the Slang Driver wrapper + type SlangContext; + + /// Opaque type for the Slang SyntaxTree + #[namespace = "slang::syntax"] + type SyntaxTree; + + /// Create a new persistent context (owns the Driver) + fn new_slang_context() -> UniquePtr; + + // Methods on SlangContext + fn add_source(self: Pin<&mut SlangContext>, path: &str); + fn add_include(self: Pin<&mut SlangContext>, path: &str); + fn add_define(self: Pin<&mut SlangContext>, def: &str); + + /// Parse all added sources. Returns true on success. + fn parse(self: Pin<&mut SlangContext>) -> Result; - fn pickle( - sources: Vec, - include_dirs: Vec, - defines: Vec, - options: SlangPrintOpts, - ) -> Result; + /// Retrieves the number of parsed syntax trees + fn get_tree_count(self: &SlangContext) -> usize; + + /// Retrieves a shared pointer to a specific syntax tree by index + fn get_tree(self: &SlangContext, index: usize) -> SharedPtr; + + /// Print a specific tree using the context's SourceManager + fn print_tree(self: &SlangContext, tree: &SyntaxTree, options: SlangPrintOpts) -> String; } } -/// Main interface for Slang bindings -pub struct Slang { - /// Source files to be pickled - sources: Vec, - /// Include directories - include_dirs: Vec, - /// Defines - defines: Vec, - /// Print options - print_opts: ffi::SlangPrintOpts, +/// A persistent Slang session +pub struct SlangSession { + ctx: UniquePtr, } -/// Main interface for interfacing with Slang -impl Slang { +impl SlangSession { + /// Creates a new Slang session pub fn new() -> Self { - Slang { - sources: Vec::new(), - include_dirs: Vec::new(), - defines: Vec::new(), - print_opts: ffi::SlangPrintOpts { - include_directives: true, - expand_includes: true, - expand_macros: true, - include_comments: true, - squash_newlines: true, - }, + Self { + ctx: ffi::new_slang_context(), } } - /// Adds source files to be pickled. - pub fn add_sources(&mut self, sources: Vec) { - self.sources.extend(sources); - } - - /// Adds source sources to be pickled, returning self for chaining. - pub fn with_sources(mut self, sources: Vec) -> Self { - self.sources.extend(sources); - self + /// Adds a source file to be parsed + pub fn add_source(&mut self, path: &str) { + self.ctx.pin_mut().add_source(path); } - /// Adds include directories. - pub fn add_include_dirs(&mut self, dirs: Vec) { - self.include_dirs.extend(dirs); + /// Adds an include directory + pub fn add_include(&mut self, path: &str) { + self.ctx.pin_mut().add_include(path); } - /// Adds include directories, returning self for chaining. - pub fn with_include_dirs(mut self, dirs: Vec) -> Self { - self.include_dirs.extend(dirs); - self + /// Adds a preprocessor define + pub fn add_define(&mut self, define: &str) { + self.ctx.pin_mut().add_define(define); } - /// Adds defines. - pub fn add_defines(&mut self, defines: Vec) { - self.defines.extend(defines); + /// Parses all added source files into syntax trees + pub fn parse(&mut self) -> Result> { + Ok(self.ctx.pin_mut().parse()?) } - /// Adds defines, returning self for chaining. - pub fn with_defines(mut self, defines: Vec) -> Self { - self.defines.extend(defines); - self - } - - /// Sets print options. - pub fn set_print_options(&mut self, print_opts: ffi::SlangPrintOpts) { - self.print_opts = print_opts; + /// Returns the parsed syntax trees as a Rust vector + pub fn get_trees(&self) -> Vec> { + let count = self.ctx.get_tree_count(); + let mut trees = Vec::with_capacity(count); + for i in 0..count { + trees.push(self.ctx.get_tree(i)); + } + trees } - /// Sets print options, returning self for chaining. - pub fn with_print_options(mut self, print_opts: ffi::SlangPrintOpts) -> Self { - self.print_opts = print_opts; - self + /// Returns an iterator over the parsed syntax trees + pub fn trees_iter(&self) -> impl Iterator> + '_ { + (0..self.ctx.get_tree_count()).map(|i| self.ctx.get_tree(i)) } - /// Pickles files based on the provided configuration. - /// Returns the pickled content or an error if parsing/processing failed. - pub fn pickle(&self) -> Result> { - // call the C++ function; errors are propagated as Rust Results - let result = ffi::pickle( - self.sources.clone(), - self.include_dirs.clone(), - self.defines.clone(), - self.print_opts.clone(), - )?; - Ok(result) + /// Prints a syntax tree with given printing options + pub fn print_tree(&self, tree: &ffi::SyntaxTree, opts: ffi::SlangPrintOpts) -> String { + self.ctx.print_tree(tree, opts) } } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 3a77b50a1..dec881c02 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -5,7 +5,7 @@ use clap::{ArgAction, Args}; -use bender_slang::{Slang, SlangPrintOpts}; +use bender_slang::{SlangPrintOpts, SlangSession}; use crate::error::*; @@ -54,26 +54,35 @@ pub struct PickleArgs { /// Execute the `pickle` subcommand. pub fn run(args: PickleArgs) -> Result<()> { - let slang = Slang::new() - .with_sources(args.files) - .with_include_dirs(args.include_dirs) - .with_defines(args.defines) - .with_print_options(SlangPrintOpts { - include_directives: args.include_directives, - expand_includes: args.expand_includes, - expand_macros: args.expand_macros, - include_comments: !args.strip_comments, - squash_newlines: args.strip_newlines, - }); - match slang.pickle() { - Ok(pickled) => { - if let Some(output) = args.output { - std::fs::write(output, pickled).expect("Failed to write output file"); - } else { - println!("{}", pickled); - }; - } - Err(cause) => return Err(Error::new(format!("Cannot pickle files: {}", cause))), + let mut slang = SlangSession::new(); + + for file in args.files.iter() { + slang.add_source(file); + } + + for include in args.include_dirs.iter() { + slang.add_include(include); + } + + for define in args.defines.iter() { + slang.add_define(define); + } + + slang + .parse() + .map_err(|cause| Error::new(format!("Cannot parse files: {}", cause)))?; + + let print_opts = SlangPrintOpts { + include_directives: args.include_directives, + expand_includes: args.expand_includes, + expand_macros: args.expand_macros, + include_comments: !args.strip_comments, + squash_newlines: args.strip_newlines, + }; + + for tree in slang.trees_iter() { + let pickled = slang.print_tree(&tree, print_opts); + println!("{}", pickled); } Ok(()) } From d195c07d38e9766b27e1236135411bcc697e29e0 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 2 Feb 2026 20:00:33 +0100 Subject: [PATCH 15/46] bender-slang(build): Align defines and flags in library and bridge build Will result in ABI mismatches i.e. segfaults otherwise --- crates/bender-slang/build.rs | 62 +++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 2d946c404..4a9e46a6e 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -9,13 +9,33 @@ fn main() { // Create the configuration builder let mut slang_lib = cmake::Config::new("vendor/slang"); - // Apply common settings + // Common defines to give to both Slang and the Bridge + // Note: It is very important to provide the same defines and flags + // to both the Slang library build and the C++ bridge build to avoid + // ABI incompatibilities. Otherwise, this will cause segfaults at runtime. + let mut common_cxx_defines = vec![ + ("SLANG_USE_MIMALLOC", "1"), + ("SLANG_USE_THREADS", "1"), + ("SLANG_BOOST_SINGLE_HEADER", "1"), + ]; + + // Add debug define if in debug build + if build_profile == "debug" { + common_cxx_defines.push(("SLANG_DEBUG", "1")); + common_cxx_defines.push(("SLANG_ASSERT_ENABLED", "1")); + }; + + // Common compiler flags + let common_cxx_flags = if target_env == "msvc" { + vec!["/std:c++20", "/EHsc", "/utf-8"] + } else { + vec!["-std=c++20"] + }; + + // Apply cmake configuration for Slang library slang_lib .define("SLANG_INCLUDE_TESTS", "OFF") .define("SLANG_INCLUDE_TOOLS", "OFF") - .define("SLANG_INCLUDE_PYSLANG", "OFF") - .define("BUILD_SHARED_LIBS", "OFF") - .define("SLANG_USE_MIMALLOC", "ON") // Forces installation into 'lib' instead of 'lib64' on some systems. .define("CMAKE_INSTALL_LIBDIR", "lib") // Disable finding system-installed packages, we want to fetch and build them from source. @@ -23,9 +43,13 @@ fn main() { .define("CMAKE_DISABLE_FIND_PACKAGE_mimalloc", "ON") .define("CMAKE_DISABLE_FIND_PACKAGE_Boost", "ON"); - // Windows / MSVC specific flags - if target_env == "msvc" { - slang_lib.cxxflag("/EHsc").cxxflag("/utf-8"); + // Apply common defines and flags + for (def, value) in common_cxx_defines.iter() { + slang_lib.define(def, *value); + slang_lib.cxxflag(format!("-D{}={}", def, value)); + } + for flag in common_cxx_flags.iter() { + slang_lib.cxxflag(flag); } // Build the slang library @@ -53,13 +77,6 @@ fn main() { bridge_build .file("cpp/slang_bridge.cpp") .flag_if_supported("-std=c++20") - // Tells Slang headers not to look for DLL import/export symbols. - .define("SLANG_STATIC_DEFINE", "1") - // Tells Slang to use vendor-provided instead of system-installed Boost header files. - .define("SLANG_BOOST_SINGLE_HEADER", "1") - .define("SLANG_DEBUG", "") - .define("SLANG_USE_THREADS", "1") - .define("SLANG_USE_MIMALLOC", "1") .include("vendor/slang/include") .include("vendor/slang/external") .include(dst.join("include")); @@ -92,14 +109,15 @@ fn main() { ); } } - // Windows / MSVC: we set the appropriate flags for C++20 and exception handling. - } else if target_env == "msvc" { - bridge_build - .flag_if_supported("/std:c++20") - .flag("/EHsc") - .flag("/utf-8"); - }; - // macOS: we leave the default dynamic linking of libc++ as is. + } + + // Apply common defines and flags to the bridge build as well + for (def, value) in common_cxx_defines.iter() { + bridge_build.define(def, *value); + } + for flag in common_cxx_flags.iter() { + bridge_build.flag(flag); + } bridge_build.compile("slang-bridge"); From 6f0ed9b5073ce24a991b036aaa8c287c676d5e8c Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 4 Feb 2026 12:40:26 +0100 Subject: [PATCH 16/46] bender-slang(bridge): Add SyntaxTree rewriter for module name prefixes/suffixes --- crates/bender-slang/cpp/slang_bridge.cpp | 152 +++++++++++++++++------ crates/bender-slang/cpp/slang_bridge.h | 12 +- crates/bender-slang/src/lib.rs | 35 +++++- src/cmd/pickle.rs | 11 +- 4 files changed, 167 insertions(+), 43 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index d0aed9786..b5af401fb 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -2,44 +2,56 @@ // Tim Fischer #include "slang_bridge.h" + #include "bender-slang/src/lib.rs.h" #include "slang/syntax/SyntaxPrinter.h" -#include +#include "slang/syntax/SyntaxVisitor.h" using namespace slang; using namespace slang::driver; using namespace slang::syntax; -SlangContext::SlangContext() { - driver.addStandardArgs(); -} +using std::memcpy; +using std::shared_ptr; +using std::string; +using std::string_view; +using std::vector; -void SlangContext::add_source(rust::Str path) { - sources.emplace_back(std::string(path)); -} +// Create a new SlangContext instance +std::unique_ptr new_slang_context() { return std::make_unique(); } -void SlangContext::add_include(rust::Str path) { - includes.emplace_back(std::string(path)); -} +// Constructor: initialize driver with standard args +SlangContext::SlangContext() { driver.addStandardArgs(); } -void SlangContext::add_define(rust::Str def) { - defines.emplace_back(std::string(def)); -} +// Add a source file path to the context +void SlangContext::add_source(rust::Str path) { sources.emplace_back(std::string(path)); } + +// Add an include path to the context +void SlangContext::add_include(rust::Str path) { includes.emplace_back(std::string(path)); } + +// Add a define to the context +void SlangContext::add_define(rust::Str def) { defines.emplace_back(std::string(def)); } bool SlangContext::parse() { - // Construct argv for the driver - std::vector arg_strings; + vector arg_strings; arg_strings.push_back("slang_tool"); - for (const auto& s : sources) arg_strings.push_back(s); - for (const auto& i : includes) { arg_strings.push_back("-I"); arg_strings.push_back(i); } - for (const auto& d : defines) { arg_strings.push_back("-D"); arg_strings.push_back(d); } + for (const auto& s : sources) + arg_strings.push_back(s); + for (const auto& i : includes) { + arg_strings.push_back("-I"); + arg_strings.push_back(i); + } + for (const auto& d : defines) { + arg_strings.push_back("-D"); + arg_strings.push_back(d); + } - std::vector c_args; - for (const auto& s : arg_strings) c_args.push_back(s.c_str()); + vector c_args; + for (const auto& s : arg_strings) + c_args.push_back(s.c_str()); if (!driver.parseCommandLine(c_args.size(), c_args.data())) { - // You might want to capture stderr here or throw a clearer error throw std::runtime_error("Failed to parse command line args"); } @@ -54,20 +66,91 @@ bool SlangContext::parse() { return ok && !hasErrors; } -size_t SlangContext::get_tree_count() const { - return driver.syntaxTrees.size(); -} +// Get the number of syntax trees parsed by the driver +size_t SlangContext::get_tree_count() const { return driver.syntaxTrees.size(); } + +// Get the syntax tree at the specified index +shared_ptr SlangContext::get_tree(size_t index) const { return driver.syntaxTrees[index]; } + +// Rewriter that adds prefix/suffix to module and instantiated hierarchy names +class SuffixPrefixRewriter : public SyntaxRewriter { + public: + SuffixPrefixRewriter(string_view prefix, string_view suffix) : prefix(prefix), suffix(suffix) {} + + // Helper to allocate and build renamed string with prefix/suffix + string_view rename(string_view name) { + size_t len = prefix.size() + name.size() + suffix.size(); + char* mem = (char*)alloc.allocate(len, 1); + memcpy(mem, prefix.data(), prefix.size()); + memcpy(mem + prefix.size(), name.data(), name.size()); + memcpy(mem + prefix.size() + name.size(), suffix.data(), suffix.size()); + return string_view(mem, len); + } + + // Renames "module foo;" -> "module foo;" + void handle(const ModuleDeclarationSyntax& node) { + if (node.header->name.isMissing()) + return; + + // Create a new name token + auto newName = rename(node.header->name.valueText()); + auto newNameToken = makeId(newName, node.header->name.trivia()); + + // Clone the header and update the name + ModuleHeaderSyntax* newHeader = deepClone(*node.header, alloc); + newHeader->name = newNameToken; -std::shared_ptr SlangContext::get_tree(size_t index) const { - if (index >= driver.syntaxTrees.size()) { - // Rust's loop bounds prevent this, but good for safety - throw std::out_of_range("Syntax tree index out of range"); + // Replace the old header with the new one + replace(*node.header, *newHeader); + + // Continue visiting child nodes + visitDefault(node); + } + + // Renames "foo i_foo();" -> "foo i_foo();" + void handle(const HierarchyInstantiationSyntax& node) { + // Check to make sure we are dealing with an identifier + // and not a built-in type e.g. `initial foo();` + if (node.type.kind == parsing::TokenKind::Identifier) { + + // Create a new name token + auto newName = rename(node.type.valueText()); + auto newNameToken = makeId(newName); + + // Clone the node and update the type token + HierarchyInstantiationSyntax* newNode = deepClone(node, alloc); + newNode->type = newNameToken; + + // Replace the old node with the new one + replace(node, *newNode, true); + } + + // Continue visiting child nodes + visitDefault(node); } - return driver.syntaxTrees[index]; + + private: + string_view prefix; + string_view suffix; +}; + +// Rename modules and instantiated hierarchy names in the given syntax tree +shared_ptr SlangContext::rename_tree(const shared_ptr tree, rust::Str prefix, + rust::Str suffix) const { + + // Convert rust::Str to string_view and instantiate rewriter + string_view prefix_str(prefix.data(), prefix.size()); + string_view suffix_str(suffix.data(), suffix.size()); + SuffixPrefixRewriter rewriter(prefix_str, suffix_str); + + // Apply the rewriter to the tree and return the transformed tree + return rewriter.transform(tree); } -rust::String SlangContext::print_tree(const SyntaxTree& tree, SlangPrintOpts options) const { - // Use the SourceManager from the driver (this context) +// Print the given syntax tree with specified options +rust::String SlangContext::print_tree(const shared_ptr tree, SlangPrintOpts options) const { + + // Set up the printer with options SyntaxPrinter printer(driver.sourceManager); printer.setIncludeDirectives(options.include_directives); @@ -76,10 +159,7 @@ rust::String SlangContext::print_tree(const SyntaxTree& tree, SlangPrintOpts opt printer.setSquashNewlines(options.squash_newlines); printer.setIncludeComments(options.include_comments); - printer.print(tree); + // Print the tree root and return as rust::String + printer.print(tree->root()); return rust::String(printer.str()); } - -std::unique_ptr new_slang_context() { - return std::make_unique(); -} diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index 4153e29eb..9d9e600bb 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -5,15 +5,16 @@ #include "rust/cxx.h" #include "slang/driver/Driver.h" #include "slang/syntax/SyntaxTree.h" + #include -#include #include +#include struct SlangPrintOpts; // Forward decl // The wrapper class exposed as "SlangContext" to Rust class SlangContext { -public: + public: SlangContext(); void add_source(rust::Str path); @@ -25,9 +26,12 @@ class SlangContext { size_t get_tree_count() const; std::shared_ptr get_tree(size_t index) const; - rust::String print_tree(const slang::syntax::SyntaxTree& tree, SlangPrintOpts options) const; + std::shared_ptr rename_tree(const std::shared_ptr, + rust::Str prefix, rust::Str suffix) const; + + rust::String print_tree(const std::shared_ptr, SlangPrintOpts options) const; -private: + private: slang::driver::Driver driver; // We buffer args to pass to driver.parseCommandLine later diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index 214912729..0d25c6b23 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -46,8 +46,20 @@ mod ffi { /// Retrieves a shared pointer to a specific syntax tree by index fn get_tree(self: &SlangContext, index: usize) -> SharedPtr; + /// Rename names in the syntax tree with a given prefix and suffix + fn rename_tree( + self: &SlangContext, + tree: SharedPtr, + prefix: &str, + suffix: &str, + ) -> SharedPtr; + /// Print a specific tree using the context's SourceManager - fn print_tree(self: &SlangContext, tree: &SyntaxTree, options: SlangPrintOpts) -> String; + fn print_tree( + self: &SlangContext, + tree: SharedPtr, + options: SlangPrintOpts, + ) -> String; } } @@ -99,8 +111,27 @@ impl SlangSession { (0..self.ctx.get_tree_count()).map(|i| self.ctx.get_tree(i)) } + /// Renames names in the syntax tree with a given prefix and suffix + pub fn rename_tree( + &self, + tree: SharedPtr, + prefix: Option<&str>, + suffix: Option<&str>, + ) -> SharedPtr { + if prefix.is_none() && suffix.is_none() { + return tree; + } + let prefix = prefix.unwrap_or(""); + let suffix = suffix.unwrap_or(""); + self.ctx.rename_tree(tree, prefix, suffix) + } + /// Prints a syntax tree with given printing options - pub fn print_tree(&self, tree: &ffi::SyntaxTree, opts: ffi::SlangPrintOpts) -> String { + pub fn print_tree( + &self, + tree: SharedPtr, + opts: ffi::SlangPrintOpts, + ) -> String { self.ctx.print_tree(tree, opts) } } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index dec881c02..6218116d4 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -31,6 +31,14 @@ pub struct PickleArgs { #[arg(short = 'D', long, action = ArgAction::Append)] defines: Vec, + /// The prefix to add to all names + #[arg(long)] + prefix: Option, + + /// The suffix to add to all names + #[arg(long)] + suffix: Option, + /// Whether to include preprocessor directives #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Print Options")] include_directives: bool, @@ -81,7 +89,8 @@ pub fn run(args: PickleArgs) -> Result<()> { }; for tree in slang.trees_iter() { - let pickled = slang.print_tree(&tree, print_opts); + let renamed_tree = slang.rename_tree(tree, args.prefix.as_deref(), args.suffix.as_deref()); + let pickled = slang.print_tree(renamed_tree, print_opts); println!("{}", pickled); } Ok(()) From fb9a076fd504ee908db84b8665ab655f9d17f467 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 4 Feb 2026 14:25:26 +0100 Subject: [PATCH 17/46] bender-slang(build): Add include guard to slang_bridge.h Prevent multiple inclusion of the header by adding a traditional #ifndef/define/endif include guard (BENDER_SLANG_BRIDGE_H). This replaces the lone #pragma once for better portability and ensures the header can be safely included multiple times across translation units. --- crates/bender-slang/cpp/slang_bridge.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index 9d9e600bb..c060e7646 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -1,7 +1,9 @@ // Copyright (c) 2025 ETH Zurich // Tim Fischer -#pragma once +#ifndef BENDER_SLANG_BRIDGE_H +#define BENDER_SLANG_BRIDGE_H + #include "rust/cxx.h" #include "slang/driver/Driver.h" #include "slang/syntax/SyntaxTree.h" @@ -41,3 +43,5 @@ class SlangContext { }; std::unique_ptr new_slang_context(); + +#endif // BENDER_SLANG_BRIDGE_H From e3414dee4cbc931fc8736ea303be490c32bdd82a Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 5 Feb 2026 23:51:45 +0100 Subject: [PATCH 18/46] bender-slang(ffi): Refactor interface (once again) --- crates/bender-slang/cpp/slang_bridge.cpp | 86 ++++++-------- crates/bender-slang/cpp/slang_bridge.h | 31 ++--- crates/bender-slang/src/lib.rs | 139 +++++++++-------------- src/cmd/pickle.rs | 35 ++---- 4 files changed, 111 insertions(+), 180 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index b5af401fb..999d3f12e 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -10,6 +10,7 @@ using namespace slang; using namespace slang::driver; using namespace slang::syntax; +using namespace slang::parsing; using std::memcpy; using std::shared_ptr; @@ -20,58 +21,40 @@ using std::vector; // Create a new SlangContext instance std::unique_ptr new_slang_context() { return std::make_unique(); } -// Constructor: initialize driver with standard args -SlangContext::SlangContext() { driver.addStandardArgs(); } +SlangContext::SlangContext() {} -// Add a source file path to the context -void SlangContext::add_source(rust::Str path) { sources.emplace_back(std::string(path)); } - -// Add an include path to the context -void SlangContext::add_include(rust::Str path) { includes.emplace_back(std::string(path)); } - -// Add a define to the context -void SlangContext::add_define(rust::Str def) { defines.emplace_back(std::string(def)); } - -bool SlangContext::parse() { - vector arg_strings; - arg_strings.push_back("slang_tool"); - - for (const auto& s : sources) - arg_strings.push_back(s); - for (const auto& i : includes) { - arg_strings.push_back("-I"); - arg_strings.push_back(i); +// Set the include paths for the preprocessor +void SlangContext::set_includes(const rust::Vec& incs) { + ppOptions.additionalIncludePaths.clear(); + for (const auto& inc : incs) { + ppOptions.additionalIncludePaths.emplace_back(std::string(inc)); } - for (const auto& d : defines) { - arg_strings.push_back("-D"); - arg_strings.push_back(d); +} + +// Sets the preprocessor defines +void SlangContext::set_defines(const rust::Vec& defs) { + ppOptions.predefines.clear(); + for (const auto& def : defs) { + ppOptions.predefines.emplace_back(std::string(def)); } +} - vector c_args; - for (const auto& s : arg_strings) - c_args.push_back(s.c_str()); +// Parses the given file and returns a syntax tree, if successful +std::shared_ptr SlangContext::parse_file(rust::Str path) { + Bag options; + options.set(ppOptions); - if (!driver.parseCommandLine(c_args.size(), c_args.data())) { - throw std::runtime_error("Failed to parse command line args"); - } + auto result = SyntaxTree::fromFile(string_view(path.data(), path.size()), sourceManager, options); - if (!driver.processOptions()) { - throw std::runtime_error("Failed to process options"); + if (!result) { + auto& err = result.error(); + std::string msg = "System Error loading '" + std::string(err.second) + "': " + err.first.message(); + throw std::runtime_error(msg); } - bool ok = driver.parseAllSources(); - // reportDiagnostics returns true if issues found, so we invert logic or check strictness - bool hasErrors = driver.reportDiagnostics(false); - - return ok && !hasErrors; + return *result; } -// Get the number of syntax trees parsed by the driver -size_t SlangContext::get_tree_count() const { return driver.syntaxTrees.size(); } - -// Get the syntax tree at the specified index -shared_ptr SlangContext::get_tree(size_t index) const { return driver.syntaxTrees[index]; } - // Rewriter that adds prefix/suffix to module and instantiated hierarchy names class SuffixPrefixRewriter : public SyntaxRewriter { public: @@ -134,24 +117,21 @@ class SuffixPrefixRewriter : public SyntaxRewriter { string_view suffix; }; -// Rename modules and instantiated hierarchy names in the given syntax tree -shared_ptr SlangContext::rename_tree(const shared_ptr tree, rust::Str prefix, - rust::Str suffix) const { - - // Convert rust::Str to string_view and instantiate rewriter - string_view prefix_str(prefix.data(), prefix.size()); - string_view suffix_str(suffix.data(), suffix.size()); - SuffixPrefixRewriter rewriter(prefix_str, suffix_str); +// Transform the given syntax tree by renaming modules and instantiated hierarchy names with the specified prefix/suffix +std::shared_ptr rename(std::shared_ptr tree, rust::Str prefix, rust::Str suffix) { + std::string_view p(prefix.data(), prefix.size()); + std::string_view s(suffix.data(), suffix.size()); - // Apply the rewriter to the tree and return the transformed tree + // SuffixPrefixRewriter is defined in the .cpp file as before + SuffixPrefixRewriter rewriter(p, s); return rewriter.transform(tree); } // Print the given syntax tree with specified options -rust::String SlangContext::print_tree(const shared_ptr tree, SlangPrintOpts options) const { +rust::String print_tree(const shared_ptr tree, SlangPrintOpts options) { // Set up the printer with options - SyntaxPrinter printer(driver.sourceManager); + SyntaxPrinter printer(tree->sourceManager()); printer.setIncludeDirectives(options.include_directives); printer.setExpandIncludes(options.expand_includes); diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index c060e7646..d599660f2 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -12,36 +12,27 @@ #include #include -struct SlangPrintOpts; // Forward decl +struct SlangPrintOpts; -// The wrapper class exposed as "SlangContext" to Rust class SlangContext { public: SlangContext(); - void add_source(rust::Str path); - void add_include(rust::Str path); - void add_define(rust::Str def); + void set_includes(const rust::Vec& includes); + void set_defines(const rust::Vec& defines); - bool parse(); - - size_t get_tree_count() const; - std::shared_ptr get_tree(size_t index) const; - - std::shared_ptr rename_tree(const std::shared_ptr, - rust::Str prefix, rust::Str suffix) const; - - rust::String print_tree(const std::shared_ptr, SlangPrintOpts options) const; + std::shared_ptr parse_file(rust::Str path); private: - slang::driver::Driver driver; - - // We buffer args to pass to driver.parseCommandLine later - std::vector sources; - std::vector includes; - std::vector defines; + slang::SourceManager sourceManager; + slang::parsing::PreprocessorOptions ppOptions; }; std::unique_ptr new_slang_context(); +std::shared_ptr rename(std::shared_ptr tree, rust::Str prefix, + rust::Str suffix); + +rust::String print_tree(std::shared_ptr tree, SlangPrintOpts options); + #endif // BENDER_SLANG_BRIDGE_H diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index 0d25c6b23..64f6811b6 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -22,116 +22,87 @@ mod ffi { // Include Slang header to define SyntaxTree type for CXX include!("slang/syntax/SyntaxTree.h"); - /// Opaque type for the Slang Driver wrapper + /// Opaque type for the Slang Context type SlangContext; /// Opaque type for the Slang SyntaxTree #[namespace = "slang::syntax"] type SyntaxTree; - /// Create a new persistent context (owns the Driver) + /// Create a new persistent context fn new_slang_context() -> UniquePtr; - // Methods on SlangContext - fn add_source(self: Pin<&mut SlangContext>, path: &str); - fn add_include(self: Pin<&mut SlangContext>, path: &str); - fn add_define(self: Pin<&mut SlangContext>, def: &str); + /// Set the include directories + fn set_includes(self: Pin<&mut SlangContext>, includes: &Vec); + /// Set the preprocessor defines + fn set_defines(self: Pin<&mut SlangContext>, defines: &Vec); - /// Parse all added sources. Returns true on success. - fn parse(self: Pin<&mut SlangContext>) -> Result; - - /// Retrieves the number of parsed syntax trees - fn get_tree_count(self: &SlangContext) -> usize; - - /// Retrieves a shared pointer to a specific syntax tree by index - fn get_tree(self: &SlangContext, index: usize) -> SharedPtr; + /// Parse all added sources. Returns a syntax tree on success, or an error message on failure. + fn parse_file(self: Pin<&mut SlangContext>, path: &str) -> Result>; /// Rename names in the syntax tree with a given prefix and suffix - fn rename_tree( - self: &SlangContext, - tree: SharedPtr, - prefix: &str, - suffix: &str, - ) -> SharedPtr; - - /// Print a specific tree using the context's SourceManager - fn print_tree( - self: &SlangContext, - tree: SharedPtr, - options: SlangPrintOpts, - ) -> String; + fn rename(tree: SharedPtr, prefix: &str, suffix: &str) + -> SharedPtr; + + /// Print a specific tree + fn print_tree(tree: SharedPtr, options: SlangPrintOpts) -> String; } } -/// A persistent Slang session -pub struct SlangSession { - ctx: UniquePtr, +/// Extension trait for SyntaxTree +pub trait SyntaxTreeExt { + fn rename(&self, prefix: Option<&str>, suffix: Option<&str>) -> Self; + fn display(&self, options: SlangPrintOpts) -> String; } -impl SlangSession { - /// Creates a new Slang session - pub fn new() -> Self { - Self { - ctx: ffi::new_slang_context(), +impl SyntaxTreeExt for SharedPtr { + /// Renames all names in the syntax tree with the given prefix and suffix + fn rename(&self, prefix: Option<&str>, suffix: Option<&str>) -> Self { + if prefix.is_none() && suffix.is_none() { + return self.clone(); } + ffi::rename(self.clone(), prefix.unwrap_or(""), suffix.unwrap_or("")) } - /// Adds a source file to be parsed - pub fn add_source(&mut self, path: &str) { - self.ctx.pin_mut().add_source(path); - } - - /// Adds an include directory - pub fn add_include(&mut self, path: &str) { - self.ctx.pin_mut().add_include(path); - } - - /// Adds a preprocessor define - pub fn add_define(&mut self, define: &str) { - self.ctx.pin_mut().add_define(define); + /// Displays the syntax tree as a string with the given options + fn display(&self, options: SlangPrintOpts) -> String { + ffi::print_tree(self.clone(), options) } +} - /// Parses all added source files into syntax trees - pub fn parse(&mut self) -> Result> { - Ok(self.ctx.pin_mut().parse()?) - } +/// Extension trait for SlangContext +pub trait SlangContextExt { + fn set_includes(self, includes: &Vec) -> Self; + fn set_defines(self, defines: &Vec) -> Self; + fn parse( + &mut self, + path: &str, + ) -> Result, Box>; +} - /// Returns the parsed syntax trees as a Rust vector - pub fn get_trees(&self) -> Vec> { - let count = self.ctx.get_tree_count(); - let mut trees = Vec::with_capacity(count); - for i in 0..count { - trees.push(self.ctx.get_tree(i)); - } - trees +impl SlangContextExt for UniquePtr { + /// Sets the include directories + fn set_includes(mut self, includes: &Vec) -> Self { + self.pin_mut().set_includes(&includes); + self } - /// Returns an iterator over the parsed syntax trees - pub fn trees_iter(&self) -> impl Iterator> + '_ { - (0..self.ctx.get_tree_count()).map(|i| self.ctx.get_tree(i)) + /// Sets the preprocessor defines + fn set_defines(mut self, defines: &Vec) -> Self { + self.pin_mut().set_defines(&defines); + self } - /// Renames names in the syntax tree with a given prefix and suffix - pub fn rename_tree( - &self, - tree: SharedPtr, - prefix: Option<&str>, - suffix: Option<&str>, - ) -> SharedPtr { - if prefix.is_none() && suffix.is_none() { - return tree; - } - let prefix = prefix.unwrap_or(""); - let suffix = suffix.unwrap_or(""); - self.ctx.rename_tree(tree, prefix, suffix) + /// Parses a source file and returns the syntax tree + fn parse( + &mut self, + path: &str, + ) -> Result, Box> { + Ok(self.pin_mut().parse_file(path)?) } +} - /// Prints a syntax tree with given printing options - pub fn print_tree( - &self, - tree: SharedPtr, - opts: ffi::SlangPrintOpts, - ) -> String { - self.ctx.print_tree(tree, opts) - } +/// Creates a new Slang session +pub fn new_session() -> UniquePtr { + ffi::new_slang_context() } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 6218116d4..62c89f429 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -5,7 +5,7 @@ use clap::{ArgAction, Args}; -use bender_slang::{SlangPrintOpts, SlangSession}; +use bender_slang::{SlangContextExt, SlangPrintOpts, SyntaxTreeExt}; use crate::error::*; @@ -62,24 +62,6 @@ pub struct PickleArgs { /// Execute the `pickle` subcommand. pub fn run(args: PickleArgs) -> Result<()> { - let mut slang = SlangSession::new(); - - for file in args.files.iter() { - slang.add_source(file); - } - - for include in args.include_dirs.iter() { - slang.add_include(include); - } - - for define in args.defines.iter() { - slang.add_define(define); - } - - slang - .parse() - .map_err(|cause| Error::new(format!("Cannot parse files: {}", cause)))?; - let print_opts = SlangPrintOpts { include_directives: args.include_directives, expand_includes: args.expand_includes, @@ -88,10 +70,17 @@ pub fn run(args: PickleArgs) -> Result<()> { squash_newlines: args.strip_newlines, }; - for tree in slang.trees_iter() { - let renamed_tree = slang.rename_tree(tree, args.prefix.as_deref(), args.suffix.as_deref()); - let pickled = slang.print_tree(renamed_tree, print_opts); - println!("{}", pickled); + let mut slang = bender_slang::new_session() + .set_includes(&args.include_dirs) + .set_defines(&args.defines); + + for source in &args.files { + let tree = slang + .parse(source) + .map_err(|cause| Error::new(format!("Cannot parse file {}: {}", source, cause)))?; + let renamed_tree = tree.rename(args.prefix.as_deref(), args.suffix.as_deref()); + println!("{}", renamed_tree.display(print_opts)); } + Ok(()) } From 79af60986ef231e16ca3559fe650d6ac0ce5630b Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 11 Feb 2026 22:55:48 +0100 Subject: [PATCH 19/46] pickle: Bender integration --- crates/bender-slang/src/lib.rs | 8 +- src/cli.rs | 2 +- src/cmd/pickle.rs | 147 ++++++++++++++++++++++++++------- src/src.rs | 2 - 4 files changed, 124 insertions(+), 35 deletions(-) diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index 64f6811b6..d0467d4ea 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -72,8 +72,8 @@ impl SyntaxTreeExt for SharedPtr { /// Extension trait for SlangContext pub trait SlangContextExt { - fn set_includes(self, includes: &Vec) -> Self; - fn set_defines(self, defines: &Vec) -> Self; + fn set_includes(&mut self, includes: &Vec) -> &mut Self; + fn set_defines(&mut self, defines: &Vec) -> &mut Self; fn parse( &mut self, path: &str, @@ -82,13 +82,13 @@ pub trait SlangContextExt { impl SlangContextExt for UniquePtr { /// Sets the include directories - fn set_includes(mut self, includes: &Vec) -> Self { + fn set_includes(&mut self, includes: &Vec) -> &mut Self { self.pin_mut().set_includes(&includes); self } /// Sets the preprocessor defines - fn set_defines(mut self, defines: &Vec) -> Self { + fn set_defines(&mut self, defines: &Vec) -> &mut Self { self.pin_mut().set_defines(&defines); self } diff --git a/src/cli.rs b/src/cli.rs index 714e34914..f14eda4bd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -332,7 +332,7 @@ pub fn main() -> Result<()> { Commands::Snapshot(args) => cmd::snapshot::run(&sess, &args), Commands::Audit(args) => cmd::audit::run(&sess, &args), #[cfg(feature = "slang")] - Commands::Pickle(args) => cmd::pickle::run(args), + Commands::Pickle(args) => cmd::pickle::run(&sess, args), Commands::Plugin(args) => { let (plugin_name, plugin_args) = args .split_first() diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 62c89f429..2ca7b12af 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -4,10 +4,17 @@ //! The `pickle` subcommand. use clap::{ArgAction, Args}; +use indexmap::IndexSet; +use tokio::runtime::Runtime; -use bender_slang::{SlangContextExt, SlangPrintOpts, SyntaxTreeExt}; - +use crate::cmd::sources::get_passed_targets; +use crate::config::{Validate, ValidationContext}; use crate::error::*; +use crate::sess::{Session, SessionIo}; +use crate::src::SourceFile; +use crate::target::TargetSet; + +use bender_slang::{SlangContextExt, SlangPrintOpts, SyntaxTreeExt}; // TODO(fischeti): Clean up the arguments and options. // At the moment, they are just directly mirroring the Slang API. @@ -15,53 +22,105 @@ use crate::error::*; /// Pickle files #[derive(Args, Debug)] pub struct PickleArgs { - /// Source files to pickle - #[arg(required = true)] + /// Additional source files to pickle files: Vec, /// The output file (defaults to stdout) + // TODO(fischeti): Actually implement this. #[arg(short, long)] output: Option, - /// Add an include directory - #[arg(short = 'I', long, action = ArgAction::Append)] - include_dirs: Vec, + /// Only include sources that match the given target + #[arg(short, long, action = ArgAction::Append, global = true)] + pub target: Vec, + + /// Specify package to show sources for + #[arg(short, long, action = ArgAction::Append, global = true)] + pub package: Vec, - /// Add defines - #[arg(short = 'D', long, action = ArgAction::Append)] - defines: Vec, + /// Specify package to exclude from sources + #[arg(long, action = ArgAction::Append, global = true)] + pub exclude: Vec, + + /// Exclude all dependencies, i.e. only top level or specified package(s) + #[arg(long, global = true)] + pub no_deps: bool, + + /// Additional include directory + #[arg(short = 'I', action = ArgAction::Append)] + include_dir: Vec, + + /// Additional preprocessor definition + #[arg(short = 'D', action = ArgAction::Append)] + define: Vec, /// The prefix to add to all names - #[arg(long)] + #[arg(long, help_heading = "Slang Options")] prefix: Option, /// The suffix to add to all names - #[arg(long)] + #[arg(long, help_heading = "Slang Options")] suffix: Option, /// Whether to include preprocessor directives - #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Print Options")] + #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Slang Options")] include_directives: bool, /// Whether to expand include directives - #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Print Options")] + #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Slang Options")] expand_includes: bool, /// Whether to expand macros - #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Print Options")] + #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Slang Options")] expand_macros: bool, /// Whether to strip comments - #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Print Options")] + #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Slang Options")] strip_comments: bool, /// Whether to strip newlines - #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Print Options")] + #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Slang Options")] strip_newlines: bool, } /// Execute the `pickle` subcommand. -pub fn run(args: PickleArgs) -> Result<()> { +pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { + // Load the source files + let rt = Runtime::new()?; + let io = SessionIo::new(sess); + let srcs = rt.block_on(io.sources(false, &[]))?; + + // Filter the sources by target. + let targets = TargetSet::new(args.target.iter().map(|s| s.as_str())); + + // Convert vector to sets for packages and excluded packages. + let package_set = IndexSet::from_iter(args.package); + let exclude_set = IndexSet::from_iter(args.exclude); + + // Filter the sources by specified packages. + let packages = &srcs.get_package_list( + sess.manifest.package.name.to_string(), + &package_set, + &exclude_set, + args.no_deps, + ); + + let (targets, packages) = get_passed_targets(sess, &rt, &io, &targets, packages, &package_set)?; + + // Filter the sources by target and package. + let srcs = srcs + .filter_targets(&targets) + .unwrap_or_default() + .filter_packages(&packages) + .unwrap_or_default(); + + // Flatten and validate the sources. + let srcs = srcs + .flatten() + .into_iter() + .map(|f| f.validate(&ValidationContext::default())) + .collect::>>()?; + let print_opts = SlangPrintOpts { include_directives: args.include_directives, expand_includes: args.expand_includes, @@ -70,16 +129,48 @@ pub fn run(args: PickleArgs) -> Result<()> { squash_newlines: args.strip_newlines, }; - let mut slang = bender_slang::new_session() - .set_includes(&args.include_dirs) - .set_defines(&args.defines); - - for source in &args.files { - let tree = slang - .parse(source) - .map_err(|cause| Error::new(format!("Cannot parse file {}: {}", source, cause)))?; - let renamed_tree = tree.rename(args.prefix.as_deref(), args.suffix.as_deref()); - println!("{}", renamed_tree.display(print_opts)); + for src_group in srcs { + let mut slang = bender_slang::new_session(); + + // Collect include directories and defines from the source group and command line arguments. + let include_dirs: Vec = src_group + .include_dirs + .iter() + .chain(src_group.export_incdirs.values().flatten()) + .map(|path| path.to_string_lossy().into_owned()) + .chain(args.include_dir.iter().cloned()) + .collect(); + + // Collect defines from the source group and command line arguments. + let defines: Vec = src_group + .defines + .iter() + .map(|(def, value)| match value { + Some(v) => format!("{def}={v}"), + None => def.to_string(), + }) + .chain(args.define.iter().cloned()) + .collect(); + + // Set the include directories and defines in the Slang session. + slang.set_includes(&include_dirs).set_defines(&defines); + + // Collect file paths from the source group. + let file_paths = src_group.files.iter().filter_map(|source| { + match source { + // TODO(fischeti): Emit warnings for VHDL sources. + SourceFile::File(path, _) => path.to_str(), + _ => None, // Skip Group/Box/etc. + } + }); + + for file_path in file_paths { + let tree = slang.parse(file_path).map_err(|cause| { + Error::new(format!("Cannot parse file {}: {}", file_path, cause)) + })?; + let renamed_tree = tree.rename(args.prefix.as_deref(), args.suffix.as_deref()); + println!("{}", renamed_tree.display(print_opts)); + } } Ok(()) diff --git a/src/src.rs b/src/src.rs index f3ccd3a1f..767dc5027 100644 --- a/src/src.rs +++ b/src/src.rs @@ -344,8 +344,6 @@ impl<'ctx> SourceGroup<'ctx> { pub enum SourceType { /// A Verilog file. Verilog, - // /// A SystemVerilog file. - // SystemVerilog, /// A VHDL file. Vhdl, /// Unknown file type From 5d82fa5647907e5e05c0f4abda2edfe590a48053 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 11:32:25 +0100 Subject: [PATCH 20/46] pickle: Filter non-verilog files and emit warnings --- Cargo.lock | 4 ++-- src/cmd/pickle.rs | 17 +++++++++++------ src/cmd/script.rs | 24 ++++++------------------ src/diagnostic.rs | 4 ++++ src/sess.rs | 17 +++++++++++++---- src/src.rs | 13 +++---------- 6 files changed, 39 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b20a20cef..0382476c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -398,7 +398,7 @@ dependencies = [ "cxxbridge-cmd", "cxxbridge-flags", "cxxbridge-macro", - "foldhash", + "foldhash 0.2.0", "link-cplusplus", ] @@ -716,7 +716,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 2ca7b12af..8e6ee9503 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -9,9 +9,10 @@ use tokio::runtime::Runtime; use crate::cmd::sources::get_passed_targets; use crate::config::{Validate, ValidationContext}; +use crate::diagnostic::Warnings; use crate::error::*; use crate::sess::{Session, SessionIo}; -use crate::src::SourceFile; +use crate::src::{SourceFile, SourceType}; use crate::target::TargetSet; use bender_slang::{SlangContextExt, SlangPrintOpts, SyntaxTreeExt}; @@ -156,12 +157,16 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { slang.set_includes(&include_dirs).set_defines(&defines); // Collect file paths from the source group. - let file_paths = src_group.files.iter().filter_map(|source| { - match source { - // TODO(fischeti): Emit warnings for VHDL sources. - SourceFile::File(path, _) => path.to_str(), - _ => None, // Skip Group/Box/etc. + let file_paths = src_group.files.iter().filter_map(|source| match source { + SourceFile::File(path, Some(SourceType::Verilog)) => path.to_str(), + // Vhdl or unknown file types are not supported by Slang, so we emit a warning and skip them. + SourceFile::File(path, _) => { + Warnings::PickleNonVerilogFile(path.to_path_buf()).emit(); + None } + // Groups should not exist at this point, + // as we have already flattened the sources. + _ => None, }); for file_path in file_paths { diff --git a/src/cmd/script.rs b/src/cmd/script.rs index e891f3caf..c7012c199 100644 --- a/src/cmd/script.rs +++ b/src/cmd/script.rs @@ -546,15 +546,7 @@ fn emit_template( separate_files_in_group( src, |f| match f { - SourceFile::File(p, fmt) => match fmt { - Some(SourceType::Verilog) => Some(SourceType::Verilog), - Some(SourceType::Vhdl) => Some(SourceType::Vhdl), - _ => match p.extension().and_then(std::ffi::OsStr::to_str) { - Some("sv") | Some("v") | Some("vp") => Some(SourceType::Verilog), - Some("vhd") | Some("vhdl") => Some(SourceType::Vhdl), - _ => Some(SourceType::Unknown), - }, - }, + SourceFile::File(_, fmt) => *fmt, _ => None, }, |src, ty, files| { @@ -611,21 +603,17 @@ fn emit_template( SourceFile::Group(_) => unreachable!(), }) .collect(), - file_type: match ty { - SourceType::Verilog => "verilog".to_string(), - SourceType::Vhdl => "vhdl".to_string(), - SourceType::Unknown => "".to_string(), - }, + file_type: Some(ty), }); }, ); } for src in &split_srcs { - match src.file_type.as_str() { - "verilog" => { + match src.file_type { + Some(SourceType::Verilog) => { all_verilog.append(&mut src.files.clone().into_iter().collect()); } - "vhdl" => { + Some(SourceType::Vhdl) => { all_vhdl.append(&mut src.files.clone().into_iter().collect()); } _ => { @@ -683,5 +671,5 @@ struct TplSrcStruct { defines: IndexSet<(String, Option)>, incdirs: IndexSet, files: IndexSet, - file_type: String, + file_type: Option, } diff --git a/src/diagnostic.rs b/src/diagnostic.rs index 0e7a9b5e0..580fc9342 100644 --- a/src/diagnostic.rs +++ b/src/diagnostic.rs @@ -394,6 +394,10 @@ pub enum Warnings { #[error("Override files in {} does not support additional fields like include_dirs, defines, etc.", fmt_pkg!(.0))] #[diagnostic(code(W33))] OverrideFilesWithExtras(String), + + #[error("File {} is not a Verilog file and will be ignored in the pickle output.", fmt_path!(.0.display()))] + #[diagnostic(code(W34))] + PickleNonVerilogFile(PathBuf), } #[cfg(test)] diff --git a/src/sess.rs b/src/sess.rs index e826d93cb..06b1524cf 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 767dc5027..f618aef5c 100644 --- a/src/src.rs +++ b/src/src.rs @@ -340,14 +340,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. @@ -356,7 +355,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>), } @@ -383,12 +382,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 60a664314354a72bd83943ed2b3571cdbf4000f5 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 15:40:05 +0100 Subject: [PATCH 21/46] bender-slang: Allow dumping AST as JSON --- crates/bender-slang/cpp/slang_bridge.cpp | 17 ++++++++++ crates/bender-slang/cpp/slang_bridge.h | 2 ++ crates/bender-slang/src/lib.rs | 10 ++++++ src/cmd/pickle.rs | 40 +++++++++++++++++++++++- 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 999d3f12e..bc68d1012 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -4,8 +4,10 @@ #include "slang_bridge.h" #include "bender-slang/src/lib.rs.h" +#include "slang/syntax/CSTSerializer.h" #include "slang/syntax/SyntaxPrinter.h" #include "slang/syntax/SyntaxVisitor.h" +#include "slang/text/Json.h" using namespace slang; using namespace slang::driver; @@ -143,3 +145,18 @@ rust::String print_tree(const shared_ptr tree, SlangPrintOpts option printer.print(tree->root()); return rust::String(printer.str()); } + +// Dumps the AST/CST to a JSON string +rust::String dump_tree_json(std::shared_ptr tree) { + JsonWriter writer; + writer.setPrettyPrint(true); + + // CSTSerializer is the class Slang uses to convert AST -> JSON + CSTSerializer serializer(writer); + + // Serialize the specific tree root + serializer.serialize(*tree); + + // Convert string_view to rust::String + return rust::String(std::string(writer.view())); +} diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index d599660f2..f479ec578 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -35,4 +35,6 @@ std::shared_ptr rename(std::shared_ptr tree, SlangPrintOpts options); +rust::String dump_tree_json(std::shared_ptr tree); + #endif // BENDER_SLANG_BRIDGE_H diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index d0467d4ea..e24fd67ea 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -46,13 +46,19 @@ mod ffi { /// Print a specific tree fn print_tree(tree: SharedPtr, options: SlangPrintOpts) -> String; + + /// Dump the syntax tree as JSON for debugging purposes + fn dump_tree_json(tree: SharedPtr) -> String; } } /// Extension trait for SyntaxTree +// TODO(fischeti): Consider using a wrapper to implement traits like Debug and Display +// instead of an extension trait. This would be more idiomatic in Rust. pub trait SyntaxTreeExt { fn rename(&self, prefix: Option<&str>, suffix: Option<&str>) -> Self; fn display(&self, options: SlangPrintOpts) -> String; + fn as_debug(&self) -> String; } impl SyntaxTreeExt for SharedPtr { @@ -68,6 +74,10 @@ impl SyntaxTreeExt for SharedPtr { fn display(&self, options: SlangPrintOpts) -> String { ffi::print_tree(self.clone(), options) } + + fn as_debug(&self) -> String { + ffi::dump_tree_json(self.clone()) + } } /// Extension trait for SlangContext diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 8e6ee9503..2ca03b590 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -3,6 +3,9 @@ //! The `pickle` subcommand. +use std::fs::File; +use std::io::{BufWriter, Write}; + use clap::{ArgAction, Args}; use indexmap::IndexSet; use tokio::runtime::Runtime; @@ -82,6 +85,10 @@ pub struct PickleArgs { /// Whether to strip newlines #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Slang Options")] strip_newlines: bool, + + /// Dump the syntax trees as JSON instead of the source code + #[arg(long, help_heading = "Slang Options")] + ast_json: bool, } /// Execute the `pickle` subcommand. @@ -130,6 +137,23 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { squash_newlines: args.strip_newlines, }; + // Setup Output Writer, either to file or stdout + let raw_writer: Box = match &args.output { + Some(path) => Box::new( + File::create(path) + .map_err(|e| Error::new(format!("Cannot create output file: {}", e)))?, + ), + None => Box::new(std::io::stdout()), + }; + let mut writer = BufWriter::new(raw_writer); + + // Start JSON Array if needed + if args.ast_json { + write!(writer, "[")?; + } + + let mut first_item = true; + for src_group in srcs { let mut slang = bender_slang::new_session(); @@ -174,9 +198,23 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { Error::new(format!("Cannot parse file {}: {}", file_path, cause)) })?; let renamed_tree = tree.rename(args.prefix.as_deref(), args.suffix.as_deref()); - println!("{}", renamed_tree.display(print_opts)); + if args.ast_json { + // JSON Array Logic: Prepend comma if not the first item + if !first_item { + write!(writer, ",")?; + } + write!(writer, "{}", renamed_tree.as_debug())?; + first_item = false; + } else { + write!(writer, "{}", renamed_tree.display(print_opts))?; + } } } + // Close JSON Array + if args.ast_json { + writeln!(writer, "]")?; + } + Ok(()) } From ace813053356d83e47be9a52ef804d8aaceec2b1 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 17:46:55 +0100 Subject: [PATCH 22/46] bender-slang(rewriter): Handle package imports, virtual interfaces and scoped names --- crates/bender-slang/cpp/slang_bridge.cpp | 62 ++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index bc68d1012..50449004e 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -73,6 +73,7 @@ class SuffixPrefixRewriter : public SyntaxRewriter { } // Renames "module foo;" -> "module foo;" + // Note: Handles packages and interfaces too. void handle(const ModuleDeclarationSyntax& node) { if (node.header->name.isMissing()) return; @@ -93,6 +94,7 @@ class SuffixPrefixRewriter : public SyntaxRewriter { } // Renames "foo i_foo();" -> "foo i_foo();" + // Note: Handles modules and interfaces. void handle(const HierarchyInstantiationSyntax& node) { // Check to make sure we are dealing with an identifier // and not a built-in type e.g. `initial foo();` @@ -114,6 +116,66 @@ class SuffixPrefixRewriter : public SyntaxRewriter { visitDefault(node); } + // Renames "import foo;" -> "import foo;" + void handle(const PackageImportItemSyntax& node) { + if (node.package.isMissing()) + return; + + auto newName = rename(node.package.valueText()); + auto newNameToken = makeId(newName, node.package.trivia()); + + PackageImportItemSyntax* newNode = deepClone(node, alloc); + newNode->package = newNameToken; + + replace(node, *newNode); + visitDefault(node); + } + + // Renames "virtual MyIntf foo;" -> "virtual MyIntf foo;" + void handle(const VirtualInterfaceTypeSyntax& node) { + if (node.name.isMissing()) + return; + + auto newName = rename(node.name.valueText()); + auto newNameToken = makeId(newName, node.name.trivia()); + + VirtualInterfaceTypeSyntax* newNode = deepClone(node, alloc); + newNode->name = newNameToken; + + replace(node, *newNode); + visitDefault(node); + } + + // Renames "foo::bar" -> "foo::bar" + void handle(const ScopedNameSyntax& node) { + // Only rename if the left side is a simple identifier (e.g., a package name) + // We ignore nested calls or parameterized classes for now. + if (node.left->kind == SyntaxKind::IdentifierName) { + auto& leftNode = node.left->as(); + auto name = leftNode.identifier.valueText(); + + // Skip built-in keywords that look like scopes + if (name != "$unit" && name != "local" && name != "super" && name != "this") { + auto newName = rename(name); + auto newNameToken = makeId(newName, leftNode.identifier.trivia()); + + // Clone the left node and update identifier + IdentifierNameSyntax* newLeft = deepClone(leftNode, alloc); + newLeft->identifier = newNameToken; + + // Clone the scoped node and attach new left + ScopedNameSyntax* newNode = deepClone(node, alloc); + newNode->left = newLeft; + + replace(node, *newNode); + } + } + + // Visit children to handle recursive scopes + // e.g., OuterPkg::InnerPkg::Item + visitDefault(node); + } + private: string_view prefix; string_view suffix; From e84defc99c17c2f42cb0cb8b1b3c939fb891a360 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 18:42:29 +0100 Subject: [PATCH 23/46] pickle: Allow to exclude names from renaming --- crates/bender-slang/cpp/slang_bridge.cpp | 19 ++++++++++++++++--- crates/bender-slang/cpp/slang_bridge.h | 2 +- crates/bender-slang/src/lib.rs | 19 ++++++++++++++----- src/cmd/pickle.rs | 10 +++++++++- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 50449004e..f2cd4bbad 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -9,6 +9,8 @@ #include "slang/syntax/SyntaxVisitor.h" #include "slang/text/Json.h" +#include + using namespace slang; using namespace slang::driver; using namespace slang::syntax; @@ -60,10 +62,14 @@ std::shared_ptr SlangContext::parse_file(rust::Str path) { // Rewriter that adds prefix/suffix to module and instantiated hierarchy names class SuffixPrefixRewriter : public SyntaxRewriter { public: - SuffixPrefixRewriter(string_view prefix, string_view suffix) : prefix(prefix), suffix(suffix) {} + SuffixPrefixRewriter(string_view prefix, string_view suffix, const std::unordered_set& excludes) + : prefix(prefix), suffix(suffix), excludes(excludes) {} // Helper to allocate and build renamed string with prefix/suffix string_view rename(string_view name) { + if (excludes.count(std::string(name))) { + return name; + } size_t len = prefix.size() + name.size() + suffix.size(); char* mem = (char*)alloc.allocate(len, 1); memcpy(mem, prefix.data(), prefix.size()); @@ -179,15 +185,22 @@ class SuffixPrefixRewriter : public SyntaxRewriter { private: string_view prefix; string_view suffix; + const std::unordered_set& excludes; }; // Transform the given syntax tree by renaming modules and instantiated hierarchy names with the specified prefix/suffix -std::shared_ptr rename(std::shared_ptr tree, rust::Str prefix, rust::Str suffix) { +std::shared_ptr rename(std::shared_ptr tree, rust::Str prefix, rust::Str suffix, + const rust::Vec& excludes) { std::string_view p(prefix.data(), prefix.size()); std::string_view s(suffix.data(), suffix.size()); + std::unordered_set excludeSet; + for (const auto& e : excludes) { + excludeSet.insert(std::string(e)); + } + // SuffixPrefixRewriter is defined in the .cpp file as before - SuffixPrefixRewriter rewriter(p, s); + SuffixPrefixRewriter rewriter(p, s, excludeSet); return rewriter.transform(tree); } diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index f479ec578..64f8232c7 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -31,7 +31,7 @@ class SlangContext { std::unique_ptr new_slang_context(); std::shared_ptr rename(std::shared_ptr tree, rust::Str prefix, - rust::Str suffix); + rust::Str suffix, const rust::Vec& excludes); rust::String print_tree(std::shared_ptr tree, SlangPrintOpts options); diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index e24fd67ea..23d0280e6 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -41,8 +41,12 @@ mod ffi { fn parse_file(self: Pin<&mut SlangContext>, path: &str) -> Result>; /// Rename names in the syntax tree with a given prefix and suffix - fn rename(tree: SharedPtr, prefix: &str, suffix: &str) - -> SharedPtr; + fn rename( + tree: SharedPtr, + prefix: &str, + suffix: &str, + excludes: &Vec, + ) -> SharedPtr; /// Print a specific tree fn print_tree(tree: SharedPtr, options: SlangPrintOpts) -> String; @@ -56,18 +60,23 @@ mod ffi { // TODO(fischeti): Consider using a wrapper to implement traits like Debug and Display // instead of an extension trait. This would be more idiomatic in Rust. pub trait SyntaxTreeExt { - fn rename(&self, prefix: Option<&str>, suffix: Option<&str>) -> Self; + fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &Vec) -> Self; fn display(&self, options: SlangPrintOpts) -> String; fn as_debug(&self) -> String; } impl SyntaxTreeExt for SharedPtr { /// Renames all names in the syntax tree with the given prefix and suffix - fn rename(&self, prefix: Option<&str>, suffix: Option<&str>) -> Self { + fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &Vec) -> Self { if prefix.is_none() && suffix.is_none() { return self.clone(); } - ffi::rename(self.clone(), prefix.unwrap_or(""), suffix.unwrap_or("")) + ffi::rename( + self.clone(), + prefix.unwrap_or(""), + suffix.unwrap_or(""), + excludes, + ) } /// Displays the syntax tree as a string with the given options diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 2ca03b590..a7b8baa45 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -66,6 +66,10 @@ pub struct PickleArgs { #[arg(long, help_heading = "Slang Options")] suffix: Option, + /// Names to exclude from renaming (modules, packages, interfaces) + #[arg(long, help_heading = "Slang Options")] + exclude_rename: Vec, + /// Whether to include preprocessor directives #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Slang Options")] include_directives: bool, @@ -197,7 +201,11 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { let tree = slang.parse(file_path).map_err(|cause| { Error::new(format!("Cannot parse file {}: {}", file_path, cause)) })?; - let renamed_tree = tree.rename(args.prefix.as_deref(), args.suffix.as_deref()); + let renamed_tree = tree.rename( + args.prefix.as_deref(), + args.suffix.as_deref(), + &args.exclude_rename, + ); if args.ast_json { // JSON Array Logic: Prepend comma if not the first item if !first_item { From 7c8ccf24c1f3a70f290e423b200ec925d16d1a1b Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 18:57:33 +0100 Subject: [PATCH 24/46] pickle: Clean up CLI --- crates/bender-slang/cpp/slang_bridge.cpp | 4 +- crates/bender-slang/src/lib.rs | 2 - src/cmd/pickle.rs | 54 +++++++++--------------- 3 files changed, 22 insertions(+), 38 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index f2cd4bbad..315c16759 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -210,8 +210,8 @@ rust::String print_tree(const shared_ptr tree, SlangPrintOpts option // Set up the printer with options SyntaxPrinter printer(tree->sourceManager()); - printer.setIncludeDirectives(options.include_directives); - printer.setExpandIncludes(options.expand_includes); + printer.setIncludeDirectives(true); + printer.setExpandIncludes(true); printer.setExpandMacros(options.expand_macros); printer.setSquashNewlines(options.squash_newlines); printer.setIncludeComments(options.include_comments); diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index 23d0280e6..b56da9268 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -10,8 +10,6 @@ mod ffi { /// Options for the syntax printer #[derive(Clone, Copy)] struct SlangPrintOpts { - include_directives: bool, - expand_includes: bool, expand_macros: bool, include_comments: bool, squash_newlines: bool, diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index a7b8baa45..e9c043d98 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -6,7 +6,7 @@ use std::fs::File; use std::io::{BufWriter, Write}; -use clap::{ArgAction, Args}; +use clap::Args; use indexmap::IndexSet; use tokio::runtime::Runtime; @@ -20,49 +20,45 @@ use crate::target::TargetSet; use bender_slang::{SlangContextExt, SlangPrintOpts, SyntaxTreeExt}; -// TODO(fischeti): Clean up the arguments and options. -// At the moment, they are just directly mirroring the Slang API. -// for debugging purposes. /// Pickle files #[derive(Args, Debug)] pub struct PickleArgs { - /// Additional source files to pickle + /// Additional source files to pickle, which are not part of the manifest. files: Vec, /// The output file (defaults to stdout) - // TODO(fischeti): Actually implement this. #[arg(short, long)] output: Option, /// Only include sources that match the given target - #[arg(short, long, action = ArgAction::Append, global = true)] + #[arg(short, long)] pub target: Vec, /// Specify package to show sources for - #[arg(short, long, action = ArgAction::Append, global = true)] + #[arg(short, long)] pub package: Vec, /// Specify package to exclude from sources - #[arg(long, action = ArgAction::Append, global = true)] + #[arg(long)] pub exclude: Vec, /// Exclude all dependencies, i.e. only top level or specified package(s) - #[arg(long, global = true)] + #[arg(long)] pub no_deps: bool, - /// Additional include directory - #[arg(short = 'I', action = ArgAction::Append)] + /// Additional include directory, which are not part of the manifest. + #[arg(short = 'I')] include_dir: Vec, - /// Additional preprocessor definition - #[arg(short = 'D', action = ArgAction::Append)] + /// Additional preprocessor definition, which are not part of the manifest. + #[arg(short = 'D')] define: Vec, - /// The prefix to add to all names + /// A prefix to add to all names (modules, packages, interfaces) #[arg(long, help_heading = "Slang Options")] prefix: Option, - /// The suffix to add to all names + /// A suffix to add to all names (modules, packages, interfaces) #[arg(long, help_heading = "Slang Options")] suffix: Option, @@ -70,25 +66,17 @@ pub struct PickleArgs { #[arg(long, help_heading = "Slang Options")] exclude_rename: Vec, - /// Whether to include preprocessor directives - #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Slang Options")] - include_directives: bool, - - /// Whether to expand include directives - #[arg(long, default_value_t = true, action = ArgAction::SetFalse, help_heading = "Slang Options")] - expand_includes: bool, - - /// Whether to expand macros - #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Slang Options")] + /// Expand macros in the output + #[arg(long, help_heading = "Slang Options")] expand_macros: bool, - /// Whether to strip comments - #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Slang Options")] + /// Strip comments from the output + #[arg(long, help_heading = "Slang Options")] strip_comments: bool, - /// Whether to strip newlines - #[arg(long, default_value_t = false, action = ArgAction::SetTrue, help_heading = "Slang Options")] - strip_newlines: bool, + /// Squash newlines in the output + #[arg(long, help_heading = "Slang Options")] + squash_newlines: bool, /// Dump the syntax trees as JSON instead of the source code #[arg(long, help_heading = "Slang Options")] @@ -134,11 +122,9 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { .collect::>>()?; let print_opts = SlangPrintOpts { - include_directives: args.include_directives, - expand_includes: args.expand_includes, expand_macros: args.expand_macros, include_comments: !args.strip_comments, - squash_newlines: args.strip_newlines, + squash_newlines: args.squash_newlines, }; // Setup Output Writer, either to file or stdout From 56ef0db6707ef21ff8287e1aa58ec321258ca4cd Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 14:09:51 +0100 Subject: [PATCH 25/46] bender-slang: Emit error when parsing fails --- crates/bender-slang/cpp/slang_bridge.cpp | 30 +++++++++++++++++++++--- crates/bender-slang/cpp/slang_bridge.h | 4 ++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 315c16759..2beeac7a3 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -4,11 +4,14 @@ #include "slang_bridge.h" #include "bender-slang/src/lib.rs.h" +#include "slang/diagnostics/DiagnosticEngine.h" +#include "slang/diagnostics/TextDiagnosticClient.h" #include "slang/syntax/CSTSerializer.h" #include "slang/syntax/SyntaxPrinter.h" #include "slang/syntax/SyntaxVisitor.h" #include "slang/text/Json.h" +#include #include using namespace slang; @@ -25,7 +28,9 @@ using std::vector; // Create a new SlangContext instance std::unique_ptr new_slang_context() { return std::make_unique(); } -SlangContext::SlangContext() {} +SlangContext::SlangContext() : diagEngine(sourceManager), diagClient(std::make_shared()) { + diagEngine.addClient(diagClient); +} // Set the include paths for the preprocessor void SlangContext::set_includes(const rust::Vec& incs) { @@ -45,10 +50,11 @@ void SlangContext::set_defines(const rust::Vec& defs) { // Parses the given file and returns a syntax tree, if successful std::shared_ptr SlangContext::parse_file(rust::Str path) { + string_view pathView(path.data(), path.size()); Bag options; options.set(ppOptions); - auto result = SyntaxTree::fromFile(string_view(path.data(), path.size()), sourceManager, options); + auto result = SyntaxTree::fromFile(pathView, sourceManager, options); if (!result) { auto& err = result.error(); @@ -56,7 +62,25 @@ std::shared_ptr SlangContext::parse_file(rust::Str path) { throw std::runtime_error(msg); } - return *result; + auto tree = *result; + diagClient->clear(); + diagEngine.clearIncludeStack(); + + bool hasErrors = false; + for (const auto& diag : tree->diagnostics()) { + hasErrors |= diag.isError(); + diagEngine.issue(diag); + } + + if (hasErrors) { + std::string rendered = diagClient->getString(); + if (rendered.empty()) { + rendered = "Failed to parse '" + std::string(pathView) + "'."; + } + throw std::runtime_error(rendered); + } + + return tree; } // Rewriter that adds prefix/suffix to module and instantiated hierarchy names diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index 64f8232c7..b10b157a6 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -5,6 +5,8 @@ #define BENDER_SLANG_BRIDGE_H #include "rust/cxx.h" +#include "slang/diagnostics/DiagnosticEngine.h" +#include "slang/diagnostics/TextDiagnosticClient.h" #include "slang/driver/Driver.h" #include "slang/syntax/SyntaxTree.h" @@ -26,6 +28,8 @@ class SlangContext { private: slang::SourceManager sourceManager; slang::parsing::PreprocessorOptions ppOptions; + slang::DiagnosticEngine diagEngine; + std::shared_ptr diagClient; }; std::unique_ptr new_slang_context(); From 8b423bd2ef44f331764658470d4b979d1cc3b0c9 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 15:00:09 +0100 Subject: [PATCH 26/46] Wrap FFI types with safe wrappers Introduce safe wrapper types for the opaque FFI objects and move the extension methods into proper Rust structs to provide a clearer, idiomatic API and safer ownership semantics. - Add SyntaxTree wrapper around SharedPtr with Clone, display, as_debug, rename, and fmt impls (Display/Debug). - Add SlangContext wrapper around UniquePtr with new, set_includes, set_defines, and parse returning a SyntaxTree. - Replace new_session() to return SlangContext instead of raw pointer. - Update callers: remove use of extension traits and change formatting at pickling to use Debug/Display impls (write!("{:?}", renamed_tree)). --- crates/bender-slang/src/lib.rs | 107 +++++++++++++++++++++------------ src/cmd/pickle.rs | 4 +- 2 files changed, 69 insertions(+), 42 deletions(-) diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index b56da9268..e05683a63 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -54,72 +54,99 @@ mod ffi { } } -/// Extension trait for SyntaxTree -// TODO(fischeti): Consider using a wrapper to implement traits like Debug and Display -// instead of an extension trait. This would be more idiomatic in Rust. -pub trait SyntaxTreeExt { - fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &Vec) -> Self; - fn display(&self, options: SlangPrintOpts) -> String; - fn as_debug(&self) -> String; +/// Wrapper around an opaque Slang syntax tree. +pub struct SyntaxTree { + inner: SharedPtr, } -impl SyntaxTreeExt for SharedPtr { +impl Clone for SyntaxTree { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl SyntaxTree { /// Renames all names in the syntax tree with the given prefix and suffix - fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &Vec) -> Self { + pub fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &Vec) -> Self { if prefix.is_none() && suffix.is_none() { return self.clone(); } - ffi::rename( - self.clone(), - prefix.unwrap_or(""), - suffix.unwrap_or(""), - excludes, - ) + Self { + inner: ffi::rename( + self.inner.clone(), + prefix.unwrap_or(""), + suffix.unwrap_or(""), + excludes, + ), + } } /// Displays the syntax tree as a string with the given options - fn display(&self, options: SlangPrintOpts) -> String { - ffi::print_tree(self.clone(), options) + pub fn display(&self, options: SlangPrintOpts) -> String { + ffi::print_tree(self.inner.clone(), options) } - fn as_debug(&self) -> String { - ffi::dump_tree_json(self.clone()) + pub fn as_debug(&self) -> String { + ffi::dump_tree_json(self.inner.clone()) } } -/// Extension trait for SlangContext -pub trait SlangContextExt { - fn set_includes(&mut self, includes: &Vec) -> &mut Self; - fn set_defines(&mut self, defines: &Vec) -> &mut Self; - fn parse( - &mut self, - path: &str, - ) -> Result, Box>; +impl std::fmt::Display for SyntaxTree { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let options = SlangPrintOpts { + expand_macros: false, + include_comments: true, + squash_newlines: false, + }; + f.write_str(&self.display(options)) + } } -impl SlangContextExt for UniquePtr { - /// Sets the include directories - fn set_includes(&mut self, includes: &Vec) -> &mut Self { - self.pin_mut().set_includes(&includes); +impl std::fmt::Debug for SyntaxTree { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.as_debug()) + } +} + +/// Wrapper around an opaque Slang context. +pub struct SlangContext { + inner: UniquePtr, +} + +impl SlangContext { + /// Creates a new Slang session. + pub fn new() -> Self { + Self { + inner: ffi::new_slang_context(), + } + } + + /// Sets the include directories. + pub fn set_includes(&mut self, includes: &Vec) -> &mut Self { + self.inner.pin_mut().set_includes(includes); self } - /// Sets the preprocessor defines - fn set_defines(&mut self, defines: &Vec) -> &mut Self { - self.pin_mut().set_defines(&defines); + /// Sets the preprocessor defines. + pub fn set_defines(&mut self, defines: &Vec) -> &mut Self { + self.inner.pin_mut().set_defines(defines); self } - /// Parses a source file and returns the syntax tree - fn parse( + /// Parses a source file and returns the syntax tree. + pub fn parse( &mut self, path: &str, - ) -> Result, Box> { - Ok(self.pin_mut().parse_file(path)?) + ) -> Result> { + Ok(SyntaxTree { + inner: self.inner.pin_mut().parse_file(path)?, + }) } } /// Creates a new Slang session -pub fn new_session() -> UniquePtr { - ffi::new_slang_context() +pub fn new_session() -> SlangContext { + SlangContext::new() } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index e9c043d98..d1c2e6fe0 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -18,7 +18,7 @@ use crate::sess::{Session, SessionIo}; use crate::src::{SourceFile, SourceType}; use crate::target::TargetSet; -use bender_slang::{SlangContextExt, SlangPrintOpts, SyntaxTreeExt}; +use bender_slang::SlangPrintOpts; /// Pickle files #[derive(Args, Debug)] @@ -197,7 +197,7 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { if !first_item { write!(writer, ",")?; } - write!(writer, "{}", renamed_tree.as_debug())?; + write!(writer, "{:?}", renamed_tree)?; first_item = false; } else { write!(writer, "{}", renamed_tree.display(print_opts))?; From 681794b0b57939df990e5d239ab64524a0a03f47 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 18:10:37 +0100 Subject: [PATCH 27/46] pickle: Filter out unreachable SyntaxTrees --- crates/bender-slang/cpp/slang_bridge.cpp | 94 ++++++++++++++++++++++++ crates/bender-slang/cpp/slang_bridge.h | 14 ++++ crates/bender-slang/src/lib.rs | 78 ++++++++++++++++++++ src/cmd/pickle.rs | 91 ++++++++++++++--------- 4 files changed, 244 insertions(+), 33 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 2beeac7a3..029548552 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -11,7 +11,9 @@ #include "slang/syntax/SyntaxVisitor.h" #include "slang/text/Json.h" +#include #include +#include #include using namespace slang; @@ -83,6 +85,15 @@ std::shared_ptr SlangContext::parse_file(rust::Str path) { return tree; } +std::unique_ptr SlangContext::parse_files(const rust::Vec& paths) { + auto out = std::make_unique(); + out->trees.reserve(paths.size()); + for (const auto& path : paths) { + out->trees.push_back(parse_file(path)); + } + return out; +} + // Rewriter that adds prefix/suffix to module and instantiated hierarchy names class SuffixPrefixRewriter : public SyntaxRewriter { public: @@ -259,3 +270,86 @@ rust::String dump_tree_json(std::shared_ptr tree) { // Convert string_view to rust::String return rust::String(std::string(writer.view())); } + +std::unique_ptr new_syntax_trees() { return std::make_unique(); } + +void append_trees(SyntaxTrees& dst, const SyntaxTrees& src) { + dst.trees.reserve(dst.trees.size() + src.trees.size()); + for (const auto& tree : src.trees) { + dst.trees.push_back(tree); + } +} + +rust::Vec reachable_tree_indices(const SyntaxTrees& trees, const rust::Vec& tops) { + const auto& treeVec = trees.trees; + + // Build a mapping from declared symbol names to the index of the tree that declares them + std::unordered_map nameToTreeIndex; + for (size_t i = 0; i < treeVec.size(); ++i) { + const auto& metadata = treeVec[i]->getMetadata(); + for (auto name : metadata.getDeclaredSymbols()) { + nameToTreeIndex.emplace(name, i); + } + } + + // Build a dependency graph where each tree points to the trees that declare symbols it references + std::vector> deps(treeVec.size()); + for (size_t i = 0; i < treeVec.size(); ++i) { + const auto& metadata = treeVec[i]->getMetadata(); + std::unordered_set seen; + for (auto ref : metadata.getReferencedSymbols()) { + auto it = nameToTreeIndex.find(ref); + // Avoid duplicate dependencies in case of multiple references to the same symbol + if (it != nameToTreeIndex.end() && seen.insert(it->second).second) { + deps[i].push_back(it->second); + } + } + } + + // Map the top module names to their corresponding tree indices + std::vector startIndices; + startIndices.reserve(tops.size()); + for (const auto& top : tops) { + std::string_view name(top.data(), top.size()); + auto it = nameToTreeIndex.find(name); + if (it == nameToTreeIndex.end()) { + throw std::runtime_error("Top module not found in any parsed source file: " + std::string(name)); + } else { + startIndices.push_back(it->second); + } + } + + // Perform a DFS from the top modules to find all reachable trees + std::vector reachable(treeVec.size(), false); + std::function dfs = [&](size_t index) { + if (reachable[index]) { + return; + } + reachable[index] = true; + for (auto dep : deps[index]) { + dfs(dep); + } + }; + + for (auto start : startIndices) { + dfs(start); + } + + // Collect the indices of reachable trees and return as rust::Vec + rust::Vec result; + for (size_t i = 0; i < reachable.size(); ++i) { + if (reachable[i]) { + result.push_back(static_cast(i)); + } + } + return result; +} + +std::size_t tree_count(const SyntaxTrees& trees) { return trees.trees.size(); } + +std::shared_ptr tree_at(const SyntaxTrees& trees, std::size_t index) { + if (index >= trees.trees.size()) { + throw std::runtime_error("Tree index out of bounds."); + } + return trees.trees[index]; +} diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index b10b157a6..9e2ff59d9 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -10,11 +10,14 @@ #include "slang/driver/Driver.h" #include "slang/syntax/SyntaxTree.h" +#include +#include #include #include #include struct SlangPrintOpts; +struct SyntaxTrees; class SlangContext { public: @@ -24,6 +27,7 @@ class SlangContext { void set_defines(const rust::Vec& defines); std::shared_ptr parse_file(rust::Str path); + std::unique_ptr parse_files(const rust::Vec& paths); private: slang::SourceManager sourceManager; @@ -41,4 +45,14 @@ rust::String print_tree(std::shared_ptr tree, SlangPr rust::String dump_tree_json(std::shared_ptr tree); +struct SyntaxTrees { + std::vector> trees; +}; + +std::unique_ptr new_syntax_trees(); +void append_trees(SyntaxTrees& dst, const SyntaxTrees& src); +rust::Vec reachable_tree_indices(const SyntaxTrees& trees, const rust::Vec& tops); +std::size_t tree_count(const SyntaxTrees& trees); +std::shared_ptr tree_at(const SyntaxTrees& trees, std::size_t index); + #endif // BENDER_SLANG_BRIDGE_H diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index e05683a63..a23326792 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -26,6 +26,8 @@ mod ffi { /// Opaque type for the Slang SyntaxTree #[namespace = "slang::syntax"] type SyntaxTree; + /// Opaque type for a batch of parsed syntax trees. + type SyntaxTrees; /// Create a new persistent context fn new_slang_context() -> UniquePtr; @@ -37,6 +39,18 @@ mod ffi { /// Parse all added sources. Returns a syntax tree on success, or an error message on failure. fn parse_file(self: Pin<&mut SlangContext>, path: &str) -> Result>; + /// Parse multiple source files and return a batch of syntax trees. + fn parse_files(self: Pin<&mut SlangContext>, paths: &Vec) -> Result>; + /// Create an empty syntax-tree batch. + fn new_syntax_trees() -> UniquePtr; + /// Appends trees from src into dst. + fn append_trees(dst: Pin<&mut SyntaxTrees>, src: &SyntaxTrees); + /// Computes reachable tree indices from the provided top names. + fn reachable_tree_indices(trees: &SyntaxTrees, tops: &Vec) -> Result>; + /// Returns the number of trees in the batch. + fn tree_count(trees: &SyntaxTrees) -> usize; + /// Returns tree at index from the batch. + fn tree_at(trees: &SyntaxTrees, index: usize) -> Result>; /// Rename names in the syntax tree with a given prefix and suffix fn rename( @@ -115,6 +129,60 @@ pub struct SlangContext { inner: UniquePtr, } +/// Wrapper around an opaque batch of syntax trees. +pub struct SyntaxTrees { + inner: UniquePtr, +} + +impl SyntaxTrees { + /// Creates an empty syntax-tree batch. + pub fn new() -> Self { + Self { + inner: ffi::new_syntax_trees(), + } + } + + /// Appends all trees from src into self. + pub fn append_trees(&mut self, src: &SyntaxTrees) { + ffi::append_trees( + self.inner.pin_mut(), + src.inner.as_ref().expect("syntax trees pointer must be valid"), + ); + } + + /// Returns tree count in this batch. + pub fn len(&self) -> usize { + ffi::tree_count(self.inner.as_ref().expect("syntax trees pointer must be valid")) + } + + /// Returns true if the batch contains no trees. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns indices reachable from top names. + pub fn reachable_indices( + &self, + tops: &Vec, + ) -> Result, Box> { + let indices = ffi::reachable_tree_indices( + self.inner.as_ref().expect("syntax trees pointer must be valid"), + tops, + )?; + Ok(indices.into_iter().map(|i| i as usize).collect()) + } + + /// Returns a tree at the provided index. + pub fn tree_at(&self, index: usize) -> Result> { + Ok(SyntaxTree { + inner: ffi::tree_at( + self.inner.as_ref().expect("syntax trees pointer must be valid"), + index, + )?, + }) + } +} + impl SlangContext { /// Creates a new Slang session. pub fn new() -> Self { @@ -144,6 +212,16 @@ impl SlangContext { inner: self.inner.pin_mut().parse_file(path)?, }) } + + /// Parses multiple source files and returns a batch of syntax trees. + pub fn parse_files( + &mut self, + paths: &Vec, + ) -> Result> { + Ok(SyntaxTrees { + inner: self.inner.pin_mut().parse_files(paths)?, + }) + } } /// Creates a new Slang session diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index d1c2e6fe0..f753b8e80 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -54,6 +54,10 @@ pub struct PickleArgs { #[arg(short = 'D')] define: Vec, + /// One or more top-level modules used to trim unreachable parsed files. + #[arg(long, help_heading = "Slang Options")] + top: Vec, + /// A prefix to add to all names (modules, packages, interfaces) #[arg(long, help_heading = "Slang Options")] prefix: Option, @@ -142,11 +146,9 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { write!(writer, "[")?; } - let mut first_item = true; - + let mut parsed_trees = bender_slang::SyntaxTrees::new(); + let mut slang = bender_slang::new_session(); for src_group in srcs { - let mut slang = bender_slang::new_session(); - // Collect include directories and defines from the source group and command line arguments. let include_dirs: Vec = src_group .include_dirs @@ -171,37 +173,60 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { slang.set_includes(&include_dirs).set_defines(&defines); // Collect file paths from the source group. - let file_paths = src_group.files.iter().filter_map(|source| match source { - SourceFile::File(path, Some(SourceType::Verilog)) => path.to_str(), - // Vhdl or unknown file types are not supported by Slang, so we emit a warning and skip them. - SourceFile::File(path, _) => { - Warnings::PickleNonVerilogFile(path.to_path_buf()).emit(); - None - } - // Groups should not exist at this point, - // as we have already flattened the sources. - _ => None, - }); - - for file_path in file_paths { - let tree = slang.parse(file_path).map_err(|cause| { - Error::new(format!("Cannot parse file {}: {}", file_path, cause)) - })?; - let renamed_tree = tree.rename( - args.prefix.as_deref(), - args.suffix.as_deref(), - &args.exclude_rename, - ); - if args.ast_json { - // JSON Array Logic: Prepend comma if not the first item - if !first_item { - write!(writer, ",")?; + let file_paths: Vec = src_group + .files + .iter() + .filter_map(|source| match source { + SourceFile::File(path, Some(SourceType::Verilog)) => { + Some(path.to_string_lossy().into_owned()) + } + // Vhdl or unknown file types are not supported by Slang, so we emit a warning and skip them. + SourceFile::File(path, _) => { + Warnings::PickleNonVerilogFile(path.to_path_buf()).emit(); + None } - write!(writer, "{:?}", renamed_tree)?; - first_item = false; - } else { - write!(writer, "{}", renamed_tree.display(print_opts))?; + // Groups should not exist at this point, + // as we have already flattened the sources. + _ => None, + }) + .collect(); + + let group_trees = slang + .parse_files(&file_paths) + .map_err(|cause| Error::new(format!("Cannot parse source file set: {}", cause)))?; + parsed_trees.append_trees(&group_trees); + } + + let reachable = if args.top.is_empty() { + (0..parsed_trees.len()).collect::>() + } else { + parsed_trees + .reachable_indices(&args.top) + .map_err(|cause| Error::new(format!("Cannot trim parsed trees by --top: {}", cause)))? + }; + + let mut first_item = true; + for idx in reachable { + let tree = parsed_trees.tree_at(idx).map_err(|cause| { + Error::new(format!( + "Cannot access parsed tree at index {}: {}", + idx, cause + )) + })?; + let renamed_tree = tree.rename( + args.prefix.as_deref(), + args.suffix.as_deref(), + &args.exclude_rename, + ); + if args.ast_json { + // JSON Array Logic: Prepend comma if not the first item + if !first_item { + write!(writer, ",")?; } + write!(writer, "{:?}", renamed_tree)?; + first_item = false; + } else { + write!(writer, "{}", renamed_tree.display(print_opts))?; } } From abe16d8c3d4cee2079fca0c08528532b39a58bcc Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 18:49:48 +0100 Subject: [PATCH 28/46] bender-slang: Use typed errors with `thiserror` --- Cargo.lock | 1 + crates/bender-slang/Cargo.toml | 1 + crates/bender-slang/src/lib.rs | 86 +++++++++++++++++++++++++--------- src/cmd/pickle.rs | 15 ++---- src/error.rs | 7 +++ 5 files changed, 75 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0382476c6..68d036af7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,7 @@ dependencies = [ "cmake", "cxx", "cxx-build", + "thiserror", ] [[package]] diff --git a/crates/bender-slang/Cargo.toml b/crates/bender-slang/Cargo.toml index 92e14714b..b660dcca0 100644 --- a/crates/bender-slang/Cargo.toml +++ b/crates/bender-slang/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] cxx = "1.0.194" +thiserror = "2.0.12" [build-dependencies] cmake = "0.1.57" diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index a23326792..3e6e1c438 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -2,9 +2,24 @@ // Tim Fischer use cxx::{SharedPtr, UniquePtr}; +use thiserror::Error; pub use ffi::SlangPrintOpts; +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum SlangError { + #[error("Failed to parse file: {message}")] + Parse { message: String }, + #[error("Failed to parse files: {message}")] + ParseFiles { message: String }, + #[error("Failed to trim files by top modules: {message}")] + TrimByTop { message: String }, + #[error("Failed to access parsed syntax tree: {message}")] + TreeAccess { message: String }, +} + #[cxx::bridge] mod ffi { /// Options for the syntax printer @@ -40,7 +55,10 @@ mod ffi { /// Parse all added sources. Returns a syntax tree on success, or an error message on failure. fn parse_file(self: Pin<&mut SlangContext>, path: &str) -> Result>; /// Parse multiple source files and return a batch of syntax trees. - fn parse_files(self: Pin<&mut SlangContext>, paths: &Vec) -> Result>; + fn parse_files( + self: Pin<&mut SlangContext>, + paths: &Vec, + ) -> Result>; /// Create an empty syntax-tree batch. fn new_syntax_trees() -> UniquePtr; /// Appends trees from src into dst. @@ -83,7 +101,12 @@ impl Clone for SyntaxTree { impl SyntaxTree { /// Renames all names in the syntax tree with the given prefix and suffix - pub fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &Vec) -> Self { + pub fn rename( + &self, + prefix: Option<&str>, + suffix: Option<&str>, + excludes: &Vec, + ) -> Self { if prefix.is_none() && suffix.is_none() { return self.clone(); } @@ -146,13 +169,19 @@ impl SyntaxTrees { pub fn append_trees(&mut self, src: &SyntaxTrees) { ffi::append_trees( self.inner.pin_mut(), - src.inner.as_ref().expect("syntax trees pointer must be valid"), + src.inner + .as_ref() + .expect("syntax trees pointer must be valid"), ); } /// Returns tree count in this batch. pub fn len(&self) -> usize { - ffi::tree_count(self.inner.as_ref().expect("syntax trees pointer must be valid")) + ffi::tree_count( + self.inner + .as_ref() + .expect("syntax trees pointer must be valid"), + ) } /// Returns true if the batch contains no trees. @@ -161,24 +190,31 @@ impl SyntaxTrees { } /// Returns indices reachable from top names. - pub fn reachable_indices( - &self, - tops: &Vec, - ) -> Result, Box> { + pub fn reachable_indices(&self, tops: &Vec) -> Result> { let indices = ffi::reachable_tree_indices( - self.inner.as_ref().expect("syntax trees pointer must be valid"), + self.inner + .as_ref() + .expect("syntax trees pointer must be valid"), tops, - )?; + ) + .map_err(|cause| SlangError::TrimByTop { + message: cause.to_string(), + })?; Ok(indices.into_iter().map(|i| i as usize).collect()) } /// Returns a tree at the provided index. - pub fn tree_at(&self, index: usize) -> Result> { + pub fn tree_at(&self, index: usize) -> Result { Ok(SyntaxTree { inner: ffi::tree_at( - self.inner.as_ref().expect("syntax trees pointer must be valid"), + self.inner + .as_ref() + .expect("syntax trees pointer must be valid"), index, - )?, + ) + .map_err(|cause| SlangError::TreeAccess { + message: cause.to_string(), + })?, }) } } @@ -204,22 +240,26 @@ impl SlangContext { } /// Parses a source file and returns the syntax tree. - pub fn parse( - &mut self, - path: &str, - ) -> Result> { + pub fn parse(&mut self, path: &str) -> Result { Ok(SyntaxTree { - inner: self.inner.pin_mut().parse_file(path)?, + inner: self + .inner + .pin_mut() + .parse_file(path) + .map_err(|cause| SlangError::Parse { + message: cause.to_string(), + })?, }) } /// Parses multiple source files and returns a batch of syntax trees. - pub fn parse_files( - &mut self, - paths: &Vec, - ) -> Result> { + pub fn parse_files(&mut self, paths: &Vec) -> Result { Ok(SyntaxTrees { - inner: self.inner.pin_mut().parse_files(paths)?, + inner: self.inner.pin_mut().parse_files(paths).map_err(|cause| { + SlangError::ParseFiles { + message: cause.to_string(), + } + })?, }) } } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index f753b8e80..6e703cab8 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -191,28 +191,19 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { }) .collect(); - let group_trees = slang - .parse_files(&file_paths) - .map_err(|cause| Error::new(format!("Cannot parse source file set: {}", cause)))?; + let group_trees = slang.parse_files(&file_paths)?; parsed_trees.append_trees(&group_trees); } let reachable = if args.top.is_empty() { (0..parsed_trees.len()).collect::>() } else { - parsed_trees - .reachable_indices(&args.top) - .map_err(|cause| Error::new(format!("Cannot trim parsed trees by --top: {}", cause)))? + parsed_trees.reachable_indices(&args.top)? }; let mut first_item = true; for idx in reachable { - let tree = parsed_trees.tree_at(idx).map_err(|cause| { - Error::new(format!( - "Cannot access parsed tree at index {}: {}", - idx, cause - )) - })?; + let tree = parsed_trees.tree_at(idx)?; let renamed_tree = tree.rename( args.prefix.as_deref(), args.suffix.as_deref(), diff --git a/src/error.rs b/src/error.rs index 02a1b9cc9..0b0f42580 100644 --- a/src/error.rs +++ b/src/error.rs @@ -145,3 +145,10 @@ impl From for Error { Error::chain("Cannot startup runtime.".to_string(), err) } } + +#[cfg(feature = "slang")] +impl From for Error { + fn from(err: bender_slang::SlangError) -> Error { + Error::chain("Slang error:", err) + } +} From a42d446b9aa3a1abcd9201108404f7a93720d73a Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 22:18:07 +0100 Subject: [PATCH 29/46] bender-slang: Unwrap instead of expect --- crates/bender-slang/src/lib.rs | 40 ++++++++++------------------------ 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index 3e6e1c438..aec79e0a2 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -167,21 +167,12 @@ impl SyntaxTrees { /// Appends all trees from src into self. pub fn append_trees(&mut self, src: &SyntaxTrees) { - ffi::append_trees( - self.inner.pin_mut(), - src.inner - .as_ref() - .expect("syntax trees pointer must be valid"), - ); + ffi::append_trees(self.inner.pin_mut(), src.inner.as_ref().unwrap()); } /// Returns tree count in this batch. pub fn len(&self) -> usize { - ffi::tree_count( - self.inner - .as_ref() - .expect("syntax trees pointer must be valid"), - ) + ffi::tree_count(self.inner.as_ref().unwrap()) } /// Returns true if the batch contains no trees. @@ -191,29 +182,22 @@ impl SyntaxTrees { /// Returns indices reachable from top names. pub fn reachable_indices(&self, tops: &Vec) -> Result> { - let indices = ffi::reachable_tree_indices( - self.inner - .as_ref() - .expect("syntax trees pointer must be valid"), - tops, - ) - .map_err(|cause| SlangError::TrimByTop { - message: cause.to_string(), - })?; + let indices = + ffi::reachable_tree_indices(self.inner.as_ref().unwrap(), tops).map_err(|cause| { + SlangError::TrimByTop { + message: cause.to_string(), + } + })?; Ok(indices.into_iter().map(|i| i as usize).collect()) } /// Returns a tree at the provided index. pub fn tree_at(&self, index: usize) -> Result { Ok(SyntaxTree { - inner: ffi::tree_at( - self.inner - .as_ref() - .expect("syntax trees pointer must be valid"), - index, - ) - .map_err(|cause| SlangError::TreeAccess { - message: cause.to_string(), + inner: ffi::tree_at(self.inner.as_ref().unwrap(), index).map_err(|cause| { + SlangError::TreeAccess { + message: cause.to_string(), + } })?, }) } From 3d2cf0526980a368a76ae351cbd36ee9c156a59a Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 00:17:00 +0100 Subject: [PATCH 30/46] bender-slang: Add documentation --- CHANGELOG.md | 3 +++ README.md | 30 ++++++++++++++++++++++++++++++ crates/bender-slang/README.md | 19 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 crates/bender-slang/README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 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 83f1e6d75..76551d858 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] @@ -546,6 +552,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 dc4610cd7eba7f9106c1e759e1d6f1ec38f8f829 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 00:32:16 +0100 Subject: [PATCH 31/46] pickle: Actually include additional sourcefiles --- src/cmd/pickle.rs | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 6e703cab8..ad0d88929 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -5,9 +5,10 @@ use std::fs::File; use std::io::{BufWriter, Write}; +use std::path::Path; use clap::Args; -use indexmap::IndexSet; +use indexmap::{IndexMap, IndexSet}; use tokio::runtime::Runtime; use crate::cmd::sources::get_passed_targets; @@ -15,7 +16,7 @@ use crate::config::{Validate, ValidationContext}; use crate::diagnostic::Warnings; use crate::error::*; use crate::sess::{Session, SessionIo}; -use crate::src::{SourceFile, SourceType}; +use crate::src::{SourceFile, SourceGroup, SourceType}; use crate::target::TargetSet; use bender_slang::SlangPrintOpts; @@ -119,12 +120,44 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { .unwrap_or_default(); // Flatten and validate the sources. - let srcs = srcs + let mut srcs = srcs .flatten() .into_iter() .map(|f| f.validate(&ValidationContext::default())) .collect::>>()?; + if !args.files.is_empty() { + let include_dirs = args + .include_dir + .iter() + .map(|d| sess.intern_path(Path::new(d))) + .collect::>(); + let defines = args + .define + .iter() + .map(|d| { + let mut parts = d.splitn(2, '='); + let name = parts.next().unwrap_or_default().trim().to_string(); + let value = parts + .next() + .map(|v| sess.intern_string(v.trim().to_string())); + (name, value) + }) + .collect::>(); + let files = args + .files + .iter() + .map(|f| SourceFile::File(sess.intern_path(Path::new(f)), Some(SourceType::Verilog))) + .collect::>(); + + srcs.push(SourceGroup { + include_dirs, + defines, + files, + ..SourceGroup::default() + }); + } + let print_opts = SlangPrintOpts { expand_macros: args.expand_macros, include_comments: !args.strip_comments, From ff4a930a1fb128ff6fadba06e47094f8a1713c0a Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 00:37:23 +0100 Subject: [PATCH 32/46] bender-slang: Fix windows build bender-slang: Fix windows build 2 --- .github/workflows/ci.yml | 4 +-- .github/workflows/cli_regression.yml | 2 +- crates/bender-slang/build.rs | 38 ++++++++++++++++++---------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8b13df41..4420a4f4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,9 +47,9 @@ jobs: with: toolchain: stable - name: Build - run: cargo build --all-features --release + run: cargo build --all-features - name: Cargo Test - run: cargo test --workspace --all-features --release + run: cargo test --workspace --all-features - name: Run unit-tests run: tests/run_all.sh shell: bash diff --git a/.github/workflows/cli_regression.yml b/.github/workflows/cli_regression.yml index 91069aefe..bfdcf9bd7 100644 --- a/.github/workflows/cli_regression.yml +++ b/.github/workflows/cli_regression.yml @@ -29,7 +29,7 @@ jobs: with: toolchain: stable - name: Run CLI Regression - run: cargo test --all-features --test cli_regression --release -- --ignored + run: cargo test --all-features --test cli_regression -- --ignored env: BENDER_TEST_GOLDEN_BRANCH: ${{ github.base_ref }} diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 4a9e46a6e..ac946b9a9 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -5,6 +5,13 @@ fn main() { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap(); let build_profile = std::env::var("PROFILE").unwrap(); + let cmake_profile = match (target_env.as_str(), build_profile.as_str()) { + // Rust MSVC links against the release CRT; + // using C++ Debug CRT (/MDd) causes LNK2038 mismatches. + ("msvc", _) => "RelWithDebInfo", + (_, "debug") => "Debug", + _ => "Release", + }; // Create the configuration builder let mut slang_lib = cmake::Config::new("vendor/slang"); @@ -20,7 +27,7 @@ fn main() { ]; // Add debug define if in debug build - if build_profile == "debug" { + if build_profile == "debug" && !(target_env == "msvc") { common_cxx_defines.push(("SLANG_DEBUG", "1")); common_cxx_defines.push(("SLANG_ASSERT_ENABLED", "1")); }; @@ -41,7 +48,8 @@ fn main() { // Disable finding system-installed packages, we want to fetch and build them from source. .define("CMAKE_DISABLE_FIND_PACKAGE_fmt", "ON") .define("CMAKE_DISABLE_FIND_PACKAGE_mimalloc", "ON") - .define("CMAKE_DISABLE_FIND_PACKAGE_Boost", "ON"); + .define("CMAKE_DISABLE_FIND_PACKAGE_Boost", "ON") + .profile(cmake_profile); // Apply common defines and flags for (def, value) in common_cxx_defines.iter() { @@ -54,22 +62,24 @@ fn main() { // Build the slang library let dst = slang_lib.build(); + let lib_dir = dst.join("lib"); // Configure Linker to find Slang static library - println!("cargo:rustc-link-search=native={}/lib", dst.display()); + println!("cargo:rustc-link-search=native={}", lib_dir.display()); println!("cargo:rustc-link-lib=static=svlang"); - // Link the additional libraries based on build profile and OS - match (build_profile.as_str(), target_env.as_str()) { - ("release", _) | (_, "msvc") => { - println!("cargo:rustc-link-lib=static=fmt"); - println!("cargo:rustc-link-lib=static=mimalloc"); - } - ("debug", _) => { - println!("cargo:rustc-link-lib=static=fmtd"); - println!("cargo:rustc-link-lib=static=mimalloc-debug"); - } - _ => unreachable!(), + // Link the additional libraries based on build profile. + let (fmt_lib, mimalloc_lib) = match (target_env.as_str(), build_profile.as_str()) { + ("msvc", _) => ("fmt", "mimalloc"), + (_, "debug") => ("fmtd", "mimalloc-debug"), + _ => ("fmt", "mimalloc"), + }; + + println!("cargo:rustc-link-lib=static={fmt_lib}"); + println!("cargo:rustc-link-lib=static={mimalloc_lib}"); + + if target_os == "windows" { + println!("cargo:rustc-link-lib=advapi32"); } // Compile the C++ Bridge From 9bc1f24017fd0232f8ca4ed096d50ad655779064 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 15:11:07 +0100 Subject: [PATCH 33/46] bender-slang: Clippy fixes and clean up --- crates/bender-slang/build.rs | 2 +- crates/bender-slang/src/lib.rs | 13 ++++++++++--- src/cmd/pickle.rs | 6 +++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index ac946b9a9..7db0396f7 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -27,7 +27,7 @@ fn main() { ]; // Add debug define if in debug build - if build_profile == "debug" && !(target_env == "msvc") { + if build_profile == "debug" && (target_env != "msvc") { common_cxx_defines.push(("SLANG_DEBUG", "1")); common_cxx_defines.push(("SLANG_ASSERT_ENABLED", "1")); }; diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index aec79e0a2..e5e070be4 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -203,6 +203,12 @@ impl SyntaxTrees { } } +impl Default for SyntaxTrees { + fn default() -> Self { + Self::new() + } +} + impl SlangContext { /// Creates a new Slang session. pub fn new() -> Self { @@ -248,7 +254,8 @@ impl SlangContext { } } -/// Creates a new Slang session -pub fn new_session() -> SlangContext { - SlangContext::new() +impl Default for SlangContext { + fn default() -> Self { + Self::new() + } } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index ad0d88929..0bdd5890e 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -19,7 +19,7 @@ use crate::sess::{Session, SessionIo}; use crate::src::{SourceFile, SourceGroup, SourceType}; use crate::target::TargetSet; -use bender_slang::SlangPrintOpts; +use bender_slang::{SlangContext, SlangPrintOpts, SyntaxTrees}; /// Pickle files #[derive(Args, Debug)] @@ -179,8 +179,8 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { write!(writer, "[")?; } - let mut parsed_trees = bender_slang::SyntaxTrees::new(); - let mut slang = bender_slang::new_session(); + let mut parsed_trees = SyntaxTrees::new(); + let mut slang = SlangContext::new(); for src_group in srcs { // Collect include directories and defines from the source group and command line arguments. let include_dirs: Vec = src_group From e989272960712019612adf7886ab93f870b8fd17 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 18 Feb 2026 21:20:57 +0100 Subject: [PATCH 34/46] bender-slang(lib): Refactor to respect lifetime of C++ objects --- crates/bender-slang/cpp/slang_bridge.cpp | 104 ++++++----- crates/bender-slang/cpp/slang_bridge.h | 30 +-- crates/bender-slang/src/lib.rs | 225 ++++++++++------------- src/cmd/pickle.rs | 26 ++- 4 files changed, 176 insertions(+), 209 deletions(-) diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 029548552..755690992 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -27,8 +27,7 @@ using std::string; using std::string_view; using std::vector; -// Create a new SlangContext instance -std::unique_ptr new_slang_context() { return std::make_unique(); } +std::unique_ptr new_slang_session() { return std::make_unique(); } SlangContext::SlangContext() : diagEngine(sourceManager), diagClient(std::make_shared()) { diagEngine.addClient(diagClient); @@ -36,62 +35,76 @@ SlangContext::SlangContext() : diagEngine(sourceManager), diagClient(std::make_s // Set the include paths for the preprocessor void SlangContext::set_includes(const rust::Vec& incs) { - ppOptions.additionalIncludePaths.clear(); for (const auto& inc : incs) { - ppOptions.additionalIncludePaths.emplace_back(std::string(inc)); + std::string incStr(inc.data(), inc.size()); + if (auto ec = sourceManager.addUserDirectories(incStr); ec) { + throw std::runtime_error("Failed to add include directory '" + incStr + "': " + ec.message()); + } } } // Sets the preprocessor defines void SlangContext::set_defines(const rust::Vec& defs) { - ppOptions.predefines.clear(); + ppOptions.predefines.reserve(defs.size()); for (const auto& def : defs) { - ppOptions.predefines.emplace_back(std::string(def)); + ppOptions.predefines.emplace_back(def.data(), def.size()); } } -// Parses the given file and returns a syntax tree, if successful -std::shared_ptr SlangContext::parse_file(rust::Str path) { - string_view pathView(path.data(), path.size()); +std::vector> SlangContext::parse_files(const rust::Vec& paths) { Bag options; options.set(ppOptions); - auto result = SyntaxTree::fromFile(pathView, sourceManager, options); + std::vector> out; + out.reserve(paths.size()); - if (!result) { - auto& err = result.error(); - std::string msg = "System Error loading '" + std::string(err.second) + "': " + err.first.message(); - throw std::runtime_error(msg); - } + for (const auto& path : paths) { + string_view pathView(path.data(), path.size()); + auto result = SyntaxTree::fromFile(pathView, sourceManager, options); - auto tree = *result; - diagClient->clear(); - diagEngine.clearIncludeStack(); + if (!result) { + auto& err = result.error(); + std::string msg = "System Error loading '" + std::string(err.second) + "': " + err.first.message(); + throw std::runtime_error(msg); + } - bool hasErrors = false; - for (const auto& diag : tree->diagnostics()) { - hasErrors |= diag.isError(); - diagEngine.issue(diag); - } + auto tree = *result; + diagClient->clear(); + diagEngine.clearIncludeStack(); - if (hasErrors) { - std::string rendered = diagClient->getString(); - if (rendered.empty()) { - rendered = "Failed to parse '" + std::string(pathView) + "'."; + bool hasErrors = false; + for (const auto& diag : tree->diagnostics()) { + hasErrors |= diag.isError(); + diagEngine.issue(diag); } - throw std::runtime_error(rendered); + + if (hasErrors) { + std::string rendered = diagClient->getString(); + if (rendered.empty()) { + rendered = "Failed to parse '" + std::string(pathView) + "'."; + } + throw std::runtime_error(rendered); + } + + out.push_back(tree); } - return tree; + return out; } -std::unique_ptr SlangContext::parse_files(const rust::Vec& paths) { - auto out = std::make_unique(); - out->trees.reserve(paths.size()); - for (const auto& path : paths) { - out->trees.push_back(parse_file(path)); +void SlangSession::parse_group(const rust::Vec& files, const rust::Vec& includes, + const rust::Vec& defines) { + auto ctx = std::make_unique(); + ctx->set_includes(includes); + ctx->set_defines(defines); + + auto parsed = ctx->parse_files(files); + allTrees.reserve(allTrees.size() + parsed.size()); + for (const auto& tree : parsed) { + allTrees.push_back(tree); } - return out; + + contexts.push_back(std::move(ctx)); } // Rewriter that adds prefix/suffix to module and instantiated hierarchy names @@ -271,17 +284,8 @@ rust::String dump_tree_json(std::shared_ptr tree) { return rust::String(std::string(writer.view())); } -std::unique_ptr new_syntax_trees() { return std::make_unique(); } - -void append_trees(SyntaxTrees& dst, const SyntaxTrees& src) { - dst.trees.reserve(dst.trees.size() + src.trees.size()); - for (const auto& tree : src.trees) { - dst.trees.push_back(tree); - } -} - -rust::Vec reachable_tree_indices(const SyntaxTrees& trees, const rust::Vec& tops) { - const auto& treeVec = trees.trees; +rust::Vec reachable_tree_indices(const SlangSession& session, const rust::Vec& tops) { + const auto& treeVec = session.trees(); // Build a mapping from declared symbol names to the index of the tree that declares them std::unordered_map nameToTreeIndex; @@ -345,11 +349,11 @@ rust::Vec reachable_tree_indices(const SyntaxTrees& trees, const return result; } -std::size_t tree_count(const SyntaxTrees& trees) { return trees.trees.size(); } +std::size_t tree_count(const SlangSession& session) { return session.trees().size(); } -std::shared_ptr tree_at(const SyntaxTrees& trees, std::size_t index) { - if (index >= trees.trees.size()) { +std::shared_ptr tree_at(const SlangSession& session, std::size_t index) { + if (index >= session.trees().size()) { throw std::runtime_error("Tree index out of bounds."); } - return trees.trees[index]; + return session.trees()[index]; } diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index 9e2ff59d9..a309b5a95 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -17,7 +17,6 @@ #include struct SlangPrintOpts; -struct SyntaxTrees; class SlangContext { public: @@ -26,8 +25,7 @@ class SlangContext { void set_includes(const rust::Vec& includes); void set_defines(const rust::Vec& defines); - std::shared_ptr parse_file(rust::Str path); - std::unique_ptr parse_files(const rust::Vec& paths); + std::vector> parse_files(const rust::Vec& paths); private: slang::SourceManager sourceManager; @@ -36,7 +34,19 @@ class SlangContext { std::shared_ptr diagClient; }; -std::unique_ptr new_slang_context(); +class SlangSession { + public: + void parse_group(const rust::Vec& files, const rust::Vec& includes, + const rust::Vec& defines); + + const std::vector>& trees() const { return allTrees; } + + private: + std::vector> contexts; + std::vector> allTrees; +}; + +std::unique_ptr new_slang_session(); std::shared_ptr rename(std::shared_ptr tree, rust::Str prefix, rust::Str suffix, const rust::Vec& excludes); @@ -45,14 +55,8 @@ rust::String print_tree(std::shared_ptr tree, SlangPr rust::String dump_tree_json(std::shared_ptr tree); -struct SyntaxTrees { - std::vector> trees; -}; - -std::unique_ptr new_syntax_trees(); -void append_trees(SyntaxTrees& dst, const SyntaxTrees& src); -rust::Vec reachable_tree_indices(const SyntaxTrees& trees, const rust::Vec& tops); -std::size_t tree_count(const SyntaxTrees& trees); -std::shared_ptr tree_at(const SyntaxTrees& trees, std::size_t index); +rust::Vec reachable_tree_indices(const SlangSession& session, const rust::Vec& tops); +std::size_t tree_count(const SlangSession& session); +std::shared_ptr tree_at(const SlangSession& session, std::size_t index); #endif // BENDER_SLANG_BRIDGE_H diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index e5e070be4..715c96f0c 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -1,6 +1,8 @@ // Copyright (c) 2025 ETH Zurich // Tim Fischer +use std::marker::PhantomData; + use cxx::{SharedPtr, UniquePtr}; use thiserror::Error; @@ -10,10 +12,8 @@ pub type Result = std::result::Result; #[derive(Debug, Error)] pub enum SlangError { - #[error("Failed to parse file: {message}")] - Parse { message: String }, - #[error("Failed to parse files: {message}")] - ParseFiles { message: String }, + #[error("Failed to parse source group: {message}")] + ParseGroup { message: String }, #[error("Failed to trim files by top modules: {message}")] TrimByTop { message: String }, #[error("Failed to access parsed syntax tree: {message}")] @@ -32,45 +32,30 @@ mod ffi { unsafe extern "C++" { include!("bender-slang/cpp/slang_bridge.h"); - // Include Slang header to define SyntaxTree type for CXX include!("slang/syntax/SyntaxTree.h"); - /// Opaque type for the Slang Context - type SlangContext; + /// Opaque session that owns parse contexts and syntax trees. + type SlangSession; - /// Opaque type for the Slang SyntaxTree + /// Opaque type for the Slang syntax tree. #[namespace = "slang::syntax"] type SyntaxTree; - /// Opaque type for a batch of parsed syntax trees. - type SyntaxTrees; - - /// Create a new persistent context - fn new_slang_context() -> UniquePtr; - - /// Set the include directories - fn set_includes(self: Pin<&mut SlangContext>, includes: &Vec); - /// Set the preprocessor defines - fn set_defines(self: Pin<&mut SlangContext>, defines: &Vec); - - /// Parse all added sources. Returns a syntax tree on success, or an error message on failure. - fn parse_file(self: Pin<&mut SlangContext>, path: &str) -> Result>; - /// Parse multiple source files and return a batch of syntax trees. - fn parse_files( - self: Pin<&mut SlangContext>, - paths: &Vec, - ) -> Result>; - /// Create an empty syntax-tree batch. - fn new_syntax_trees() -> UniquePtr; - /// Appends trees from src into dst. - fn append_trees(dst: Pin<&mut SyntaxTrees>, src: &SyntaxTrees); - /// Computes reachable tree indices from the provided top names. - fn reachable_tree_indices(trees: &SyntaxTrees, tops: &Vec) -> Result>; - /// Returns the number of trees in the batch. - fn tree_count(trees: &SyntaxTrees) -> usize; - /// Returns tree at index from the batch. - fn tree_at(trees: &SyntaxTrees, index: usize) -> Result>; - - /// Rename names in the syntax tree with a given prefix and suffix + + fn new_slang_session() -> UniquePtr; + + fn parse_group( + self: Pin<&mut SlangSession>, + files: &Vec, + includes: &Vec, + defines: &Vec, + ) -> Result<()>; + + fn reachable_tree_indices(session: &SlangSession, tops: &Vec) -> Result>; + + fn tree_count(session: &SlangSession) -> usize; + + fn tree_at(session: &SlangSession, index: usize) -> Result>; + fn rename( tree: SharedPtr, prefix: &str, @@ -78,59 +63,62 @@ mod ffi { excludes: &Vec, ) -> SharedPtr; - /// Print a specific tree fn print_tree(tree: SharedPtr, options: SlangPrintOpts) -> String; - /// Dump the syntax tree as JSON for debugging purposes fn dump_tree_json(tree: SharedPtr) -> String; } } -/// Wrapper around an opaque Slang syntax tree. -pub struct SyntaxTree { +/// Public owner for all parsed trees and parse contexts. +pub struct SlangSession { + inner: UniquePtr, +} + +/// Borrowed syntax-tree handle tied to the owning session lifetime. +pub struct SyntaxTree<'a> { inner: SharedPtr, + _session: PhantomData<&'a SlangSession>, } -impl Clone for SyntaxTree { +impl<'a> Clone for SyntaxTree<'a> { fn clone(&self) -> Self { Self { inner: self.inner.clone(), + _session: PhantomData, } } } -impl SyntaxTree { - /// Renames all names in the syntax tree with the given prefix and suffix - pub fn rename( - &self, - prefix: Option<&str>, - suffix: Option<&str>, - excludes: &Vec, - ) -> Self { +impl<'a> SyntaxTree<'a> { + /// Renames all names in the syntax tree with the given prefix and suffix. + pub fn rename(&self, prefix: Option<&str>, suffix: Option<&str>, excludes: &[String]) -> Self { if prefix.is_none() && suffix.is_none() { return self.clone(); } + let excludes = excludes.to_vec(); Self { inner: ffi::rename( self.inner.clone(), prefix.unwrap_or(""), suffix.unwrap_or(""), - excludes, + &excludes, ), + _session: PhantomData, } } - /// Displays the syntax tree as a string with the given options + /// Displays the syntax tree as a string with the given options. pub fn display(&self, options: SlangPrintOpts) -> String { ffi::print_tree(self.inner.clone(), options) } + /// Dumps the syntax tree as JSON for debugging purposes. pub fn as_debug(&self) -> String { ffi::dump_tree_json(self.inner.clone()) } } -impl std::fmt::Display for SyntaxTree { +impl std::fmt::Display for SyntaxTree<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let options = SlangPrintOpts { expand_macros: false, @@ -141,49 +129,62 @@ impl std::fmt::Display for SyntaxTree { } } -impl std::fmt::Debug for SyntaxTree { +impl std::fmt::Debug for SyntaxTree<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.as_debug()) } } -/// Wrapper around an opaque Slang context. -pub struct SlangContext { - inner: UniquePtr, -} - -/// Wrapper around an opaque batch of syntax trees. -pub struct SyntaxTrees { - inner: UniquePtr, -} - -impl SyntaxTrees { - /// Creates an empty syntax-tree batch. +impl SlangSession { pub fn new() -> Self { Self { - inner: ffi::new_syntax_trees(), + inner: ffi::new_slang_session(), } } - /// Appends all trees from src into self. - pub fn append_trees(&mut self, src: &SyntaxTrees) { - ffi::append_trees(self.inner.pin_mut(), src.inner.as_ref().unwrap()); + /// Parses one source group with scoped include directories and defines. + pub fn parse_group( + &mut self, + files: &[String], + includes: &[String], + defines: &[String], + ) -> Result> { + let files_vec = files.to_vec(); + let includes_vec = includes.to_vec(); + let defines_vec = defines.to_vec(); + + let start = self.tree_count(); + self.inner + .pin_mut() + .parse_group(&files_vec, &includes_vec, &defines_vec) + .map_err(|cause| SlangError::ParseGroup { + message: cause.to_string(), + })?; + + let end = self.tree_count(); + Ok((start..end).collect()) } - /// Returns tree count in this batch. - pub fn len(&self) -> usize { + /// Returns the total number of parsed syntax trees in the session. + pub fn tree_count(&self) -> usize { ffi::tree_count(self.inner.as_ref().unwrap()) } - /// Returns true if the batch contains no trees. - pub fn is_empty(&self) -> bool { - self.len() == 0 + /// Returns all parsed syntax trees in the session. + pub fn all_trees(&self) -> Result>> { + let count = self.tree_count(); + let mut out = Vec::with_capacity(count); + for idx in 0..count { + out.push(self.tree(idx)?); + } + Ok(out) } - /// Returns indices reachable from top names. - pub fn reachable_indices(&self, tops: &Vec) -> Result> { + /// Returns the indices of syntax trees reachable from the given top modules. + pub fn reachable_indices(&self, tops: &[String]) -> Result> { + let tops = tops.to_vec(); let indices = - ffi::reachable_tree_indices(self.inner.as_ref().unwrap(), tops).map_err(|cause| { + ffi::reachable_tree_indices(self.inner.as_ref().unwrap(), &tops).map_err(|cause| { SlangError::TrimByTop { message: cause.to_string(), } @@ -191,70 +192,30 @@ impl SyntaxTrees { Ok(indices.into_iter().map(|i| i as usize).collect()) } - /// Returns a tree at the provided index. - pub fn tree_at(&self, index: usize) -> Result { - Ok(SyntaxTree { - inner: ffi::tree_at(self.inner.as_ref().unwrap(), index).map_err(|cause| { - SlangError::TreeAccess { - message: cause.to_string(), - } - })?, - }) - } -} - -impl Default for SyntaxTrees { - fn default() -> Self { - Self::new() - } -} - -impl SlangContext { - /// Creates a new Slang session. - pub fn new() -> Self { - Self { - inner: ffi::new_slang_context(), + /// Returns syntax trees reachable from the given top modules. + pub fn reachable_trees(&self, tops: &[String]) -> Result>> { + let indices = self.reachable_indices(tops)?; + let mut out = Vec::with_capacity(indices.len()); + for idx in indices { + out.push(self.tree(idx)?); } + Ok(out) } - /// Sets the include directories. - pub fn set_includes(&mut self, includes: &Vec) -> &mut Self { - self.inner.pin_mut().set_includes(includes); - self - } - - /// Sets the preprocessor defines. - pub fn set_defines(&mut self, defines: &Vec) -> &mut Self { - self.inner.pin_mut().set_defines(defines); - self - } - - /// Parses a source file and returns the syntax tree. - pub fn parse(&mut self, path: &str) -> Result { + /// Returns a handle to the syntax tree at the given index. + pub fn tree(&self, index: usize) -> Result> { Ok(SyntaxTree { - inner: self - .inner - .pin_mut() - .parse_file(path) - .map_err(|cause| SlangError::Parse { - message: cause.to_string(), - })?, - }) - } - - /// Parses multiple source files and returns a batch of syntax trees. - pub fn parse_files(&mut self, paths: &Vec) -> Result { - Ok(SyntaxTrees { - inner: self.inner.pin_mut().parse_files(paths).map_err(|cause| { - SlangError::ParseFiles { + inner: ffi::tree_at(self.inner.as_ref().unwrap(), index).map_err(|cause| { + SlangError::TreeAccess { message: cause.to_string(), } })?, + _session: PhantomData, }) } } -impl Default for SlangContext { +impl Default for SlangSession { fn default() -> Self { Self::new() } diff --git a/src/cmd/pickle.rs b/src/cmd/pickle.rs index 0bdd5890e..50e210a48 100644 --- a/src/cmd/pickle.rs +++ b/src/cmd/pickle.rs @@ -19,7 +19,7 @@ use crate::sess::{Session, SessionIo}; use crate::src::{SourceFile, SourceGroup, SourceType}; use crate::target::TargetSet; -use bender_slang::{SlangContext, SlangPrintOpts, SyntaxTrees}; +use bender_slang::{SlangPrintOpts, SlangSession}; /// Pickle files #[derive(Args, Debug)] @@ -179,16 +179,17 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { write!(writer, "[")?; } - let mut parsed_trees = SyntaxTrees::new(); - let mut slang = SlangContext::new(); + let mut session = SlangSession::new(); for src_group in srcs { - // Collect include directories and defines from the source group and command line arguments. + // Collect include directories from the source group and command line arguments. let include_dirs: Vec = src_group .include_dirs .iter() .chain(src_group.export_incdirs.values().flatten()) .map(|path| path.to_string_lossy().into_owned()) .chain(args.include_dir.iter().cloned()) + .collect::>() + .into_iter() .collect(); // Collect defines from the source group and command line arguments. @@ -200,11 +201,10 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { None => def.to_string(), }) .chain(args.define.iter().cloned()) + .collect::>() + .into_iter() .collect(); - // Set the include directories and defines in the Slang session. - slang.set_includes(&include_dirs).set_defines(&defines); - // Collect file paths from the source group. let file_paths: Vec = src_group .files @@ -224,19 +224,17 @@ pub fn run(sess: &Session, args: PickleArgs) -> Result<()> { }) .collect(); - let group_trees = slang.parse_files(&file_paths)?; - parsed_trees.append_trees(&group_trees); + session.parse_group(&file_paths, &include_dirs, &defines)?; } - let reachable = if args.top.is_empty() { - (0..parsed_trees.len()).collect::>() + let trees = if args.top.is_empty() { + session.all_trees()? } else { - parsed_trees.reachable_indices(&args.top)? + session.reachable_trees(&args.top)? }; let mut first_item = true; - for idx in reachable { - let tree = parsed_trees.tree_at(idx)?; + for tree in trees { let renamed_tree = tree.rename( args.prefix.as_deref(), args.suffix.as_deref(), From 1a04ee90d6c161cc32c68bec8ec00d9f2693b084 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 22 Feb 2026 18:27:37 +0100 Subject: [PATCH 35/46] bender-slang: Cannonicalize include paths on windows --- Cargo.lock | 1 + crates/bender-slang/Cargo.toml | 3 +++ crates/bender-slang/src/lib.rs | 22 +++++++++++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 68d036af7..1f30d10ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,7 @@ dependencies = [ "cmake", "cxx", "cxx-build", + "dunce", "thiserror", ] diff --git a/crates/bender-slang/Cargo.toml b/crates/bender-slang/Cargo.toml index b660dcca0..bdf157918 100644 --- a/crates/bender-slang/Cargo.toml +++ b/crates/bender-slang/Cargo.toml @@ -7,6 +7,9 @@ edition = "2024" cxx = "1.0.194" thiserror = "2.0.12" +[target.'cfg(windows)'.dependencies] +dunce = "1.0.4" + [build-dependencies] cmake = "0.1.57" cxx-build = "1.0.194" diff --git a/crates/bender-slang/src/lib.rs b/crates/bender-slang/src/lib.rs index 715c96f0c..9372e0499 100644 --- a/crates/bender-slang/src/lib.rs +++ b/crates/bender-slang/src/lib.rs @@ -150,7 +150,7 @@ impl SlangSession { defines: &[String], ) -> Result> { let files_vec = files.to_vec(); - let includes_vec = includes.to_vec(); + let includes_vec = normalize_include_dirs(includes)?; let defines_vec = defines.to_vec(); let start = self.tree_count(); @@ -220,3 +220,23 @@ impl Default for SlangSession { Self::new() } } + +#[cfg(windows)] +fn normalize_include_dirs(includes: &[String]) -> Result> { + let mut out = Vec::with_capacity(includes.len()); + for include in includes { + let canonical = dunce::canonicalize(include).map_err(|cause| SlangError::ParseGroup { + message: format!( + "Failed to canonicalize include directory '{}': {}", + include, cause + ), + })?; + out.push(canonical.to_string_lossy().into_owned()); + } + Ok(out) +} + +#[cfg(not(windows))] +fn normalize_include_dirs(includes: &[String]) -> Result> { + Ok(includes.to_vec()) +} From 3092bc8a37653c902c188a5bb27340ff212551ab Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 4 Feb 2026 12:36:18 +0100 Subject: [PATCH 36/46] Add .clang-format and update .gitignore --- .clang-format | 18 ++++++++++++++++++ .gitignore | 12 ++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 .clang-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..4e0c9e095 --- /dev/null +++ b/.clang-format @@ -0,0 +1,18 @@ +--- +Language: Cpp +BasedOnStyle: LLVM + +# 4 spaces everywhere +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +ContinuationIndentWidth: 4 + +# Modern C++ style +Standard: c++20 +ColumnLimit: 120 +PointerAlignment: Left + +# Organize includes +SortIncludes: true +IncludeBlocks: Regroup diff --git a/.gitignore b/.gitignore index 796da3f24..00964c7e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ -.* -!/.ci/ -!.git* -!.travis.yml -/target -/tests/tmp +# Cargo build files +target + +# Temporary test files +tests/**/tmp +tests/**/.bender From 1cfc3ded1ba0360d0ab2f5272811829a98efb562 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 12 Feb 2026 15:40:40 +0100 Subject: [PATCH 37/46] tests: Add pickle testing repo --- tests/pickle/Bender.lock | 1 + tests/pickle/Bender.yml | 15 +++++++++++++ tests/pickle/include/macros.svh | 6 ++++++ tests/pickle/src/bus_intf.sv | 21 +++++++++++++++++++ tests/pickle/src/common_pkg.sv | 13 ++++++++++++ tests/pickle/src/top.sv | 37 +++++++++++++++++++++++++++++++++ 6 files changed, 93 insertions(+) create mode 100644 tests/pickle/Bender.lock create mode 100644 tests/pickle/Bender.yml create mode 100644 tests/pickle/include/macros.svh create mode 100644 tests/pickle/src/bus_intf.sv create mode 100644 tests/pickle/src/common_pkg.sv create mode 100644 tests/pickle/src/top.sv diff --git a/tests/pickle/Bender.lock b/tests/pickle/Bender.lock new file mode 100644 index 000000000..c33c0b6df --- /dev/null +++ b/tests/pickle/Bender.lock @@ -0,0 +1 @@ +packages: {} diff --git a/tests/pickle/Bender.yml b/tests/pickle/Bender.yml new file mode 100644 index 000000000..fdc42d00e --- /dev/null +++ b/tests/pickle/Bender.yml @@ -0,0 +1,15 @@ +package: + name: pickle_repo + +sources: + - defines: + ENABLE_LOGGING: 1 + files: + - src/common_pkg.sv + - src/bus_intf.sv + + - target: top + include_dirs: + - include + files: + - src/top.sv diff --git a/tests/pickle/include/macros.svh b/tests/pickle/include/macros.svh new file mode 100644 index 000000000..a041281aa --- /dev/null +++ b/tests/pickle/include/macros.svh @@ -0,0 +1,6 @@ +// Simple macro to test if includes are resolved correctly +`define LOG(msg) \ + $display("[LOG]: %s", msg); + +// A constant used in the RTL +localparam int unsigned DataWidth = 32; diff --git a/tests/pickle/src/bus_intf.sv b/tests/pickle/src/bus_intf.sv new file mode 100644 index 000000000..bcd581028 --- /dev/null +++ b/tests/pickle/src/bus_intf.sv @@ -0,0 +1,21 @@ +interface bus_intf #( + parameter int Width = 32 +) ( + input logic clk +); + logic [Width-1:0] addr; + logic [Width-1:0] data; + logic valid; + logic ready; + + modport master ( + output addr, data, valid, + input ready + ); + + modport slave ( + input addr, data, valid, + output ready + ); + +endinterface diff --git a/tests/pickle/src/common_pkg.sv b/tests/pickle/src/common_pkg.sv new file mode 100644 index 000000000..7a2d02d59 --- /dev/null +++ b/tests/pickle/src/common_pkg.sv @@ -0,0 +1,13 @@ +package common_pkg; + + typedef enum logic [1:0] { + Idle = 2'b00, + Busy = 2'b01, + Error = 2'b11 + } state_t; + + function automatic logic is_error(state_t s); + return s == Error; + endfunction + +endpackage diff --git a/tests/pickle/src/top.sv b/tests/pickle/src/top.sv new file mode 100644 index 000000000..daaab9edc --- /dev/null +++ b/tests/pickle/src/top.sv @@ -0,0 +1,37 @@ +`include "macros.svh" + +import common_pkg::*; + +module top ( + input logic clk, + input logic rst_n +); + + // Interface Instantiation + bus_intf #(.WIDTH(DATA_WIDTH)) axi_bus ( + .clk(clk) + ); + + // Virtual Interface Type + virtual bus_intf v_if_handle; + + initial begin + v_if_handle = axi_bus; + +`ifdef ENABLE_LOGGING + `LOG("TopModule started successfully!") +`endif + end + + // Type Usage from Package (state_t) + common_pkg::state_t current_state; + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + current_state <= Idle; + end else begin + current_state <= Busy; + end + end + +endmodule From 01020d14b7b70c67d615b73b0ae3dc4bb951f626 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 22:48:46 +0100 Subject: [PATCH 38/46] bender-slang: Add unit and integration tests --- crates/bender-slang/tests/basic.rs | 37 +++++++++++++++ tests/cli_regression.rs | 3 ++ tests/pickle.rs | 75 ++++++++++++++++++++++++++++++ tests/pickle/Bender.yml | 4 ++ tests/pickle/src/broken.sv | 2 + tests/pickle/src/core.sv | 3 ++ tests/pickle/src/leaf.sv | 2 + tests/pickle/src/top.sv | 2 + tests/pickle/src/unused_leaf.sv | 2 + tests/pickle/src/unused_top.sv | 3 ++ 10 files changed, 133 insertions(+) create mode 100644 crates/bender-slang/tests/basic.rs create mode 100644 tests/pickle.rs create mode 100644 tests/pickle/src/broken.sv create mode 100644 tests/pickle/src/core.sv create mode 100644 tests/pickle/src/leaf.sv create mode 100644 tests/pickle/src/unused_leaf.sv create mode 100644 tests/pickle/src/unused_top.sv diff --git a/crates/bender-slang/tests/basic.rs b/crates/bender-slang/tests/basic.rs new file mode 100644 index 000000000..03d45538f --- /dev/null +++ b/crates/bender-slang/tests/basic.rs @@ -0,0 +1,37 @@ +use std::path::PathBuf; + +fn fixture_path(rel: &str) -> String { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .join("tests/pickle") + .join(rel) + .canonicalize() + .expect("valid fixture path") + .to_string_lossy() + .into_owned() +} + +#[test] +fn parse_valid_file_succeeds() { + let mut session = bender_slang::SlangSession::new(); + let files = vec![fixture_path("src/top.sv")]; + let includes = vec![fixture_path("include")]; + let defines = vec![]; + assert!(session.parse_group(&files, &includes, &defines).is_ok()); + assert_eq!(session.tree_count(), 1); +} + +#[test] +fn parse_invalid_file_returns_parse_error() { + let mut session = bender_slang::SlangSession::new(); + let files = vec![fixture_path("src/broken.sv")]; + let includes = vec![]; + let defines = vec![]; + let result = session.parse_group(&files, &includes, &defines); + + match result { + Err(bender_slang::SlangError::ParseGroup { .. }) => {} + Err(other) => panic!("expected SlangError::ParseGroup, got {other}"), + Ok(_) => panic!("expected parse to fail"), + } +} diff --git a/tests/cli_regression.rs b/tests/cli_regression.rs index c75b37bfe..66f652eeb 100644 --- a/tests/cli_regression.rs +++ b/tests/cli_regression.rs @@ -161,5 +161,8 @@ regression_tests! { packages: &["packages"], packages_graph: &["packages", "--graph"], packages_flat: &["packages", "--flat"], + // Enable once the golden binary is built with `slang` support. + // pickle_basic: &["pickle", "--target", "top"], + // pickle_top_trim: &["pickle", "--target", "top", "--top", "top"], } diff --git a/tests/pickle.rs b/tests/pickle.rs new file mode 100644 index 000000000..307b8bf2b --- /dev/null +++ b/tests/pickle.rs @@ -0,0 +1,75 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +#[cfg(feature = "slang")] +mod tests { + use assert_cmd::cargo; + + fn run_pickle(args: &[&str]) -> String { + let mut full_args = vec!["-d", "tests/pickle", "pickle"]; + full_args.extend(args); + + let out = cargo::cargo_bin_cmd!() + .args(&full_args) + .output() + .expect("Failed to execute bender binary"); + + assert!( + out.status.success(), + "pickle command failed.\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + + String::from_utf8(out.stdout).expect("stdout must be utf-8") + } + + #[test] + fn pickle_top_trim_filters_unreachable_modules() { + let full = run_pickle(&["--target", "top"]); + assert!(full.contains("module unused_top;")); + assert!(full.contains("module unused_leaf;")); + + let trimmed = run_pickle(&["--target", "top", "--top", "top"]); + assert!(trimmed.contains("module top (")); + assert!(trimmed.contains("module core;")); + assert!(trimmed.contains("module leaf;")); + assert!(!trimmed.contains("module unused_top;")); + assert!(!trimmed.contains("module unused_leaf;")); + } + + #[test] + fn pickle_rename_applies_prefix_and_suffix() { + let renamed = run_pickle(&[ + "--target", "top", "--top", "top", "--prefix", "p_", "--suffix", "_s", + ]); + + assert!(renamed.contains("module p_top_s (")); + assert!(renamed.contains("module p_core_s;")); + assert!(renamed.contains("module p_leaf_s;")); + } + + #[test] + fn pickle_exclude_rename_keeps_selected_names() { + let renamed = run_pickle(&[ + "--target", + "top", + "--top", + "top", + "--prefix", + "p_", + "--suffix", + "_s", + "--exclude-rename", + "top", + "--exclude-rename", + "core", + ]); + + assert!(renamed.contains("module top (")); + assert!(renamed.contains("module core;")); + assert!(renamed.contains("module p_leaf_s;")); + assert!(!renamed.contains("module p_top_s (")); + assert!(!renamed.contains("module p_core_s;")); + } +} diff --git a/tests/pickle/Bender.yml b/tests/pickle/Bender.yml index fdc42d00e..c5724f952 100644 --- a/tests/pickle/Bender.yml +++ b/tests/pickle/Bender.yml @@ -7,6 +7,10 @@ sources: files: - src/common_pkg.sv - src/bus_intf.sv + - src/leaf.sv + - src/core.sv + - src/unused_leaf.sv + - src/unused_top.sv - target: top include_dirs: diff --git a/tests/pickle/src/broken.sv b/tests/pickle/src/broken.sv new file mode 100644 index 000000000..0fbcdfa50 --- /dev/null +++ b/tests/pickle/src/broken.sv @@ -0,0 +1,2 @@ +module broken(; +endmodule diff --git a/tests/pickle/src/core.sv b/tests/pickle/src/core.sv new file mode 100644 index 000000000..ce4f14c49 --- /dev/null +++ b/tests/pickle/src/core.sv @@ -0,0 +1,3 @@ +module core; + leaf u_leaf(); +endmodule diff --git a/tests/pickle/src/leaf.sv b/tests/pickle/src/leaf.sv new file mode 100644 index 000000000..5a7a547a2 --- /dev/null +++ b/tests/pickle/src/leaf.sv @@ -0,0 +1,2 @@ +module leaf; +endmodule diff --git a/tests/pickle/src/top.sv b/tests/pickle/src/top.sv index daaab9edc..2365ab895 100644 --- a/tests/pickle/src/top.sv +++ b/tests/pickle/src/top.sv @@ -7,6 +7,8 @@ module top ( input logic rst_n ); + core u_core(); + // Interface Instantiation bus_intf #(.WIDTH(DATA_WIDTH)) axi_bus ( .clk(clk) diff --git a/tests/pickle/src/unused_leaf.sv b/tests/pickle/src/unused_leaf.sv new file mode 100644 index 000000000..f7d261d00 --- /dev/null +++ b/tests/pickle/src/unused_leaf.sv @@ -0,0 +1,2 @@ +module unused_leaf; +endmodule diff --git a/tests/pickle/src/unused_top.sv b/tests/pickle/src/unused_top.sv new file mode 100644 index 000000000..a62e36504 --- /dev/null +++ b/tests/pickle/src/unused_top.sv @@ -0,0 +1,3 @@ +module unused_top; + unused_leaf u_unused_leaf(); +endmodule From 46f325449091967a659bb0098191d95bb3836f0c Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 23:46:21 +0100 Subject: [PATCH 39/46] bender-slang: Add `.clangd` file for IDE support --- .clangd | 15 ++++++++++ .gitignore | 3 ++ crates/bender-slang/build.rs | 38 +++++++++++++++++++++++- crates/bender-slang/cpp/slang_bridge.cpp | 3 -- crates/bender-slang/cpp/slang_bridge.h | 4 +-- 5 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 .clangd diff --git a/.clangd b/.clangd new file mode 100644 index 000000000..e1f5bf28a --- /dev/null +++ b/.clangd @@ -0,0 +1,15 @@ +If: + PathMatch: (^|.*/)crates/bender-slang/cpp/.*\.(h|hpp|hh|c|cc|cpp|cxx)$ +CompileFlags: + Add: + - -std=c++20 + - -fno-cxx-modules + - -I. + - -I../../../crates + - -I../vendor/slang/include + - -I../vendor/slang/external + - -I../../../target/slang-generated-include + - -I../../../target/cxxbridge + - -DSLANG_USE_MIMALLOC=1 + - -DSLANG_USE_THREADS=1 + - -DSLANG_BOOST_SINGLE_HEADER=1 diff --git a/.gitignore b/.gitignore index 00964c7e3..eeeddced9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ target # Temporary test files tests/**/tmp tests/**/.bender + +# clangd +.cache/clangd diff --git a/crates/bender-slang/build.rs b/crates/bender-slang/build.rs index 7db0396f7..faf39b8f3 100644 --- a/crates/bender-slang/build.rs +++ b/crates/bender-slang/build.rs @@ -1,6 +1,37 @@ // Copyright (c) 2025 ETH Zurich // Tim Fischer +#[cfg(unix)] +// We create a symlink from the generated include directory to a stable location in the target directory +// so that tools like clangd can find the headers without needing to know the exact OUT_DIR path. +// This is purely for improving the development experience and is not necessary for the build itself. +fn refresh_include_symlink(generated_include_dir: &std::path::Path) { + use std::ffi::OsStr; + use std::fs; + use std::os::unix::fs::symlink; + use std::path::PathBuf; + + let Ok(out_dir) = std::env::var("OUT_DIR") else { + return; + }; + let out_dir = PathBuf::from(out_dir); + + let Some(target_root) = out_dir + .ancestors() + .find(|path| path.file_name() == Some(OsStr::new("target"))) + else { + return; + }; + + let stable_link = target_root.join("slang-generated-include"); + let _ = fs::remove_file(&stable_link); + let _ = fs::remove_dir_all(&stable_link); + let _ = symlink(generated_include_dir, &stable_link); +} + +#[cfg(not(unix))] +fn refresh_include_symlink(_generated_include_dir: &std::path::Path) {} + fn main() { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap(); @@ -64,6 +95,11 @@ fn main() { let dst = slang_lib.build(); let lib_dir = dst.join("lib"); + // Create a symlink for the generated include directory + if target_os == "linux" || target_os == "macos" { + refresh_include_symlink(&dst.join("include")); + } + // Configure Linker to find Slang static library println!("cargo:rustc-link-search=native={}", lib_dir.display()); println!("cargo:rustc-link-lib=static=svlang"); @@ -97,7 +133,7 @@ fn main() { let compiler = std::env::var("CXX").unwrap_or_else(|_| "g++".to_string()); // We search for the static libstdc++ file using g++ let output = std::process::Command::new(&compiler) - .args(&["-print-file-name=libstdc++.a"]) + .args(["-print-file-name=libstdc++.a"]) .output() .expect("Failed to run g++"); diff --git a/crates/bender-slang/cpp/slang_bridge.cpp b/crates/bender-slang/cpp/slang_bridge.cpp index 755690992..7e7c02eb7 100644 --- a/crates/bender-slang/cpp/slang_bridge.cpp +++ b/crates/bender-slang/cpp/slang_bridge.cpp @@ -4,8 +4,6 @@ #include "slang_bridge.h" #include "bender-slang/src/lib.rs.h" -#include "slang/diagnostics/DiagnosticEngine.h" -#include "slang/diagnostics/TextDiagnosticClient.h" #include "slang/syntax/CSTSerializer.h" #include "slang/syntax/SyntaxPrinter.h" #include "slang/syntax/SyntaxVisitor.h" @@ -17,7 +15,6 @@ #include using namespace slang; -using namespace slang::driver; using namespace slang::syntax; using namespace slang::parsing; diff --git a/crates/bender-slang/cpp/slang_bridge.h b/crates/bender-slang/cpp/slang_bridge.h index a309b5a95..faa4431d1 100644 --- a/crates/bender-slang/cpp/slang_bridge.h +++ b/crates/bender-slang/cpp/slang_bridge.h @@ -7,13 +7,13 @@ #include "rust/cxx.h" #include "slang/diagnostics/DiagnosticEngine.h" #include "slang/diagnostics/TextDiagnosticClient.h" -#include "slang/driver/Driver.h" +#include "slang/parsing/Preprocessor.h" #include "slang/syntax/SyntaxTree.h" +#include "slang/text/SourceManager.h" #include #include #include -#include #include struct SlangPrintOpts; From 0486a5817a76de82b6c2c6e5c9f284a742569beb Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 16 Feb 2026 23:56:27 +0100 Subject: [PATCH 40/46] ci: Add clang-format check + separate rustfmt --- .github/workflows/ci.yml | 3 --- .github/workflows/formatting.yml | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/formatting.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4420a4f4e..d7bcfff33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,13 +26,10 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust}} - components: rustfmt - name: Build run: cargo build --all-features - name: Cargo Test run: cargo test --workspace --all-features - - name: Format (fix with `cargo fmt`) - run: cargo fmt -- --check - name: Run unit-tests run: tests/run_all.sh shell: bash diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml new file mode 100644 index 000000000..f81cf6392 --- /dev/null +++ b/.github/workflows/formatting.yml @@ -0,0 +1,35 @@ +name: formatting + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +jobs: + rustfmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: rustfmt + - name: Check Rust formatting + run: cargo fmt -- --check + + clang-format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + - name: Check C/C++ formatting + uses: DoozyX/clang-format-lint-action@v0.18 + with: + source: "." + extensions: "h,hpp,c,cc,cpp,cxx" + exclude: "./crates/bender-slang/vendor" From 4aa780618524269314362398a96c890972a3b97d Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 13:38:08 +0100 Subject: [PATCH 41/46] ci: Run on PRs to non-main branches --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7bcfff33..825a89cba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: [master] pull_request: - branches: [master] workflow_dispatch: jobs: From 8831a93af1011f5856d077ff81c274e59af44cfc Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 09:05:02 +0100 Subject: [PATCH 42/46] ci: Add release build jobs --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 825a89cba..162d3d95b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,26 @@ jobs: run: tests/run_all.sh shell: bash + release-build: + name: Release Build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - name: Build (release) + run: cargo build --release --all-features + clippy_check: name: Clippy runs-on: ubuntu-latest From 2fb5ce8769a45ff5681e4cdbf74ccb7d5f57813a Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 11:11:04 +0100 Subject: [PATCH 43/46] ci(release): Clone recursively, use all features, allow dry-run workflow dispatch --- .github/workflows/release.yaml | 38 +++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 2df026bd0..ae5e1f63b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,6 +4,12 @@ on: release: types: [created] workflow_dispatch: + inputs: + publish_assets: + description: "Upload release assets" + required: false + default: false + type: boolean jobs: release_amd64: @@ -61,7 +67,9 @@ jobs: platform: - linux/amd64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + submodules: recursive - name: OS Build run: | export full_tgtname=${{ matrix.os }} @@ -85,6 +93,7 @@ jobs: .github/scripts/package.sh $platform $tgtname; shell: bash - name: Upload Release Asset + if: ${{ github.event_name == 'release' || inputs.publish_assets }} uses: softprops/action-gh-release@v1 with: tag_name: ${{ github.event.release.tag_name }} @@ -106,7 +115,9 @@ jobs: platform: - linux/arm64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + submodules: recursive - name: OS Build run: | export full_tgtname=${{ matrix.os }} @@ -130,6 +141,7 @@ jobs: .github/scripts/package.sh $platform $tgtname; shell: bash - name: Upload Release Asset + if: ${{ github.event_name == 'release' || inputs.publish_assets }} uses: softprops/action-gh-release@v1 with: tag_name: ${{ github.event.release.tag_name }} @@ -141,7 +153,9 @@ jobs: # Use container that supports old GLIBC versions and (hopefully) many linux OSs # container: quay.io/pypa/manylinux2014_x86_64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + submodules: recursive - name: Setup Dockerfile run: | touch Dockerfile @@ -175,6 +189,7 @@ jobs: run: .github/scripts/package.sh amd64 shell: bash - name: Upload Release Asset + if: ${{ github.event_name == 'release' || inputs.publish_assets }} uses: softprops/action-gh-release@v1 with: tag_name: ${{ github.event.release.tag_name }} @@ -186,7 +201,9 @@ jobs: # Use container that supports old GLIBC versions and (hopefully) many linux OSs # container: quay.io/pypa/manylinux2014_aarch64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + submodules: recursive - name: Setup Dockerfile run: | touch Dockerfile @@ -220,6 +237,7 @@ jobs: run: .github/scripts/package.sh arm64 shell: bash - name: Upload Release Asset + if: ${{ github.event_name == 'release' || inputs.publish_assets }} uses: softprops/action-gh-release@v1 with: tag_name: ${{ github.event.release.tag_name }} @@ -229,7 +247,9 @@ jobs: release-macos: runs-on: macos-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + submodules: recursive - name: Install Rust run: | curl --proto '=https' --tlsv1.2 -sSf https://https://sh.rustup.rs | sh -s -- -y --default-toolchain stable @@ -256,6 +276,7 @@ jobs: run: | gtar -czf $ARTIFACT_PATHNAME -C "./target/universal2-apple-darwin/release" --owner=0 --group=0 bender - name: Upload Release Asset + if: ${{ github.event_name == 'release' || inputs.publish_assets }} uses: softprops/action-gh-release@v1 with: tag_name: ${{ github.event.release.tag_name }} @@ -265,13 +286,15 @@ jobs: release-windows: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 + with: + submodules: recursive - name: Install Rust uses: dtolnay/rust-toolchain@stable with: toolchain: stable - name: Build - run: cargo build --release + run: cargo build --release --all-features - name: Get Artifact Name shell: bash run: | @@ -289,6 +312,7 @@ jobs: cp target/release/bender.exe . & 'C:\Program Files\Git\usr\bin\tar.exe' czf $Env:ARTIFACT_PATHNAME --owner=0 --group=0 bender.exe - name: Upload Release Asset + if: ${{ github.event_name == 'release' || inputs.publish_assets }} uses: softprops/action-gh-release@v1 with: tag_name: ${{ github.event.release.tag_name }} From 524e56c7eddb894946c905c132835e490f8fecd9 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 14:35:17 +0100 Subject: [PATCH 44/46] ci(release): Add separate builds for slang and non-slang versions --- .github/scripts/gen_dockerfile.sh | 58 ------ .github/workflows/release.yaml | 312 ++++++++++-------------------- 2 files changed, 100 insertions(+), 270 deletions(-) delete mode 100755 .github/scripts/gen_dockerfile.sh diff --git a/.github/scripts/gen_dockerfile.sh b/.github/scripts/gen_dockerfile.sh deleted file mode 100755 index 512e9700e..000000000 --- a/.github/scripts/gen_dockerfile.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -export filename="Dockerfile" -rm -f $filename -touch $filename - -if [ $(echo $full_tgtname | cut -d ':' -f 1) = "rhel" ]; then - export maj_version=$(echo $full_tgtname | cut -d ':' -f 2) - export full_tgtname=redhat/ubi$(echo $maj_version | cut -d '.' -f 1):$(echo $full_tgtname | cut -d ':' -f 2) - if [ $(echo $full_tgtname | cut -d ':' -f 2 | cut -d '.' -f 1) = '9' ]; then - if [ $(echo $full_tgtname | cut -d ':' -f 2 | cut -d '.' -f 2) = '0' ]; then - export full_tgtname=$full_tgtname.0 - fi - if [ $(echo $full_tgtname | cut -d ':' -f 2 | cut -d '.' -f 2) = '1' ]; then - export full_tgtname=$full_tgtname.0 - fi - fi -fi - -echo "FROM $full_tgtname" >> $filename -echo >> $filename -if [ $(echo $full_tgtname | cut -d ':' -f 1) = "centos" ]; then - echo "RUN sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo" >> $filename - echo "RUN sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo" >> $filename - echo "RUN sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo" >> $filename - - echo "RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*" >> $filename - echo "RUN sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*" >> $filename - echo 'RUN yum group install "Development Tools" -y && yum clean all' >> $filename -fi -if [ $(echo $full_tgtname | cut -d ':' -f 1) = "ubuntu" ]; then - echo 'RUN apt update && apt -y install build-essential curl' >> $filename -fi -if [ $(echo $full_tgtname | cut -d ':' -f 1) = "fedora" ]; then - echo 'RUN dnf -y update && dnf -y install @development-tools' >> $filename -fi -if [ $(echo $full_tgtname | cut -d ':' -f 1) = "debian" ]; then - echo 'RUN apt update && apt -y install build-essential curl gcc make' >> $filename -fi -if [ $(echo $full_tgtname | cut -d ':' -f 1) = "almalinux" ]; then - if [ $(echo $full_tgtname | cut -d ':' -f 2 | cut -d '.' -f 1) = '8' ]; then - echo 'RUN rpm --import https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux' >> $filename - fi - echo 'RUN dnf -y update && dnf -y group install "Development Tools"' >> $filename -fi -if [[ $(echo $full_tgtname | cut -d ':' -f 1) == "redhat"* ]]; then - echo 'RUN dnf -y install gcc' >> $filename -fi -echo >> $filename -echo 'ENV RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo' >> $filename -echo 'ENV PATH=$CARGO_HOME/bin:$PATH' >> $filename -echo >> $filename -echo 'RUN mkdir -p "$CARGO_HOME" && mkdir -p "$RUSTUP_HOME" && \' >> $filename -echo ' curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable && \' >> $filename -echo ' chmod -R a=rwX $CARGO_HOME' >> $filename -echo >> $filename -echo 'WORKDIR /source' >> $filename - diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ae5e1f63b..1506bf5f7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -12,310 +12,198 @@ on: type: boolean jobs: - release_amd64: + release-linux-compat-amd64: runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - rust: - - stable - os: - - centos:7.4.1708 - - centos:7.6.1810 - - centos:7.7.1908 - - centos:7.8.2003 - - centos:7.9.2009 - - ubuntu:18.04 - - ubuntu:20.04 - - ubuntu:22.04 - - ubuntu:24.04 - - fedora:42 - - fedora:43 - - debian:11 - - debian:12 - - debian:13 - - rhel:8.6 - - rhel:8.7 - - rhel:8.8 - - rhel:8.9 - - rhel:8.10 - - rhel:9.0 - - rhel:9.1 - - rhel:9.2 - - rhel:9.3 - - rhel:9.4 - - rhel:9.5 - - rhel:9.6 - - rhel:9.7 - - rhel:10.0 - - rhel:10.1 - - almalinux:8.6 - - almalinux:8.7 - - almalinux:8.8 - - almalinux:8.9 - - almalinux:8.10 - - almalinux:9.0 - - almalinux:9.1 - - almalinux:9.2 - - almalinux:9.3 - - almalinux:9.4 - - almalinux:9.5 - - almalinux:9.6 - - almalinux:9.7 - - almalinux:10.0 - - almalinux:10.1 - platform: - - linux/amd64 steps: - uses: actions/checkout@v6 with: submodules: recursive - - name: OS Build + - name: Build (old-glibc baseline) run: | - export full_tgtname=${{ matrix.os }} - export tgtname=$(echo ${{ matrix.os }} | tr -d ':') - export full_platform=${{ matrix.platform }} - export platform=$(echo ${{ matrix.platform }} | awk -F'/' '{print $NF}') - .github/scripts/gen_dockerfile.sh - docker build ./ -t $tgtname-$platform --platform $full_platform docker run \ -t --rm \ -v "$GITHUB_WORKSPACE:/source" \ - -v "$GITHUB_WORKSPACE/target/$platform/$tgtname:/source/target" \ - --platform $full_platform \ - $tgtname-$platform \ - cargo build --release --all-features; - shell: bash - - name: OS Create Package - run: | - export tgtname=$(echo ${{ matrix.os }} | tr -d ':') - export platform=$(echo ${{ matrix.platform }} | awk -F'/' '{print $NF}') - .github/scripts/package.sh $platform $tgtname; + -v "$GITHUB_WORKSPACE/target/amd64:/source/target" \ + --platform linux/amd64 \ + quay.io/pypa/manylinux2014_x86_64 \ + /bin/bash -lc ' + set -euo pipefail + export RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo + export PATH=$CARGO_HOME/bin:$PATH + mkdir -p "$CARGO_HOME" "$RUSTUP_HOME" + curl -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + cd /source + cargo build --release + ' + - name: Create Package + run: .github/scripts/package.sh amd64 shell: bash - name: Upload Release Asset if: ${{ github.event_name == 'release' || inputs.publish_assets }} uses: softprops/action-gh-release@v1 with: - tag_name: ${{ github.event.release.tag_name }} - files: bender-*.tar.gz + tag_name: ${{ github.event.release.tag_name || github.ref_name }} + files: bender-*-x86_64-linux-gnu.tar.gz env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - release_arm64: + + release-linux-compat-arm64: runs-on: ubuntu-24.04-arm - strategy: - fail-fast: false - matrix: - rust: - - stable - os: - - ubuntu:18.04 - - ubuntu:20.04 - - ubuntu:22.04 - - ubuntu:24.04 - platform: - - linux/arm64 steps: - uses: actions/checkout@v6 with: submodules: recursive - - name: OS Build + - name: Build (old-glibc baseline) run: | - export full_tgtname=${{ matrix.os }} - export tgtname=$(echo ${{ matrix.os }} | tr -d ':') - export full_platform=${{ matrix.platform }} - export platform=$(echo ${{ matrix.platform }} | awk -F'/' '{print $NF}') - .github/scripts/gen_dockerfile.sh - docker build ./ -t $tgtname-$platform --platform $full_platform docker run \ -t --rm \ -v "$GITHUB_WORKSPACE:/source" \ - -v "$GITHUB_WORKSPACE/target/$platform/$tgtname:/source/target" \ - --platform $full_platform \ - $tgtname-$platform \ - cargo build --release --all-features; - shell: bash - - name: OS Create Package - run: | - export tgtname=$(echo ${{ matrix.os }} | tr -d ':') - export platform=$(echo ${{ matrix.platform }} | awk -F'/' '{print $NF}') - .github/scripts/package.sh $platform $tgtname; + -v "$GITHUB_WORKSPACE/target/arm64:/source/target" \ + --platform linux/arm64 \ + quay.io/pypa/manylinux2014_aarch64 \ + /bin/bash -lc ' + set -euo pipefail + export RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo + export PATH=$CARGO_HOME/bin:$PATH + mkdir -p "$CARGO_HOME" "$RUSTUP_HOME" + curl -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + cd /source + cargo build --release + ' + - name: Create Package + run: .github/scripts/package.sh arm64 shell: bash - name: Upload Release Asset if: ${{ github.event_name == 'release' || inputs.publish_assets }} uses: softprops/action-gh-release@v1 with: - tag_name: ${{ github.event.release.tag_name }} - files: bender-*.tar.gz + tag_name: ${{ github.event.release.tag_name || github.ref_name }} + files: bender-*-arm64-linux-gnu.tar.gz env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - release-gnu_amd64: + + release-linux-modern-amd64: runs-on: ubuntu-latest - # Use container that supports old GLIBC versions and (hopefully) many linux OSs - # container: quay.io/pypa/manylinux2014_x86_64 steps: - uses: actions/checkout@v6 with: submodules: recursive - - name: Setup Dockerfile - run: | - touch Dockerfile - echo "FROM quay.io/pypa/manylinux2014_x86_64" >> Dockerfile - echo "RUN sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo" >> Dockerfile - echo "RUN sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo" >> Dockerfile - echo "RUN sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo" >> Dockerfile - - echo "RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*" >> Dockerfile - echo "RUN sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*" >> Dockerfile - echo "RUN yum group install "Development Tools" -y && yum clean all" >> Dockerfile - echo 'ENV RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo' >> Dockerfile - echo 'ENV PATH=$CARGO_HOME/bin:$PATH' >> Dockerfile - echo >> Dockerfile - echo 'RUN mkdir -p "$CARGO_HOME" && mkdir -p "$RUSTUP_HOME" && \' >> Dockerfile - echo ' curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable && \' >> Dockerfile - echo ' chmod -R a=rwX $CARGO_HOME' >> Dockerfile - echo >> Dockerfile - echo 'WORKDIR /source' >> Dockerfile - - name: OS build - run: | - docker build ./ -t manylinux-amd64 --platform linux/amd64 - docker run \ - -t --rm \ - -v "$GITHUB_WORKSPACE:/source" \ - -v "$GITHUB_WORKSPACE/target/amd64:/source/target" \ - --platform linux/amd64 \ - manylinux-amd64 \ - cargo build --release --all-features; - - name: GNU Create Package - run: .github/scripts/package.sh amd64 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - name: Build (all features) + run: cargo build --release --all-features + - name: Create Package shell: bash + run: | + if [[ "$GITHUB_REF" =~ ^refs/tags/v.*$ ]]; then + PKG_VERSION=$(echo "$GITHUB_REF" | sed -n 's/^refs\/tags\/v//p') + else + PKG_VERSION=$(echo "$GITHUB_REF" | sed -n 's/^refs\/tags\///p') + fi + ARTIFACT_PATHNAME="bender-$PKG_VERSION-x86_64-unknown-linux-gnu+slang.tar.gz" + tar -czf "$ARTIFACT_PATHNAME" -C "./target/release" --owner=0 --group=0 bender - name: Upload Release Asset if: ${{ github.event_name == 'release' || inputs.publish_assets }} uses: softprops/action-gh-release@v1 with: - tag_name: ${{ github.event.release.tag_name }} - files: bender-*.tar.gz + tag_name: ${{ github.event.release.tag_name || github.ref_name }} + files: bender-*-x86_64-unknown-linux-gnu+slang.tar.gz env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - release-gnu_arm64: + + release-linux-modern-arm64: runs-on: ubuntu-24.04-arm - # Use container that supports old GLIBC versions and (hopefully) many linux OSs - # container: quay.io/pypa/manylinux2014_aarch64 steps: - uses: actions/checkout@v6 with: submodules: recursive - - name: Setup Dockerfile - run: | - touch Dockerfile - echo "FROM quay.io/pypa/manylinux2014_aarch64" >> Dockerfile - echo "RUN sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo" >> Dockerfile - echo "RUN sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo" >> Dockerfile - echo "RUN sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo" >> Dockerfile - - echo "RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*" >> Dockerfile - echo "RUN sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*" >> Dockerfile - echo "RUN yum group install "Development Tools" -y && yum clean all" >> Dockerfile - echo 'ENV RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo' >> Dockerfile - echo 'ENV PATH=$CARGO_HOME/bin:$PATH' >> Dockerfile - echo >> Dockerfile - echo 'RUN mkdir -p "$CARGO_HOME" && mkdir -p "$RUSTUP_HOME" && \' >> Dockerfile - echo ' curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable && \' >> Dockerfile - echo ' chmod -R a=rwX $CARGO_HOME' >> Dockerfile - echo >> Dockerfile - echo 'WORKDIR /source' >> Dockerfile - - name: OS build - run: | - docker build ./ -t manylinux-arm64 --platform linux/arm64 - docker run \ - -t --rm \ - -v "$GITHUB_WORKSPACE:/source" \ - -v "$GITHUB_WORKSPACE/target/arm64:/source/target" \ - --platform linux/arm64 \ - manylinux-arm64 \ - cargo build --release --all-features; - - name: GNU Create Package - run: .github/scripts/package.sh arm64 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - name: Build (all features) + run: cargo build --release --all-features + - name: Create Package shell: bash + run: | + if [[ "$GITHUB_REF" =~ ^refs/tags/v.*$ ]]; then + PKG_VERSION=$(echo "$GITHUB_REF" | sed -n 's/^refs\/tags\/v//p') + else + PKG_VERSION=$(echo "$GITHUB_REF" | sed -n 's/^refs\/tags\///p') + fi + ARTIFACT_PATHNAME="bender-$PKG_VERSION-aarch64-unknown-linux-gnu+slang.tar.gz" + tar -czf "$ARTIFACT_PATHNAME" -C "./target/release" --owner=0 --group=0 bender - name: Upload Release Asset if: ${{ github.event_name == 'release' || inputs.publish_assets }} uses: softprops/action-gh-release@v1 with: - tag_name: ${{ github.event.release.tag_name }} - files: bender-*.tar.gz + tag_name: ${{ github.event.release.tag_name || github.ref_name }} + files: bender-*-aarch64-unknown-linux-gnu+slang.tar.gz env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + release-macos: runs-on: macos-latest steps: - uses: actions/checkout@v6 with: submodules: recursive - - name: Install Rust - run: | - curl --proto '=https' --tlsv1.2 -sSf https://https://sh.rustup.rs | sh -s -- -y --default-toolchain stable - echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable - name: universal2 install run: | rustup target add x86_64-apple-darwin rustup target add aarch64-apple-darwin cargo install universal2 - - name: MacOS Build + - name: Build (all features) run: cargo-universal2 --release --all-features - - name: Get Artifact Name - run: | - if [[ "$GITHUB_REF" =~ ^refs/tags/v.*$ ]]; then \ - PKG_VERSION=$(echo $GITHUB_REF | sed -n 's/^refs\/tags\/v//p'); \ - else \ - PKG_VERSION=$(echo $GITHUB_REF | sed -n 's/^refs\/tags\///p'); \ - fi - ARTIFACT_PATHNAME="bender-$PKG_VERSION-universal-apple-darwin.tar.gz" - ARTIFACT_NAME=$(basename $ARTIFACT_PATHNAME) - echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >> $GITHUB_ENV - echo "ARTIFACT_PATHNAME=${ARTIFACT_PATHNAME}" >> $GITHUB_ENV - name: Create Package + shell: bash run: | - gtar -czf $ARTIFACT_PATHNAME -C "./target/universal2-apple-darwin/release" --owner=0 --group=0 bender + if [[ "$GITHUB_REF" =~ ^refs/tags/v.*$ ]]; then + PKG_VERSION=$(echo "$GITHUB_REF" | sed -n 's/^refs\/tags\/v//p') + else + PKG_VERSION=$(echo "$GITHUB_REF" | sed -n 's/^refs\/tags\///p') + fi + ARTIFACT_PATHNAME="bender-$PKG_VERSION-universal-apple-darwin+slang.tar.gz" + gtar -czf "$ARTIFACT_PATHNAME" -C "./target/universal2-apple-darwin/release" --owner=0 --group=0 bender - name: Upload Release Asset if: ${{ github.event_name == 'release' || inputs.publish_assets }} uses: softprops/action-gh-release@v1 with: - tag_name: ${{ github.event.release.tag_name }} - files: bender-*.tar.gz + tag_name: ${{ github.event.release.tag_name || github.ref_name }} + files: bender-*-universal-apple-darwin+slang.tar.gz env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + release-windows: runs-on: windows-latest steps: - uses: actions/checkout@v6 with: submodules: recursive - - name: Install Rust - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@stable with: toolchain: stable - - name: Build + - name: Build (all features) run: cargo build --release --all-features - - name: Get Artifact Name + - name: Create Package shell: bash run: | - if [[ "$GITHUB_REF" =~ ^refs/tags/v.*$ ]]; then \ - PKG_VERSION=$(echo $GITHUB_REF | sed -n 's/^refs\/tags\/v//p'); \ - else \ - PKG_VERSION=$(echo $GITHUB_REF | sed -n 's/^refs\/tags\///p'); \ + if [[ "$GITHUB_REF" =~ ^refs/tags/v.*$ ]]; then + PKG_VERSION=$(echo "$GITHUB_REF" | sed -n 's/^refs\/tags\/v//p') + else + PKG_VERSION=$(echo "$GITHUB_REF" | sed -n 's/^refs\/tags\///p') fi - ARTIFACT_PATHNAME="bender-$PKG_VERSION-x86_64-pc-windows-msvc.tar.gz" - ARTIFACT_NAME=$(basename $ARTIFACT_PATHNAME) - echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >> $GITHUB_ENV - echo "ARTIFACT_PATHNAME=${ARTIFACT_PATHNAME}" >> $GITHUB_ENV - - name: Create Package - run: | + ARTIFACT_PATHNAME="bender-$PKG_VERSION-x86_64-pc-windows-msvc+slang.tar.gz" cp target/release/bender.exe . - & 'C:\Program Files\Git\usr\bin\tar.exe' czf $Env:ARTIFACT_PATHNAME --owner=0 --group=0 bender.exe + tar -czf "$ARTIFACT_PATHNAME" --owner=0 --group=0 bender.exe - name: Upload Release Asset if: ${{ github.event_name == 'release' || inputs.publish_assets }} uses: softprops/action-gh-release@v1 with: - tag_name: ${{ github.event.release.tag_name }} - files: bender-*.tar.gz + tag_name: ${{ github.event.release.tag_name || github.ref_name }} + files: bender-*-x86_64-pc-windows-msvc+slang.tar.gz env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 4e9d9dcd956596962c9cfe6d4105df0ab64725be Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 14:51:27 +0100 Subject: [PATCH 45/46] ci: Cache rust builds --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 162d3d95b..63762ebb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,10 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust}} + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-test-${{ runner.os }}-${{ matrix.rust }} + cache-workspace-crates: "true" - name: Build run: cargo build --all-features - name: Cargo Test @@ -42,6 +46,10 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: toolchain: stable + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-test-windows-${{ runner.os }}-stable + cache-workspace-crates: "true" - name: Build run: cargo build --all-features - name: Cargo Test @@ -59,6 +67,10 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: toolchain: stable + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-test-macos-${{ runner.os }}-stable + cache-workspace-crates: "true" - name: Build run: cargo build --all-features - name: Cargo Test @@ -84,6 +96,10 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: toolchain: stable + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-release-build-${{ runner.os }}-stable + cache-workspace-crates: "true" - name: Build (release) run: cargo build --release --all-features @@ -98,6 +114,10 @@ jobs: with: toolchain: stable components: clippy + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-clippy-${{ runner.os }}-stable + cache-workspace-crates: "true" - run: cargo clippy --all-features unused-deps: @@ -108,6 +128,10 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: toolchain: stable + - uses: Swatinem/rust-cache@v2 + with: + shared-key: ci-unused-deps-${{ runner.os }}-stable + cache-workspace-crates: "true" - name: Install machete run: cargo install cargo-machete - name: Check for unused dependencies From b73a33faab28e68e7088d07959930c29e97a4321 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 17 Feb 2026 15:23:29 +0100 Subject: [PATCH 46/46] ci: Cancel ongoing workflows --- .github/workflows/ci.yml | 4 ++++ .github/workflows/cli_regression.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63762ebb1..ae2ea81d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,10 @@ on: pull_request: workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-latest diff --git a/.github/workflows/cli_regression.yml b/.github/workflows/cli_regression.yml index bfdcf9bd7..71b5780a8 100644 --- a/.github/workflows/cli_regression.yml +++ b/.github/workflows/cli_regression.yml @@ -4,6 +4,10 @@ on: pull_request: workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-latest