From d86572f325369cf3978aa59e3b5e4ccf18d1a350 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Wed, 15 Apr 2026 08:49:45 +0200 Subject: [PATCH 1/4] Fix issue that an outer class instance (`TextDocumentState`) wasn't properly initialized yet when an inner class instance (`Update`) was created. The inner class instance would use an uninitialized field of the outer class instance, resulting in an NPE. --- .../main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java index 89f889fe5..9ea6fb8a0 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java @@ -74,11 +74,11 @@ public TextDocumentState( this.parser = parser; this.location = location; + this.lastWithoutErrors = new AtomicReference<>(); + this.last = new AtomicReference<>(); var u = new Update(initialVersion, initialContent, initialTimestamp); this.current = new AtomicReference<>(new Versioned<>(initialVersion, u)); - this.lastWithoutErrors = new AtomicReference<>(); - this.last = new AtomicReference<>(); } public ISourceLocation getLocation() { From 2d7e28ac6f2cb0f07a495b20c4f00ac0cde8b7a9 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 14 Apr 2026 18:00:39 +0200 Subject: [PATCH 2/4] Fix issue that tree updates of document states post-parsing weren't explicitly executed on the worker pool yet (but possibly on the request pool) --- .../org/rascalmpl/vscode/lsp/TextDocumentState.java | 12 ++++++++---- .../parametric/ParametricTextDocumentService.java | 2 +- .../vscode/lsp/rascal/RascalTextDocumentService.java | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java index 9ea6fb8a0..0e9965765 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; @@ -62,6 +63,7 @@ public class TextDocumentState { private final BiFunction> parser; private final ISourceLocation location; + private final ExecutorService exec; private final AtomicReference> current; private final AtomicReference<@Nullable Versioned> lastWithoutErrors; @@ -70,12 +72,14 @@ public class TextDocumentState { public TextDocumentState( BiFunction> parser, ISourceLocation location, - int initialVersion, String initialContent, long initialTimestamp) { + int initialVersion, String initialContent, long initialTimestamp, + ExecutorService exec) { this.parser = parser; this.location = location; this.lastWithoutErrors = new AtomicReference<>(); this.last = new AtomicReference<>(); + this.exec = exec; var u = new Update(initialVersion, initialContent, initialTimestamp); this.current = new AtomicReference<>(new Versioned<>(initialVersion, u)); @@ -200,7 +204,7 @@ public CompletableFuture>> getDiagnosticsAs private void parse() { try { parser.apply(location, content) - .whenComplete((ITree t, Throwable e) -> { + .whenCompleteAsync((ITree t, Throwable e) -> { if (e instanceof CompletionException && e.getCause() != null) { e = e.getCause(); } @@ -221,7 +225,7 @@ private void parse() { // Complete future to get diagnostics var diagnostics = new Versioned<>(version, diagnosticsList); diagnosticsAsync.complete(diagnostics); - }); + }, exec); } catch (NoContributionException e) { logger.debug("Ignoring missing parser for {}", location, e); treeAsync.completeOnTimeout(new Versioned<>(version, IRascalValueFactory.getInstance().character(0), timestamp), 60, TimeUnit.SECONDS); @@ -255,6 +259,6 @@ public long getLastModified() { public TextDocumentState changeParser(BiFunction> parsing) { var c = getCurrentContent(); - return new TextDocumentState(parsing, this.location, c.version(), c.get(), getLastModified()); + return new TextDocumentState(parsing, this.location, c.version(), c.get(), getLastModified(), exec); } } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index c024c43ee..97e85b6e6 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -713,7 +713,7 @@ private ParametricFileFacts facts(ISourceLocation doc) { private TextDocumentState open(TextDocumentItem doc, long timestamp) { return files.computeIfAbsent(Locations.toLoc(doc), - l -> new TextDocumentState(contributions(l)::parsing, l, doc.getVersion(), doc.getText(), timestamp)); + l -> new TextDocumentState(contributions(l)::parsing, l, doc.getVersion(), doc.getText(), timestamp, exec)); } private TextDocumentState getFile(ISourceLocation loc) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index e581a3273..a13d62b58 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -557,7 +557,7 @@ private static T last(List l) { private TextDocumentState open(TextDocumentItem doc, long timestamp) { return documents.computeIfAbsent(Locations.toLoc(doc), - l -> new TextDocumentState(availableRascalServices()::parseSourceFile, l, doc.getVersion(), doc.getText(), timestamp)); + l -> new TextDocumentState(availableRascalServices()::parseSourceFile, l, doc.getVersion(), doc.getText(), timestamp, exec)); } private TextDocumentState getFile(TextDocumentIdentifier doc) { From f77d928cc32f869c9ab94035bacf7414f8a04ba7 Mon Sep 17 00:00:00 2001 From: Sung-Shik Jongmans Date: Wed, 15 Apr 2026 09:55:31 +0200 Subject: [PATCH 3/4] Add try/catch block and logger call in post-parsing action to make future debugging easier --- .../vscode/lsp/TextDocumentState.java | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java index 0e9965765..772326ac6 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java @@ -205,26 +205,33 @@ private void parse() { try { parser.apply(location, content) .whenCompleteAsync((ITree t, Throwable e) -> { - if (e instanceof CompletionException && e.getCause() != null) { - e = e.getCause(); - } - var diagnosticsList = toDiagnosticsList(t, e); // `t` and `e` are nullable - - // Complete future to get the tree - if (t == null) { - treeAsync.completeExceptionally(e); - } else { - var tree = new Versioned<>(version, t, timestamp); - Versioned.replaceIfNewer(last, tree); - if (diagnosticsList.isEmpty()) { - Versioned.replaceIfNewer(lastWithoutErrors, tree); + try { + if (e instanceof CompletionException && e.getCause() != null) { + e = e.getCause(); + } + var diagnosticsList = toDiagnosticsList(t, e); // `t` and `e` are nullable + + // Complete future to get the tree + if (t == null) { + treeAsync.completeExceptionally(e); + } else { + var tree = new Versioned<>(version, t, timestamp); + Versioned.replaceIfNewer(last, tree); + if (diagnosticsList.isEmpty()) { + Versioned.replaceIfNewer(lastWithoutErrors, tree); + } + treeAsync.complete(tree); } - treeAsync.complete(tree); - } - // Complete future to get diagnostics - var diagnostics = new Versioned<>(version, diagnosticsList); - diagnosticsAsync.complete(diagnostics); + // Complete future to get diagnostics + var diagnostics = new Versioned<>(version, diagnosticsList); + diagnosticsAsync.complete(diagnostics); + } catch (Exception exc) { + // The action of `whenCompleteAsync` shouldn't throw an exception (see JavaDoc): if it + // unexpectedly does, then it is almost surely a bug, but the exception is swallowed, so it + // is very hard to debug. The try/catch block and logger call aim to make it easier. + logger.error("Unexpected exception after parsing: {}", exc); + } }, exec); } catch (NoContributionException e) { logger.debug("Ignoring missing parser for {}", location, e); From c49045b93d2e48f7b5284881ca03821dd6143231 Mon Sep 17 00:00:00 2001 From: sungshik <16154899+sungshik@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:50:28 +0200 Subject: [PATCH 4/4] Update rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java Co-authored-by: Toine Hartman --- .../main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java index 772326ac6..8d01c50c7 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java @@ -230,7 +230,7 @@ private void parse() { // The action of `whenCompleteAsync` shouldn't throw an exception (see JavaDoc): if it // unexpectedly does, then it is almost surely a bug, but the exception is swallowed, so it // is very hard to debug. The try/catch block and logger call aim to make it easier. - logger.error("Unexpected exception after parsing: {}", exc); + logger.error("Unexpected exception after parsing", exc); } }, exec); } catch (NoContributionException e) {