Skip to content

Commit 1e718cd

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 1e718cd

File tree

7 files changed

+333
-22
lines changed

7 files changed

+333
-22
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: 242 additions & 18 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 =
257343
llvm::MemoryBuffer::getMemBufferCopy(contents, uri.file())) {
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,58 @@ 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+
bufferIDMap[slangBuffer.id.getId()] = mainBufferId;
413+
}
414+
287415
auto diagClient = std::make_shared<LSPDiagnosticClient>(*this, diagnostics);
288416
driver.diagEngine.addClient(diagClient);
289417

@@ -308,6 +436,49 @@ VerilogDocument::VerilogDocument(
308436
if (failed(compilation))
309437
return;
310438

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

@@ -322,19 +493,29 @@ VerilogDocument::getLspLocation(slang::SourceLocation loc) const {
322493
auto line = slangSourceManager.getLineNumber(loc) - 1;
323494
auto column = slangSourceManager.getColumnNumber(loc) - 1;
324495
auto it = bufferIDMap.find(loc.buffer().getId());
325-
// Check if the current buffer is the main file.
326496
if (it != bufferIDMap.end() && it->second == sourceMgr.getMainFileID())
327497
return llvm::lsp::Location(uri, llvm::lsp::Range(Position(line, column)));
328498

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-
}
499+
llvm::StringRef fileName = slangSourceManager.getFileName(loc);
500+
// Ensure absolute path for LSP:
501+
llvm::SmallString<256> abs(fileName);
502+
if (!llvm::sys::path::is_absolute(abs)) {
503+
// Try realPath first
504+
if (std::error_code ec = llvm::sys::fs::real_path(fileName, abs)) {
505+
// Fallback: make it absolute relative to the process CWD
506+
llvm::sys::fs::current_path(abs); // abs = CWD
507+
llvm::sys::path::append(abs, fileName);
508+
}
509+
}
337510

511+
if (auto uriOrErr = llvm::lsp::URIForFile::fromFile(abs)) {
512+
if (auto e = uriOrErr.takeError())
513+
return llvm::lsp::Location();
514+
return llvm::lsp::Location(*uriOrErr,
515+
llvm::lsp::Range(Position(line, column)));
516+
}
517+
return llvm::lsp::Location();
518+
}
338519
return llvm::lsp::Location();
339520
}
340521

@@ -524,6 +705,13 @@ struct VerilogIndexer : slang::ast::ASTVisitor<VerilogIndexer, true, true> {
524705
return;
525706
assert(range.start().valid() && range.end().valid() &&
526707
"range must be valid");
708+
709+
// TODO: This implementation does not handle expanded MACROs. Return
710+
// instead.
711+
if (range.start() >= range.end()) {
712+
return;
713+
}
714+
527715
index.insertSymbol(symbol, range, isDefinition);
528716
}
529717

@@ -581,7 +769,30 @@ struct VerilogIndexer : slang::ast::ASTVisitor<VerilogIndexer, true, true> {
581769
insertSymbol(def, item->package.location(), false);
582770
}
583771
}
772+
visitDefault(expr);
773+
}
774+
775+
void visit(const slang::ast::InstanceSymbol &expr) {
776+
auto *def = &expr.getDefinition();
777+
if (!def)
778+
return;
584779

780+
// Add the module definition
781+
insertSymbol(def, def->location, /*isDefinition=*/true);
782+
783+
// Walk up the syntax tree until we hit the type token;
784+
// Link that token back to the instance declaration.
785+
if (auto *hierInst =
786+
expr.getSyntax()
787+
->as_if<slang::syntax::HierarchicalInstanceSyntax>())
788+
if (auto *modInst =
789+
hierInst->parent
790+
->as_if<slang::syntax::HierarchyInstantiationSyntax>())
791+
if (modInst->type)
792+
insertSymbol(def, modInst->type.location(), false);
793+
794+
// Link the module instance name back to the module definition
795+
insertSymbol(def, expr.location, /*isDefinition=*/false);
585796
visitDefault(expr);
586797
}
587798

@@ -608,7 +819,13 @@ struct VerilogIndexer : slang::ast::ASTVisitor<VerilogIndexer, true, true> {
608819
void VerilogIndex::initialize(slang::ast::Compilation &compilation) {
609820
const auto &root = compilation.getRoot();
610821
VerilogIndexer visitor(*this);
822+
611823
for (auto *inst : root.topInstances) {
824+
825+
// Skip all modules, interfaces, etc. that are not defined in this files
826+
if (!(document.getLspLocation(inst->location).uri == document.getURI()))
827+
continue;
828+
612829
// Visit the body of the instance.
613830
inst->body.visit(visitor);
614831

@@ -780,10 +997,17 @@ void circt::lsp::VerilogServer::findReferencesOf(
780997
void VerilogIndex::insertSymbol(const slang::ast::Symbol *symbol,
781998
slang::SourceRange from, bool isDefinition) {
782999
assert(from.start().valid() && from.end().valid());
1000+
1001+
// TODO: Currently doesn't handle expanded macros
1002+
if (!from.start().valid() || !from.end().valid() ||
1003+
from.start() >= from.end())
1004+
return;
1005+
7831006
const char *startLoc = getDocument().getSMLoc(from.start()).getPointer();
7841007
const char *endLoc = getDocument().getSMLoc(from.end()).getPointer() + 1;
785-
if (!startLoc || !endLoc)
1008+
if (!startLoc || !endLoc || startLoc >= endLoc)
7861009
return;
1010+
7871011
assert(startLoc && endLoc);
7881012

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

0 commit comments

Comments
 (0)