Skip to content

Commit 63e53c6

Browse files
committed
[circt-verilog-lsp] Add -C command line option
This PR extends the CIRCT Verilog LSP server with support for project command files (`-C` / `--command-file`) and improves robustness around source locations and diagnostics. - **Command file support** - Added `commandFiles` option to `VerilogServerOptions`. - Extended the CLI with `-C`/`--command-file` flags to pass one or more command files. - Each buffer’s `Driver` now processes command files, filtering out the current main buffer to avoid duplication. - Implemented `removeFileToTemp` helper to strip the main file from command files and materialize a temporary version. - **Source location handling** - Improved absolute/real path resolution when constructing LSP URIs. - Fallbacks added for when `slang::SourceManager` has no full path info. - **Compilation setup** - Ensure all definitions in the buffer/project scope are treated as top modules. - **Indexing improvements** - Skip top instances not defined in the current file. - Added guards against invalid or empty ranges (especially macro expansions).
1 parent 7de78ca commit 63e53c6

File tree

7 files changed

+320
-23
lines changed

7 files changed

+320
-23
lines changed

include/circt/Tools/circt-verilog-lsp-server/CirctVerilogLspServerMain.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,17 @@ namespace circt {
3535
namespace lsp {
3636
struct VerilogServerOptions {
3737
VerilogServerOptions(const std::vector<std::string> &libDirs,
38-
const std::vector<std::string> &extraSourceLocationDirs)
39-
: libDirs(libDirs), extraSourceLocationDirs(extraSourceLocationDirs) {}
38+
const std::vector<std::string> &extraSourceLocationDirs,
39+
const std::vector<std::string> &commandFiles)
40+
: libDirs(libDirs), extraSourceLocationDirs(extraSourceLocationDirs),
41+
commandFiles(commandFiles) {}
4042
/// Additional list of RTL directories to search.
4143
const std::vector<std::string> &libDirs;
42-
4344
/// Additional list of external source directories to search.
4445
const std::vector<std::string> &extraSourceLocationDirs;
46+
/// Additional list of command files that reference dependencies of the
47+
/// project.
48+
const std::vector<std::string> &commandFiles;
4549
};
4650
// namespace lsp
4751

lib/Tools/circt-verilog-lsp-server/LSPServer.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#include "LSPServer.h"
1010
#include "VerilogServerImpl/VerilogServer.h"
11+
#include "llvm/Support/JSON.h"
1112
#include "llvm/Support/LSP/Protocol.h"
1213
#include "llvm/Support/LSP/Transport.h"
1314
#include <optional>

lib/Tools/circt-verilog-lsp-server/VerilogServerImpl/VerilogServer.cpp

Lines changed: 229 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
#include "mlir/IR/MLIRContext.h"
2424
#include "slang/ast/ASTVisitor.h"
2525
#include "slang/ast/Compilation.h"
26+
#include "slang/ast/Scope.h"
27+
#include "slang/ast/symbols/CompilationUnitSymbols.h"
2628
#include "slang/diagnostics/DiagnosticClient.h"
2729
#include "slang/diagnostics/Diagnostics.h"
2830
#include "slang/driver/Driver.h"
@@ -43,6 +45,8 @@
4345
#include "llvm/Support/SMLoc.h"
4446
#include "llvm/Support/SourceMgr.h"
4547

48+
#include <filesystem>
49+
#include <fstream>
4650
#include <memory>
4751
#include <optional>
4852
#include <string>
@@ -248,15 +252,98 @@ void LSPDiagnosticClient::report(const slang::ReportedDiagnostic &slangDiag) {
248252
// VerilogDocument
249253
//===----------------------------------------------------------------------===//
250254

255+
// Filter out the main buffer file from the command file list, if it is in
256+
// there.
257+
static inline bool
258+
mainBufferFileInCommandFileList(const std::string &cmdfileStr,
259+
const std::string &targetAbsStr) {
260+
namespace fs = std::filesystem;
261+
262+
fs::path cmdfile(cmdfileStr);
263+
fs::path targetAbs(targetAbsStr);
264+
const fs::path base = cmdfile.parent_path();
265+
std::error_code ec;
266+
267+
// Normalize target
268+
fs::path target = fs::weakly_canonical(targetAbs, ec);
269+
if (ec) {
270+
ec.clear();
271+
target = targetAbs.lexically_normal();
272+
}
273+
274+
std::ifstream in(cmdfile);
275+
if (!in) {
276+
// return the original path so the caller still has a valid file to use
277+
return false;
278+
}
279+
280+
std::string line;
281+
282+
while (std::getline(in, line)) {
283+
std::string s = line;
284+
s.erase(0, s.find_first_not_of(" \t\r\n")); // left-trim
285+
286+
// Keep plusargs
287+
if (!s.empty() &&
288+
(s.rfind("+incdir+", 0) == 0 || s.rfind("+define+", 0) == 0 ||
289+
s.rfind("-I", 0) == 0 || s.rfind("-D", 0) == 0)) {
290+
continue;
291+
}
292+
// Preserve blank lines
293+
if (s.empty()) {
294+
continue;
295+
}
296+
297+
// Treat everything else as a path (relative entries resolved vs. cmdfile
298+
// dir)
299+
fs::path candRel = s;
300+
fs::path candAbs = candRel.is_absolute() ? candRel : (base / candRel);
301+
302+
fs::path cand = fs::weakly_canonical(candAbs, ec);
303+
if (ec) {
304+
ec.clear();
305+
cand = candAbs.lexically_normal();
306+
}
307+
308+
if (cand == target) {
309+
return true;
310+
}
311+
}
312+
313+
return false;
314+
}
315+
316+
static inline bool mainBufferFileInCommandFileList(llvm::StringRef cmdfile,
317+
llvm::StringRef target_abs) {
318+
return mainBufferFileInCommandFileList(cmdfile.str(), target_abs.str());
319+
}
320+
251321
VerilogDocument::VerilogDocument(
252322
VerilogServerContext &context, const llvm::lsp::URIForFile &uri,
253323
StringRef contents, std::vector<llvm::lsp::Diagnostic> &diagnostics)
254324
: globalContext(context), index(*this), uri(uri) {
255-
unsigned int bufferId;
325+
unsigned int mainBufferId;
326+
bool skipMainBufferSlangImport = false;
327+
328+
llvm::SmallString<256> canonPath(uri.file());
329+
if (std::error_code ec = llvm::sys::fs::real_path(uri.file(), canonPath))
330+
canonPath = uri.file(); // fall back, but try to keep it absolute
331+
332+
// --- Apply project command files (the “-C”s) to this per-buffer driver ---
333+
for (const std::string &cmdFile : context.options.commandFiles) {
334+
if (!driver.processCommandFiles(cmdFile, false, true)) {
335+
circt::lsp::Logger::error(Twine("Failed to open command file ") +
336+
cmdFile);
337+
}
338+
skipMainBufferSlangImport |=
339+
mainBufferFileInCommandFileList(cmdFile, canonPath);
340+
}
341+
256342
if (auto memBufferOwn =
257-
llvm::MemoryBuffer::getMemBufferCopy(contents, uri.file())) {
343+
llvm::MemoryBuffer::getMemBufferCopy(contents, canonPath)) {
258344

259-
bufferId = sourceMgr.AddNewSourceBuffer(std::move(memBufferOwn), SMLoc());
345+
mainBufferId =
346+
sourceMgr.AddNewSourceBuffer(std::move(memBufferOwn), SMLoc());
260347
} else {
261348
circt::lsp::Logger::error(
262349
Twine("Failed to create memory buffer for file ") + uri.file());
@@ -273,17 +360,57 @@ VerilogDocument::VerilogDocument(
273360
context.options.libDirs.end());
274361

275362
// Populate source managers.
276-
const llvm::MemoryBuffer *memBuffer = sourceMgr.getMemoryBuffer(bufferId);
363+
const llvm::MemoryBuffer *memBuffer = sourceMgr.getMemoryBuffer(mainBufferId);
364+
365+
// This block compiles the top file to determine all definitions
366+
// This is used in a second pass to declare all those definitions
367+
// as top modules, so they are elaborated and subsequently indexed.
368+
slang::driver::Driver topDriver;
369+
370+
auto topSlangBuffer =
371+
topDriver.sourceManager.assignText(uri.file(), memBuffer->getBuffer());
372+
topDriver.sourceLoader.addBuffer(topSlangBuffer);
373+
374+
topDriver.options.compilationFlags.emplace(
375+
slang::ast::CompilationFlags::LintMode, false);
376+
topDriver.options.compilationFlags.emplace(
377+
slang::ast::CompilationFlags::DisableInstanceCaching, false);
378+
379+
if (!topDriver.processOptions()) {
380+
return;
381+
}
382+
383+
if (!topDriver.parseAllSources()) {
384+
circt::lsp::Logger::error(Twine("Failed to parse Verilog file ") +
385+
uri.file());
386+
return;
387+
}
388+
389+
FailureOr<std::unique_ptr<slang::ast::Compilation>> topCompilation =
390+
topDriver.createCompilation();
391+
if (failed(topCompilation))
392+
return;
393+
394+
std::vector<std::string> topModules;
395+
for (const auto *defs : (*topCompilation)->getDefinitions())
396+
topModules.emplace_back(defs->name);
397+
398+
// Make sure that all possible definitions in the main buffer are
399+
// topModules!
400+
driver.options.topModules = topModules;
277401

278402
for (const auto &libDir : libDirs) {
279403
driver.sourceLoader.addSearchDirectories(libDir);
280404
}
281405

282-
// Assign text to slang.
283-
auto slangBuffer =
284-
driver.sourceManager.assignText(uri.file(), memBuffer->getBuffer());
285-
driver.sourceLoader.addBuffer(slangBuffer);
286-
bufferIDMap[slangBuffer.id.getId()] = bufferId;
406+
// If the main buffer is **not** present in a command file, add it into
407+
// slang's source manager and bind to llvm source manager.
408+
if (!skipMainBufferSlangImport) {
409+
auto slangBuffer =
410+
driver.sourceManager.assignText(uri.file(), memBuffer->getBuffer());
411+
driver.sourceLoader.addBuffer(slangBuffer);
412+
}
413+
287414
auto diagClient = std::make_shared<LSPDiagnosticClient>(*this, diagnostics);
288415
driver.diagEngine.addClient(diagClient);
289416

@@ -308,6 +435,38 @@ VerilogDocument::VerilogDocument(
308435
if (failed(compilation))
309436
return;
310437

438+
// If the main buffer is present in a command file, compile it only once
439+
// and import directly from the command file; then figure out which buffer id
440+
// it was assigned and bind to llvm source manager.
441+
llvm::SmallString<256> slangCanonPath("");
442+
std::string slangRawPath;
443+
std::unique_ptr<llvm::MemoryBuffer> newBuffer;
444+
uint32_t newBufferId;
445+
446+
// Iterate through all buffers in the slang compilation and set up
447+
// a binding to the LLVM Source Manager.
448+
auto *sourceManager = (**compilation).getSourceManager();
449+
for (auto slangBuffer : sourceManager->getAllBuffers()) {
450+
slangRawPath = sourceManager->getRawFileName(slangBuffer);
451+
if (std::error_code ec =
452+
llvm::sys::fs::real_path(slangRawPath, slangCanonPath))
453+
slangCanonPath = slangRawPath;
454+
455+
if (slangCanonPath == canonPath) {
456+
bufferIDMap[slangBuffer.getId()] = mainBufferId;
457+
continue;
458+
}
459+
460+
if (!bufferIDMap.contains(slangBuffer.getId())) {
461+
newBuffer = llvm::MemoryBuffer::getMemBufferCopy(
462+
sourceManager->getSourceText(slangBuffer), slangCanonPath);
463+
newBufferId = sourceMgr.AddNewSourceBuffer(std::move(newBuffer), SMLoc());
464+
bufferIDMap[slangBuffer.getId()] = newBufferId;
465+
continue;
466+
}
467+
circt::lsp::Logger::error(Twine("Failed to add buffer ID! "));
468+
}
469+
311470
for (auto &diag : (*compilation)->getAllDiagnostics())
312471
driver.diagEngine.issue(diag);
313472

@@ -322,19 +481,27 @@ VerilogDocument::getLspLocation(slang::SourceLocation loc) const {
322481
auto line = slangSourceManager.getLineNumber(loc) - 1;
323482
auto column = slangSourceManager.getColumnNumber(loc) - 1;
324483
auto it = bufferIDMap.find(loc.buffer().getId());
325-
// Check if the current buffer is the main file.
326484
if (it != bufferIDMap.end() && it->second == sourceMgr.getMainFileID())
327485
return llvm::lsp::Location(uri, llvm::lsp::Range(Position(line, column)));
328486

329-
// Otherwise, construct URI from slang source manager.
330-
auto fileName = slangSourceManager.getFileName(loc);
331-
auto loc = llvm::lsp::URIForFile::fromFile(fileName);
332-
if (auto e = loc.takeError())
333-
return llvm::lsp::Location();
334-
return llvm::lsp::Location(loc.get(),
335-
llvm::lsp::Range(Position(line, column)));
336-
}
487+
llvm::StringRef fileName = slangSourceManager.getFileName(loc);
488+
// Ensure absolute path for LSP:
489+
llvm::SmallString<256> abs(fileName);
490+
if (!llvm::sys::path::is_absolute(abs)) {
491+
// Try realPath first
492+
if (std::error_code ec = llvm::sys::fs::real_path(fileName, abs)) {
493+
// Fallback: make it absolute relative to the process CWD
494+
llvm::sys::fs::current_path(abs); // abs = CWD
495+
llvm::sys::path::append(abs, fileName);
496+
}
497+
}
337498

499+
if (auto uriOrErr = llvm::lsp::URIForFile::fromFile(abs)) {
500+
return llvm::lsp::Location(*uriOrErr,
501+
llvm::lsp::Range(Position(line, column)));
502+
}
503+
return llvm::lsp::Location();
504+
}
338505
return llvm::lsp::Location();
339506
}
340507

@@ -524,6 +691,13 @@ struct VerilogIndexer : slang::ast::ASTVisitor<VerilogIndexer, true, true> {
524691
return;
525692
assert(range.start().valid() && range.end().valid() &&
526693
"range must be valid");
694+
695+
// TODO: This implementation does not handle expanded MACROs. Return
696+
// instead.
697+
if (range.start() >= range.end()) {
698+
return;
699+
}
700+
527701
index.insertSymbol(symbol, range, isDefinition);
528702
}
529703

@@ -581,7 +755,30 @@ struct VerilogIndexer : slang::ast::ASTVisitor<VerilogIndexer, true, true> {
581755
insertSymbol(def, item->package.location(), false);
582756
}
583757
}
758+
visitDefault(expr);
759+
}
760+
761+
void visit(const slang::ast::InstanceSymbol &expr) {
762+
auto *def = &expr.getDefinition();
763+
if (!def)
764+
return;
584765

766+
// Add the module definition
767+
insertSymbol(def, def->location, /*isDefinition=*/true);
768+
769+
// Walk up the syntax tree until we hit the type token;
770+
// Link that token back to the instance declaration.
771+
if (auto *hierInst =
772+
expr.getSyntax()
773+
->as_if<slang::syntax::HierarchicalInstanceSyntax>())
774+
if (auto *modInst =
775+
hierInst->parent
776+
->as_if<slang::syntax::HierarchyInstantiationSyntax>())
777+
if (modInst->type)
778+
insertSymbol(def, modInst->type.location(), false);
779+
780+
// Link the module instance name back to the module definition
781+
insertSymbol(def, expr.location, /*isDefinition=*/false);
585782
visitDefault(expr);
586783
}
587784

@@ -608,7 +805,13 @@ struct VerilogIndexer : slang::ast::ASTVisitor<VerilogIndexer, true, true> {
608805
void VerilogIndex::initialize(slang::ast::Compilation &compilation) {
609806
const auto &root = compilation.getRoot();
610807
VerilogIndexer visitor(*this);
808+
611809
for (auto *inst : root.topInstances) {
810+
811+
// Skip all modules, interfaces, etc. that are not defined in this files
812+
if (!(document.getLspLocation(inst->location).uri == document.getURI()))
813+
continue;
814+
612815
// Visit the body of the instance.
613816
inst->body.visit(visitor);
614817

@@ -780,10 +983,17 @@ void circt::lsp::VerilogServer::findReferencesOf(
780983
void VerilogIndex::insertSymbol(const slang::ast::Symbol *symbol,
781984
slang::SourceRange from, bool isDefinition) {
782985
assert(from.start().valid() && from.end().valid());
986+
987+
// TODO: Currently doesn't handle expanded macros
988+
if (!from.start().valid() || !from.end().valid() ||
989+
from.start() >= from.end())
990+
return;
991+
783992
const char *startLoc = getDocument().getSMLoc(from.start()).getPointer();
784993
const char *endLoc = getDocument().getSMLoc(from.end()).getPointer() + 1;
785-
if (!startLoc || !endLoc)
994+
if (!startLoc || !endLoc || startLoc >= endLoc)
786995
return;
996+
787997
assert(startLoc && endLoc);
788998

789999
if (startLoc != endLoc && !intervalMap.overlaps(startLoc, endLoc)) {

0 commit comments

Comments
 (0)