Skip to content

Commit 7b5b9c2

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 7b5b9c2

File tree

7 files changed

+331
-23
lines changed

7 files changed

+331
-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: 240 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,87 @@ 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+
261+
std::filesystem::path cmdfile(cmdfileStr);
262+
std::filesystem::path targetAbs(targetAbsStr);
263+
const std::filesystem::path base = cmdfile.parent_path();
264+
std::error_code ec;
265+
// Normalize target
266+
std::filesystem::path target =
267+
std::filesystem::weakly_canonical(targetAbs, ec);
268+
if (ec) {
269+
ec.clear();
270+
target = targetAbs.lexically_normal();
271+
}
272+
std::ifstream in(cmdfile);
273+
if (!in)
274+
return false;
275+
276+
std::string line;
277+
while (std::getline(in, line)) {
278+
std::string s = line;
279+
s.erase(0, s.find_first_not_of(" \t\r\n")); // left-trim
280+
if (!s.empty() &&
281+
(s.rfind("+incdir+", 0) == 0 || s.rfind("+define+", 0) == 0 ||
282+
s.rfind("-I", 0) == 0 || s.rfind("-D", 0) == 0)) {
283+
continue;
284+
}
285+
if (s.empty()) {
286+
continue;
287+
}
288+
// Treat everything else as a path (relative entries resolved vs. cmdfile
289+
// dir)
290+
std::filesystem::path candRel = s;
291+
std::filesystem::path candAbs =
292+
candRel.is_absolute() ? candRel : (base / candRel);
293+
std::filesystem::path cand = std::filesystem::weakly_canonical(candAbs, ec);
294+
if (ec) {
295+
ec.clear();
296+
cand = candAbs.lexically_normal();
297+
}
298+
299+
if (cand == target)
300+
return true;
301+
}
302+
return false;
303+
}
304+
305+
static inline bool mainBufferFileInCommandFileList(llvm::StringRef cmdfile,
306+
llvm::StringRef target_abs) {
307+
return mainBufferFileInCommandFileList(cmdfile.str(), target_abs.str());
308+
}
309+
251310
VerilogDocument::VerilogDocument(
252311
VerilogServerContext &context, const llvm::lsp::URIForFile &uri,
253312
StringRef contents, std::vector<llvm::lsp::Diagnostic> &diagnostics)
254313
: globalContext(context), index(*this), uri(uri) {
255-
unsigned int bufferId;
314+
unsigned int mainBufferId;
315+
bool skipMainBufferSlangImport = false;
316+
317+
llvm::SmallString<256> canonPath(uri.file());
318+
if (std::error_code ec = llvm::sys::fs::real_path(uri.file(), canonPath))
319+
canonPath = uri.file(); // fall back, but try to keep it absolute
320+
321+
// --- Apply project command files (the “-C”s) to this per-buffer driver ---
322+
for (const std::string &cmdFile : context.options.commandFiles) {
323+
if (!driver.processCommandFiles(cmdFile, false, true)) {
324+
circt::lsp::Logger::error(Twine("Failed to open command file ") +
325+
cmdFile);
326+
}
327+
skipMainBufferSlangImport |=
328+
mainBufferFileInCommandFileList(cmdFile, canonPath);
329+
}
330+
256331
if (auto memBufferOwn =
257332
llvm::MemoryBuffer::getMemBufferCopy(contents, uri.file())) {
258333

259-
bufferId = sourceMgr.AddNewSourceBuffer(std::move(memBufferOwn), SMLoc());
334+
mainBufferId =
335+
sourceMgr.AddNewSourceBuffer(std::move(memBufferOwn), SMLoc());
260336
} else {
261337
circt::lsp::Logger::error(
262338
Twine("Failed to create memory buffer for file ") + uri.file());
@@ -273,17 +349,58 @@ VerilogDocument::VerilogDocument(
273349
context.options.libDirs.end());
274350

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

278391
for (const auto &libDir : libDirs) {
279392
driver.sourceLoader.addSearchDirectories(libDir);
280393
}
281394

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;
395+
// If the main buffer is **not** present in a command file, add it into
396+
// slang's source manager and bind to llvm source manager.
397+
if (!skipMainBufferSlangImport) {
398+
auto slangBuffer =
399+
driver.sourceManager.assignText(uri.file(), memBuffer->getBuffer());
400+
driver.sourceLoader.addBuffer(slangBuffer);
401+
bufferIDMap[slangBuffer.id.getId()] = mainBufferId;
402+
}
403+
287404
auto diagClient = std::make_shared<LSPDiagnosticClient>(*this, diagnostics);
288405
driver.diagEngine.addClient(diagClient);
289406

@@ -308,6 +425,50 @@ VerilogDocument::VerilogDocument(
308425
if (failed(compilation))
309426
return;
310427

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

@@ -322,19 +483,29 @@ VerilogDocument::getLspLocation(slang::SourceLocation loc) const {
322483
auto line = slangSourceManager.getLineNumber(loc) - 1;
323484
auto column = slangSourceManager.getColumnNumber(loc) - 1;
324485
auto it = bufferIDMap.find(loc.buffer().getId());
325-
// Check if the current buffer is the main file.
326486
if (it != bufferIDMap.end() && it->second == sourceMgr.getMainFileID())
327487
return llvm::lsp::Location(uri, llvm::lsp::Range(Position(line, column)));
328488

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-
}
489+
llvm::StringRef fileName = slangSourceManager.getFileName(loc);
490+
// Ensure absolute path for LSP:
491+
llvm::SmallString<256> abs(fileName);
492+
if (!llvm::sys::path::is_absolute(abs)) {
493+
// Try realPath first
494+
if (std::error_code ec = llvm::sys::fs::real_path(fileName, abs)) {
495+
// Fallback: make it absolute relative to the process CWD
496+
llvm::sys::fs::current_path(abs); // abs = CWD
497+
llvm::sys::path::append(abs, fileName);
498+
}
499+
}
337500

501+
if (auto uriOrErr = llvm::lsp::URIForFile::fromFile(abs)) {
502+
if (auto e = uriOrErr.takeError())
503+
return llvm::lsp::Location();
504+
return llvm::lsp::Location(*uriOrErr,
505+
llvm::lsp::Range(Position(line, column)));
506+
}
507+
return llvm::lsp::Location();
508+
}
338509
return llvm::lsp::Location();
339510
}
340511

@@ -353,13 +524,20 @@ VerilogDocument::getLspLocation(slang::SourceRange range) const {
353524

354525
llvm::SMLoc VerilogDocument::getSMLoc(slang::SourceLocation loc) {
355526
auto bufferID = loc.buffer().getId();
527+
llvm::SmallString<256> slangCanonPath("");
356528

357529
// Check if the source is already opened by LLVM source manager.
358530
auto bufferIDMapIt = bufferIDMap.find(bufferID);
359531
if (bufferIDMapIt == bufferIDMap.end()) {
360532
// If not, open the source file and add it to the LLVM source manager.
361533
auto path = getSlangSourceManager().getFullPath(loc.buffer());
362-
auto memBuffer = llvm::MemoryBuffer::getFile(path.string());
534+
535+
// If file is not open yet and not a real path, skip it.
536+
if (std::error_code ec =
537+
llvm::sys::fs::real_path(path.string(), slangCanonPath))
538+
return llvm::SMLoc();
539+
540+
auto memBuffer = llvm::MemoryBuffer::getFile(slangCanonPath);
363541
if (!memBuffer) {
364542
circt::lsp::Logger::error(
365543
"Failed to open file: " + path.filename().string() +
@@ -524,6 +702,13 @@ struct VerilogIndexer : slang::ast::ASTVisitor<VerilogIndexer, true, true> {
524702
return;
525703
assert(range.start().valid() && range.end().valid() &&
526704
"range must be valid");
705+
706+
// TODO: This implementation does not handle expanded MACROs. Return
707+
// instead.
708+
if (range.start() >= range.end()) {
709+
return;
710+
}
711+
527712
index.insertSymbol(symbol, range, isDefinition);
528713
}
529714

@@ -581,7 +766,30 @@ struct VerilogIndexer : slang::ast::ASTVisitor<VerilogIndexer, true, true> {
581766
insertSymbol(def, item->package.location(), false);
582767
}
583768
}
769+
visitDefault(expr);
770+
}
584771

772+
void visit(const slang::ast::InstanceSymbol &expr) {
773+
auto *def = &expr.getDefinition();
774+
if (!def)
775+
return;
776+
777+
// Add the module definition
778+
insertSymbol(def, def->location, /*isDefinition=*/true);
779+
780+
// Walk up the syntax tree until we hit the type token;
781+
// Link that token back to the instance declaration.
782+
if (auto *hierInst =
783+
expr.getSyntax()
784+
->as_if<slang::syntax::HierarchicalInstanceSyntax>())
785+
if (auto *modInst =
786+
hierInst->parent
787+
->as_if<slang::syntax::HierarchyInstantiationSyntax>())
788+
if (modInst->type)
789+
insertSymbol(def, modInst->type.location(), false);
790+
791+
// Link the module instance name back to the module definition
792+
insertSymbol(def, expr.location, /*isDefinition=*/false);
585793
visitDefault(expr);
586794
}
587795

@@ -608,7 +816,13 @@ struct VerilogIndexer : slang::ast::ASTVisitor<VerilogIndexer, true, true> {
608816
void VerilogIndex::initialize(slang::ast::Compilation &compilation) {
609817
const auto &root = compilation.getRoot();
610818
VerilogIndexer visitor(*this);
819+
611820
for (auto *inst : root.topInstances) {
821+
822+
// Skip all modules, interfaces, etc. that are not defined in this files
823+
if (!(document.getLspLocation(inst->location).uri == document.getURI()))
824+
continue;
825+
612826
// Visit the body of the instance.
613827
inst->body.visit(visitor);
614828

@@ -780,10 +994,17 @@ void circt::lsp::VerilogServer::findReferencesOf(
780994
void VerilogIndex::insertSymbol(const slang::ast::Symbol *symbol,
781995
slang::SourceRange from, bool isDefinition) {
782996
assert(from.start().valid() && from.end().valid());
997+
998+
// TODO: Currently doesn't handle expanded macros
999+
if (!from.start().valid() || !from.end().valid() ||
1000+
from.start() >= from.end())
1001+
return;
1002+
7831003
const char *startLoc = getDocument().getSMLoc(from.start()).getPointer();
7841004
const char *endLoc = getDocument().getSMLoc(from.end()).getPointer() + 1;
785-
if (!startLoc || !endLoc)
1005+
if (!startLoc || !endLoc || startLoc >= endLoc)
7861006
return;
1007+
7871008
assert(startLoc && endLoc);
7881009

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

0 commit comments

Comments
 (0)