Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/formatting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,3 @@ jobs:
with:
source: "."
extensions: "h,hpp,c,cc,cpp,cxx"
exclude: "./crates/bender-slang/vendor"
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ inherits = "release"
lto = "thin"

[dependencies]
bender-slang = { version = "0.1.1", path = "crates/bender-slang", optional = true }
bender-slang = { version = "0.2.0", path = "crates/bender-slang", optional = true }

serde = { version = "1", features = ["derive"] }
serde_yaml_ng = "0.10"
Expand Down
2 changes: 1 addition & 1 deletion crates/bender-slang/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bender-slang"
version = "0.1.1"
version = "0.2.0"
edition = "2024"
description = "Internal bender crate: Rust bindings for the Slang SystemVerilog parser"
license = "Apache-2.0"
Expand Down
68 changes: 36 additions & 32 deletions crates/bender-slang/build.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,6 @@
// Copyright (c) 2025 ETH Zurich
// Tim Fischer <fischeti@iis.ee.ethz.ch>

// Generates cpp/compile_flags.txt so that clangd gets the correct include paths
// for the C++ bridge files. The file is written to the cpp/ directory and should
// be gitignored. It is picked up automatically by clangd for all files in that directory.
fn generate_compile_flags(
manifest_dir: &std::path::Path,
dst: &std::path::Path,
includes: &[&std::path::Path],
defines: &[(&str, &str)],
) {
use std::ffi::OsStr;

let Some(target_root) = dst
.ancestors()
.find(|p| p.file_name() == Some(OsStr::new("target")))
else {
return;
};

let flags: Vec<String> = ["-x", "c++", "-std=c++20", "-fno-cxx-modules"]
.map(str::to_string)
.into_iter()
.chain(includes.iter().map(|p| format!("-I{}", p.display())))
.chain([format!("-I{}", target_root.join("cxxbridge").display())])
.chain(defines.iter().map(|(k, v)| format!("-D{}={}", k, v)))
.collect();

let _ = std::fs::write(
manifest_dir.join("cpp/compile_flags.txt"),
flags.join("\n") + "\n",
);
}

fn main() {
let manifest_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());

Expand Down Expand Up @@ -171,3 +139,39 @@ fn main() {
println!("cargo:rerun-if-changed=cpp/print.cpp");
println!("cargo:rerun-if-changed=cpp/analysis.cpp");
}

// Generates cpp/compile_flags.txt so that clangd gets the correct include paths
// for the C++ bridge files. The file is written to the cpp/ directory and should
// be gitignored. It is picked up automatically by clangd for all files in that directory.
fn generate_compile_flags(
manifest_dir: &std::path::Path,
dst: &std::path::Path,
includes: &[&std::path::Path],
defines: &[(&str, &str)],
) {
use std::ffi::OsStr;

let Some(target_root) = dst
.ancestors()
.find(|p| p.file_name() == Some(OsStr::new("target")))
else {
return;
};

let bridge_crate_include = manifest_dir.parent().unwrap_or(manifest_dir);
let flags: Vec<String> = ["-x", "c++", "-std=c++20", "-fno-cxx-modules"]
.map(str::to_string)
.into_iter()
.chain(includes.iter().map(|p| format!("-I{}", p.display())))
.chain([
format!("-I{}", target_root.join("cxxbridge").display()),
format!("-I{}", bridge_crate_include.display()),
])
.chain(defines.iter().map(|(k, v)| format!("-D{}={}", k, v)))
.collect();

let _ = std::fs::write(
manifest_dir.join("cpp/compile_flags.txt"),
flags.join("\n") + "\n",
);
}
65 changes: 42 additions & 23 deletions crates/bender-slang/cpp/analysis.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) 2025 ETH Zurich
// Tim Fischer <fischeti@iis.ee.ethz.ch>

#include "bender-slang/src/lib.rs.h"
#include "slang/syntax/AllSyntax.h"
#include "slang_bridge.h"

Expand Down Expand Up @@ -32,8 +33,28 @@ static bool stderr_is_tty() {
static const slang::DiagCode kDuplicateTopLevelDecl(slang::DiagSubsystem::General, 9999);
static constexpr std::string_view kDuplicateTopLevelDeclFormat = "module '{}' overwrites previous definition in '{}'";

rust::Vec<std::uint32_t> reachable_tree_indices(const SlangSession& session, const rust::Vec<rust::String>& tops) {
const auto& treeVec = session.trees();
// Converts an internal per-tree record into the cxx shared struct handed across the bridge.
static ParsedTree to_parsed(const TreeEntry& entry) {
ParsedTree pt;
pt.tree = entry.tree;
pt.path = rust::String(entry.path);
pt.parsed_ok = entry.parsedOk;
pt.encrypted = entry.encrypted;
return pt;
}

// Returns every parsed tree in the session, each bundled with its per-file facts. The order
// matches parse order across all parse_group calls.
rust::Vec<ParsedTree> all_trees(const SlangSession& session) {
rust::Vec<ParsedTree> out;
for (const auto& entry : session.entries()) {
out.push_back(to_parsed(entry));
}
return out;
}

rust::Vec<ParsedTree> reachable_trees(const SlangSession& session, const rust::Vec<rust::String>& tops) {
const auto& entries = session.entries();

// One engine+client per distinct SourceManager. Each parse group creates its own
// SourceManager (see SlangContext), so trees from different groups need different
Expand All @@ -60,8 +81,8 @@ rust::Vec<std::uint32_t> reachable_tree_indices(const SlangSession& session, con
// Build the name-to-tree-index map with last-wins semantics, emitting a warning
// whenever a later definition overwrites an earlier one.
std::unordered_map<std::string_view, size_t> nameToTreeIndex;
for (size_t i = 0; i < treeVec.size(); ++i) {
const auto& metadata = treeVec[i]->getMetadata();
for (size_t i = 0; i < entries.size(); ++i) {
const auto& metadata = entries[i].tree->getMetadata();

auto checkAndInsert = [&](std::string_view name, slang::SourceLocation loc) {
if (name.empty())
Expand All @@ -70,12 +91,12 @@ rust::Vec<std::uint32_t> reachable_tree_indices(const SlangSession& session, con
if (inserted)
return;

const auto& prevBufferIds = treeVec[it->second]->getSourceBufferIds();
std::string_view prevFile = prevBufferIds.empty()
? std::string_view("<unknown>")
: treeVec[it->second]->sourceManager().getRawFileName(prevBufferIds[0]);
const auto& prevBufferIds = entries[it->second].tree->getSourceBufferIds();
std::string_view prevFile =
prevBufferIds.empty() ? std::string_view("<unknown>")
: entries[it->second].tree->sourceManager().getRawFileName(prevBufferIds[0]);

auto& state = diagFor(treeVec[i]->sourceManager());
auto& state = diagFor(entries[i].tree->sourceManager());
slang::Diagnostic diag(kDuplicateTopLevelDecl, loc);
diag << name;
diag << prevFile;
Expand All @@ -93,9 +114,9 @@ rust::Vec<std::uint32_t> reachable_tree_indices(const SlangSession& session, con

// Build a dependency graph where each tree points to the trees that declare
// symbols it references.
std::vector<std::vector<size_t>> deps(treeVec.size());
for (size_t i = 0; i < treeVec.size(); ++i) {
const auto& metadata = treeVec[i]->getMetadata();
std::vector<std::vector<size_t>> deps(entries.size());
for (size_t i = 0; i < entries.size(); ++i) {
const auto& metadata = entries[i].tree->getMetadata();
std::unordered_set<size_t> seen;
for (auto ref : metadata.getReferencedSymbols()) {
auto it = nameToTreeIndex.find(ref);
Expand All @@ -121,7 +142,7 @@ rust::Vec<std::uint32_t> reachable_tree_indices(const SlangSession& session, con
}

// Perform a DFS from the top modules to find all reachable trees.
std::vector<bool> reachable(treeVec.size(), false);
std::vector<bool> reachable(entries.size(), false);
std::function<void(size_t)> dfs = [&](size_t index) {
if (reachable[index]) {
return;
Expand All @@ -136,26 +157,24 @@ rust::Vec<std::uint32_t> reachable_tree_indices(const SlangSession& session, con
dfs(start);
}

rust::Vec<std::uint32_t> result;
rust::Vec<ParsedTree> result;
for (size_t i = 0; i < reachable.size(); ++i) {
if (reachable[i]) {
result.push_back(static_cast<std::uint32_t>(i));
result.push_back(to_parsed(entries[i]));
}
}
return result;
}

// Returns the deduped, canonical filesystem paths of every header file that was actually loaded
// via `include directives while parsing the requested trees. Trees from different parse groups
// may live in different SourceManagers, so the lookup is per-tree.
rust::Vec<rust::String> resolved_include_paths_for(const SlangSession& session,
const rust::Vec<std::uint32_t>& tree_indices) {
const auto& treeVec = session.trees();
// via `include directives while parsing the given trees. Trees from different parse groups may
// live in different SourceManagers, so the lookup is per-tree.
rust::Vec<rust::String> resolved_include_paths_for(const rust::Vec<ParsedTree>& trees) {
std::unordered_set<std::string> uniquePaths;
for (auto idx : tree_indices) {
if (idx >= treeVec.size())
for (const auto& parsed : trees) {
const auto& tree = parsed.tree;
if (!tree)
continue;
const auto& tree = treeVec[idx];
const auto& sm = tree->sourceManager();
for (const auto& inc : tree->getIncludeDirectives()) {
if (!inc.buffer.id.valid())
Expand Down
51 changes: 25 additions & 26 deletions crates/bender-slang/cpp/session.cpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// Copyright (c) 2025 ETH Zurich
// Tim Fischer <fischeti@iis.ee.ethz.ch>

#include "slang/diagnostics/PreprocessorDiags.h"
#include "slang_bridge.h"

#include <iostream>
#include <stdexcept>

using namespace slang;
using namespace slang::syntax;

using std::shared_ptr;
using std::string;
using std::string_view;

Expand All @@ -34,19 +35,23 @@ void SlangContext::set_defines(const rust::Vec<rust::String>& defs) {
}
}

// Parses a list of source files and returns the resulting syntax trees as a vector (of shared pointers).
// If any file fails to parse, an exception is thrown with the error message(s) from the diagnostic engine.
std::vector<std::shared_ptr<SyntaxTree>> SlangContext::parse_files(const rust::Vec<rust::String>& paths) {
// Parses a list of source files and returns one TreeEntry per file, each bundling the resulting
// syntax tree with the per-file facts slang reported (path, parse success, encryption).
// System-level errors (file unreadable, etc.) throw; per-file parse errors are surfaced
// non-fatally via the TreeEntry::parsedOk flag so the caller can apply policy.
std::vector<TreeEntry> SlangContext::parse_files(const rust::Vec<rust::String>& paths) {
Bag options;
options.set(ppOptions);

std::vector<std::shared_ptr<SyntaxTree>> out;
std::vector<TreeEntry> out;
out.reserve(paths.size());

for (const auto& path : paths) {
string_view pathView(path.data(), path.size());
auto result = SyntaxTree::fromFile(pathView, sourceManager, options);

// A system-level failure (file unreadable, etc.) is still fatal: the caller asked for
// this path and we couldn't even open it. Parse errors are tolerated below.
if (!result) {
auto& err = result.error();
std::string msg = "System Error loading '" + std::string(err.second) + "': " + err.first.message();
Expand All @@ -58,20 +63,24 @@ std::vector<std::shared_ptr<SyntaxTree>> SlangContext::parse_files(const rust::V
diagEngine.clearIncludeStack();

bool hasErrors = false;
bool hasProtectDiag = false;
for (const auto& diag : tree->diagnostics()) {
hasErrors |= diag.isError();
if (diag.code == slang::diag::ProtectedEnvelope) {
hasProtectDiag = true;
}
diagEngine.issue(diag);
}

// Surface diagnostics for any file with errors, but keep going — the Rust side decides
// what to do with the (possibly partial) tree. The encrypted flag lets the Rust side
// discriminate IEEE-1735 encrypted IP (auto-tolerated) from real syntax bugs (fail by
// default; tolerate with --allow-broken).
if (hasErrors) {
std::string rendered = diagClient->getString();
if (rendered.empty()) {
rendered = "Failed to parse '" + std::string(pathView) + "'.";
}
throw std::runtime_error(rendered);
std::cerr << diagClient->getString();
}

out.push_back(tree);
out.push_back(TreeEntry{tree, std::string(path.data(), path.size()), !hasErrors, hasProtectDiag});
}

return out;
Expand All @@ -86,23 +95,13 @@ void SlangSession::parse_group(const rust::Vec<rust::String>& files, const rust:
ctx->set_includes(includes);
ctx->set_defines(defines);

// Parse the files and store the resulting syntax trees in the session.
// Parse the files and append the resulting per-tree records to the session, so callers can
// decide how to handle partially-parsed files.
auto parsed = ctx->parse_files(files);
allTrees.reserve(allTrees.size() + parsed.size());
for (const auto& tree : parsed) {
allTrees.push_back(tree);
treeEntries.reserve(treeEntries.size() + parsed.size());
for (auto& entry : parsed) {
treeEntries.push_back(std::move(entry));
}

contexts.push_back(std::move(ctx));
}

// Returns the number of syntax trees currently stored in the session.
std::size_t tree_count(const SlangSession& session) { return session.trees().size(); }

// Returns the syntax tree at the given index in the session.
std::shared_ptr<SyntaxTree> tree_at(const SlangSession& session, std::size_t index) {
if (index >= session.trees().size()) {
throw std::runtime_error("Tree index out of bounds.");
}
return session.trees()[index];
}
Loading
Loading