[mlir][PDLL] Add initial support for a PDLL compilation database

The compilation database acts in a similar way to the compilation database
(compile_commands.json) used by clang-tidy, i.e. it provides additional
information about the compilation of project files to help the language
server. The main piece of information provided by the PDLL compilation
database in this commit is the set of include directories used when processing
the input .pdll file. This allows for the server to properly process .pdll files
that use includes anchored by the include directories set up in the build system.

The structure of the textual form of a compilation database is a yaml file
containing documents of the following form:

```
--- !FileInfo:
  filepath: <string> - Absolute file path of the file.
  includes: <string> - Semi-colon delimited list of include directories.
```

This commit also adds support to cmake for automatically generating
a `pdll_compile_commands.yml` file at the top-level of the build
directory.

Differential Revision: https://reviews.llvm.org/D124076
This commit is contained in:
River Riddle 2022-04-19 23:53:42 -07:00
parent 597fde54a8
commit fb5a59f6e1
14 changed files with 355 additions and 66 deletions

View File

@ -7,6 +7,10 @@ function(mlir_tablegen ofn)
PARENT_SCOPE)
endfunction()
# Clear out any pre-existing compile_commands file before processing. This
# allows for generating a clean compile_commands on each configure.
file(REMOVE ${CMAKE_BINARY_DIR}/pdll_compile_commands.yml)
# Declare a PDLL library in the current directory.
function(add_mlir_pdll_library target inputFile ofn)
set(LLVM_TARGET_DEFINITIONS ${inputFile})
@ -15,6 +19,28 @@ function(add_mlir_pdll_library target inputFile ofn)
set(TABLEGEN_OUTPUT ${TABLEGEN_OUTPUT} ${CMAKE_CURRENT_BINARY_DIR}/${ofn}
PARENT_SCOPE)
# Get the current set of include paths for this pdll file.
cmake_parse_arguments(ARG "" "" "DEPENDS;EXTRA_INCLUDES" ${ARGN})
get_directory_property(tblgen_includes INCLUDE_DIRECTORIES)
list(APPEND tblgen_includes ${ARG_EXTRA_INCLUDES})
# Filter out any empty include items.
list(REMOVE_ITEM tblgen_includes "")
# Build the absolute path for the current input file.
if (IS_ABSOLUTE ${LLVM_TARGET_DEFINITIONS})
set(LLVM_TARGET_DEFINITIONS_ABSOLUTE ${inputFile})
else()
set(LLVM_TARGET_DEFINITIONS_ABSOLUTE ${CMAKE_CURRENT_SOURCE_DIR}/${inputFile})
endif()
# Append the includes used for this file to the pdll_compilation_commands
# file.
file(APPEND ${CMAKE_BINARY_DIR}/pdll_compile_commands.yml
"--- !FileInfo:\n"
" filepath: \"${LLVM_TARGET_DEFINITIONS_ABSOLUTE}\"\n"
" includes: \"${CMAKE_CURRENT_SOURCE_DIR};${tblgen_includes}\"\n"
)
add_public_tablegen_target(${target})
endfunction()

View File

@ -1,4 +1,5 @@
llvm_add_library(MLIRPdllLspServerLib
CompilationDatabase.cpp
LSPServer.cpp
PDLLServer.cpp
MlirPdllLspServerMain.cpp

View File

@ -0,0 +1,81 @@
//===- CompilationDatabase.cpp - PDLL Compilation Database ----------------===//
//
// 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 "CompilationDatabase.h"
#include "../lsp-server-support/Logging.h"
#include "mlir/Support/FileUtilities.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/YAMLTraits.h"
using namespace mlir;
using namespace mlir::lsp;
//===----------------------------------------------------------------------===//
// CompilationDatabase
//===----------------------------------------------------------------------===//
LLVM_YAML_IS_DOCUMENT_LIST_VECTOR(CompilationDatabase::FileInfo)
namespace llvm {
namespace yaml {
template <>
struct MappingTraits<CompilationDatabase::FileInfo> {
static void mapping(IO &io, CompilationDatabase::FileInfo &info) {
io.mapRequired("filepath", info.filename);
// Parse the includes from the yaml stream. These are in the form of a
// semi-colon delimited list.
std::string combinedIncludes;
io.mapRequired("includes", combinedIncludes);
for (StringRef include : llvm::split(combinedIncludes, ";")) {
if (!include.empty())
info.includeDirs.push_back(include.str());
}
}
};
} // end namespace yaml
} // end namespace llvm
CompilationDatabase::CompilationDatabase(ArrayRef<std::string> databases) {
for (StringRef filename : databases)
loadDatabase(filename);
}
const CompilationDatabase::FileInfo *
CompilationDatabase::getFileInfo(StringRef filename) const {
auto it = files.find(filename);
return it == files.end() ? nullptr : &it->second;
}
void CompilationDatabase::loadDatabase(StringRef filename) {
if (filename.empty())
return;
// Set up the input file.
std::string errorMessage;
std::unique_ptr<llvm::MemoryBuffer> inputFile =
openInputFile(filename, &errorMessage);
if (!inputFile) {
Logger::error("Failed to open compilation database: {0}", errorMessage);
return;
}
llvm::yaml::Input yaml(inputFile->getBuffer());
// Parse the yaml description and add any new files to the database.
std::vector<FileInfo> parsedFiles;
yaml >> parsedFiles;
for (auto &file : parsedFiles) {
auto it = files.try_emplace(file.filename, std::move(file));
// If we encounter a duplicate file, log a warning and ignore it.
if (!it.second) {
Logger::info("Duplicate .pdll file in compilation database: {0}",
file.filename);
}
}
}

View File

@ -0,0 +1,58 @@
//===- CompilationDatabase.h - PDLL Compilation Database --------*- C++ -*-===//
//
// 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_MLIR_TOOLS_MLIRPDLLSPSERVER_COMPILATIONDATABASE_H_
#define LIB_MLIR_TOOLS_MLIRPDLLSPSERVER_COMPILATIONDATABASE_H_
#include "mlir/Support/LLVM.h"
#include "llvm/ADT/StringMap.h"
#include <memory>
#include <string>
namespace mlir {
namespace lsp {
/// This class contains a collection of compilation information for files
/// provided to the language server, such as the available include directories.
/// This database acts as an aggregate in-memory form of compilation databases
/// used by the current language client. The textual form of a compilation
/// database is a YAML file containing documents of the following form:
///
/// --- !FileInfo:
/// filepath: <string> - Absolute file path of the file.
/// includes: <string> - Semi-colon delimited list of include directories.
///
class CompilationDatabase {
public:
/// Compilation information for a specific file within the database.
struct FileInfo {
/// The absolute path to the file.
std::string filename;
/// The include directories available for the file.
std::vector<std::string> includeDirs;
};
/// Construct a compilation database from the provided files containing YAML
/// descriptions of the database.
CompilationDatabase(ArrayRef<std::string> databases);
/// Get the compilation information for the provided file, or nullptr if the
/// database doesn't include information for `filename`.
const FileInfo *getFileInfo(StringRef filename) const;
private:
/// Load the given database file into this database.
void loadDatabase(StringRef filename);
/// A map of filename to file information for each known file within the
/// databases.
llvm::StringMap<FileInfo> files;
};
} // namespace lsp
} // namespace mlir
#endif // LIB_MLIR_TOOLS_MLIRPDLLSPSERVER_COMPILATIONDATABASE_H_

View File

@ -54,6 +54,10 @@ LogicalResult mlir::MlirPdllLspServerMain(int argc, char **argv) {
llvm::cl::list<std::string> extraIncludeDirs(
"pdll-extra-dir", llvm::cl::desc("Extra directory of include files"),
llvm::cl::value_desc("directory"), llvm::cl::Prefix);
llvm::cl::list<std::string> compilationDatabases(
"pdll-compilation-database",
llvm::cl::desc("Compilation YAML databases containing additional "
"compilation information for .pdll files"));
llvm::cl::ParseCommandLineOptions(argc, argv, "PDLL LSP Language Server");
@ -71,7 +75,7 @@ LogicalResult mlir::MlirPdllLspServerMain(int argc, char **argv) {
JSONTransport transport(stdin, llvm::outs(), inputStyle, prettyPrint);
// Configure the servers and start the main language server.
PDLLServer::Options options(extraIncludeDirs);
PDLLServer::Options options(compilationDatabases, extraIncludeDirs);
PDLLServer server(options);
return runPdllLSPServer(server, transport);
}

View File

@ -10,6 +10,7 @@
#include "../lsp-server-support/Logging.h"
#include "../lsp-server-support/Protocol.h"
#include "CompilationDatabase.h"
#include "mlir/Tools/PDLL/AST/Context.h"
#include "mlir/Tools/PDLL/AST/Nodes.h"
#include "mlir/Tools/PDLL/AST/Types.h"
@ -325,7 +326,7 @@ PDLDocument::PDLDocument(const lsp::URIForFile &uri, StringRef contents,
return;
}
// TODO: Properly provide include directories from the client.
// Build the set of include directories for this file.
llvm::SmallString<32> uriDirectory(uri.file());
llvm::sys::path::remove_filename(uriDirectory);
includeDirs.push_back(uriDirectory.str().str());
@ -1225,11 +1226,16 @@ PDLTextFileChunk &PDLTextFile::getChunkFor(lsp::Position &pos) {
//===----------------------------------------------------------------------===//
struct lsp::PDLLServer::Impl {
explicit Impl(const Options &options) : options(options) {}
explicit Impl(const Options &options)
: options(options), compilationDatabase(options.compilationDatabases) {}
/// PDLL LSP options.
const Options &options;
/// The compilation database containing additional information for files
/// passed to the server.
lsp::CompilationDatabase compilationDatabase;
/// The files held by the server, mapped by their URI file name.
llvm::StringMap<std::unique_ptr<PDLTextFile>> files;
};
@ -1245,8 +1251,12 @@ lsp::PDLLServer::~PDLLServer() = default;
void lsp::PDLLServer::addOrUpdateDocument(
const URIForFile &uri, StringRef contents, int64_t version,
std::vector<Diagnostic> &diagnostics) {
std::vector<std::string> additionalIncludeDirs = impl->options.extraDirs;
if (auto *fileInfo = impl->compilationDatabase.getFileInfo(uri.file()))
llvm::append_range(additionalIncludeDirs, fileInfo->includeDirs);
impl->files[uri.file()] = std::make_unique<PDLTextFile>(
uri, contents, version, impl->options.extraDirs, diagnostics);
uri, contents, version, additionalIncludeDirs, diagnostics);
}
Optional<int64_t> lsp::PDLLServer::removeDocument(const URIForFile &uri) {

View File

@ -10,12 +10,14 @@
#define LIB_MLIR_TOOLS_MLIRPDLLSPSERVER_SERVER_H_
#include "mlir/Support/LLVM.h"
#include "llvm/ADT/StringRef.h"
#include <memory>
#include <string>
namespace mlir {
namespace lsp {
struct Diagnostic;
class CompilationDatabase;
struct CompletionList;
struct DocumentSymbol;
struct Hover;
@ -30,7 +32,13 @@ class URIForFile;
class PDLLServer {
public:
struct Options {
Options(const std::vector<std::string> &extraDirs) : extraDirs(extraDirs){};
Options(const std::vector<std::string> &compilationDatabases,
const std::vector<std::string> &extraDirs)
: compilationDatabases(compilationDatabases), extraDirs(extraDirs) {}
/// The filenames for databases containing compilation commands for PDLL
/// files passed to the server.
const std::vector<std::string> &compilationDatabases;
/// Additional list of include directories to search.
const std::vector<std::string> &extraDirs;

View File

@ -0,0 +1,21 @@
// RUN: echo -e '--- !FileInfo:\n filepath: "/foo.pdll"\n includes: "%S;%S/../../include"' > %t.yml
// RUN: mlir-pdll-lsp-server -pdll-compilation-database=%t.yml -lit-test < %s | FileCheck %s
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"pdll","capabilities":{},"trace":"off"}}
// -----
{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{
"uri":"test:///foo.pdll",
"languageId":"pdll",
"version":1,
"text":"#include \"include/included.td\"\n#include \"include/included.pdll\""
}}}
// Check that we can properly process the includes without errors.
// CHECK: "method": "textDocument/publishDiagnostics",
// CHECK-NEXT: "params": {
// CHECK-NEXT: "diagnostics": [],
// CHECK-NEXT: "uri": "test:///foo.pdll",
// CHECK-NEXT: "version": 1
// CHECK-NEXT: }
// -----
{"jsonrpc":"2.0","id":7,"method":"shutdown"}
// -----
{"jsonrpc":"2.0","method":"exit"}

View File

@ -0,0 +1,2 @@
// This file is merely to test the processing of includes, it has
// no other purpose or contents.

View File

@ -0,0 +1,4 @@
include "mlir/IR/OpBase.td"
// This file is merely to test the processing of includes, it has
// no other purpose or contents.

View File

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

View File

@ -124,6 +124,11 @@
"type": "string",
"description": "The file path of the mlir-pdll-lsp-server executable."
},
"mlir.pdll_compilation_databases": {
"scope": "resource",
"type": "array",
"description": "A list of `pdll_compile_commands.yml` database files containing information about .pdll files processed by the server."
},
"mlir.onSettingsChanged": {
"type": "string",
"default": "prompt",

View File

@ -41,41 +41,45 @@ async function promptRestart(settingName: string, promptMessage: string) {
* Activate watchers that track configuration changes for the given workspace
* folder, or null if the workspace is top-level.
*/
export async function activate(mlirContext: MLIRContext,
workspaceFolder: vscode.WorkspaceFolder,
serverSetting: string, serverPath: string) {
export async function activate(
mlirContext: MLIRContext, workspaceFolder: vscode.WorkspaceFolder,
serverSettings: string[], serverPaths: string[]) {
// When a configuration change happens, check to see if we should restart the
// server.
mlirContext.subscriptions.push(vscode.workspace.onDidChangeConfiguration(event => {
const expandedSetting = `mlir.${serverSetting}`;
if (event.affectsConfiguration(expandedSetting, workspaceFolder)) {
promptRestart(
'onSettingsChanged',
`setting '${
expandedSetting}' has changed. Do you want to reload the server?`);
for (const serverSetting of serverSettings) {
const expandedSetting = `mlir.${serverSetting}`;
if (event.affectsConfiguration(expandedSetting, workspaceFolder)) {
promptRestart(
'onSettingsChanged',
`setting '${
expandedSetting}' has changed. Do you want to reload the server?`);
}
}
}));
// If the server path actually exists, track it in case it changes. Check that
// the path actually exists.
if (serverPath === '') {
return;
}
// Setup watchers for the provided server paths.
const fileWatcherConfig = {
disableGlobbing : true,
followSymlinks : true,
ignoreInitial : true,
awaitWriteFinish : true,
};
const fileWatcher = chokidar.watch(serverPath, fileWatcherConfig);
fileWatcher.on('all', (event, _filename, _details) => {
if (event != 'unlink') {
promptRestart(
'onSettingsChanged',
'MLIR language server binary has changed. Do you want to reload the server?');
for (const serverPath of serverPaths) {
if (serverPath === '') {
return;
}
});
mlirContext.subscriptions.push(
new vscode.Disposable(() => { fileWatcher.close(); }));
// If the server path actually exists, track it in case it changes.
const fileWatcher = chokidar.watch(serverPath, fileWatcherConfig);
fileWatcher.on('all', (event, _filename, _details) => {
if (event != 'unlink') {
promptRestart(
'onSettingsChanged',
'MLIR language server file has changed. Do you want to reload the server?');
}
});
mlirContext.subscriptions.push(
new vscode.Disposable(() => { fileWatcher.close(); }));
}
}

View File

@ -85,6 +85,43 @@ export class MLIRContext implements vscode.Disposable {
}));
}
/**
* Prepare the server options for a PDLL server, e.g. populating any
* accessible compilation databases.
*/
async preparePDLLServerOptions(workspaceFolder: vscode.WorkspaceFolder,
configsToWatch: string[],
pathsToWatch: string[],
additionalServerArgs: string[]) {
// Process the compilation databases attached for the workspace folder.
let databases =
config.get<string[]>('pdll_compilation_databases', workspaceFolder);
// If no databases were explicitly specified, default to a database in the
// 'build' directory within the current workspace.
if (databases.length === 0) {
if (workspaceFolder) {
databases.push(workspaceFolder.uri.fsPath +
'/build/pdll_compile_commands.yml');
}
// Otherwise, try to resolve each of the paths.
} else {
for await (let database of databases) {
database = await this.resolvePath(database, '', workspaceFolder);
}
}
configsToWatch.push('pdll_compilation_databases');
pathsToWatch.push(...databases);
// Setup the compilation databases as additional arguments to pass to the
// server.
databases.filter(database => database !== '');
additionalServerArgs.push(...databases.map(
(database) => `--pdll-compilation-database=${database}`));
}
/**
* Activate the language client for the given language in the given workspace
* folder.
@ -93,12 +130,27 @@ export class MLIRContext implements vscode.Disposable {
serverSettingName: string, languageName: string,
outputChannel: vscode.OutputChannel):
Promise<vscodelc.LanguageClient> {
let configsToWatch: string[] = [];
let filepathsToWatch: string[] = [];
let additionalServerArgs: string[] = [];
// Initialize additional configurations for this server.
if (languageName === 'pdll') {
await this.preparePDLLServerOptions(workspaceFolder, configsToWatch,
filepathsToWatch,
additionalServerArgs);
}
// Try to activate the language client.
const [server, serverPath] = await this.startLanguageClient(
workspaceFolder, outputChannel, serverSettingName, languageName);
workspaceFolder, outputChannel, serverSettingName, languageName,
additionalServerArgs);
configsToWatch.push(serverSettingName);
filepathsToWatch.push(serverPath);
// Watch for configuration changes on this folder.
await configWatcher.activate(this, workspaceFolder, serverSettingName,
serverPath);
await configWatcher.activate(this, workspaceFolder, configsToWatch,
filepathsToWatch);
return server;
}
@ -109,7 +161,8 @@ export class MLIRContext implements vscode.Disposable {
*/
async startLanguageClient(workspaceFolder: vscode.WorkspaceFolder,
outputChannel: vscode.OutputChannel,
serverSettingName: string, languageName: string):
serverSettingName: string, languageName: string,
additionalServerArgs: string[]):
Promise<[ vscodelc.LanguageClient, string ]> {
const clientTitle = languageName.toUpperCase() + ' Language Client';
@ -146,12 +199,12 @@ export class MLIRContext implements vscode.Disposable {
run : {
command : serverPath,
transport : vscodelc.TransportKind.stdio,
args : []
args : additionalServerArgs
},
debug : {
command : serverPath,
transport : vscodelc.TransportKind.stdio,
args : []
args : additionalServerArgs
}
};
@ -216,6 +269,45 @@ export class MLIRContext implements vscode.Disposable {
return '';
}
/**
* Try to resolve the given path, or the default path, with an optional
* workspace folder. If a path could not be resolved, just returns the
* input filePath.
*/
async resolvePath(filePath: string, defaultPath: string,
workspaceFolder: vscode.WorkspaceFolder): Promise<string> {
const configPath = filePath;
// If the path is already fully resolved, there is nothing to do.
if (path.isAbsolute(filePath)) {
return filePath;
}
// If a path hasn't been set, try to use the default path.
if (filePath === '') {
if (defaultPath === '') {
return filePath;
}
filePath = defaultPath;
// Fallthrough to try resolving the default path.
}
// Try to resolve the path relative to the workspace.
let filePattern: vscode.GlobPattern = '**/' + filePath;
if (workspaceFolder) {
filePattern = new vscode.RelativePattern(workspaceFolder, filePattern);
}
let foundUris = await vscode.workspace.findFiles(filePattern, null, 1);
if (foundUris.length === 0) {
// If we couldn't resolve it, just return the original path anyways. The
// file might not exist yet.
return configPath;
}
// Otherwise, return the resolved path.
return foundUris[0].fsPath;
}
/**
* Try to resolve the path for the given server setting, with an optional
* workspace folder.
@ -223,37 +315,9 @@ export class MLIRContext implements vscode.Disposable {
async resolveServerPath(serverSettingName: string,
workspaceFolder: vscode.WorkspaceFolder):
Promise<string> {
const configServerPath =
config.get<string>(serverSettingName, workspaceFolder);
let serverPath = configServerPath;
// If the path is already fully resolved, there is nothing to do.
if (path.isAbsolute(serverPath)) {
return serverPath;
}
// If a path hasn't been set, try to use the default path.
if (serverPath === '') {
serverPath = MLIRContext.getDefaultServerFilename(serverSettingName);
if (serverPath === '') {
return serverPath;
}
// Fallthrough to try resolving the default path.
}
// Try to resolve the path relative to the workspace.
let filePattern: vscode.GlobPattern = '**/' + serverPath;
if (workspaceFolder) {
filePattern = new vscode.RelativePattern(workspaceFolder, filePattern);
}
let foundUris = await vscode.workspace.findFiles(filePattern, null, 1);
if (foundUris.length === 0) {
// If we couldn't resolve it, just return the current configuration path
// anyways. The file might not exist yet.
return configServerPath;
}
// Otherwise, return the resolved path.
return foundUris[0].fsPath;
const serverPath = config.get<string>(serverSettingName, workspaceFolder);
const defaultPath = MLIRContext.getDefaultServerFilename(serverSettingName);
return this.resolvePath(serverPath, defaultPath, workspaceFolder);
}
dispose() {