Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
110 changes: 104 additions & 6 deletions crates/bender-slang/cpp/analysis.cpp
Original file line number Diff line number Diff line change
@@ -1,26 +1,94 @@
// Copyright (c) 2025 ETH Zurich
// Tim Fischer <fischeti@iis.ee.ethz.ch>

#include "slang/syntax/AllSyntax.h"
#include "slang_bridge.h"

#include <functional>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string_view>
#ifdef _WIN32
#include <io.h>
#else
#include <unistd.h>
#endif
#include <unordered_map>
#include <unordered_set>

using namespace slang;

static bool stderr_is_tty() {
#ifdef _WIN32
return _isatty(_fileno(stderr)) != 0;
#else
return isatty(STDERR_FILENO) != 0;
#endif
}

// Diagnostic for "a later top-level declaration shadows an earlier one with the same name".
// Lives in the General subsystem; the code is arbitrary but stable.
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();

// Build a mapping from declared symbol names to the index of the tree that
// declares them.
// One engine+client per distinct SourceManager. Each parse group creates its own
// SourceManager (see SlangContext), so trees from different groups need different
// engines; trees within a group share one.
struct DiagState {
std::unique_ptr<slang::DiagnosticEngine> engine;
std::shared_ptr<slang::TextDiagnosticClient> client;
};
std::unordered_map<const slang::SourceManager*, DiagState> diagStates;
const bool tty = stderr_is_tty();
auto diagFor = [&](const slang::SourceManager& sm) -> DiagState& {
auto [it, inserted] = diagStates.try_emplace(&sm);
if (inserted) {
it->second.engine = std::make_unique<slang::DiagnosticEngine>(sm);
it->second.client = std::make_shared<slang::TextDiagnosticClient>();
it->second.client->showColors(tty);
it->second.engine->addClient(it->second.client);
it->second.engine->setMessage(kDuplicateTopLevelDecl, std::string(kDuplicateTopLevelDeclFormat));
it->second.engine->setSeverity(kDuplicateTopLevelDecl, slang::DiagnosticSeverity::Warning);
}
return it->second;
};

// 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 (auto name : metadata.getDeclaredSymbols()) {
nameToTreeIndex.emplace(name, i);
}

auto checkAndInsert = [&](std::string_view name, slang::SourceLocation loc) {
if (name.empty())
return;
auto [it, inserted] = nameToTreeIndex.emplace(name, i);
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]);

auto& state = diagFor(treeVec[i]->sourceManager());
slang::Diagnostic diag(kDuplicateTopLevelDecl, loc);
diag << name;
diag << prevFile;
state.client->clear();
state.engine->issue(diag);
std::cerr << state.client->getString();
it->second = i;
};

for (const auto& [decl, _] : metadata.nodeMeta)
checkAndInsert(decl->header->name.valueText(), decl->header->name.location());
for (const auto classDecl : metadata.classDecls)
checkAndInsert(classDecl->name.valueText(), classDecl->name.location());
}

// Build a dependency graph where each tree points to the trees that declare
Expand All @@ -46,7 +114,8 @@ rust::Vec<std::uint32_t> reachable_tree_indices(const SlangSession& session, con
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));
throw std::runtime_error("Top module '" + std::string(name) + "' not found among " +
std::to_string(nameToTreeIndex.size()) + " known top-level declarations.");
}
startIndices.push_back(it->second);
}
Expand Down Expand Up @@ -75,3 +144,32 @@ rust::Vec<std::uint32_t> reachable_tree_indices(const SlangSession& session, con
}
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();
std::unordered_set<std::string> uniquePaths;
for (auto idx : tree_indices) {
if (idx >= treeVec.size())
continue;
const auto& tree = treeVec[idx];
const auto& sm = tree->sourceManager();
for (const auto& inc : tree->getIncludeDirectives()) {
if (!inc.buffer.id.valid())
continue;
const auto& fullPath = sm.getFullPath(inc.buffer.id);
if (!fullPath.empty()) {
uniquePaths.insert(fullPath.string());
}
}
}
rust::Vec<rust::String> out;
out.reserve(uniquePaths.size());
for (const auto& p : uniquePaths) {
out.push_back(rust::String(p));
}
return out;
}
2 changes: 2 additions & 0 deletions crates/bender-slang/cpp/slang_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ rust::String print_tree(std::shared_ptr<slang::syntax::SyntaxTree> tree, SlangPr
rust::String dump_tree_json(std::shared_ptr<slang::syntax::SyntaxTree> tree);

rust::Vec<std::uint32_t> reachable_tree_indices(const SlangSession& session, const rust::Vec<rust::String>& tops);
rust::Vec<rust::String> resolved_include_paths_for(const SlangSession& session,
const rust::Vec<std::uint32_t>& tree_indices);
std::size_t tree_count(const SlangSession& session);
std::shared_ptr<slang::syntax::SyntaxTree> tree_at(const SlangSession& session, std::size_t index);
std::uint64_t renamed_declarations(const SyntaxTreeRewriter& rewriter);
Expand Down
17 changes: 17 additions & 0 deletions crates/bender-slang/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ mod ffi {

fn reachable_tree_indices(session: &SlangSession, tops: &Vec<String>) -> Result<Vec<u32>>;

fn resolved_include_paths_for(
session: &SlangSession,
tree_indices: &Vec<u32>,
) -> Result<Vec<String>>;

fn tree_count(session: &SlangSession) -> usize;

fn tree_at(session: &SlangSession, index: usize) -> Result<SharedPtr<SyntaxTree>>;
Expand Down Expand Up @@ -198,6 +203,18 @@ impl SlangSession {
Ok(indices.into_iter().map(|i| i as usize).collect())
}

/// Returns the canonical filesystem paths of every header that was actually loaded via an
/// `include directive while parsing the given trees. Useful for figuring out which include
/// directories were actually consulted.
pub fn resolved_include_paths(&self, tree_indices: &[usize]) -> Result<Vec<String>> {
let indices: Vec<u32> = tree_indices.iter().map(|&i| i as u32).collect();
let paths = ffi::resolved_include_paths_for(self.inner.as_ref().unwrap(), &indices)
.map_err(|cause| SlangError::TrimByTop {
message: cause.to_string(),
})?;
Ok(paths.into_iter().collect())
}

/// Returns syntax trees reachable from the given top modules.
pub fn reachable_trees(&self, tops: &[String]) -> Result<Vec<SyntaxTree<'_>>> {
let indices = self.reachable_indices(tops)?;
Expand Down
Loading
Loading