[circt-verilog-lsp-server] Add Verilog Language Server (#8234)

This is a first commit to add a Language Server Protocol (LSP) implementation
for Verilog/SystemVerilog using the slang frontend. This enables IDE features
like syntax error checking and diagnostics.

The server is built as circt-verilog-lsp-server and integrates with CIRCT's
existing Verilog import capabilities. It leverages MLIR's LSP server support
libraries for the protocol implementation.
This commit is contained in:
Hideto Ueno 2025-02-27 16:36:53 -08:00 committed by GitHub
parent c7c7352daf
commit 6128e12465
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1116 additions and 30 deletions

View File

@ -0,0 +1,30 @@
# slang uses exceptions
set(LLVM_REQUIRES_EH ON)
set(LLVM_REQUIRES_RTTI ON)
# For ABI compatibility, define the DEBUG macro in debug builds. Slang sets this
# internally. If we don't set this here as well, header-defined things like the
# destructor of `Driver`, which is generated in ImportVerilog's compilation
# unit, will destroy a different set of fields than what was potentially built
# or modified by code compiled in the Slang compilation unit.
add_compile_definitions($<$<CONFIG:Debug>:DEBUG>)
# Disable some compiler warnings caused by slang headers such that the
# `ImportVerilog` build doesn't spew out a ton of warnings that are not related
# to CIRCT.
if (MSVC)
# No idea what to put here
else ()
# slang uses exceptions; we intercept these in ImportVerilog
add_compile_options(-fexceptions)
add_compile_options(-frtti)
# slang has some classes with virtual funcs but non-virtual destructor.
add_compile_options(-Wno-non-virtual-dtor)
# some other warnings we've seen
add_compile_options(-Wno-c++98-compat-extra-semi)
add_compile_options(-Wno-ctad-maybe-unsupported)
add_compile_options(-Wno-cast-qual)
# visitor switch statements cover all cases but have default
add_compile_options(-Wno-covered-switch-default)
endif ()

View File

@ -0,0 +1,51 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// Main entry function for circt-verilog-lsp-server for when built as standalone
// binary.
//
//===----------------------------------------------------------------------===//
#ifndef CIRCT_TOOLS_CIRCT_VERILOG_LSP_SERVER_CIRCTVERILOGLSPSERVERMAIN_H
#define CIRCT_TOOLS_CIRCT_VERILOG_LSP_SERVER_CIRCTVERILOGLSPSERVERMAIN_H
#include "mlir/Support/LLVM.h"
#include "llvm/ADT/StringRef.h"
#include <memory>
#include <optional>
#include <string>
#include <vector>
namespace llvm {
struct LogicalResult;
} // namespace llvm
namespace mlir {
namespace lsp {
class JSONTransport;
} // namespace lsp
} // namespace mlir
namespace circt {
namespace lsp {
struct VerilogServerOptions {
VerilogServerOptions(const std::vector<std::string> &libDirs)
: libDirs(libDirs) {}
/// Additional list of RTL directories to search.
const std::vector<std::string> &libDirs;
};
// namespace lsp
/// Implementation for tools like `circt-verilog-lsp-server`.
llvm::LogicalResult
CirctVerilogLspServerMain(const VerilogServerOptions &options,
mlir::lsp::JSONTransport &transport);
} // namespace lsp
} // namespace circt
#endif // CIRCT_TOOLS_CIRCT_VERILOG_LSP_SERVER_CIRCTVERILOGLSPSERVERMAIN_H

View File

@ -1,32 +1,4 @@
# slang uses exceptions
set(LLVM_REQUIRES_EH ON)
set(LLVM_REQUIRES_RTTI ON)
# For ABI compatibility, define the DEBUG macro in debug builds. Slang sets this
# internally. If we don't set this here as well, header-defined things like the
# destructor of `Driver`, which is generated in ImportVerilog's compilation
# unit, will destroy a different set of fields than what was potentially built
# or modified by code compiled in the Slang compilation unit.
add_compile_definitions($<$<CONFIG:Debug>:DEBUG>)
# Disable some compiler warnings caused by slang headers such that the
# `ImportVerilog` build doesn't spew out a ton of warnings that are not related
# to CIRCT.
if (MSVC)
# No idea what to put here
else ()
# slang uses exceptions; we intercept these in ImportVerilog
add_compile_options(-fexceptions)
add_compile_options(-frtti)
# slang has some classes with virtual funcs but non-virtual destructor.
add_compile_options(-Wno-non-virtual-dtor)
# some other warnings we've seen
add_compile_options(-Wno-c++98-compat-extra-semi)
add_compile_options(-Wno-ctad-maybe-unsupported)
add_compile_options(-Wno-cast-qual)
# visitor switch statements cover all cases but have default
add_compile_options(-Wno-covered-switch-default)
endif ()
include(SlangCompilerOptions)
add_circt_translation_library(CIRCTImportVerilog
Expressions.cpp

View File

@ -1,3 +1,7 @@
add_subdirectory(circt-bmc)
add_subdirectory(circt-lec)
add_subdirectory(rtgtool)
if(CIRCT_SLANG_FRONTEND_ENABLED)
add_subdirectory(circt-verilog-lsp-server)
endif()

View File

@ -0,0 +1,13 @@
add_subdirectory(Utils)
add_subdirectory(VerilogServerImpl)
add_circt_library(CIRCTVerilogLspServerLib
CirctVerilogLspServerMain.cpp
LSPServer.cpp
ADDITIONAL_HEADER_DIRS
${MLIR_MAIN_INCLUDE_DIR}/circt/Tools/circt-verilog-lsp-server
LINK_LIBS PUBLIC
CIRCTVerilogLspServerImpl
MLIRLspServerSupportLib
)

View File

@ -0,0 +1,22 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "circt/Tools/circt-verilog-lsp-server/CirctVerilogLspServerMain.h"
#include "LSPServer.h"
#include "VerilogServerImpl/VerilogServer.h"
#include "mlir/Tools/lsp-server-support/Transport.h"
using namespace mlir;
using namespace mlir::lsp;
llvm::LogicalResult circt::lsp::CirctVerilogLspServerMain(
const circt::lsp::VerilogServerOptions &options,
mlir::lsp::JSONTransport &transport) {
circt::lsp::VerilogServer server(options);
return circt::lsp::runVerilogLSPServer(server, transport);
}

View File

@ -0,0 +1,163 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "LSPServer.h"
#include "VerilogServerImpl/VerilogServer.h"
#include "mlir/Tools/lsp-server-support/Protocol.h"
#include "mlir/Tools/lsp-server-support/Transport.h"
#include <optional>
#define DEBUG_TYPE "circt-verilog-lsp-server"
using namespace mlir;
using namespace mlir::lsp;
//===----------------------------------------------------------------------===//
// LSPServer
//===----------------------------------------------------------------------===//
namespace {
struct LSPServer {
LSPServer(circt::lsp::VerilogServer &server, JSONTransport &transport)
: server(server), transport(transport) {}
//===--------------------------------------------------------------------===//
// Initialization
//===--------------------------------------------------------------------===//
void onInitialize(const InitializeParams &params,
Callback<llvm::json::Value> reply);
void onInitialized(const InitializedParams &params);
void onShutdown(const NoParams &params, Callback<std::nullptr_t> reply);
//===--------------------------------------------------------------------===//
// Document Change
//===--------------------------------------------------------------------===//
void onDocumentDidOpen(const DidOpenTextDocumentParams &params);
void onDocumentDidClose(const DidCloseTextDocumentParams &params);
void onDocumentDidChange(const DidChangeTextDocumentParams &params);
//===--------------------------------------------------------------------===//
// Fields
//===--------------------------------------------------------------------===//
circt::lsp::VerilogServer &server;
JSONTransport &transport;
/// An outgoing notification used to send diagnostics to the client when they
/// are ready to be processed.
OutgoingNotification<PublishDiagnosticsParams> publishDiagnostics;
/// Used to indicate that the 'shutdown' request was received from the
/// Language Server client.
bool shutdownRequestReceived = false;
};
} // namespace
//===----------------------------------------------------------------------===//
// Initialization
//===----------------------------------------------------------------------===//
void LSPServer::onInitialize(const InitializeParams &params,
Callback<llvm::json::Value> reply) {
// Send a response with the capabilities of this server.
llvm::json::Object serverCaps{
{"textDocumentSync",
llvm::json::Object{
{"openClose", true},
{"change", (int)TextDocumentSyncKind::Incremental},
{"save", true},
}}};
llvm::json::Object result{
{{"serverInfo", llvm::json::Object{{"name", "circt-verilog-lsp-server"},
{"version", "0.0.1"}}},
{"capabilities", std::move(serverCaps)}}};
reply(std::move(result));
}
void LSPServer::onInitialized(const InitializedParams &) {}
void LSPServer::onShutdown(const NoParams &, Callback<std::nullptr_t> reply) {
shutdownRequestReceived = true;
reply(nullptr);
}
//===----------------------------------------------------------------------===//
// Document Change
//===----------------------------------------------------------------------===//
void LSPServer::onDocumentDidOpen(const DidOpenTextDocumentParams &params) {
PublishDiagnosticsParams diagParams(params.textDocument.uri,
params.textDocument.version);
server.addDocument(params.textDocument.uri, params.textDocument.text,
params.textDocument.version, diagParams.diagnostics);
// Publish any recorded diagnostics.
publishDiagnostics(diagParams);
}
void LSPServer::onDocumentDidClose(const DidCloseTextDocumentParams &params) {
std::optional<int64_t> version =
server.removeDocument(params.textDocument.uri);
if (!version)
return;
// Empty out the diagnostics shown for this document. This will clear out
// anything currently displayed by the client for this document (e.g. in the
// "Problems" pane of VSCode).
publishDiagnostics(
PublishDiagnosticsParams(params.textDocument.uri, *version));
}
void LSPServer::onDocumentDidChange(const DidChangeTextDocumentParams &params) {
PublishDiagnosticsParams diagParams(params.textDocument.uri,
params.textDocument.version);
server.updateDocument(params.textDocument.uri, params.contentChanges,
params.textDocument.version, diagParams.diagnostics);
// Publish any recorded diagnostics.
publishDiagnostics(diagParams);
}
//===----------------------------------------------------------------------===//
// Entry Point
//===----------------------------------------------------------------------===//
LogicalResult circt::lsp::runVerilogLSPServer(VerilogServer &server,
JSONTransport &transport) {
LSPServer lspServer(server, transport);
MessageHandler messageHandler(transport);
// Initialization
messageHandler.method("initialize", &lspServer, &LSPServer::onInitialize);
messageHandler.notification("initialized", &lspServer,
&LSPServer::onInitialized);
messageHandler.method("shutdown", &lspServer, &LSPServer::onShutdown);
// Document Changes
messageHandler.notification("textDocument/didOpen", &lspServer,
&LSPServer::onDocumentDidOpen);
messageHandler.notification("textDocument/didClose", &lspServer,
&LSPServer::onDocumentDidClose);
messageHandler.notification("textDocument/didChange", &lspServer,
&LSPServer::onDocumentDidChange);
// Diagnostics
lspServer.publishDiagnostics =
messageHandler.outgoingNotification<PublishDiagnosticsParams>(
"textDocument/publishDiagnostics");
// Run the main loop of the transport.
if (llvm::Error error = transport.run(messageHandler)) {
Logger::error("Transport error: {0}", error);
llvm::consumeError(std::move(error));
return failure();
}
return success(lspServer.shutdownRequestReceived);
}

View File

@ -0,0 +1,35 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#ifndef LIB_CIRCT_TOOLS_CIRCT_VERILOG_LSP_LSPSERVER_H
#define LIB_CIRCT_TOOLS_CIRCT_VERILOG_LSP_LSPSERVER_H
#include <memory>
namespace llvm {
struct LogicalResult;
} // namespace llvm
namespace mlir {
namespace lsp {
class JSONTransport;
} // namespace lsp
} // namespace mlir
namespace circt {
namespace lsp {
class VerilogServer;
/// Run the main loop of the LSP server using the given Verilog server and
/// transport.
llvm::LogicalResult runVerilogLSPServer(VerilogServer &server,
mlir::lsp::JSONTransport &transport);
} // namespace lsp
} // namespace circt
#endif // LIB_CIRCT_TOOLS_CIRCT_VERILOG_LSP_LSPSERVER_H

View File

@ -0,0 +1,16 @@
##===----------------------------------------------------------------------===//
##
## This directory contains the utilities for the Verilog LSP server. slang
## currently requires RTTI but LLVM may not be built with RTTI. In that case
## functions within VerilogServer.cpp use RTTI of LLVM, it will cause
## compilation error. To avoid this, we specialize these functions in this
## directory.
##
##===----------------------------------------------------------------------===//
add_circt_library(CIRCTVerilogLspServerUtils
LSPUtils.cpp
LINK_LIBS PUBLIC
MLIRLspServerSupportLib
)

View File

@ -0,0 +1,26 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file contains utilities for CIRCT Verilog LSP server.
//
//===----------------------------------------------------------------------===//
#include "LSPUtils.h"
#include "mlir/Tools/lsp-server-support/Logging.h"
void circt::lsp::Logger::error(Twine message) {
mlir::lsp::Logger::error("{}", message);
}
void circt::lsp::Logger::info(Twine message) {
mlir::lsp::Logger::info("{}", message);
}
void circt::lsp::Logger::debug(Twine message) {
mlir::lsp::Logger::debug("{}", message);
}

View File

@ -0,0 +1,18 @@
#ifndef CIRCT_SUPPORT_LSPUTILS_H
#define CIRCT_SUPPORT_LSPUTILS_H
#include "circt/Support/LLVM.h"
namespace circt {
namespace lsp {
namespace Logger {
// These are specialization of logger functions. Slang requires RTTI but
// usually LLVM is not built with RTTI. So it causes compilation error when
// these functions are used in `VerilogServerImpl`.
void error(Twine message);
void info(Twine message);
void debug(Twine message);
} // namespace Logger
} // namespace lsp
} // namespace circt
#endif // CIRCT_SUPPORT_LSPUTILS_H

View File

@ -0,0 +1,18 @@
# Include the Slang compiler options
include(SlangCompilerOptions)
add_circt_library(CIRCTVerilogLspServerImpl
VerilogServer.cpp
ADDITIONAL_HEADER_DIRS
${MLIR_MAIN_INCLUDE_DIR}/circt/Tools/crct-verilog-lsp-server
DEPENDS
slang_slang
LINK_LIBS PUBLIC
MLIRLspServerSupportLib
CIRCTVerilogLspServerUtils
PRIVATE
slang_slang
)

View File

@ -0,0 +1,351 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file implements the VerilogServer class, which is responsible for
// managing the state of the Verilog server. VerilogServer keeps track of the
// contents of all open text documents, and each document has a slang
// compilation result.
//
//===----------------------------------------------------------------------===//
#include "VerilogServer.h"
#include "../Utils/LSPUtils.h"
#include "circt/Support/LLVM.h"
#include "circt/Tools/circt-verilog-lsp-server/CirctVerilogLspServerMain.h"
#include "mlir/Tools/lsp-server-support/Logging.h"
#include "mlir/Tools/lsp-server-support/Protocol.h"
#include "slang/ast/Compilation.h"
#include "slang/diagnostics/DiagnosticClient.h"
#include "slang/diagnostics/Diagnostics.h"
#include "slang/driver/Driver.h"
#include "slang/syntax/SyntaxTree.h"
#include "slang/text/SourceLocation.h"
#include "slang/text/SourceManager.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/SourceMgr.h"
#include <memory>
#include <optional>
using namespace mlir;
using namespace mlir::lsp;
using namespace circt::lsp;
using namespace circt;
static mlir::lsp::DiagnosticSeverity
getSeverity(slang::DiagnosticSeverity severity) {
switch (severity) {
case slang::DiagnosticSeverity::Fatal:
case slang::DiagnosticSeverity::Error:
return mlir::lsp::DiagnosticSeverity::Error;
case slang::DiagnosticSeverity::Warning:
return mlir::lsp::DiagnosticSeverity::Warning;
case slang::DiagnosticSeverity::Ignored:
case slang::DiagnosticSeverity::Note:
return mlir::lsp::DiagnosticSeverity::Information;
}
llvm_unreachable("all slang diagnostic severities should be handled");
return mlir::lsp::DiagnosticSeverity::Error;
}
namespace {
// A global context carried around by the server.
struct VerilogServerContext {
VerilogServerContext(const VerilogServerOptions &options)
: options(options) {}
const VerilogServerOptions &options;
};
//===----------------------------------------------------------------------===//
// VerilogDocument
//===----------------------------------------------------------------------===//
/// This class represents all of the information pertaining to a specific
/// Verilog document.
class LSPDiagnosticClient;
struct VerilogDocument {
VerilogDocument(VerilogServerContext &globalContext,
const mlir::lsp::URIForFile &uri, StringRef contents,
std::vector<mlir::lsp::Diagnostic> &diagnostics);
VerilogDocument(const VerilogDocument &) = delete;
VerilogDocument &operator=(const VerilogDocument &) = delete;
const mlir::lsp::URIForFile &getURI() const { return uri; }
llvm::SourceMgr &getSourceMgr() { return sourceMgr; }
llvm::SmallDenseMap<uint32_t, uint32_t> &getBufferIDMap() {
return bufferIDMap;
}
const slang::SourceManager &getSlangSourceManager() const {
return driver.sourceManager;
}
// Return LSP location from slang location.
mlir::lsp::Location getLspLocation(slang::SourceLocation loc) const;
private:
// A map from slang buffer ID to the corresponding buffer ID in the LLVM
// source manager.
llvm::SmallDenseMap<uint32_t, uint32_t> bufferIDMap;
// The compilation result.
FailureOr<std::unique_ptr<slang::ast::Compilation>> compilation;
// The slang driver.
slang::driver::Driver driver;
// The LLVM source manager.
llvm::SourceMgr sourceMgr;
// The URI of the document.
mlir::lsp::URIForFile uri;
};
} // namespace
//===----------------------------------------------------------------------===//
// LSPDiagnosticClient
//===----------------------------------------------------------------------===//
namespace {
/// A converter that can be plugged into a slang `DiagnosticEngine` as a
/// client that will map slang diagnostics to LSP diagnostics.
class LSPDiagnosticClient : public slang::DiagnosticClient {
const VerilogDocument &document;
std::vector<mlir::lsp::Diagnostic> &diags;
public:
LSPDiagnosticClient(const VerilogDocument &document,
std::vector<mlir::lsp::Diagnostic> &diags)
: document(document), diags(diags) {}
void report(const slang::ReportedDiagnostic &slangDiag) override;
};
} // namespace
void LSPDiagnosticClient::report(const slang::ReportedDiagnostic &slangDiag) {
auto loc = document.getLspLocation(slangDiag.location);
// Show only the diagnostics in the current file.
if (loc.uri != document.getURI())
return;
auto &mlirDiag = diags.emplace_back();
mlirDiag.severity = getSeverity(slangDiag.severity);
mlirDiag.range = loc.range;
mlirDiag.source = "slang";
mlirDiag.message = slangDiag.formattedMessage;
}
//===----------------------------------------------------------------------===//
// VerilogDocument
//===----------------------------------------------------------------------===//
VerilogDocument::VerilogDocument(
VerilogServerContext &context, const mlir::lsp::URIForFile &uri,
StringRef contents, std::vector<mlir::lsp::Diagnostic> &diagnostics)
: uri(uri) {
unsigned int bufferId;
if (auto memBufferOwn =
llvm::MemoryBuffer::getMemBufferCopy(contents, uri.file())) {
bufferId = sourceMgr.AddNewSourceBuffer(std::move(memBufferOwn), SMLoc());
} else {
circt::lsp::Logger::error(
Twine("Failed to create memory buffer for file ") + uri.file());
return;
}
// Build the set of include directories for this file.
llvm::SmallString<32> uriDirectory(uri.file());
llvm::sys::path::remove_filename(uriDirectory);
std::vector<std::string> libDirs;
libDirs.push_back(uriDirectory.str().str());
libDirs.insert(libDirs.end(), context.options.libDirs.begin(),
context.options.libDirs.end());
// Populate source managers.
const llvm::MemoryBuffer *memBuffer = sourceMgr.getMemoryBuffer(bufferId);
driver.options.libDirs = libDirs;
// Assign text to slang.
auto slangBuffer =
driver.sourceManager.assignText(uri.file(), memBuffer->getBuffer());
driver.buffers.push_back(slangBuffer);
bufferIDMap[slangBuffer.id.getId()] = bufferId;
auto diagClient = std::make_shared<LSPDiagnosticClient>(*this, diagnostics);
driver.diagEngine.addClient(diagClient);
if (!driver.parseAllSources()) {
circt::lsp::Logger::error(Twine("Failed to parse Verilog file ") +
uri.file());
return;
}
compilation = driver.createCompilation();
for (auto &diag : (*compilation)->getAllDiagnostics())
driver.diagEngine.issue(diag);
}
mlir::lsp::Location
VerilogDocument::getLspLocation(slang::SourceLocation loc) const {
if (loc && loc.buffer() != slang::SourceLocation::NoLocation.buffer()) {
const auto &slangSourceManager = getSlangSourceManager();
auto line = slangSourceManager.getLineNumber(loc) - 1;
auto column = slangSourceManager.getColumnNumber(loc) - 1;
auto it = bufferIDMap.find(loc.buffer().getId());
// Check if the current buffer is the main file.
if (it != bufferIDMap.end() && it->second == sourceMgr.getMainFileID())
return mlir::lsp::Location(uri, mlir::lsp::Range(Position(line, column)));
// Otherwise, construct URI from slang source manager.
auto fileName = slangSourceManager.getFileName(loc);
auto loc = mlir::lsp::URIForFile::fromFile(
slangSourceManager.makeAbsolutePath(fileName));
if (auto e = loc.takeError())
return mlir::lsp::Location();
return mlir::lsp::Location(loc.get(),
mlir::lsp::Range(Position(line, column)));
}
return mlir::lsp::Location();
}
//===----------------------------------------------------------------------===//
// VerilogTextFile
//===----------------------------------------------------------------------===//
namespace {
/// This class represents a text file containing one or more Verilog
/// documents.
class VerilogTextFile {
public:
VerilogTextFile(VerilogServerContext &globalContext,
const mlir::lsp::URIForFile &uri, StringRef fileContents,
int64_t version,
std::vector<mlir::lsp::Diagnostic> &diagnostics);
/// Return the current version of this text file.
int64_t getVersion() const { return version; }
/// Update the file to the new version using the provided set of content
/// changes. Returns failure if the update was unsuccessful.
LogicalResult
update(const mlir::lsp::URIForFile &uri, int64_t newVersion,
ArrayRef<mlir::lsp::TextDocumentContentChangeEvent> changes,
std::vector<mlir::lsp::Diagnostic> &diagnostics);
private:
/// Initialize the text file from the given file contents.
void initialize(const mlir::lsp::URIForFile &uri, int64_t newVersion,
std::vector<mlir::lsp::Diagnostic> &diagnostics);
VerilogServerContext &context;
/// The full string contents of the file.
std::string contents;
/// The version of this file.
int64_t version = 0;
/// The chunks of this file. The order of these chunks is the order in which
/// they appear in the text file.
std::unique_ptr<VerilogDocument> document;
};
} // namespace
VerilogTextFile::VerilogTextFile(
VerilogServerContext &context, const mlir::lsp::URIForFile &uri,
StringRef fileContents, int64_t version,
std::vector<mlir::lsp::Diagnostic> &diagnostics)
: context(context), contents(fileContents.str()) {
initialize(uri, version, diagnostics);
}
LogicalResult VerilogTextFile::update(
const mlir::lsp::URIForFile &uri, int64_t newVersion,
ArrayRef<mlir::lsp::TextDocumentContentChangeEvent> changes,
std::vector<mlir::lsp::Diagnostic> &diagnostics) {
if (failed(mlir::lsp::TextDocumentContentChangeEvent::applyTo(changes,
contents))) {
circt::lsp::Logger::error(Twine("Failed to update contents of ") +
uri.file());
return failure();
}
// If the file contents were properly changed, reinitialize the text file.
initialize(uri, newVersion, diagnostics);
return success();
}
void VerilogTextFile::initialize(
const mlir::lsp::URIForFile &uri, int64_t newVersion,
std::vector<mlir::lsp::Diagnostic> &diagnostics) {
version = newVersion;
document =
std::make_unique<VerilogDocument>(context, uri, contents, diagnostics);
}
//===----------------------------------------------------------------------===//
// VerilogServer::Impl
//===----------------------------------------------------------------------===//
struct circt::lsp::VerilogServer::Impl {
explicit Impl(const VerilogServerOptions &options) : context(options) {}
/// The files held by the server, mapped by their URI file name.
llvm::StringMap<std::unique_ptr<VerilogTextFile>> files;
VerilogServerContext context;
};
//===----------------------------------------------------------------------===//
// VerilogServer
//===----------------------------------------------------------------------===//
circt::lsp::VerilogServer::VerilogServer(const VerilogServerOptions &options)
: impl(std::make_unique<Impl>(options)) {}
circt::lsp::VerilogServer::~VerilogServer() = default;
void circt::lsp::VerilogServer::addDocument(
const URIForFile &uri, StringRef contents, int64_t version,
std::vector<mlir::lsp::Diagnostic> &diagnostics) {
impl->files[uri.file()] = std::make_unique<VerilogTextFile>(
impl->context, uri, contents, version, diagnostics);
}
void circt::lsp::VerilogServer::updateDocument(
const URIForFile &uri,
ArrayRef<mlir::lsp::TextDocumentContentChangeEvent> changes,
int64_t version, std::vector<mlir::lsp::Diagnostic> &diagnostics) {
// Check that we actually have a document for this uri.
auto it = impl->files.find(uri.file());
if (it == impl->files.end())
return;
// Try to update the document. If we fail, erase the file from the server. A
// failed updated generally means we've fallen out of sync somewhere.
if (failed(it->second->update(uri, version, changes, diagnostics)))
impl->files.erase(it);
}
std::optional<int64_t>
circt::lsp::VerilogServer::removeDocument(const URIForFile &uri) {
auto it = impl->files.find(uri.file());
if (it == impl->files.end())
return std::nullopt;
int64_t version = it->second->getVersion();
impl->files.erase(it);
return version;
}

View File

@ -0,0 +1,71 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// Main entry function for circt-verilog-lsp-server for when built as standalone
// binary.
//
//===----------------------------------------------------------------------===//
#ifndef LIB_CIRCT_TOOLS_CIRCT_VERILOG_LSP_SERVER_VERILOGSERVER_H_
#define LIB_CIRCT_TOOLS_CIRCT_VERILOG_LSP_SERVER_VERILOGSERVER_H_
#include "mlir/Support/LLVM.h"
#include "llvm/ADT/StringRef.h"
#include <memory>
#include <optional>
#include <vector>
namespace mlir {
namespace lsp {
struct Diagnostic;
struct TextDocumentContentChangeEvent;
class URIForFile;
} // namespace lsp
} // namespace mlir
namespace circt {
namespace lsp {
struct VerilogServerOptions;
using TextDocumentContentChangeEvent =
mlir::lsp::TextDocumentContentChangeEvent;
using URIForFile = mlir::lsp::URIForFile;
using Diagnostic = mlir::lsp::Diagnostic;
/// This class implements all of the Verilog related functionality necessary for
/// a language server. This class allows for keeping the Verilog specific logic
/// separate from the logic that involves LSP server/client communication.
class VerilogServer {
public:
VerilogServer(const circt::lsp::VerilogServerOptions &options);
~VerilogServer();
/// Add the document, with the provided `version`, at the given URI. Any
/// diagnostics emitted for this document should be added to `diagnostics`.
void addDocument(const URIForFile &uri, llvm::StringRef contents,
int64_t version, std::vector<Diagnostic> &diagnostics);
/// Update the document, with the provided `version`, at the given URI. Any
/// diagnostics emitted for this document should be added to `diagnostics`.
void updateDocument(const URIForFile &uri,
llvm::ArrayRef<TextDocumentContentChangeEvent> changes,
int64_t version, std::vector<Diagnostic> &diagnostics);
/// Remove the document with the given uri. Returns the version of the removed
/// document, or std::nullopt if the uri did not have a corresponding document
/// within the server.
std::optional<int64_t> removeDocument(const URIForFile &uri);
private:
struct Impl;
std::unique_ptr<Impl> impl;
};
} // namespace lsp
} // namespace circt
#endif // LIB_CIRCT_TOOLS_CIRCT_VERILOG_LSP_SERVER_VERILOGSERVER_H_

View File

@ -48,6 +48,7 @@ endif()
if(CIRCT_SLANG_FRONTEND_ENABLED)
list(APPEND CIRCT_TEST_DEPENDS circt-verilog)
list(APPEND CIRCT_TEST_DEPENDS circt-verilog-lsp-server)
endif()
add_lit_testsuite(check-circt "Running the CIRCT regression tests"

View File

@ -0,0 +1,52 @@
// RUN: circt-verilog-lsp-server -lit-test < %s | FileCheck %s
// REQUIRES: slang
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"verilog","capabilities":{},"trace":"off"}}
// -----
{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{
"uri":"test:///diagnostic.sv",
"languageId":"verilog",
"version":1,
"text":"module foo()\n wire bar;\nendmodule"
}}}
// CHECK: "method": "textDocument/publishDiagnostics",
// CHECK-NEXT: "params": {
// CHECK-NEXT: "diagnostics": [
// CHECK-NEXT: {
// CHECK-NEXT: "message": "expected ';'",
// CHECK-NEXT: "range": {
// CHECK-NEXT: "end": {
// CHECK-NEXT: "character": 12,
// CHECK-NEXT: "line": 0
// CHECK-NEXT: },
// CHECK-NEXT: "start": {
// CHECK-NEXT: "character": 12,
// CHECK-NEXT: "line": 0
// CHECK-NEXT: }
// CHECK-NEXT: },
// CHECK-NEXT: "severity": 1,
// CHECK-NEXT: "source": "slang"
// CHECK-NEXT: },
// CHECK-NEXT: {
// CHECK-NEXT: "message": "unused net 'bar'",
// CHECK-NEXT: "range": {
// CHECK-NEXT: "end": {
// CHECK-NEXT: "character": 6,
// CHECK-NEXT: "line": 1
// CHECK-NEXT: },
// CHECK-NEXT: "start": {
// CHECK-NEXT: "character": 6,
// CHECK-NEXT: "line": 1
// CHECK-NEXT: }
// CHECK-NEXT: },
// CHECK-NEXT: "severity": 2,
// CHECK-NEXT: "source": "slang"
// CHECK-NEXT: }
// CHECK-NEXT: ],
// CHECK-NEXT: "uri": "test:///diagnostic.sv",
// CHECK-NEXT: "version": 1
// CHECK-NEXT: }
// CHECK-NEXT: }
// -----
{"jsonrpc":"2.0","id":7,"method":"shutdown"}
// -----
{"jsonrpc":"2.0","method":"exit"}

View File

@ -0,0 +1,38 @@
// RUN: circt-verilog-lsp-server -lit-test < %s | FileCheck %s
// RUN: circt-verilog-lsp-server --libdir %S/include -lit-test < %s | FileCheck %s --check-prefix=CHECK-INCLUDE
// REQUIRES: slang
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"verilog","capabilities":{},"trace":"off"}}
// -----
{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{
"uri":"test:///diagnostic.sv",
"languageId":"verilog",
"version":1,
"text":"module Foo();\n Bar bar();\nendmodule"
}}}
// CHECK: "method": "textDocument/publishDiagnostics",
// CHECK-NEXT: "params": {
// CHECK-NEXT: "diagnostics": [
// CHECK-NEXT: {
// CHECK-NEXT: "message": "unknown module 'Bar'",
// CHECK-NEXT: "range": {
// CHECK-NEXT: "end": {
// CHECK-NEXT: "character": 1,
// CHECK-NEXT: "line": 1
// CHECK-NEXT: },
// CHECK-NEXT: "start": {
// CHECK-NEXT: "character": 1,
// CHECK-NEXT: "line": 1
// CHECK-NEXT: }
// CHECK-NEXT: },
// CHECK-NEXT: "severity": 1,
// CHECK-NEXT: "source": "slang"
// CHECK-NEXT: }
// CHECK-NEXT: ],
// CHECK-NEXT: "uri": "test:///diagnostic.sv",
// CHECK-NEXT: "version": 1
// CHECK-NEXT: }
// CHECK-INCLUDE-NOT: unknown module 'Bar'
// -----
{"jsonrpc":"2.0","id":7,"method":"shutdown"}
// -----
{"jsonrpc":"2.0","method":"exit"}

View File

@ -0,0 +1,2 @@
module Bar();
endmodule

View File

@ -0,0 +1,26 @@
// RUN: circt-verilog-lsp-server -lit-test < %s | FileCheck %s
// REQUIRES: slang
// Test initialize request parameters with rootUri
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootUri":"test:///workspace","capabilities":{},"trace":"off"}}
// CHECK: "id": 0,
// CHECK-NEXT: "jsonrpc": "2.0",
// CHECK-NEXT: "result": {
// CHECK-NEXT: "capabilities": {
// CHECK-NEXT: "textDocumentSync": {
// CHECK-NEXT: "change": 2,
// CHECK-NEXT: "openClose": true,
// CHECK-NEXT: "save": true
// CHECK-NEXT: }
// CHECK-NEXT: },
// CHECK-NEXT: "serverInfo": {
// CHECK-NEXT: "name": "circt-verilog-lsp-server",
// CHECK-NEXT: "version": "{{.*}}"
// CHECK-NEXT: }
// CHECK-NEXT: }
// -----
{"jsonrpc":"2.0","id":3,"method":"shutdown"}
// CHECK: "id": 3,
// CHECK-NEXT: "jsonrpc": "2.0",
// CHECK-NEXT: "result": null
// -----
{"jsonrpc":"2.0","method":"exit"}

View File

@ -0,0 +1 @@
config.excludes = ["include"]

View File

@ -0,0 +1,35 @@
// RUN: circt-verilog-lsp-server -lit-test < %s | FileCheck -strict-whitespace %s
// REQUIRES: slang
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"verilog","capabilities":{},"trace":"off"}}
// -----
{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{
"uri":"test:///foo.sv",
"languageId":"verilog",
"version":1,
"text":"module Foo(); "
}}}
// CHECK: "method": "textDocument/publishDiagnostics",
// CHECK-NEXT: "params": {
// CHECK: "diagnostics": [
// CHECK: "message": "expected 'endmodule'",
// -----
{"jsonrpc":"2.0","method":"textDocument/didChange","params":{"textDocument":{
"uri":"test:///foo.sv",
"version":2
}, "contentChanges": [{
"range":{
"start":{"line":0,"character":14},
"end":{"line":0,"character":14}
},
"text": " endmodule"
}]}}
// CHECK: "method": "textDocument/publishDiagnostics",
// CHECK-NEXT: "params": {
// CHECK-NEXT: "diagnostics": [],
// CHECK-NEXT: "uri": "test:///foo.sv",
// CHECK-NEXT: "version": 2
// CHECK-NEXT: }
// -----
{"jsonrpc":"2.0","id":3,"method":"shutdown"}
// -----
{"jsonrpc":"2.0","method":"exit"}

View File

@ -0,0 +1,29 @@
// RUN: circt-verilog-lsp-server -lit-test < %s | FileCheck -strict-whitespace %s
// REQUIRES: slang
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"verilog","capabilities":{},"trace":"off"}}
// -----
{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{
"uri":"test:///foo.sv",
"languageId":"verilog",
"version":1,
"text":"module Foo(); "
}}}
// CHECK: "method": "textDocument/publishDiagnostics",
// CHECK-NEXT: "params": {
// CHECK: "diagnostics": [
// CHECK: "message": "expected 'endmodule'",
// -----
{"jsonrpc":"2.0","method":"textDocument/didClose","params":{"textDocument":{
"uri":"test:///foo.sv",
"version":2
}}}
// CHECK: "method": "textDocument/publishDiagnostics",
// CHECK-NEXT: "params": {
// CHECK-NEXT: "diagnostics": [],
// CHECK-NEXT: "uri": "test:///foo.sv",
// CHECK-NEXT: "version": 1
// CHECK-NEXT: }
// -----
{"jsonrpc":"2.0","id":3,"method":"shutdown"}
// -----
{"jsonrpc":"2.0","method":"exit"}

View File

@ -22,7 +22,7 @@ config.name = 'CIRCT'
config.test_format = lit.formats.ShTest(not llvm_config.use_lit_shell)
# suffixes: A list of file extensions to treat as test files.
config.suffixes = ['.td', '.mlir', '.ll', '.fir', '.sv']
config.suffixes = ['.td', '.mlir', '.ll', '.fir', '.sv', '.test']
# test_source_root: The root path where tests are located.
config.test_source_root = os.path.dirname(__file__)
@ -91,5 +91,6 @@ if config.scheduling_or_tools != "":
if config.slang_frontend_enabled:
config.available_features.add('slang')
tools.append('circt-verilog')
tools.append('circt-verilog-lsp-server')
llvm_config.add_tool_substitutions(tools, tool_dirs)

View File

@ -20,4 +20,5 @@ add_subdirectory(py-split-input-file)
if(CIRCT_SLANG_FRONTEND_ENABLED)
add_subdirectory(circt-verilog)
add_subdirectory(circt-verilog-lsp-server)
endif()

View File

@ -0,0 +1,9 @@
set(libs
CIRCTVerilogLspServerLib
)
add_circt_tool(circt-verilog-lsp-server circt-verilog-lsp-server.cpp DEPENDS ${libs})
target_link_libraries(circt-verilog-lsp-server PRIVATE ${libs})
llvm_update_compile_flags(circt-verilog-lsp-server)
mlir_check_all_link_libraries(circt-verilog-lsp-server)

View File

@ -0,0 +1,101 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file implements a utility to run CIRCT Verilog LSP server.
//
//===----------------------------------------------------------------------===//
#include "circt/Tools/circt-verilog-lsp-server/CirctVerilogLspServerMain.h"
#include "mlir/Tools/lsp-server-support/Logging.h"
#include "mlir/Tools/lsp-server-support/Transport.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/Program.h"
using namespace mlir;
using namespace mlir::lsp;
int main(int argc, char **argv) {
//===--------------------------------------------------------------------===//
// LSP options
//===--------------------------------------------------------------------===//
llvm::cl::opt<Logger::Level> logLevel{
"log",
llvm::cl::desc("Verbosity of log messages written to stderr"),
llvm::cl::values(
clEnumValN(Logger::Level::Error, "error", "Error messages only"),
clEnumValN(Logger::Level::Info, "info",
"High level execution tracing"),
clEnumValN(Logger::Level::Debug, "verbose", "Low level details")),
llvm::cl::init(Logger::Level::Info),
};
llvm::cl::opt<mlir::lsp::JSONStreamStyle> inputStyle{
"input-style",
llvm::cl::desc("Input JSON stream encoding"),
llvm::cl::values(clEnumValN(mlir::lsp::JSONStreamStyle::Standard,
"standard", "usual LSP protocol"),
clEnumValN(mlir::lsp::JSONStreamStyle::Delimited,
"delimited",
"messages delimited by `// -----` lines, "
"with // comment support")),
llvm::cl::init(mlir::lsp::JSONStreamStyle::Standard),
llvm::cl::Hidden,
};
//===--------------------------------------------------------------------===//
// Include paths
//===--------------------------------------------------------------------===//
llvm::cl::list<std::string> libDirs{
"y",
llvm::cl::desc(
"Library search paths, which will be searched for missing modules"),
llvm::cl::value_desc("dir"), llvm::cl::Prefix};
llvm::cl::alias libDirsLong{"libdir", llvm::cl::desc("Alias for -y"),
llvm::cl::aliasopt(libDirs), llvm::cl::NotHidden};
//===--------------------------------------------------------------------===//
// Testing
//===--------------------------------------------------------------------===//
llvm::cl::opt<bool> prettyPrint{
"pretty",
llvm::cl::desc("Pretty-print JSON output"),
llvm::cl::init(false),
};
llvm::cl::opt<bool> litTest{
"lit-test",
llvm::cl::desc(
"Abbreviation for -input-style=delimited -pretty -log=verbose. "
"Intended to simplify lit tests"),
llvm::cl::init(false),
};
llvm::cl::ParseCommandLineOptions(argc, argv, "Verilog LSP Language Server");
if (litTest) {
inputStyle = mlir::lsp::JSONStreamStyle::Delimited;
logLevel = mlir::lsp::Logger::Level::Debug;
prettyPrint = true;
}
// Configure the logger.
mlir::lsp::Logger::setLogLevel(logLevel);
// Configure the transport used for communication.
(void)llvm::sys::ChangeStdinToBinary();
mlir::lsp::JSONTransport transport(stdin, llvm::outs(), inputStyle,
prettyPrint);
// Configure the servers and start the main language server.
circt::lsp::VerilogServerOptions options(libDirs);
return failed(circt::lsp::CirctVerilogLspServerMain(options, transport));
}