Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error recovery for Rascal #490

Draft
wants to merge 39 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
20948c6
Update `parse` call to use error recovery
sungshik Oct 8, 2024
ea87d9e
Update `pom.xml` for development
sungshik Oct 8, 2024
6fa7a82
Add reporting of diagnostics for error nodes
sungshik Oct 8, 2024
1aa3634
Make error recovery backward-compatible
sungshik Oct 8, 2024
35a3b02
Add flag to `loadParser` call to set allowRecovery to false for now
sungshik Oct 8, 2024
ea9919f
Fix a bug that adds irrelevant diagnostics
sungshik Oct 9, 2024
2d5dc04
Add exception handler when the location of a parse error is malformed
sungshik Oct 9, 2024
9643852
Add first version parse debouncing
sungshik Oct 11, 2024
f94f08f
Merge branch 'main' into error-recovery/rascal
PieterOlivier Oct 15, 2024
04eaedd
Revert "Add first version parse debouncing"
PieterOlivier Oct 15, 2024
a3dbfbd
Show skipped part and use error tree for outlining
PieterOlivier Oct 16, 2024
17bcab1
Add debounce to parsing in `TextDocumentState`
sungshik Oct 24, 2024
b085657
Merge branch 'error-recovery/rascal' into error-recovery/rascal-debou…
sungshik Oct 24, 2024
6b35288
Use `compareAndSet` instead of `weak...`
sungshik Oct 24, 2024
2749e49
Fix typos in documentation
sungshik Oct 24, 2024
f1bacae
Merge pull request #475 from usethesource/error-recovery/rascal-debou…
sungshik Oct 24, 2024
9d803d0
Merge pull request #476 from usethesource/recovery/skipped
PieterOlivier Oct 25, 2024
b947d04
Upgrade `pom.xml` and `package.sh`
sungshik Oct 25, 2024
073b675
Both outline and codelenses support can now handle error trees
PieterOlivier Oct 25, 2024
5d60fde
Removed spurious hasErrors
PieterOlivier Oct 25, 2024
91d0e6b
Merge pull request #491 from usethesource/recovery/outline-and-code-l…
PieterOlivier Oct 25, 2024
a9d5a06
Move parse error processing (including error nodes) completely to `Te…
sungshik Oct 25, 2024
770561e
Merge branch 'error-recovery/rascal' into error-recovery/rascal-diagn…
sungshik Oct 25, 2024
aeb1a9f
Refine parse error messages
sungshik Oct 25, 2024
90ab10c
Remove unused imports
sungshik Oct 28, 2024
d86c659
Move class `Debouncer` to its own file
sungshik Oct 28, 2024
b095617
Merge pull request #492 from usethesource/error-recovery/rascal-diagn…
sungshik Oct 30, 2024
ea02b93
Merge branch 'main' into error-recovery/rascal
PieterOlivier Nov 5, 2024
92cc913
Remove function that should have been deleted during merge from main
PieterOlivier Nov 5, 2024
066c5df
Bumped rascal version number
PieterOlivier Nov 6, 2024
ed3dd87
Simplify debouncer (joint with @PieterOlivier)
sungshik Nov 8, 2024
7f16037
Improve API of `DebouncedSupplier`
sungshik Nov 8, 2024
761ab06
Add tests for `DebouncedSupplier`
sungshik Nov 8, 2024
15aaa0f
Improve documentations of `DebouncedSupplier`
sungshik Nov 8, 2024
b81dfc4
Revert signature change to `parseIfNotParsing`
sungshik Nov 8, 2024
87e5959
Rename methods to make the names more precise
sungshik Nov 8, 2024
9653e26
Merge branch 'main' into error-recovery/rascal
PieterOlivier Nov 10, 2024
5a123b2
Merge pull request #512 from usethesource/error-recovery/rascal-simpl…
sungshik Nov 11, 2024
819d7a3
Merge branch 'main' into error-recovery/rascal
PieterOlivier Nov 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rascal-lsp/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
<dependency>
<groupId>org.rascalmpl</groupId>
<artifactId>rascal</artifactId>
<version>0.40.11</version>
<version>0.40.9-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.rascalmpl</groupId>
Expand Down
295 changes: 249 additions & 46 deletions rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,24 @@
*/
package org.rascalmpl.vscode.lsp;

import java.time.Duration;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.eclipse.lsp4j.Diagnostic;
import org.rascalmpl.library.util.ErrorRecovery;
import org.rascalmpl.values.RascalValueFactory;
import org.rascalmpl.values.ValueFactoryFactory;
import org.rascalmpl.values.parsetrees.ITree;
import org.rascalmpl.vscode.lsp.util.Versioned;

Expand All @@ -45,64 +59,253 @@
* and ParametricTextDocumentService.
*/
public class TextDocumentState {

private static final ErrorRecovery RECOVERY =
new ErrorRecovery((RascalValueFactory) ValueFactoryFactory.getValueFactory());

private final BiFunction<ISourceLocation, String, CompletableFuture<ITree>> parser;
private final ISourceLocation location;

@SuppressWarnings("java:S3077") // Visibility of writes is enough
private volatile Update current;
private final Debouncer<Versioned<ITree>> currentTreeAsyncDebouncer;

private final AtomicReference<@MonotonicNonNull Versioned<ITree>> lastWithoutErrors;
private final AtomicReference<@MonotonicNonNull Versioned<ITree>> last;

private final ISourceLocation file;
@SuppressWarnings("java:S3077") // we are use volatile correctly
private volatile Versioned<String> currentContent;
@SuppressWarnings("java:S3077") // we are use volatile correctly
private volatile @MonotonicNonNull Versioned<ITree> lastFullTree;
@SuppressWarnings("java:S3077") // we are use volatile correctly
private volatile CompletableFuture<Versioned<ITree>> currentTree;
public TextDocumentState(
BiFunction<ISourceLocation, String, CompletableFuture<ITree>> parser,
ISourceLocation location, int initialVersion, String initialContent) {

public TextDocumentState(BiFunction<ISourceLocation, String, CompletableFuture<ITree>> parser, ISourceLocation file, int initialVersion, String initialContent) {
this.parser = parser;
this.file = file;
this.currentContent = new Versioned<>(initialVersion, initialContent);
this.currentTree = newTreeAsync(initialVersion, initialContent);
}

/**
* The current call of this method guarantees that, until the next call,
* each intermediate call of `getCurrentTreeAsync` returns (a future for) a
* *correct* versioned tree. This means that:
* - the version of the tree is parameter `version`;
* - the tree is produced by parsing parameter `content`.
*
* Thus, callers of `getCurrentTreeAsync` are guaranteed to obtain a
* consistent <version, tree> pair.
*/
public CompletableFuture<Versioned<ITree>> update(int version, String content) {
currentContent = new Versioned<>(version, content);
var newTree = newTreeAsync(version, content);
currentTree = newTree;
return newTree;
}

@SuppressWarnings("java:S1181") // we want to catch all Java exceptions from the parser
private CompletableFuture<Versioned<ITree>> newTreeAsync(int version, String content) {
return parser.apply(file, content)
.thenApply(t -> new Versioned<ITree>(version, t))
.whenComplete((r, t) -> {
if (r != null) {
lastFullTree = r;
}
});
this.location = location;

this.current = new Update(initialVersion, initialContent);
this.currentTreeAsyncDebouncer = new Debouncer<>(50,
this::getCurrentTreeAsyncIfParsing, this::getCurrentTreeAsync);

this.lastWithoutErrors = new AtomicReference<>();
this.last = new AtomicReference<>();
}

public ISourceLocation getLocation() {
return location;
}

public void update(int version, String content) {
current = new Update(version, content);
// The creation of the `Update` object doesn't trigger the parser yet.
// This happens only when the tree is requested.
}

public Versioned<String> getCurrentContent() {
return current.getContent();
}

public CompletableFuture<Versioned<ITree>> getCurrentTreeAsync() {
return currentTree;
return current.getTreeAsync(); // Triggers the parser
}

public @MonotonicNonNull Versioned<ITree> getMostRecentTree() {
return lastFullTree;
public CompletableFuture<Versioned<ITree>> getCurrentTreeAsync(Duration delay) {
return currentTreeAsyncDebouncer.get(delay);
}

public ISourceLocation getLocation() {
return file;
public @Nullable CompletableFuture<Versioned<ITree>> getCurrentTreeAsyncIfParsing() {
var update = current;
return update.isParsing() ? update.getTreeAsync() : null;
}

public Versioned<String> getCurrentContent() {
return currentContent;
public CompletableFuture<Versioned<List<Diagnostic>>> getCurrentDiagnostics() {
throw new UnsupportedOperationException();
// TODO: In a separate PR
}

public @MonotonicNonNull Versioned<ITree> getLastTree() {
return last.get();
}

public @MonotonicNonNull Versioned<ITree> getLastTreeWithoutErrors() {
return lastWithoutErrors.get();
}

private class Update {
private final int version;
private final String content;
private final CompletableFuture<Versioned<ITree>> treeAsync;
private final AtomicBoolean parsing;

public Update(int version, String content) {
this.version = version;
this.content = content;
this.treeAsync = new CompletableFuture<>();
this.parsing = new AtomicBoolean(false);
}

public Versioned<String> getContent() {
return new Versioned<>(version, content);
}

public CompletableFuture<Versioned<ITree>> getTreeAsync() {
parseIfNotParsing();
return treeAsync;
}

public boolean isParsing() {
return parsing.get();
}

private void parseIfNotParsing() {
if (parsing.compareAndSet(false, true)) {
parser
.apply(location, content)
.thenApply(t -> new Versioned<>(version, t))
.whenComplete((t, error) -> {
if (t != null) {
var errors = RECOVERY.findAllErrors(t.get());
if (errors.isEmpty()) {
Versioned.replaceIfNewer(lastWithoutErrors, t);
}
Versioned.replaceIfNewer(last, t);
treeAsync.complete(t);
}
if (error != null) {
treeAsync.completeExceptionally(error);
}
});
}
}
}
}

/**
* A *debouncer* is an object to get a *resource* from an *underlying resource
* provider* with a certain delay. From the perspective of the debouncer, the
* underlying resource provider has two states: initialized and not-initialized.
*
* 1. While the underlying resource provider is not-initialized (e.g., the
* computation of a parse tree has not yet started), the debouncer waits
* until the delay is over.
*
* 2. When the underlying resource provider becomes initialized (e.g., the
* computation of a parse tree has started, but possibly not yet finished),
* the debouncer returns a future for the resource.
*
* 3. When the underlying resource provider is not-initialized, but the delay
* is over, the debouncer forcibly initializes the resource (e.g., it starts
* the asynchronous computation of a parse tree) and returns a future for
* the resource.
*/
class Debouncer<T> {

// A debouncer is implemented using a *delayed executor* as `scheduler`. The
// idea is to *periodically* check the state of the underlying resource
// provider. More precisely, each time when the resource is requested,
// immediately check if case 2 or case 3 (above) are applicable. If so,
// return. If not, schedule a *delayed future* to retry the request, to be
// completed after a small `period` (e.g., 50 milliseconds).
//
// The reason why multiple futures are scheduled in small periods, instead
// of a single future for the entire large delay, is that futures (of type
// `CompletableFuture`) cannot be interrupted.

private final int period; // Milliseconds
private final Executor scheduler;

// At any point in time, only one delayed future to retry the request for
// the resource should be `scheduled`, tied with the total remaining delay.
// For bookkeeping, a *stamped reference* is used. The reference is the
// delayed future, while the stamp is the remaining delay *upon completion
// of the delayed future*.

private final AtomicStampedReference<@Nullable CompletableFuture<T>> scheduled;

// The underlying resource provider is represented abstractly in terms of
// two suppliers, each of which corresponds with a state of the underlying
// resource provider. `getIfInitialized` should return `null` iff the
// underlying resource provider is not-initialized.

private final Supplier<@Nullable CompletableFuture<T>> getIfInitialized;
private final Supplier<CompletableFuture<T>> initializeAndGet;

public Debouncer(Duration period,
Supplier<@Nullable CompletableFuture<T>> getIfInitialized,
Supplier<CompletableFuture<T>> initializeAndGet) {

this(Math.toIntExact(period.toMillis()), getIfInitialized, initializeAndGet);
}

public Debouncer(int period,
sungshik marked this conversation as resolved.
Show resolved Hide resolved
Supplier<@Nullable CompletableFuture<T>> getIfInitialized,
Supplier<CompletableFuture<T>> initializeAndGet) {

this.period = period;
this.scheduler = CompletableFuture.delayedExecutor(period, TimeUnit.MILLISECONDS);
this.scheduled = new AtomicStampedReference<>(null, 0);
this.getIfInitialized = getIfInitialized;
this.initializeAndGet = initializeAndGet;
}

public CompletableFuture<T> get(Duration delay) {
return get(Math.toIntExact(delay.toMillis()));
}

public CompletableFuture<T> get(int delay) {
return schedule(delay, false);
}

private CompletableFuture<T> schedule(int delay, boolean reschedule) {

// Get a consistent old stamp and old reference
var oldRef = scheduled.getReference();
var oldStamp = scheduled.getStamp();
while (!scheduled.compareAndSet(oldRef, oldRef, oldStamp, oldStamp));

// Compute a new reference (= delayed future to retry this method)
var delayArg = new CompletableFuture<Integer>();
var newRef = delayArg
.thenApplyAsync(this::reschedule, scheduler)
.thenCompose(Function.identity());

// Compute a new stamp
var delayRemaining = Math.max(oldStamp, delay);
var newStamp = delayRemaining - period;

// If the underlying resource provider is initialized, then return the
// future to get the resource
var future = getIfInitialized.get();
if (future != null && scheduled.compareAndSet(oldRef, null, oldStamp, 0)) {
return future;
}

// Otherwise, if the delay is over already, then initialize the
// underlying resource provider and return the future to get the
// resource
if (delayRemaining <= 0 && scheduled.compareAndSet(oldRef, null, oldStamp, 0)) {
return initializeAndGet.get();
}

// Otherwise (i.e., the delay isn't over yet), if a delayed future to
// retry this method hasn't been scheduled yet, or if it must be
// rescheduled regardless, then schedule it
if ((oldRef == null || reschedule) && scheduled.compareAndSet(oldRef, newRef, oldStamp, newStamp)) {
delayArg.complete(newStamp);
return newRef;
}

// Otherwise (i.e, the delay is not yet over, but a delayed future has
// been scheduled already), then update the remaining delay; it will be
// used by the already-scheduled delayed future.
if (scheduled.attemptStamp(oldRef, newStamp)) {
return oldRef;
}

// When this point is reached, concurrent modifications to the stamp or
// the reference in `scheduled` have happened. In that case, retry
// immediately.
return schedule(delay, reschedule);
}

private CompletableFuture<T> reschedule(int delay) {
return schedule(delay, true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ private void triggerBuilder(TextDocumentIdentifier doc) {
private TextDocumentState updateContents(VersionedTextDocumentIdentifier doc, String newContents) {
TextDocumentState file = getFile(doc);
logger.trace("New contents for {}", doc);
handleParsingErrors(file, file.update(doc.getVersion(), newContents));
file.update(doc.getVersion(), newContents);
handleParsingErrors(file, file.getCurrentTreeAsync()); // Warning: Might be a later version (when a concurrent update happened)
return file;
}

Expand Down Expand Up @@ -342,7 +343,7 @@ public CompletableFuture<List<InlayHint>> inlayHint(InlayHintParams params) {
final TextDocumentState file = getFile(params.getTextDocument());
final ILanguageContributions contrib = contributions(params.getTextDocument());
return recoverExceptions(
recoverExceptions(file.getCurrentTreeAsync(), file::getMostRecentTree)
recoverExceptions(file.getCurrentTreeAsync(), file::getLastTreeWithoutErrors)
.thenApply(Versioned::get)
.thenApply(contrib::inlayHint)
.thenCompose(InterruptibleFuture::get)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ private static Either<IFunction, Exception> loadParser(ParserSpecification spec)
try {
logger.debug("Loading parser {} at {}", reifiedType, spec.getParserLocation());
// this hides all the loading and instantiation details of Rascal-generated parsers
var parser = vf.loadParser(reifiedType, spec.getParserLocation(), VF.bool(spec.getAllowAmbiguity()), VF.bool(false), VF.bool(false), vf.set());
var parser = vf.loadParser(reifiedType, spec.getParserLocation(), VF.bool(spec.getAllowAmbiguity()), VF.bool(false), VF.bool(false), VF.bool(false), vf.set());
logger.debug("Got parser: {}", parser);
return Either.forLeft(parser);
}
Expand Down
Loading