Skip to content

Commit

Permalink
LSP: Using a virtual filesystem (#524)
Browse files Browse the repository at this point in the history
* LSP: Using a virtual filesystem

Add a primitive notion of a "virtual filesystem" (which is just a
`Map<String, String>` for now) to represent abra files that require
typechecking but which have not yet been saved to disk. For now, the
contents of this map are wholesale replaced upon receipt of a
`textDocument/didChange` notification from the LSP client, and removed
upon `textDocument/didSave` (since at that point, the file can once
again be read from disk to obtain the latest version).

In the future, there ought to be a more sophisticated way of
representing the file contents in the virtual filesystem, which
leverages the Incremental `contentChange` option for
`textDocument/didChange` notifications, rather than having to wastefully
parse and replace the entire contents of the document each time. In
fact, the LSP currently falls over when trying to operate on large
files, most likely for this reason.

* Postponing virtual filesystem changes

Using this current approach, the entire contents of the changed file
will be sent in `textDocument/didChange` events (as well as
`textDocument/didOpen`, which is less relevant but still wasteful).
Until a more performant system is developed to leverage Incremental
didChange events, let's only run diagnostics on save and instruct the
client to not send didOpen or didChange events. Additionally, we
instruct the client to not send the file contents in the didSave event.
  • Loading branch information
kengorab authored Dec 24, 2024
1 parent fa5702b commit 2fcbb08
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 79 deletions.
71 changes: 35 additions & 36 deletions projects/compiler/src/typechecker.abra
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ enum TokenizeAndParseError {
export type ModuleLoader {
stdRoot: String
parsedModules: Map<String, ParsedModule> = {}
documentStore: Map<String, String> = {}
useDocumentStore: Bool = false
_invalidated: Map<String, Bool> = {}
_virtualFileSystem: Map<String, String>? = None

func usingVirtualFileSystem(stdRoot: String, fileMap: Map<String, String>): ModuleLoader =
ModuleLoader(stdRoot: stdRoot, _virtualFileSystem: Some(fileMap))

func resolvePath(self, modulePath: String, relativeTo: String?): String {
if relativeTo |relativeTo| {
Expand All @@ -28,12 +29,12 @@ export type ModuleLoader {
func hasSeenModule(self, modulePathAbs: String): Bool = self.parsedModules.containsKey(modulePathAbs)

func invalidateModule(self, modulePathAbs: String) {
self._invalidated[modulePathAbs] = true
self.parsedModules.remove(modulePathAbs)
}

func _loadFileContents(self, modulePathAbs: String): String? {
if self.useDocumentStore {
val inMemContents = self.documentStore[modulePathAbs]
if self._virtualFileSystem |vfs| {
val inMemContents = vfs[modulePathAbs]
if inMemContents return inMemContents
}

Expand All @@ -44,22 +45,15 @@ export type ModuleLoader {
}

func tokenizeAndParse(self, modulePath: String): Result<ParsedModule, TokenizeAndParseError> {
if self.parsedModules[modulePath] |m| {
val invalidated = self._invalidated[modulePath] ?: false
if !invalidated return Ok(m)
}
if self.parsedModules[modulePath] |m| return Ok(m)

val parsedModule = if self._loadFileContents(modulePath) |contents| {
match Lexer.tokenize(contents) {
Ok(tokens) => {
val module = match Parser.parse(tokens) {
match Parser.parse(tokens) {
Ok(parsedModule) => parsedModule
Err(error) => return Err(TokenizeAndParseError.ParseError(error))
}

self._invalidated[modulePath] = false

module
}
Err(error) => return Err(TokenizeAndParseError.LexerError(error))
}
Expand Down Expand Up @@ -1495,28 +1489,31 @@ export type Typechecker {
func typecheckEntrypoint(self, modulePathAbs: String): Result<TypedModule, TypecheckerError> {
val preludeModulePathSegs = getAbsolutePath(self.moduleLoader.stdRoot + "/prelude.abra")
val preludeModulePathAbs = "/" + preludeModulePathSegs.join("/")
try self._typecheckModule(preludeModulePathAbs)

val preludeStructs = [
self.project.preludeIntStruct,
self.project.preludeFloatStruct,
self.project.preludeBoolStruct,
self.project.preludeCharStruct,
self.project.preludeStringStruct,
self.project.preludeArrayStruct,
self.project.preludeMapStruct,
self.project.preludeSetStruct,
]
for struct in preludeStructs {
if struct.label.position == Position(line: 0, col: 0) unreachable("Improperly initialized prelude struct ${struct.label.name}")
}

val preludeEnums = [
self.project.preludeOptionEnum,
self.project.preludeResultEnum,
]
for enum_ in preludeEnums {
if enum_.label.position == Position(line: 0, col: 0) unreachable("Improperly initialized prelude enum ${enum_.label.name}")
if !self.project.modules[preludeModulePathAbs] {
try self._typecheckModule(preludeModulePathAbs)

val preludeStructs = [
self.project.preludeIntStruct,
self.project.preludeFloatStruct,
self.project.preludeBoolStruct,
self.project.preludeCharStruct,
self.project.preludeStringStruct,
self.project.preludeArrayStruct,
self.project.preludeMapStruct,
self.project.preludeSetStruct,
]
for struct in preludeStructs {
if struct.label.position == Position(line: 0, col: 0) unreachable("Improperly initialized prelude struct ${struct.label.name}")
}

val preludeEnums = [
self.project.preludeOptionEnum,
self.project.preludeResultEnum,
]
for enum_ in preludeEnums {
if enum_.label.position == Position(line: 0, col: 0) unreachable("Improperly initialized prelude enum ${enum_.label.name}")
}
}

self._typecheckModule(modulePathAbs)
Expand Down Expand Up @@ -2070,6 +2067,8 @@ export type Typechecker {
}

func _typecheckModule(self, modulePathAbs: String): Result<TypedModule, TypecheckerError> {
if self.project.modules[modulePathAbs] |mod| return Ok(mod)

self.typecheckingBuiltin = if modulePathAbs == self.moduleLoader.stdRoot + "/prelude.abra" {
self.project.preludeScope = self.currentScope.makeChild("module_prelude", ScopeKind.Module(id: -1))
Some(BuiltinModule.Prelude)
Expand Down
88 changes: 60 additions & 28 deletions projects/lsp/src/language_service.abra
Original file line number Diff line number Diff line change
@@ -1,35 +1,54 @@
import "fs" as fs
import "process" as process
import JsonValue from "json"
import log from "./log"
import ModuleLoader, Project, Typechecker, TypecheckerErrorKind from "../../compiler/src/typechecker"
import RequestMessage, NotificationMessage, ResponseMessage, ResponseResult, ResponseError, ResponseErrorCode, ServerCapabilities, TextDocumentSyncKind, ServerInfo, TextDocumentItem, VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent, Diagnostic, DiagnosticSeverity from "./lsp_spec"
import RequestMessage, NotificationMessage, ResponseMessage, ResponseResult, ResponseError, ResponseErrorCode, ServerCapabilities, TextDocumentSyncOptions, TextDocumentSyncKind, SaveOptions, ServerInfo, TextDocumentItem, TextDocumentIdentifier, VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent, Diagnostic, DiagnosticSeverity from "./lsp_spec"

export val contentLengthHeader = "Content-Length: "
export val bogusMessageId = -999

val abraStdRoot = if process.getEnvVar("ABRA_HOME") |v| v else {
println("Could not find ABRA_HOME (make sure \$ABRA_HOME environment variable is set)")
process.exit(1)
}

val moduleLoader = ModuleLoader(stdRoot: abraStdRoot, useDocumentStore: true)
val documentStore = moduleLoader.documentStore
val project = Project()
val typechecker = Typechecker(moduleLoader: moduleLoader, project: project)

export type AbraLanguageService {
initialized: Bool = false
root: String = ""
// _virtualFileSystem: Map<String, String>
_moduleLoader: ModuleLoader
_project: Project
_initialized: Bool = false
_root: String = ""

func new(abraStdRoot: String): AbraLanguageService {
// val virtualFileSystem: Map<String, String> = {}
// val moduleLoader = ModuleLoader.usingVirtualFileSystem(stdRoot: abraStdRoot, fileMap: virtualFileSystem)
val moduleLoader = ModuleLoader(stdRoot: abraStdRoot)
val project = Project()

AbraLanguageService(
// _virtualFileSystem: virtualFileSystem,
_moduleLoader: moduleLoader,
_project: project,
)
}

// Request Message handlers

func _initialize(self, id: Int, processId: Int?, rootPath: String?): ResponseMessage {
self.root = if rootPath |p| p else return internalError(id, "rootPath required")
self.initialized = true

self._root = if rootPath |p| p else return internalError(id, "rootPath required")
self._initialized = true

// Instruct the client to not send textDocument/didOpen or textDocument/didClose events,
// and to send textDocument/didSave events, but do not include file contents when saved.
//
// Additionally, for now, instruct the client to NOT send textDocument/didChange events;
// sending the full contents each time (replacing the file in the ModuleLoader's vfs) is
// pretty wasteful and causes problems with larger files. Eventually, I'll implement a
// more performant model using the Incremental updates, but for now the client will only
// receive diagnostics from the server when the file is saved (which is file, frankly).
val result = ResponseResult.Initialize(
capabilities: ServerCapabilities(textDocumentSync: Some(TextDocumentSyncKind.Full)),
capabilities: ServerCapabilities(
textDocumentSync: Some(TextDocumentSyncOptions(
openClose: Some(false),
change: Some(TextDocumentSyncKind.None_),
save: Some(SaveOptions(includeText: Some(false)))
))
),
serverInfo: ServerInfo(name: "abra-lsp", version: Some("0.0.1"))
)
ResponseMessage.Success(id: id, result: Some(result))
Expand All @@ -38,32 +57,42 @@ export type AbraLanguageService {
// Notification handlers

func _textDocumentDidOpen(self, textDocument: TextDocumentItem) {
// textDocument/didOpen events are currently not sent by the client (see self._initialize)
val diagnostics = self._runTypecheckerStartingAtUri(textDocument.uri)
if !diagnostics.isEmpty() {
val notif = NotificationMessage.TextDocumentPublishDiagnostics(uri: textDocument.uri, diagnostics: diagnostics)
self.sendNotification(notif)
}
val notif = NotificationMessage.TextDocumentPublishDiagnostics(uri: textDocument.uri, diagnostics: diagnostics)
self.sendNotification(notif)
}

func _textDocumentDidChange(self, textDocument: VersionedTextDocumentIdentifier, contentChanges: TextDocumentContentChangeEvent[]) {
// textDocument/didChange events are currently not sent by the client (see self._initialize)
if contentChanges.isEmpty() return

val filePath = textDocument.uri.replaceAll("file://", "")
for changeEvent in contentChanges {
match changeEvent {
TextDocumentContentChangeEvent.Incremental => todo("TextDocumentContentChangeEvent.Incremental")
TextDocumentContentChangeEvent.Full(text) => {
documentStore[filePath] = text
// self._virtualFileSystem[filePath] = text
}
}
}
moduleLoader.invalidateModule(filePath)
self._moduleLoader.invalidateModule(filePath)
self._project.modules.remove(filePath)

val diagnostics = self._runTypecheckerStartingAtUri(textDocument.uri)
if !diagnostics.isEmpty() {
val notif = NotificationMessage.TextDocumentPublishDiagnostics(uri: textDocument.uri, diagnostics: diagnostics)
self.sendNotification(notif)
}
val notif = NotificationMessage.TextDocumentPublishDiagnostics(uri: textDocument.uri, diagnostics: diagnostics)
self.sendNotification(notif)
}

func _textDocumentDidSave(self, textDocument: TextDocumentIdentifier) {
val filePath = textDocument.uri.replaceAll("file://", "")
self._moduleLoader.invalidateModule(filePath)
self._project.modules.remove(filePath)
// self._virtualFileSystem.remove(filePath)

val diagnostics = self._runTypecheckerStartingAtUri(textDocument.uri)
val notif = NotificationMessage.TextDocumentPublishDiagnostics(uri: textDocument.uri, diagnostics: diagnostics)
self.sendNotification(notif)
}

// Compiler bridge
Expand All @@ -72,6 +101,8 @@ export type AbraLanguageService {
// todo: what happens if it's not a `file://` uri?
val filePath = uri.replaceAll("file://", "")

val typechecker = Typechecker(moduleLoader: self._moduleLoader, project: self._project)

match typechecker.typecheckEntrypoint(filePath) {
Ok => []
Err(e) => {
Expand Down Expand Up @@ -127,6 +158,7 @@ export type AbraLanguageService {
NotificationMessage.Initialized => { /* no-op */ }
NotificationMessage.TextDocumentDidOpen(textDocument) => self._textDocumentDidOpen(textDocument)
NotificationMessage.TextDocumentDidChange(textDocument, contentChanges) => self._textDocumentDidChange(textDocument, contentChanges)
NotificationMessage.TextDocumentDidSave(textDocument) => self._textDocumentDidSave(textDocument)
}
}

Expand Down
Loading

0 comments on commit 2fcbb08

Please sign in to comment.