From a1513b50e233b33cd1d92cd299a60879b4dc21c0 Mon Sep 17 00:00:00 2001 From: "Daniel A. A. Pelsmaeker" <647530+Virtlink@users.noreply.github.com> Date: Tue, 2 Nov 2021 15:13:31 +0100 Subject: [PATCH 1/8] Add JsglrParseInput completionMode + cursorOffset --- .../java/mb/jsglr/common/JsglrParseInput.java | 64 +++++++++++++++---- .../java/mb/jsglr/pie/JsglrParseTaskDef.java | 8 ++- .../mb/jsglr/pie/JsglrParseTaskInput.java | 4 ++ .../adapter_project/Instance.java.mustache | 7 +- .../adapter/parser/ParseTaskDef.java.mustache | 13 +++- .../spoofax/task/reusable/TigerParse.java | 13 +++- 6 files changed, 91 insertions(+), 18 deletions(-) diff --git a/core/jsglr.common/src/main/java/mb/jsglr/common/JsglrParseInput.java b/core/jsglr.common/src/main/java/mb/jsglr/common/JsglrParseInput.java index 84154821c..ebcd61c0b 100644 --- a/core/jsglr.common/src/main/java/mb/jsglr/common/JsglrParseInput.java +++ b/core/jsglr.common/src/main/java/mb/jsglr/common/JsglrParseInput.java @@ -6,18 +6,53 @@ import org.checkerframework.checker.nullness.qual.Nullable; import java.io.Serializable; +import java.util.Objects; + +/** + * JSGLR parser input. + */ +public final class JsglrParseInput implements Serializable { -public class JsglrParseInput implements Serializable { public final Text text; public final String startSymbol; public final @Nullable ResourceKey fileHint; public final @Nullable ResourcePath rootDirectoryHint; + public final boolean codeCompletionMode; + public final int cursorOffset; - public JsglrParseInput(Text text, String startSymbol, @Nullable ResourceKey fileHint, @Nullable ResourcePath rootDirectoryHint) { + /** + * Initializes a new instance of the {@link JsglrParseInput} class. + * + * @param text the text to be parsed + * @param startSymbol the start symbol of the grammar + * @param fileHint a hint of the file being parsed; or {@code null} when not specified + * @param rootDirectoryHint a hint of the project root directory; or {@code null} when not specified + * @param codeCompletionMode whether to parse in completion mode + * @param cursorOffset the zero-based cursor offset in the text + */ + public JsglrParseInput( + Text text, + String startSymbol, + @Nullable ResourceKey fileHint, + @Nullable ResourcePath rootDirectoryHint, + boolean codeCompletionMode, + int cursorOffset + ) { this.text = text; this.startSymbol = startSymbol; this.fileHint = fileHint; this.rootDirectoryHint = rootDirectoryHint; + this.codeCompletionMode = codeCompletionMode; + this.cursorOffset = cursorOffset; + } + + public JsglrParseInput( + Text text, + String startSymbol, + @Nullable ResourceKey fileHint, + @Nullable ResourcePath rootDirectoryHint + ) { + this(text, startSymbol, fileHint, rootDirectoryHint, false, 0); } public JsglrParseInput(Text text, String startSymbol, @Nullable ResourceKey fileHint) { @@ -44,18 +79,23 @@ public JsglrParseInput(String text, String startSymbol) { if(this == o) return true; if(o == null || getClass() != o.getClass()) return false; final JsglrParseInput that = (JsglrParseInput)o; - if(!text.equals(that.text)) return false; - if(!startSymbol.equals(that.startSymbol)) return false; - if(fileHint != null ? !fileHint.equals(that.fileHint) : that.fileHint != null) return false; - return rootDirectoryHint != null ? rootDirectoryHint.equals(that.rootDirectoryHint) : that.rootDirectoryHint == null; + return this.text.equals(that.text) + && this.startSymbol.equals(that.startSymbol) + && Objects.equals(this.fileHint, that.fileHint) + && Objects.equals(this.rootDirectoryHint, that.rootDirectoryHint) + && this.codeCompletionMode == that.codeCompletionMode + && this.cursorOffset == that.cursorOffset; } @Override public int hashCode() { - int result = text.hashCode(); - result = 31 * result + startSymbol.hashCode(); - result = 31 * result + (fileHint != null ? fileHint.hashCode() : 0); - result = 31 * result + (rootDirectoryHint != null ? rootDirectoryHint.hashCode() : 0); - return result; + return Objects.hash( + text, + startSymbol, + fileHint, + rootDirectoryHint, + codeCompletionMode, + cursorOffset + ); } @Override public String toString() { @@ -64,6 +104,8 @@ public JsglrParseInput(String text, String startSymbol) { ", startSymbol='" + startSymbol + '\'' + ", fileHint=" + fileHint + ", rootDirectoryHint=" + rootDirectoryHint + + ", codeCompletionMode=" + codeCompletionMode + + ", cursorOffset=" + cursorOffset + '}'; } } diff --git a/core/jsglr.pie/src/main/java/mb/jsglr/pie/JsglrParseTaskDef.java b/core/jsglr.pie/src/main/java/mb/jsglr/pie/JsglrParseTaskDef.java index 1a5f9da28..7b29a8f18 100644 --- a/core/jsglr.pie/src/main/java/mb/jsglr/pie/JsglrParseTaskDef.java +++ b/core/jsglr.pie/src/main/java/mb/jsglr/pie/JsglrParseTaskDef.java @@ -89,7 +89,9 @@ protected abstract Result parse( Text text, @Nullable String startSymbol, @Nullable ResourceKey fileHint, - @Nullable ResourcePath rootDirectoryHint + @Nullable ResourcePath rootDirectoryHint, + boolean codeCompletionMode, + int cursorOffset ) throws IOException, InterruptedException; @@ -98,8 +100,10 @@ public Result exec(ExecContext context, J final @Nullable String startSymbol = input.startSymbol().orElse(null); final @Nullable ResourceKey fileHint = input.fileHint().getOr(null); final @Nullable ResourcePath rootDirectoryHint = input.rootDirectoryHint().orElse(null); + final boolean codeCompletionMode = input.codeCompletionMode(); + final int cursorOffset = input.cursorOffset(); try { - return parse(context, context.require(input.textSupplier()), startSymbol, fileHint, rootDirectoryHint); + return parse(context, context.require(input.textSupplier()), startSymbol, fileHint, rootDirectoryHint, codeCompletionMode, cursorOffset); } catch(UncheckedIOException e) { return Result.ofErr(JsglrParseException.readStringFail(e.getCause(), startSymbol, fileHint, rootDirectoryHint)); } diff --git a/core/jsglr.pie/src/main/java/mb/jsglr/pie/JsglrParseTaskInput.java b/core/jsglr.pie/src/main/java/mb/jsglr/pie/JsglrParseTaskInput.java index f2487337c..0d6eabf85 100644 --- a/core/jsglr.pie/src/main/java/mb/jsglr/pie/JsglrParseTaskInput.java +++ b/core/jsglr.pie/src/main/java/mb/jsglr/pie/JsglrParseTaskInput.java @@ -119,4 +119,8 @@ static Builder builder(JsglrParseTaskDef parse) { } Optional rootDirectoryHint(); + + @Value.Default default boolean codeCompletionMode() { return false; } + + @Value.Default default int cursorOffset() { return Integer.MAX_VALUE; } } diff --git a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/adapter_project/Instance.java.mustache b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/adapter_project/Instance.java.mustache index 64bbf6d88..c974e7059 100644 --- a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/adapter_project/Instance.java.mustache +++ b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/adapter_project/Instance.java.mustache @@ -100,7 +100,12 @@ public class {{baseInstance.id}} implements LanguageInstance @Override public Task> createStyleTask(ResourceKey file, @Nullable ResourcePath rootDirectoryHint) { {{#parseInjection}} - return {{styleInjection.name}}.createTask({{this.name}}.inputBuilder().withFile(file).rootDirectoryHint(Optional.ofNullable(rootDirectoryHint)).buildRecoverableTokensSupplier().map(TokensResultToOkFunction.instance)); + return {{styleInjection.name}}.createTask({{this.name}}.inputBuilder() + .withFile(file) + .rootDirectoryHint(Optional.ofNullable(rootDirectoryHint)) + .buildRecoverableTokensSupplier() + .map(TokensResultToOkFunction.instance) + ); {{/parseInjection}} {{^parseInjection}} return {{styleInjection.name}}.createTask((ctx) -> mb.pie.api.None.instance); diff --git a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/parser/ParseTaskDef.java.mustache b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/parser/ParseTaskDef.java.mustache index 2a615cff7..a0436c204 100644 --- a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/parser/ParseTaskDef.java.mustache +++ b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/parser/ParseTaskDef.java.mustache @@ -38,7 +38,9 @@ public class {{baseParseTaskDef.id}} extends JsglrParseTaskDef { Text text, @Nullable String startSymbol, @Nullable ResourceKey fileHint, - @Nullable ResourcePath rootDirectoryHint + @Nullable ResourcePath rootDirectoryHint, + boolean codeCompletionMode, + int cursorOffset ) throws IOException, InterruptedException { context.require(classLoaderResources.tryGetAsNativeDefinitionResource("{{languageProjectInput.parseTableAtermFileRelativePath}}")); {{#languageProjectInput.isJsglr2}} @@ -50,7 +52,14 @@ public class {{baseParseTaskDef.id}} extends JsglrParseTaskDef { context.require(classLoaderResources.tryGetAsNativeResource({{languageProjectInput.parseTable.qualifiedId}}.class), ResourceStampers.hashFile()); final {{languageProjectInput.parser.qualifiedId}} parser = parserProvider.get(); try { - return Result.ofOk(parser.parse(new JsglrParseInput(text, startSymbol != null ? startSymbol : "{{languageProjectInput.startSymbol}}", fileHint, rootDirectoryHint))); + return Result.ofOk(parser.parse(new JsglrParseInput( + text, + startSymbol != null ? startSymbol : "{{languageProjectInput.startSymbol}}", + fileHint, + rootDirectoryHint, + codeCompletionMode, + cursorOffset + ))); } catch(JsglrParseException e) { return Result.ofErr(e); } diff --git a/example/tiger/manual/tiger.spoofax/src/main/java/mb/tiger/spoofax/task/reusable/TigerParse.java b/example/tiger/manual/tiger.spoofax/src/main/java/mb/tiger/spoofax/task/reusable/TigerParse.java index a86802275..935af087f 100644 --- a/example/tiger/manual/tiger.spoofax/src/main/java/mb/tiger/spoofax/task/reusable/TigerParse.java +++ b/example/tiger/manual/tiger.spoofax/src/main/java/mb/tiger/spoofax/task/reusable/TigerParse.java @@ -33,11 +33,20 @@ public class TigerParse extends JsglrParseTaskDef { Text text, @Nullable String startSymbol, @Nullable ResourceKey fileHint, - @Nullable ResourcePath rootDirectoryHint + @Nullable ResourcePath rootDirectoryHint, + boolean codeCompletionMode, + int cursorOffset ) throws InterruptedException { final TigerParser parser = parserProvider.get(); try { - return Result.ofOk(parser.parse(new JsglrParseInput(text, startSymbol != null ? startSymbol : "Module", fileHint, rootDirectoryHint))); + return Result.ofOk(parser.parse(new JsglrParseInput( + text, + startSymbol != null ? startSymbol : "Module", + fileHint, + rootDirectoryHint, + codeCompletionMode, + cursorOffset + ))); } catch(JsglrParseException e) { return Result.ofErr(e); } From 9584aace691c3f928a0b4e234bc7deaf2eca1a03 Mon Sep 17 00:00:00 2001 From: "Daniel A. A. Pelsmaeker" <647530+Virtlink@users.noreply.github.com> Date: Wed, 3 Nov 2021 16:41:14 +0100 Subject: [PATCH 2/8] Use code completion parser # Conflicts: # core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java # lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/CompileSdf3.java --- .../java/mb/jsglr1/common/JSGLR1Parser.java | 1 + .../pie/CodeCompletionTaskDef.java | 17 +++++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/core/jsglr1.common/src/main/java/mb/jsglr1/common/JSGLR1Parser.java b/core/jsglr1.common/src/main/java/mb/jsglr1/common/JSGLR1Parser.java index 39c8a29c7..918fe78a5 100644 --- a/core/jsglr1.common/src/main/java/mb/jsglr1/common/JSGLR1Parser.java +++ b/core/jsglr1.common/src/main/java/mb/jsglr1/common/JSGLR1Parser.java @@ -47,6 +47,7 @@ public JSGLR1Parser(JSGLR1ParseTable parseTable, ITermFactory termFactory) { public JsglrParseOutput parse(JsglrParseInput input) throws JsglrParseException, InterruptedException { try { + parser.setCompletionParse(input.codeCompletionMode, input.cursorOffset); final SGLRParseResult result = parser.parse(input.text.toString(), input.fileHint != null ? input.fileHint.toString() : null, input.startSymbol); if(result.output == null) { throw new RuntimeException("BUG: parser returned null output even though parsing did not fail"); diff --git a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java index 6b2454ec1..57022d745 100644 --- a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java +++ b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java @@ -319,10 +319,11 @@ public Execution( // Parse the AST if (eventHandler != null) eventHandler.beginParse(); - final Result parsedAstResult = parse(); + final Result parsedAstResult = parse(primarySelection); if (parsedAstResult.isErr()) return parsedAstResult.ignoreValueIfErr(); final IStrategoTerm parsedAst = parsedAstResult.unwrapUnchecked(); if (eventHandler != null) eventHandler.endParse(); + log.trace("Parsed completion AST: " + parsedAst); // Prepare the AST (explicate, add term indices, upgrade placeholders) if (eventHandler != null) eventHandler.beginPreparation(); @@ -334,8 +335,8 @@ public Execution( final PlaceholderVarMap placeholderVarMap = new PlaceholderVarMap(file.toString()); final Result upgradedAstResult = upgradePlaceholders(statixAst, placeholderVarMap); if (upgradedAstResult.isErr()) return upgradedAstResult.ignoreValueIfErr(); - final ITerm upgradedAst = upgradedAstResult.unwrapUnchecked(); - final ITermVar placeholder = getCompletionPlaceholder(upgradedAst); + final ITerm upgradedAst = upgradedAstResult.unwrap(); + final ITermVar placeholder = getCompletionPlaceholder(upgradedAst, primarySelection); final SolverState initialState = createInitialSolverState(upgradedAst, statixSecName, statixRootPredicateName, placeholderVarMap); if (eventHandler != null) eventHandler.endPreparation(); @@ -379,12 +380,15 @@ public Execution( /** * Parses the input file. * + * @param selection code selection * @return the AST of the file */ - private Result parse() { + private Result parse(Region selection) { return context.require(parseTask.inputBuilder() .withFile(file) .rootDirectoryHint(Optional.ofNullable(rootDirectoryHint)) + .codeCompletionMode(true) + .cursorOffset(selection.getStartOffset() /* TODO: Support the whole selection? */) .buildRecoverableAstSupplier() ); } @@ -500,10 +504,11 @@ private ITerm toStatix(IStrategoTerm ast) { * Determines the placeholder being completed. * * @param ast the AST to inspect + * @param selection code selection * @return the term variable of the placeholder being completed */ - private ITermVar getCompletionPlaceholder(ITerm ast) { - @Nullable ITermVar placeholderVar = findPlaceholderAt(ast, primarySelection.getStartOffset() /* TODO: Support the whole selection? */); + private ITermVar getCompletionPlaceholder(ITerm ast, Region selection) { + @Nullable ITermVar placeholderVar = findPlaceholderAt(ast, selection.getStartOffset() /* TODO: Support the whole selection? */); if (placeholderVar == null) { throw new IllegalStateException("Completion failed: we don't know the placeholder."); } From af05728f844ac10bbd261a15ffc048a5e38f322c Mon Sep 17 00:00:00 2001 From: "Daniel A. A. Pelsmaeker" <647530+Virtlink@users.noreply.github.com> Date: Mon, 8 Nov 2021 11:09:34 +0100 Subject: [PATCH 3/8] Fix exception --- .../mb/statix/codecompletion/pie/CodeCompletionTaskDef.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java index 57022d745..af17959cb 100644 --- a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java +++ b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java @@ -313,7 +313,7 @@ public Execution( * @return the code completion result * @throws Exception if an exception occurred */ - public Result complete() throws InterruptedException { + public Result complete() throws Exception { @Nullable final CodeCompletionEventHandler eventHandler = eventHandlerProvider.get(); if (eventHandler != null) eventHandler.begin(); From ee0442b848b1fc263ec81ed1f49dd11e8bc6d48e Mon Sep 17 00:00:00 2001 From: "Daniel A. A. Pelsmaeker" <647530+Virtlink@users.noreply.github.com> Date: Sun, 2 Jan 2022 14:16:08 +0100 Subject: [PATCH 4/8] Fix manual Tiger Eclipse plugin --- example/tiger/manual/tiger.eclipse/plugin.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/example/tiger/manual/tiger.eclipse/plugin.xml b/example/tiger/manual/tiger.eclipse/plugin.xml index 72753548c..fffa00db6 100755 --- a/example/tiger/manual/tiger.eclipse/plugin.xml +++ b/example/tiger/manual/tiger.eclipse/plugin.xml @@ -1,8 +1,9 @@ - - + + + From b52c72ea4b23958231fe00935855cd00204272ed Mon Sep 17 00:00:00 2001 From: "Daniel A. A. Pelsmaeker" <647530+Virtlink@users.noreply.github.com> Date: Sun, 2 Jan 2022 17:42:38 +0100 Subject: [PATCH 5/8] Add completion parse table to CFG & tasks --- .../Spoofax2ParserLanguageCompiler.java | 22 +++++- .../language/ParserLanguageCompiler.java | 37 +++++++++ .../adapter_project/Module.java.mustache | 10 +++ .../adapter/parser/ParseTaskDef.java.mustache | 10 ++- .../CompletionParserFactory.java.mustache | 20 +++++ .../language/parser/ParseTable.java.mustache | 7 +- .../parser/ParserFactory.java.mustache | 6 +- .../spoofaxcore/tiger/TigerInputs.java | 2 + .../cfg.spoofax2/syntax/part/language.sdf3 | 26 ++++--- .../java/mb/cfg/convert/CfgAstToObject.java | 23 ++++-- .../java/mb/cfg/metalang/CfgSdf3Config.java | 27 +++++++ .../java/mb/cfg/metalang/CfgSdf3Source.java | 21 ++++- .../mb/esv/task/spoofax/EsvParseWrapper.java | 4 +- .../sdf3/task/spec/Sdf3ParseTableToFile.java | 77 +++++++++++++++---- .../task/spoofax/StatixParseWrapper.java | 4 +- .../task/spoofax/StrategoParseWrapper.java | 4 +- .../lwb/compiler/gradle/LanguagePlugin.kt | 14 +++- .../lwb/compiler/sdf3/SpoofaxSdf3Check.java | 19 ++++- .../lwb/compiler/sdf3/SpoofaxSdf3Compile.java | 54 +++++++++++-- .../lwb/compiler/sdf3/SpoofaxSdf3Config.java | 59 ++++++++++++-- .../compiler/sdf3/SpoofaxSdf3Configure.java | 19 ++++- 21 files changed, 405 insertions(+), 60 deletions(-) create mode 100644 core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/language/parser/CompletionParserFactory.java.mustache diff --git a/core/spoofax.compiler.spoofax2/src/main/java/mb/spoofax/compiler/spoofax2/language/Spoofax2ParserLanguageCompiler.java b/core/spoofax.compiler.spoofax2/src/main/java/mb/spoofax/compiler/spoofax2/language/Spoofax2ParserLanguageCompiler.java index 27353317a..b13a94ded 100644 --- a/core/spoofax.compiler.spoofax2/src/main/java/mb/spoofax/compiler/spoofax2/language/Spoofax2ParserLanguageCompiler.java +++ b/core/spoofax.compiler.spoofax2/src/main/java/mb/spoofax/compiler/spoofax2/language/Spoofax2ParserLanguageCompiler.java @@ -33,7 +33,9 @@ public class Spoofax2ParserLanguageCompiler implements TaskDef getCopyResources(Input input) { return ListView.of( input.parseTableAtermFileRelativePath(), - input.parseTablePersistedFileRelativePath() + input.parseTablePersistedFileRelativePath(), + input.completionParseTableAtermFileRelativePath(), + input.completionParseTablePersistedFileRelativePath() ); } @@ -61,10 +63,28 @@ class Builder extends Spoofax2ParserLanguageCompilerData.Input.Builder {} return "target/metaborg/table.bin"; } + /** + * @return path to the completion parse table aterm file to copy, relative to the Spoofax 2 language specification project + * directory. + */ + @Value.Default default String completionParseTableAtermFileRelativePath() { + return "target/metaborg/sdf-completions.tbl"; + } + + /** + * @return path to the completion parse table persisted file to copy, relative to the Spoofax 2 language specification + * project directory. + */ + @Value.Default default String completionParseTablePersistedFileRelativePath() { + return "target/metaborg/table-completions.bin"; + } + default void syncTo(ParserLanguageCompiler.Input.Builder builder) { builder.parseTableAtermFileRelativePath(parseTableAtermFileRelativePath()); builder.parseTablePersistedFileRelativePath(parseTablePersistedFileRelativePath()); + builder.completionParseTableAtermFileRelativePath(completionParseTableAtermFileRelativePath()); + builder.completionParseTablePersistedFileRelativePath(completionParseTablePersistedFileRelativePath()); } } } diff --git a/core/spoofax.compiler/src/main/java/mb/spoofax/compiler/language/ParserLanguageCompiler.java b/core/spoofax.compiler/src/main/java/mb/spoofax/compiler/language/ParserLanguageCompiler.java index c59e61dfb..3f722b9b4 100644 --- a/core/spoofax.compiler/src/main/java/mb/spoofax/compiler/language/ParserLanguageCompiler.java +++ b/core/spoofax.compiler/src/main/java/mb/spoofax/compiler/language/ParserLanguageCompiler.java @@ -22,12 +22,14 @@ public class ParserLanguageCompiler { private final TemplateWriter tableTemplate; private final TemplateWriter parserTemplate; private final TemplateWriter factoryTemplate; + private final TemplateWriter completionFactoryTemplate; @Inject public ParserLanguageCompiler(TemplateCompiler templateCompiler) { templateCompiler = templateCompiler.loadingFromClass(getClass()); this.tableTemplate = templateCompiler.getOrCompileToWriter("parser/ParseTable.java.mustache"); this.parserTemplate = templateCompiler.getOrCompileToWriter("parser/Parser.java.mustache"); this.factoryTemplate = templateCompiler.getOrCompileToWriter("parser/ParserFactory.java.mustache"); + this.completionFactoryTemplate = templateCompiler.getOrCompileToWriter("parser/CompletionParserFactory.java.mustache"); } @@ -37,6 +39,7 @@ public None compile(ExecContext context, Input input) throws IOException { tableTemplate.write(context, input.baseParseTable().file(generatedJavaSourcesDirectory), input); parserTemplate.write(context, input.baseParser().file(generatedJavaSourcesDirectory), input); factoryTemplate.write(context, input.baseParserFactory().file(generatedJavaSourcesDirectory), input); + completionFactoryTemplate.write(context, input.baseCompletionParserFactory().file(generatedJavaSourcesDirectory), input); return None.instance; } @@ -74,6 +77,16 @@ class Builder extends ParserLanguageCompilerData.Input.Builder {} */ String parseTablePersistedFileRelativePath(); + /** + * @return path to the completion parse table aterm file to load, relative to the classloader resources. + */ + String completionParseTableAtermFileRelativePath(); + + /** + * @return path to the completion parse table persisted file to load, relative to the classloader resources. + */ + String completionParseTablePersistedFileRelativePath(); + @Value.Default default ParserVariant variant() { return ParserVariant.jsglr1(); } @@ -126,6 +139,30 @@ default TypeInfo parserFactory() { return extendParserFactory().orElseGet(this::baseParserFactory); } + // Completion Parser + + @Value.Default default TypeInfo baseCompletionParser() { + return TypeInfo.of(languageProject().packageId(), shared().defaultClassPrefix() + "Parser"); + } + + Optional extendCompletionParser(); + + default TypeInfo completionParser() { + return extendCompletionParser().orElseGet(this::baseCompletionParser); + } + + // Completion Parser factory + + @Value.Default default TypeInfo baseCompletionParserFactory() { + return TypeInfo.of(languageProject().packageId(), shared().defaultClassPrefix() + "CompletionParserFactory"); + } + + Optional extendCompletionParserFactory(); + + default TypeInfo completionParserFactory() { + return extendCompletionParserFactory().orElseGet(this::baseCompletionParserFactory); + } + /// Mustache template helpers diff --git a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/adapter_project/Module.java.mustache b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/adapter_project/Module.java.mustache index d6815a671..be150b922 100644 --- a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/adapter_project/Module.java.mustache +++ b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/adapter_project/Module.java.mustache @@ -78,6 +78,16 @@ public class {{baseModule.id}} { return parserFactory.create(); } + @Provides @{{scope.id}} @Named("completion") + static {{this.languageProjectInput.completionParserFactory.qualifiedId}} provideCompletionParserFactory(@{{qualifier.id}}("definition-directory") HierarchicalResource definitionDir) { + return new {{this.languageProjectInput.completionParserFactory.qualifiedId}}(definitionDir); + } + + @Provides @Named("completion") /* Unscoped: parser has state, so create a new parser every call. */ + static {{this.languageProjectInput.completionParser.qualifiedId}} provideCompletionParser(@Named("completion") {{this.languageProjectInput.completionParserFactory.qualifiedId}} parserFactory) { + return parserFactory.create(); + } + @Provides @{{scope.id}} static ITermFactory provideTermFactory() { return new org.spoofax.jsglr.client.imploder.ImploderOriginTermFactory(new TermFactory()); diff --git a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/parser/ParseTaskDef.java.mustache b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/parser/ParseTaskDef.java.mustache index a0436c204..2524e669c 100644 --- a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/parser/ParseTaskDef.java.mustache +++ b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/parser/ParseTaskDef.java.mustache @@ -13,6 +13,7 @@ import mb.resource.hierarchical.ResourcePath; import org.checkerframework.checker.nullness.qual.Nullable; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Provider; import java.io.IOException; @@ -20,13 +21,16 @@ import java.io.IOException; public class {{baseParseTaskDef.id}} extends JsglrParseTaskDef { private final {{classLoaderResourcesInput.classLoaderResources.qualifiedId}} classLoaderResources; private final Provider<{{languageProjectInput.parser.qualifiedId}}> parserProvider; + private final Provider<{{languageProjectInput.parser.qualifiedId}}> completionParserProvider; @Inject public {{baseParseTaskDef.id}}( {{classLoaderResourcesInput.classLoaderResources.qualifiedId}} classLoaderResources, - Provider<{{languageProjectInput.parser.qualifiedId}}> parserProvider + Provider<{{languageProjectInput.parser.qualifiedId}}> parserProvider, + @Named("completion") Provider<{{languageProjectInput.parser.qualifiedId}}> completionParserProvider ) { this.classLoaderResources = classLoaderResources; this.parserProvider = parserProvider; + this.completionParserProvider = completionParserProvider; } @Override public String getId() { @@ -43,14 +47,16 @@ public class {{baseParseTaskDef.id}} extends JsglrParseTaskDef { int cursorOffset ) throws IOException, InterruptedException { context.require(classLoaderResources.tryGetAsNativeDefinitionResource("{{languageProjectInput.parseTableAtermFileRelativePath}}")); + context.require(classLoaderResources.tryGetAsNativeDefinitionResource("{{languageProjectInput.completionParseTableAtermFileRelativePath}}")); {{#languageProjectInput.isJsglr2}} context.require(classLoaderResources.tryGetAsNativeDefinitionResource("{{languageProjectInput.parseTablePersistedFileRelativePath}}")); + context.require(classLoaderResources.tryGetAsNativeDefinitionResource("{{languageProjectInput.completionParseTablePersistedFileRelativePath}}")); {{/languageProjectInput.isJsglr2}} context.require(classLoaderResources.tryGetAsNativeResource(getClass()), ResourceStampers.hashFile()); context.require(classLoaderResources.tryGetAsNativeResource({{languageProjectInput.parser.qualifiedId}}.class), ResourceStampers.hashFile()); context.require(classLoaderResources.tryGetAsNativeResource({{languageProjectInput.parserFactory.qualifiedId}}.class), ResourceStampers.hashFile()); context.require(classLoaderResources.tryGetAsNativeResource({{languageProjectInput.parseTable.qualifiedId}}.class), ResourceStampers.hashFile()); - final {{languageProjectInput.parser.qualifiedId}} parser = parserProvider.get(); + final {{languageProjectInput.parser.qualifiedId}} parser = codeCompletionMode ? completionParserProvider.get() : parserProvider.get(); try { return Result.ofOk(parser.parse(new JsglrParseInput( text, diff --git a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/language/parser/CompletionParserFactory.java.mustache b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/language/parser/CompletionParserFactory.java.mustache new file mode 100644 index 000000000..12d1e0ee6 --- /dev/null +++ b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/language/parser/CompletionParserFactory.java.mustache @@ -0,0 +1,20 @@ +package {{baseParserFactory.packageId}}; + +import mb.resource.hierarchical.HierarchicalResource; +import mb.spoofax.compiler.interfaces.spoofaxcore.ParserFactory; + +public class {{baseCompletionParserFactory.id}} implements ParserFactory { + private final {{parseTable.qualifiedId}} parseTable; + + public {{baseCompletionParserFactory.id}}(HierarchicalResource definitionDir) { + final HierarchicalResource atermFile = definitionDir.appendRelativePath("{{completionParseTableAtermFileRelativePath}}"); +{{#isJsglr2}} + final HierarchicalResource persistedFile = definitionDir.appendRelativePath("{{completionParseTablePersistedFileRelativePath}}"); +{{/isJsglr2}} + this.parseTable = {{parseTable.qualifiedId}}.fromResources(atermFile{{#isJsglr2}}, persistedFile{{/isJsglr2}}); + } + + @Override public {{completionParser.qualifiedId}} create() { + return new {{completionParser.qualifiedId}}(parseTable); + } +} diff --git a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/language/parser/ParseTable.java.mustache b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/language/parser/ParseTable.java.mustache index 4bc7953ba..48b9f127f 100644 --- a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/language/parser/ParseTable.java.mustache +++ b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/language/parser/ParseTable.java.mustache @@ -13,11 +13,16 @@ public class {{baseParseTable.id}} implements Serializable { this.parseTable = parseTable; } + @Deprecated // Use `ParserFactory.create()` instead. public static {{baseParseTable.id}} fromDefinitionDir(HierarchicalResource definitionDir) { final HierarchicalResource atermFile = definitionDir.appendRelativePath("{{parseTableAtermFileRelativePath}}"); {{#isJsglr2}} - final HierarchicalResource persistedFile = definitionDir.appendRelativePath("{{parseTablePersistedFileRelativePath}}"); + final HierarchicalResource persistedFile = definitionDir.appendRelativePath("{{parseTablePersistedFileRelativePath}}"); {{/isJsglr2}} + return fromResources(atermFile{{#isJsglr2}}, persistedFile{{/isJsglr2}}); + } + + public static {{baseParseTable.id}} fromResources(HierarchicalResource atermFile{{#isJsglr2}}, HierarchicalResource persistedFile{{/isJsglr2}}) { try(final InputStream atermInputStream = atermFile.openRead(){{#isJsglr2}}; final InputStream persistedInputStream = persistedFile.openRead(){{/isJsglr2}}) { final {{parseTableType.qualifiedId}} parseTable = {{parseTableType.qualifiedId}}.fromStream(atermInputStream{{#isJsglr2}}, persistedInputStream{{/isJsglr2}}); return new {{baseParseTable.id}}(parseTable); diff --git a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/language/parser/ParserFactory.java.mustache b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/language/parser/ParserFactory.java.mustache index 7472f07d0..ede97cd57 100644 --- a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/language/parser/ParserFactory.java.mustache +++ b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/language/parser/ParserFactory.java.mustache @@ -7,7 +7,11 @@ public class {{baseParserFactory.id}} implements ParserFactory { private final {{parseTable.qualifiedId}} parseTable; public {{baseParserFactory.id}}(HierarchicalResource definitionDir) { - this.parseTable = {{parseTable.qualifiedId}}.fromDefinitionDir(definitionDir); + final HierarchicalResource atermFile = definitionDir.appendRelativePath("{{parseTableAtermFileRelativePath}}"); +{{#isJsglr2}} + final HierarchicalResource persistedFile = definitionDir.appendRelativePath("{{parseTablePersistedFileRelativePath}}"); +{{/isJsglr2}} + this.parseTable = {{parseTable.qualifiedId}}.fromResources(atermFile{{#isJsglr2}}, persistedFile{{/isJsglr2}}); } @Override public {{parser.qualifiedId}} create() { diff --git a/core/spoofax.compiler/src/test/java/mb/spoofax/compiler/spoofaxcore/tiger/TigerInputs.java b/core/spoofax.compiler/src/test/java/mb/spoofax/compiler/spoofaxcore/tiger/TigerInputs.java index 5523d30a1..5136571ed 100644 --- a/core/spoofax.compiler/src/test/java/mb/spoofax/compiler/spoofaxcore/tiger/TigerInputs.java +++ b/core/spoofax.compiler/src/test/java/mb/spoofax/compiler/spoofaxcore/tiger/TigerInputs.java @@ -184,6 +184,8 @@ private void setLanguageProjectCompilerInput(ResourcePath rootDirectory, Shared languageProjectCompilerInputBuilder.withParser() .parseTableAtermFileRelativePath("target/metaborg/sdf.tbl") .parseTablePersistedFileRelativePath("target/metaborg/table.bin") + .completionParseTableAtermFileRelativePath("target/metaborg/sdf-completions.tbl") + .completionParseTablePersistedFileRelativePath("target/metaborg/table-completions.bin") .startSymbol("Module"); languageProjectCompilerInputBuilder.withStyler() .packedEsvRelativePath("target/metaborg/editor.esv.af"); diff --git a/lwb/metalang/cfg/cfg.spoofax2/syntax/part/language.sdf3 b/lwb/metalang/cfg/cfg.spoofax2/syntax/part/language.sdf3 index 13e546fd4..6c522ce1d 100644 --- a/lwb/metalang/cfg/cfg.spoofax2/syntax/part/language.sdf3 +++ b/lwb/metalang/cfg/cfg.spoofax2/syntax/part/language.sdf3 @@ -16,19 +16,21 @@ context-free syntax <{Sdf3Option "\n"}*> }> Sdf3Option.Sdf3Source = > - + Sdf3Source.Sdf3Files = }> Sdf3FilesOption.Sdf3FilesMainSourceDirectory = > Sdf3FilesOption.Sdf3FilesMainFile = > - + Sdf3Source.Sdf3Prebuilt = }> - Sdf3PrebuiltOption.Sdf3PrebuiltParseTableAtermFile = > - Sdf3PrebuiltOption.Sdf3PrebuiltParseTablePersistedFile = > - + Sdf3PrebuiltOption.Sdf3PrebuiltParseTableAtermFile = > + Sdf3PrebuiltOption.Sdf3PrebuiltParseTablePersistedFile = > + Sdf3PrebuiltOption.Sdf3PrebuiltCompletionParseTableAtermFile = > + Sdf3PrebuiltOption.Sdf3PrebuiltCompletionParseTablePersistedFile = > + // TODO: move into source after CC lab. Sdf3Option.Sdf3ParseTableGeneratorSection = @@ -50,7 +52,7 @@ context-free syntax <{EsvOption "\n"}*> }> EsvOption.EsvSource = > - + EsvSource.EsvFiles = }> @@ -58,7 +60,7 @@ context-free syntax EsvFilesOption.EsvFilesMainFile = > EsvFilesOption.EsvFilesIncludeDirectory = > EsvFilesOption.EsvFilesIncludeLibspoofax2Exports = > - + EsvSource.EsvPrebuilt = }> @@ -74,14 +76,14 @@ context-free syntax <{StatixOption "\n"}*> }> StatixOption.StatixSource = > - + StatixSource.StatixFiles = }> StatixFilesOption.StatixFilesMainSourceDirectory = > StatixFilesOption.StatixFilesMainFile = > StatixFilesOption.StatixFilesIncludeDirectory = > - + StatixSource.StatixPrebuilt = }> @@ -100,16 +102,16 @@ context-free syntax <{StrategoOption "\n"}*> }> StrategoOption.StrategoSource = > - + StrategoSource.StrategoFiles = }> StrategoFilesOption.StrategoFilesMainSourceDirectory = > StrategoFilesOption.StrategoFilesMainFile = > StrategoFilesOption.StrategoFilesIncludeDirectory = > - + // TODO: move into source after CC lab. StrategoOption.StrategoSdf3StatixExplicationGen = > - + StrategoOption.StrategoLanguageStrategyAffix = > StrategoOption.StrategoOutputJavaPackageId = > diff --git a/lwb/metalang/cfg/cfg/src/main/java/mb/cfg/convert/CfgAstToObject.java b/lwb/metalang/cfg/cfg/src/main/java/mb/cfg/convert/CfgAstToObject.java index fb4c75837..d7de8f62b 100644 --- a/lwb/metalang/cfg/cfg/src/main/java/mb/cfg/convert/CfgAstToObject.java +++ b/lwb/metalang/cfg/cfg/src/main/java/mb/cfg/convert/CfgAstToObject.java @@ -163,17 +163,28 @@ public static Output convert( builder.source(CfgSdf3Source.files(filesSourceBuilder.build())); } else if(TermUtils.isAppl(source, "Sdf3Prebuilt", 1)) { final Parts prebuiltParts = subParts.subParts(source.getSubterm(0)); - final Option atermFile = prebuiltParts.getOneSubtermAsExistingFile("Sdf3PrebuiltParseTableAtermFile", rootDirectory, "SDF3 prebuilt parse table ATerm file"); - final Option persistedFile = prebuiltParts.getOneSubtermAsExistingFile("Sdf3PrebuiltParseTablePersistedFile", rootDirectory, "SDF3 prebuilt parse table persisted file"); - if(!atermFile.isSome()) { + final Option parseTableAtermFile = prebuiltParts.getOneSubtermAsExistingFile("Sdf3PrebuiltParseTableAtermFile", rootDirectory, "SDF3 prebuilt parse table ATerm file"); + final Option parseTablePersistedFile = prebuiltParts.getOneSubtermAsExistingFile("Sdf3PrebuiltParseTablePersistedFile", rootDirectory, "SDF3 prebuilt parse table persisted file"); + final Option completionParseTableAtermFile = prebuiltParts.getOneSubtermAsExistingFile("Sdf3PrebuiltCompletionParseTableAtermFile", rootDirectory, "SDF3 prebuilt completion parse table ATerm file"); + final Option completionParseTablePersistedFile = prebuiltParts.getOneSubtermAsExistingFile("Sdf3PrebuiltCompletionParseTablePersistedFile", rootDirectory, "SDF3 prebuilt completion parse table persisted file"); + if(!parseTableAtermFile.isSome()) { messagesBuilder.addMessage("parse-table-aterm-file = $Path option is missing", Severity.Error, cfgFile, TermTracer.getRegion(source)); } - if(!persistedFile.isSome()) { + if(!parseTablePersistedFile.isSome()) { messagesBuilder.addMessage("parse-table-persisted-file = $Path option is missing", Severity.Error, cfgFile, TermTracer.getRegion(source)); } - if(atermFile.isSome() && persistedFile.isSome()) { - builder.source(CfgSdf3Source.prebuilt(atermFile.unwrap(), persistedFile.unwrap())); + if(!completionParseTableAtermFile.isSome()) { + messagesBuilder.addMessage("completion-parse-table-aterm-file = $Path option is missing", Severity.Error, cfgFile, TermTracer.getRegion(source)); } + if(!completionParseTablePersistedFile.isSome()) { + messagesBuilder.addMessage("completion-parse-table-persisted-file = $Path option is missing", Severity.Error, cfgFile, TermTracer.getRegion(source)); + } + builder.source(CfgSdf3Source.prebuilt( + parseTableAtermFile.unwrap(), + parseTablePersistedFile.unwrap(), + completionParseTableAtermFile.unwrap(), + completionParseTablePersistedFile.unwrap() + )); } else { throw new InvalidAstShapeException("SDF3 source", source); } diff --git a/lwb/metalang/cfg/cfg/src/main/java/mb/cfg/metalang/CfgSdf3Config.java b/lwb/metalang/cfg/cfg/src/main/java/mb/cfg/metalang/CfgSdf3Config.java index f60cd4406..35b1143bb 100644 --- a/lwb/metalang/cfg/cfg/src/main/java/mb/cfg/metalang/CfgSdf3Config.java +++ b/lwb/metalang/cfg/cfg/src/main/java/mb/cfg/metalang/CfgSdf3Config.java @@ -56,6 +56,7 @@ public static ResourcePath getDefaultMainFile(ResourcePath mainSourceDirectory) return false; } + // Normal Parse Table @Value.Default default String parseTableAtermFileRelativePath() { return "sdf.tbl"; @@ -79,6 +80,30 @@ default ResourcePath parseTablePersistedOutputFile() { ; } + // Completion Parse Table + + @Value.Default default String completionParseTableAtermFileRelativePath() { + return "sdf-completions.tbl"; + } + + default ResourcePath completionParseTableAtermOutputFile() { + return compileLanguageShared().generatedResourcesDirectory() // Generated resources directory, so that Gradle includes the parse table in the JAR file. + .appendRelativePath(compileLanguageShared().languageProject().packagePath()) // Append package path to make location unique, enabling JAR files to be merged. + .appendRelativePath(completionParseTableAtermFileRelativePath()) // Append the relative path to the parse table. + ; + } + + @Value.Default default String completionParseTablePersistedFileRelativePath() { + return "sdf-completions.bin"; + } + + default ResourcePath completionParseTablePersistedOutputFile() { + return compileLanguageShared().generatedResourcesDirectory() // Generated resources directory, so that Gradle includes the parse table in the JAR file. + .appendRelativePath(compileLanguageShared().languageProject().packagePath()) // Append package path to make location unique, enabling JAR files to be merged. + .appendRelativePath(completionParseTablePersistedFileRelativePath()) // Append the relative path to the parse table. + ; + } + /// Automatically provided sub-inputs @@ -88,5 +113,7 @@ default ResourcePath parseTablePersistedOutputFile() { default void syncTo(ParserLanguageCompiler.Input.Builder builder) { builder.parseTableAtermFileRelativePath(parseTableAtermFileRelativePath()); builder.parseTablePersistedFileRelativePath(parseTablePersistedFileRelativePath()); + builder.completionParseTableAtermFileRelativePath(completionParseTableAtermFileRelativePath()); + builder.completionParseTablePersistedFileRelativePath(completionParseTablePersistedFileRelativePath()); } } diff --git a/lwb/metalang/cfg/cfg/src/main/java/mb/cfg/metalang/CfgSdf3Source.java b/lwb/metalang/cfg/cfg/src/main/java/mb/cfg/metalang/CfgSdf3Source.java index 8c331caea..274359c01 100644 --- a/lwb/metalang/cfg/cfg/src/main/java/mb/cfg/metalang/CfgSdf3Source.java +++ b/lwb/metalang/cfg/cfg/src/main/java/mb/cfg/metalang/CfgSdf3Source.java @@ -41,15 +41,30 @@ public static ResourcePath getDefaultMainSourceDirectory(CompileLanguageSpecific interface Cases { R files(Files files); - R prebuilt(ResourcePath inputParseTableAtermFile, ResourcePath inputParseTablePersistedFile); + R prebuilt( + ResourcePath inputParseTableAtermFile, + ResourcePath inputParseTablePersistedFile, + ResourcePath inputCompletionParseTableAtermFile, + ResourcePath inputCompletionParseTablePersistedFile + ); } public static CfgSdf3Source files(Files files) { return CfgSdf3Sources.files(files); } - public static CfgSdf3Source prebuilt(ResourcePath inputParseTableAtermFile, ResourcePath inputParseTablePersistedFile) { - return CfgSdf3Sources.prebuilt(inputParseTableAtermFile, inputParseTablePersistedFile); + public static CfgSdf3Source prebuilt( + ResourcePath inputParseTableAtermFile, + ResourcePath inputParseTablePersistedFile, + ResourcePath inputCompletionParseTableAtermFile, + ResourcePath inputCompletionParseTablePersistedFile + ) { + return CfgSdf3Sources.prebuilt( + inputParseTableAtermFile, + inputParseTablePersistedFile, + inputCompletionParseTableAtermFile, + inputCompletionParseTablePersistedFile + ); } diff --git a/lwb/metalang/esv/esv/src/main/java/mb/esv/task/spoofax/EsvParseWrapper.java b/lwb/metalang/esv/esv/src/main/java/mb/esv/task/spoofax/EsvParseWrapper.java index fd057e49f..bd3e31a47 100644 --- a/lwb/metalang/esv/esv/src/main/java/mb/esv/task/spoofax/EsvParseWrapper.java +++ b/lwb/metalang/esv/esv/src/main/java/mb/esv/task/spoofax/EsvParseWrapper.java @@ -11,6 +11,7 @@ import mb.pie.api.stamp.resource.ResourceStampers; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Provider; public class EsvParseWrapper extends EsvParse { @@ -20,9 +21,10 @@ public class EsvParseWrapper extends EsvParse { @Inject public EsvParseWrapper( EsvClassLoaderResources classLoaderResources, Provider parserProvider, + @Named("completion") Provider completionParserProvider, EsvConfigFunctionWrapper configFunctionWrapper ) { - super(classLoaderResources, parserProvider); + super(classLoaderResources, parserProvider, completionParserProvider); this.classLoaderResources = classLoaderResources; this.configFunctionWrapper = configFunctionWrapper; } diff --git a/lwb/metalang/sdf3/sdf3/src/main/java/mb/sdf3/task/spec/Sdf3ParseTableToFile.java b/lwb/metalang/sdf3/sdf3/src/main/java/mb/sdf3/task/spec/Sdf3ParseTableToFile.java index e189134eb..ffd6e54c3 100644 --- a/lwb/metalang/sdf3/sdf3/src/main/java/mb/sdf3/task/spec/Sdf3ParseTableToFile.java +++ b/lwb/metalang/sdf3/sdf3/src/main/java/mb/sdf3/task/spec/Sdf3ParseTableToFile.java @@ -24,44 +24,67 @@ public class Sdf3ParseTableToFile implements TaskDef> { public static class Input implements Serializable { private final Supplier> parseTableSupplier; + private final Supplier> completionParseTableSupplier; private final ResourcePath atermOutputFile; private final ResourcePath persistedOutputFile; + private final ResourcePath completionAtermOutputFile; + private final ResourcePath completionPersistedOutputFile; public Input( Supplier> parseTableSupplier, + Supplier> completionParseTableSupplier, ResourcePath atermOutputFile, - ResourcePath persistedOutputFile + ResourcePath persistedOutputFile, + ResourcePath completionAtermOutputFile, + ResourcePath completionPersistedOutputFile ) { this.parseTableSupplier = parseTableSupplier; + this.completionParseTableSupplier = completionParseTableSupplier; this.atermOutputFile = atermOutputFile; this.persistedOutputFile = persistedOutputFile; + this.completionAtermOutputFile = completionAtermOutputFile; + this.completionPersistedOutputFile = completionPersistedOutputFile; } public Key getKey() { - return new Key(atermOutputFile, persistedOutputFile); + return new Key( + atermOutputFile, + persistedOutputFile, + completionAtermOutputFile, + completionPersistedOutputFile + ); } @Override public boolean equals(@Nullable Object o) { if(this == o) return true; if(o == null || getClass() != o.getClass()) return false; - final Input input = (Input)o; - if(!parseTableSupplier.equals(input.parseTableSupplier)) return false; - if(!atermOutputFile.equals(input.atermOutputFile)) return false; - return persistedOutputFile.equals(input.persistedOutputFile); + final Input that = (Input)o; + return this.parseTableSupplier.equals(that.parseTableSupplier) + && this.completionParseTableSupplier.equals(that.completionParseTableSupplier) + && this.atermOutputFile.equals(that.atermOutputFile) + && this.persistedOutputFile.equals(that.persistedOutputFile) + && this.completionAtermOutputFile.equals(that.completionAtermOutputFile) + && this.completionPersistedOutputFile.equals(that.completionPersistedOutputFile); } @Override public int hashCode() { int result = parseTableSupplier.hashCode(); + result = 31 * result + completionParseTableSupplier.hashCode(); result = 31 * result + atermOutputFile.hashCode(); result = 31 * result + persistedOutputFile.hashCode(); + result = 31 * result + completionAtermOutputFile.hashCode(); + result = 31 * result + completionPersistedOutputFile.hashCode(); return result; } @Override public String toString() { return "Sdf3ParseTableToFile$Input{" + "parseTableSupplier=" + parseTableSupplier + + ", completionParseTableSupplier=" + completionParseTableSupplier + ", atermOutputFile=" + atermOutputFile + ", persistedOutputFile=" + persistedOutputFile + + ", completionAtermOutputFile=" + completionAtermOutputFile + + ", completionPersistedOutputFile=" + completionPersistedOutputFile + '}'; } } @@ -69,23 +92,31 @@ public Key getKey() { public static class Key implements Serializable { private final ResourcePath atermOutputFile; private final ResourcePath persistedOutputFile; + private final ResourcePath completionAtermOutputFile; + private final ResourcePath completionPersistedOutputFile; - public Key(ResourcePath atermOutputFile, ResourcePath persistedOutputFile) { + public Key(ResourcePath atermOutputFile, ResourcePath persistedOutputFile, ResourcePath completionAtermOutputFile, ResourcePath completionPersistedOutputFile) { this.atermOutputFile = atermOutputFile; this.persistedOutputFile = persistedOutputFile; + this.completionAtermOutputFile = completionAtermOutputFile; + this.completionPersistedOutputFile = completionPersistedOutputFile; } @Override public boolean equals(@Nullable Object o) { if(this == o) return true; if(o == null || getClass() != o.getClass()) return false; - final Key key = (Key)o; - if(!atermOutputFile.equals(key.atermOutputFile)) return false; - return persistedOutputFile.equals(key.persistedOutputFile); + final Key that = (Key)o; + return this.atermOutputFile.equals(that.atermOutputFile) + && this.persistedOutputFile.equals(that.persistedOutputFile) + && this.completionAtermOutputFile.equals(that.completionAtermOutputFile) + && this.completionPersistedOutputFile.equals(that.completionPersistedOutputFile); } @Override public int hashCode() { int result = atermOutputFile.hashCode(); result = 31 * result + persistedOutputFile.hashCode(); + result = 31 * result + completionAtermOutputFile.hashCode(); + result = 31 * result + completionPersistedOutputFile.hashCode(); return result; } @@ -93,6 +124,8 @@ public Key(ResourcePath atermOutputFile, ResourcePath persistedOutputFile) { return "Sdf3ParseTableToFile$Key{" + "atermOutputFile=" + atermOutputFile + ", persistedOutputFile=" + persistedOutputFile + + ", completionAtermOutputFile=" + completionAtermOutputFile + + ", completionPersistedOutputFile=" + completionPersistedOutputFile + '}'; } } @@ -104,14 +137,32 @@ public Key(ResourcePath atermOutputFile, ResourcePath persistedOutputFile) { } @Override public Result exec(ExecContext context, Input input) throws IOException { - return context.require(input.parseTableSupplier) + return writeParseTable( + context, + input.parseTableSupplier, + context.getHierarchicalResource(input.atermOutputFile), + context.getHierarchicalResource(input.persistedOutputFile) + ).and(writeParseTable( + context, + input.completionParseTableSupplier, + context.getHierarchicalResource(input.completionAtermOutputFile), + context.getHierarchicalResource(input.completionPersistedOutputFile) + )); + } + + @SuppressWarnings("unchecked") + private Result writeParseTable( + ExecContext context, + Supplier> parseTableSupplier, + HierarchicalResource atermOutputFile, + HierarchicalResource persistedOutputFile + ) { + return (Result)context.require(parseTableSupplier) .mapCatching(parseTable -> { - final HierarchicalResource atermOutputFile = context.getHierarchicalResource(input.atermOutputFile); atermOutputFile.ensureFileExists(); atermOutputFile.writeString(ParseTableIO.generateATerm(parseTable).toString(), StandardCharsets.UTF_8); context.provide(atermOutputFile); - final HierarchicalResource persistedOutputFile = context.getHierarchicalResource(input.persistedOutputFile); persistedOutputFile.ensureFileExists(); try(final ObjectOutputStream stream = new ObjectOutputStream(persistedOutputFile.openWrite())) { stream.writeObject(parseTable); diff --git a/lwb/metalang/statix/statix/src/main/java/mb/statix/task/spoofax/StatixParseWrapper.java b/lwb/metalang/statix/statix/src/main/java/mb/statix/task/spoofax/StatixParseWrapper.java index 4d87eda89..963ac9b47 100644 --- a/lwb/metalang/statix/statix/src/main/java/mb/statix/task/spoofax/StatixParseWrapper.java +++ b/lwb/metalang/statix/statix/src/main/java/mb/statix/task/spoofax/StatixParseWrapper.java @@ -13,6 +13,7 @@ import mb.statix.task.StatixParse; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Provider; @StatixScope @@ -23,9 +24,10 @@ public class StatixParseWrapper extends StatixParse { @Inject public StatixParseWrapper( StatixClassLoaderResources classLoaderResources, Provider parserProvider, + @Named("completion") Provider completionParserProvider, StatixConfigFunctionWrapper configFunctionWrapper ) { - super(classLoaderResources, parserProvider); + super(classLoaderResources, parserProvider, completionParserProvider); this.classLoaderResources = classLoaderResources; this.configFunctionWrapper = configFunctionWrapper; } diff --git a/lwb/metalang/stratego/stratego/src/main/java/mb/str/task/spoofax/StrategoParseWrapper.java b/lwb/metalang/stratego/stratego/src/main/java/mb/str/task/spoofax/StrategoParseWrapper.java index f77f4840b..35c102076 100644 --- a/lwb/metalang/stratego/stratego/src/main/java/mb/str/task/spoofax/StrategoParseWrapper.java +++ b/lwb/metalang/stratego/stratego/src/main/java/mb/str/task/spoofax/StrategoParseWrapper.java @@ -13,6 +13,7 @@ import mb.str.task.StrategoParse; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Provider; @StrategoScope @@ -23,9 +24,10 @@ public class StrategoParseWrapper extends StrategoParse { @Inject public StrategoParseWrapper( StrategoClassLoaderResources classLoaderResources, Provider parserProvider, + @Named("completion") Provider completionParserProvider, StrategoAnalyzeConfigFunctionWrapper configFunctionWrapper ) { - super(classLoaderResources, parserProvider); + super(classLoaderResources, parserProvider, completionParserProvider); this.classLoaderResources = classLoaderResources; this.configFunctionWrapper = configFunctionWrapper; } diff --git a/lwb/spoofax.lwb.compiler.gradle/src/main/kotlin/mb/spoofax/lwb/compiler/gradle/LanguagePlugin.kt b/lwb/spoofax.lwb.compiler.gradle/src/main/kotlin/mb/spoofax/lwb/compiler/gradle/LanguagePlugin.kt index 3ce36dee4..33391f667 100644 --- a/lwb/spoofax.lwb.compiler.gradle/src/main/kotlin/mb/spoofax/lwb/compiler/gradle/LanguagePlugin.kt +++ b/lwb/spoofax.lwb.compiler.gradle/src/main/kotlin/mb/spoofax/lwb/compiler/gradle/LanguagePlugin.kt @@ -195,7 +195,7 @@ class LanguagePluginInstance( inputs.files(project.fileTree(dir) { include("**/*.sdf3") }) } } - .prebuilt { inputParseTableAtermFile, inputParseTablePersistedFile -> + .prebuilt { inputParseTableAtermFile, inputParseTablePersistedFile, inputCompletionParseTableAtermFile, inputCompletionParseTablePersistedFile -> // Input: prebuilt input files inputParseTableAtermFile.tryAsLocal("SDF3 prebuilt parse table ATerm file") { file -> inputs.file(file) @@ -203,6 +203,12 @@ class LanguagePluginInstance( inputParseTablePersistedFile.tryAsLocal("SDF3 prebuilt parse table persisted file") { file -> inputs.file(file) } + inputCompletionParseTableAtermFile.tryAsLocal("SDF3 prebuilt completion parse table ATerm file") { file -> + inputs.file(file) + } + inputCompletionParseTablePersistedFile.tryAsLocal("SDF3 prebuilt completion parse table persisted file") { file -> + inputs.file(file) + } } // Output: output files sdf3Config.parseTableAtermOutputFile().tryAsLocal("SDF3 parse table ATerm output file") { file -> @@ -211,6 +217,12 @@ class LanguagePluginInstance( sdf3Config.parseTablePersistedOutputFile().tryAsLocal("SDF3 parse table persisted output file") { file -> outputs.file(file) } + sdf3Config.completionParseTableAtermOutputFile().tryAsLocal("SDF3 completion parse table ATerm output file") { file -> + outputs.file(file) + } + sdf3Config.completionParseTablePersistedOutputFile().tryAsLocal("SDF3 completion parse table persisted output file") { file -> + outputs.file(file) + } } input.esv().ifPresent { esvConfig -> esvConfig.source().caseOf() diff --git a/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Check.java b/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Check.java index 2019cb113..788c68b3b 100644 --- a/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Check.java +++ b/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Check.java @@ -43,8 +43,23 @@ public KeyedMessages exec(ExecContext context, ResourcePath rootDirectory) { public KeyedMessages check(ExecContext context, SpoofaxSdf3Config config) { return config.caseOf() - .files((sdf3SpecConfig, outputParseTableAtermFile, outputParseTablePersistedFile) -> context.require(check, sdf3SpecConfig)) - .prebuilt((inputParseTableAtermFile, inputParseTablePersistedFile, outputParseTableAtermFile, outputParseTablePersistedFile) -> KeyedMessages.of()) + .files(( + sdf3SpecConfig, + outputParseTableAtermFile, + outputParseTablePersistedFile, + outputCompletionParseTableAtermFile, + outputCompletionParseTablePersistedFile + ) -> context.require(check, sdf3SpecConfig)) + .prebuilt(( + inputParseTableAtermFile, + inputParseTablePersistedFile, + inputCompletionParseTableAtermFile, + inputCompletionParseTablePersistedFile, + outputParseTableAtermFile, + outputParseTablePersistedFile, + outputCompletionParseTableAtermFile, + outputCompletionParseTablePersistedFile + ) -> KeyedMessages.of()) ; } } diff --git a/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Compile.java b/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Compile.java index c1c2d2136..d7ffae398 100644 --- a/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Compile.java +++ b/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Compile.java @@ -71,8 +71,40 @@ public Result exec(ExecContext conte public Result compile(ExecContext context, SpoofaxSdf3Config config) { return config.caseOf() - .files((sdf3SpecConfig, outputParseTableAtermFile, outputParseTablePersistedFile) -> compileFromSourceFiles(context, sdf3SpecConfig, outputParseTableAtermFile, outputParseTablePersistedFile)) - .prebuilt((inputParseTableAtermFile, inputParseTablePersistedFile, outputParseTableAtermFile, outputParseTablePersistedFile) -> copyPrebuilt(context, inputParseTableAtermFile, inputParseTablePersistedFile, outputParseTableAtermFile, outputParseTablePersistedFile)) + .files(( + sdf3SpecConfig, + outputParseTableAtermFile, + outputParseTablePersistedFile, + outputCompletionParseTableAtermFile, + outputCompletionParseTablePersistedFile + ) -> compileFromSourceFiles( + context, + sdf3SpecConfig, + outputParseTableAtermFile, + outputParseTablePersistedFile, + outputCompletionParseTableAtermFile, + outputCompletionParseTablePersistedFile + )) + .prebuilt(( + inputParseTableAtermFile, + inputParseTablePersistedFile, + inputCompletionParseTableAtermFile, + inputCompletionParseTablePersistedFile, + outputParseTableAtermFile, + outputParseTablePersistedFile, + outputCompletionParseTableAtermFile, + outputCompletionParseTablePersistedFile + ) -> copyPrebuilt( + context, + inputParseTableAtermFile, + inputParseTablePersistedFile, + inputCompletionParseTableAtermFile, + inputCompletionParseTablePersistedFile, + outputParseTableAtermFile, + outputParseTablePersistedFile, + outputCompletionParseTableAtermFile, + outputCompletionParseTablePersistedFile + )) ; } @@ -80,7 +112,9 @@ public Result compileFromSourceFiles ExecContext context, Sdf3SpecConfig config, ResourcePath outputParseTableAtermFile, - ResourcePath outputParseTablePersistedFile + ResourcePath outputParseTablePersistedFile, + ResourcePath outputCompletionParseTableAtermFile, + ResourcePath outputCompletionParseTablePersistedFile ) { final KeyedMessages messages = context.require(check, config); if(messages.containsError()) { @@ -88,10 +122,14 @@ public Result compileFromSourceFiles } final Supplier> parseTableSupplier = toParseTable.createSupplier(new Sdf3SpecToParseTable.Input(config, false)); + final Supplier> completionParseTableSupplier = toParseTable.createSupplier(new Sdf3SpecToParseTable.Input(config, true)); final Result compileResult = context.require(parseTableToFile, new Sdf3ParseTableToFile.Input( parseTableSupplier, + completionParseTableSupplier, outputParseTableAtermFile, - outputParseTablePersistedFile + outputParseTablePersistedFile, + outputCompletionParseTableAtermFile, + outputCompletionParseTablePersistedFile )); if(compileResult.isErr()) { return Result.ofErr(SpoofaxSdf3CompileException.parseTableCompileFail(compileResult.getErr())); @@ -104,12 +142,18 @@ public Result copyPrebuilt( ExecContext context, ResourcePath inputParseTableAtermFilePath, ResourcePath inputParseTablePersistedFilePath, + ResourcePath inputCompletionParseTableAtermFilePath, + ResourcePath inputCompletionParseTablePersistedFilePath, ResourcePath outputParseTableAtermFilePath, - ResourcePath outputParseTablePersistedFilePath + ResourcePath outputParseTablePersistedFilePath, + ResourcePath outputCompletionParseTableAtermFilePath, + ResourcePath outputCompletionParseTablePersistedFilePath ) { try { TaskCopyUtil.copy(context, inputParseTableAtermFilePath, outputParseTableAtermFilePath); TaskCopyUtil.copy(context, inputParseTablePersistedFilePath, outputParseTablePersistedFilePath); + TaskCopyUtil.copy(context, inputCompletionParseTableAtermFilePath, outputCompletionParseTableAtermFilePath); + TaskCopyUtil.copy(context, inputCompletionParseTablePersistedFilePath, outputCompletionParseTablePersistedFilePath); } catch(IOException e) { return Result.ofErr(SpoofaxSdf3CompileException.parseTableCompileFail(e)); } diff --git a/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Config.java b/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Config.java index f8a4cd8e6..2a7b34cad 100644 --- a/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Config.java +++ b/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Config.java @@ -14,17 +14,62 @@ @ADT public abstract class SpoofaxSdf3Config implements Serializable { public interface Cases { - R files(Sdf3SpecConfig sdf3SpecConfig, ResourcePath outputParseTableAtermFile, ResourcePath outputParseTablePersistedFile); - - R prebuilt(ResourcePath inputParseTableAtermFile, ResourcePath inputParseTablePersistedFile, ResourcePath outputParseTableAtermFile, ResourcePath outputParseTablePersistedFile); + R files( + Sdf3SpecConfig sdf3SpecConfig, + ResourcePath outputParseTableAtermFile, + ResourcePath outputParseTablePersistedFile, + ResourcePath outputCompletionParseTableAtermFile, + ResourcePath outputCompletionParseTablePersistedFile + ); + + R prebuilt( + ResourcePath inputParseTableAtermFile, + ResourcePath inputParseTablePersistedFile, + ResourcePath inputCompletionParseTableAtermFile, + ResourcePath inputCompletionParseTablePersistedFile, + ResourcePath outputParseTableAtermFile, + ResourcePath outputParseTablePersistedFile, + ResourcePath outputCompletionParseTableAtermFile, + ResourcePath outputCompletionParseTablePersistedFile + ); } - public static SpoofaxSdf3Config files(Sdf3SpecConfig sdf3SpecConfig, ResourcePath outputParseTableAtermFile, ResourcePath outputParseTablePersistedFile) { - return SpoofaxSdf3Configs.files(sdf3SpecConfig, outputParseTableAtermFile, outputParseTablePersistedFile); + public static SpoofaxSdf3Config files( + Sdf3SpecConfig sdf3SpecConfig, + ResourcePath outputParseTableAtermFile, + ResourcePath outputParseTablePersistedFile, + ResourcePath outputCompletionParseTableAtermFile, + ResourcePath outputCompletionParseTablePersistedFile + ) { + return SpoofaxSdf3Configs.files( + sdf3SpecConfig, + outputParseTableAtermFile, + outputParseTablePersistedFile, + outputCompletionParseTableAtermFile, + outputCompletionParseTablePersistedFile + ); } - public static SpoofaxSdf3Config prebuilt(ResourcePath inputParseTableAtermFile, ResourcePath inputParseTablePersistedFile, ResourcePath outputParseTableAtermFile, ResourcePath outputParseTablePersistedFile) { - return SpoofaxSdf3Configs.prebuilt(inputParseTableAtermFile, inputParseTablePersistedFile, outputParseTableAtermFile, outputParseTablePersistedFile); + public static SpoofaxSdf3Config prebuilt( + ResourcePath inputParseTableAtermFile, + ResourcePath inputParseTablePersistedFile, + ResourcePath inputCompletionParseTableAtermFile, + ResourcePath inputCompletionParseTablePersistedFile, + ResourcePath outputParseTableAtermFile, + ResourcePath outputParseTablePersistedFile, + ResourcePath outputCompletionParseTableAtermFile, + ResourcePath outputCompletionParseTablePersistedFile + ) { + return SpoofaxSdf3Configs.prebuilt( + inputParseTableAtermFile, + inputParseTablePersistedFile, + inputCompletionParseTableAtermFile, + inputCompletionParseTablePersistedFile, + outputParseTableAtermFile, + outputParseTablePersistedFile, + outputCompletionParseTableAtermFile, + outputCompletionParseTablePersistedFile + ); } diff --git a/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Configure.java b/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Configure.java index ce476f93a..0cedc42c4 100644 --- a/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Configure.java +++ b/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/sdf3/SpoofaxSdf3Configure.java @@ -58,7 +58,8 @@ public Result configure( try { return cfgSdf3Config.source().caseOf() .files(files -> configureSourceFilesCatching(context, rootDirectory, cfgSdf3Config, files)) - .prebuilt((inputParseTableAtermFile, inputParseTablePersistedFile) -> configurePrebuilt(inputParseTableAtermFile, inputParseTablePersistedFile, cfgSdf3Config)); + .prebuilt((inputParseTableAtermFile, inputParseTablePersistedFile, inputCompletionParseTableAtermFile, inputCompletionParseTablePersistedFile) -> configurePrebuilt( + inputParseTableAtermFile, inputParseTablePersistedFile, inputCompletionParseTableAtermFile, inputCompletionParseTablePersistedFile, cfgSdf3Config)); } catch(UncheckedIOException e) { throw e.getCause(); } @@ -100,19 +101,31 @@ public Result configureSourceF cfgSdf3Config.createLayoutSensitiveParseTable() ); final Sdf3SpecConfig sdf3SpecConfig = new Sdf3SpecConfig(rootDirectory, mainSourceDirectory.getPath(), mainFile.getPath(), parseTableConfiguration); - return Result.ofOk(SpoofaxSdf3Config.files(sdf3SpecConfig, cfgSdf3Config.parseTableAtermOutputFile(), cfgSdf3Config.parseTablePersistedOutputFile())); + return Result.ofOk(SpoofaxSdf3Config.files( + sdf3SpecConfig, + cfgSdf3Config.parseTableAtermOutputFile(), + cfgSdf3Config.parseTablePersistedOutputFile(), + cfgSdf3Config.completionParseTableAtermOutputFile(), + cfgSdf3Config.completionParseTablePersistedOutputFile() + )); } public Result configurePrebuilt( ResourcePath inputParseTableAtermFile, ResourcePath inputParseTablePersistedFile, + ResourcePath inputCompletionParseTableAtermFile, + ResourcePath inputCompletionParseTablePersistedFile, CfgSdf3Config cfgSdf3Config ) { return Result.ofOk(SpoofaxSdf3Config.prebuilt( inputParseTableAtermFile, inputParseTablePersistedFile, + inputCompletionParseTableAtermFile, + inputCompletionParseTablePersistedFile, cfgSdf3Config.parseTableAtermOutputFile(), - cfgSdf3Config.parseTablePersistedOutputFile() + cfgSdf3Config.parseTablePersistedOutputFile(), + cfgSdf3Config.completionParseTableAtermOutputFile(), + cfgSdf3Config.completionParseTablePersistedOutputFile() )); } From 5b04f789a06b170737fc5fbbd342afd9320b39cc Mon Sep 17 00:00:00 2001 From: "Daniel A. A. Pelsmaeker" <647530+Virtlink@users.noreply.github.com> Date: Fri, 7 Jan 2022 12:10:18 +0100 Subject: [PATCH 6/8] Attempt to implement placeholder inference --- .../adapter/AdapterProjectCompiler.java | 1 + .../CodeCompletionAdapterCompiler.java | 16 + .../AstWithPlaceholdersTaskDef.java.mustache | 27 ++ .../CodeCompletionTaskDef.java.mustache | 4 +- .../CodeCompletionCompilerTest.java | 1 + .../pie/AstWithPlaceholdersTaskDef.java | 308 ++++++++++++++++++ .../pie/CodeCompletionTaskDef.java | 46 ++- .../pie/CodeCompletionUtils.java | 287 +++++++++++++++- .../statix/codecompletion/pie/Fragment.java | 129 ++++++++ .../codecompletion/pie/NablTermFragment.java | 99 ++++++ .../pie/StrategoTermFragment.java | 92 ++++++ .../pie/TermTransformation.java | 113 +++++++ .../task/Sdf3ExtStatixGenerateStratego.java | 2 +- .../stratego/SpoofaxStrategoConfigure.java | 15 +- 14 files changed, 1122 insertions(+), 18 deletions(-) create mode 100644 core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/code_completion/AstWithPlaceholdersTaskDef.java.mustache create mode 100644 core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/AstWithPlaceholdersTaskDef.java create mode 100644 core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/Fragment.java create mode 100644 core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/NablTermFragment.java create mode 100644 core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/StrategoTermFragment.java create mode 100644 core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/TermTransformation.java diff --git a/core/spoofax.compiler/src/main/java/mb/spoofax/compiler/adapter/AdapterProjectCompiler.java b/core/spoofax.compiler/src/main/java/mb/spoofax/compiler/adapter/AdapterProjectCompiler.java index 337801ed8..e1812ac06 100644 --- a/core/spoofax.compiler/src/main/java/mb/spoofax/compiler/adapter/AdapterProjectCompiler.java +++ b/core/spoofax.compiler/src/main/java/mb/spoofax/compiler/adapter/AdapterProjectCompiler.java @@ -449,6 +449,7 @@ class Builder extends AdapterProjectCompilerData.Input.Builder {} }); codeCompletion().ifPresent((i) -> { taskDefs.add(i.codeCompletionTaskDef(), i.baseCodeCompletionTaskDef()); + taskDefs.add(i.astWithPlaceholdersTaskDef(), i.baseAstWithPlaceholdersTaskDef()); taskDefs.add(i.statixSpecTaskDef(), i.baseStatixSpecTaskDef()); }); referenceResolution().ifPresent((i) -> { diff --git a/core/spoofax.compiler/src/main/java/mb/spoofax/compiler/adapter/CodeCompletionAdapterCompiler.java b/core/spoofax.compiler/src/main/java/mb/spoofax/compiler/adapter/CodeCompletionAdapterCompiler.java index c0d1b8a66..d40ddefdd 100644 --- a/core/spoofax.compiler/src/main/java/mb/spoofax/compiler/adapter/CodeCompletionAdapterCompiler.java +++ b/core/spoofax.compiler/src/main/java/mb/spoofax/compiler/adapter/CodeCompletionAdapterCompiler.java @@ -24,6 +24,7 @@ @Value.Enclosing public class CodeCompletionAdapterCompiler { private final TemplateWriter codeCompletionTaskDefTemplate; + private final TemplateWriter astWithPlaceholdersTaskDefTemplate; private final TemplateWriter statixSpecTaskDefTemplate; /** @@ -34,6 +35,7 @@ public class CodeCompletionAdapterCompiler { @Inject public CodeCompletionAdapterCompiler(TemplateCompiler templateCompiler) { templateCompiler = templateCompiler.loadingFromClass(getClass()); this.codeCompletionTaskDefTemplate = templateCompiler.getOrCompileToWriter("code_completion/CodeCompletionTaskDef.java.mustache"); + this.astWithPlaceholdersTaskDefTemplate = templateCompiler.getOrCompileToWriter("code_completion/AstWithPlaceholdersTaskDef.java.mustache"); this.statixSpecTaskDefTemplate = templateCompiler.getOrCompileToWriter("code_completion/StatixSpecTaskDef.java.mustache"); } @@ -41,6 +43,7 @@ public None compile(ExecContext context, Input input) throws IOException { if(input.classKind().isManual()) return None.instance; // Nothing to generate: return. final ResourcePath generatedJavaSourcesDirectory = input.generatedJavaSourcesDirectory(); codeCompletionTaskDefTemplate.write(context, input.baseCodeCompletionTaskDef().file(generatedJavaSourcesDirectory), input); + astWithPlaceholdersTaskDefTemplate.write(context, input.baseAstWithPlaceholdersTaskDef().file(generatedJavaSourcesDirectory), input); statixSpecTaskDefTemplate.write(context, input.baseStatixSpecTaskDef().file(generatedJavaSourcesDirectory), input); return None.instance; } @@ -84,6 +87,18 @@ default TypeInfo codeCompletionTaskDef() { return extendCompleteTaskDef().orElseGet(this::baseCodeCompletionTaskDef); } + // AST-with-placeholders task definition + + @Value.Default default TypeInfo baseAstWithPlaceholdersTaskDef() { + return TypeInfo.of(adapterProject().taskPackageId(), shared().defaultClassPrefix() + "AstWithPlaceholdersTaskDef"); + } + + Optional extendAstWithPlaceholdersTaskDef(); + + default TypeInfo astWithPlaceholdersTaskDef() { + return extendAstWithPlaceholdersTaskDef().orElseGet(this::baseAstWithPlaceholdersTaskDef); + } + // Statix Spec task definition @Value.Default default TypeInfo baseStatixSpecTaskDef() { @@ -125,6 +140,7 @@ default ListView javaSourceFiles() { final ResourcePath generatedJavaSourcesDirectory = generatedJavaSourcesDirectory(); return ListView.of( baseCodeCompletionTaskDef().file(generatedJavaSourcesDirectory), + baseAstWithPlaceholdersTaskDef().file(generatedJavaSourcesDirectory), baseStatixSpecTaskDef().file(generatedJavaSourcesDirectory) ); } diff --git a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/code_completion/AstWithPlaceholdersTaskDef.java.mustache b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/code_completion/AstWithPlaceholdersTaskDef.java.mustache new file mode 100644 index 000000000..a36829936 --- /dev/null +++ b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/code_completion/AstWithPlaceholdersTaskDef.java.mustache @@ -0,0 +1,27 @@ +package {{baseAstWithPlaceholdersTaskDef.packageId}}; + +import mb.log.api.LoggerFactory; +import mb.statix.codecompletion.pie.AstWithPlaceholdersTaskDef; + +import javax.inject.Inject; + +@{{adapterProject.scope.qualifiedId}} +public class {{baseAstWithPlaceholdersTaskDef.id}} extends AstWithPlaceholdersTaskDef { + + @Inject + public {{baseAstWithPlaceholdersTaskDef.id}}( + {{strategoRuntimeInput.getStrategoRuntimeProviderTaskDef.qualifiedId}} getStrategoRuntimeProviderTask, + LoggerFactory loggerFactory + ) { + super( + getStrategoRuntimeProviderTask, + loggerFactory + ); + } + + @Override + public String getId() { + return "{{baseAstWithPlaceholdersTaskDef.id}}"; + } + +} diff --git a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/code_completion/CodeCompletionTaskDef.java.mustache b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/code_completion/CodeCompletionTaskDef.java.mustache index 8372f1cf8..86f251a53 100644 --- a/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/code_completion/CodeCompletionTaskDef.java.mustache +++ b/core/spoofax.compiler/src/main/resources/mb/spoofax/compiler/adapter/code_completion/CodeCompletionTaskDef.java.mustache @@ -6,7 +6,7 @@ import mb.log.api.LoggerFactory; import mb.nabl2.terms.stratego.StrategoTerms; import mb.pie.api.ExecContext; import mb.pie.api.stamp.resource.ResourceStampers; -import mb.statix.codecompletion.pie.CodeCompletionEventHandlerBase; +import mb.statix.codecompletion.pie.AstWithPlaceholdersTaskDef; import mb.statix.codecompletion.pie.CodeCompletionTaskDef; import mb.tego.strategies.runtime.TegoRuntime; import org.checkerframework.checker.nullness.qual.Nullable; @@ -22,6 +22,7 @@ public class {{baseCodeCompletionTaskDef.id}} extends CodeCompletionTaskDef { public {{baseCodeCompletionTaskDef.id}}( {{parserInput.parseTaskDef.qualifiedId}} parseTask, {{constraintAnalyzerInput.analyzeFileTaskDef.qualifiedId}} analyzeFileTask, + {{astWithPlaceholdersTaskDef.qualifiedId}} astWithPlaceholdersTaskDef, {{strategoRuntimeInput.getStrategoRuntimeProviderTaskDef.qualifiedId}} getStrategoRuntimeProviderTask, TegoRuntime tegoRuntime, {{statixSpecTaskDef.qualifiedId}} statixSpec, @@ -32,6 +33,7 @@ public class {{baseCodeCompletionTaskDef.id}} extends CodeCompletionTaskDef { super( parseTask, analyzeFileTask, + astWithPlaceholdersTaskDef, getStrategoRuntimeProviderTask, tegoRuntime, statixSpec, diff --git a/core/spoofax.compiler/src/test/java/mb/spoofax/compiler/spoofaxcore/CodeCompletionCompilerTest.java b/core/spoofax.compiler/src/test/java/mb/spoofax/compiler/spoofaxcore/CodeCompletionCompilerTest.java index b035372e3..2bb5a0386 100644 --- a/core/spoofax.compiler/src/test/java/mb/spoofax/compiler/spoofaxcore/CodeCompletionCompilerTest.java +++ b/core/spoofax.compiler/src/test/java/mb/spoofax/compiler/spoofaxcore/CodeCompletionCompilerTest.java @@ -14,6 +14,7 @@ class CodeCompletionCompilerTest extends TestBase { component.getCodeCompletionAdapterCompiler().compile(new MockExecContext(), adapterProjectInput); fileAssertions.scopedExists(adapterProjectInput.generatedJavaSourcesDirectory(), (s) -> { s.assertPublicJavaClass(adapterProjectInput.baseCodeCompletionTaskDef(), "TigerCodeCompletionTaskDef"); + s.assertPublicJavaClass(adapterProjectInput.baseAstWithPlaceholdersTaskDef(), "TigerAstWithPlaceholdersTaskDef"); }); } } diff --git a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/AstWithPlaceholdersTaskDef.java b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/AstWithPlaceholdersTaskDef.java new file mode 100644 index 000000000..c5203db8d --- /dev/null +++ b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/AstWithPlaceholdersTaskDef.java @@ -0,0 +1,308 @@ +package mb.statix.codecompletion.pie; + +import mb.common.region.Region; +import mb.common.result.Result; +import mb.log.api.Logger; +import mb.log.api.LoggerFactory; +import mb.pie.api.ExecContext; +import mb.pie.api.None; +import mb.pie.api.Supplier; +import mb.pie.api.TaskDef; +import mb.stratego.common.StrategoRuntime; +import mb.stratego.pie.GetStrategoRuntimeProvider; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spoofax.interpreter.terms.IStrategoAppl; +import org.spoofax.interpreter.terms.IStrategoPlaceholder; +import org.spoofax.interpreter.terms.IStrategoTerm; +import org.spoofax.interpreter.terms.ITermFactory; +import org.spoofax.interpreter.terms.TermType; +import org.spoofax.jsglr.client.imploder.ImploderAttachment; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Parses the input and returns an AST with placeholders around the caret position. + */ +public class AstWithPlaceholdersTaskDef implements TaskDef> { + + private final Logger log; + private final GetStrategoRuntimeProvider getStrategoRuntimeProviderTask; + + /** + * Initializes a new instance of the {@link AstWithPlaceholdersTaskDef} class. + * + * @param getStrategoRuntimeProviderTask the Stratego runtime provider task + * @param loggerFactory the logger factory + */ + public AstWithPlaceholdersTaskDef( + GetStrategoRuntimeProvider getStrategoRuntimeProviderTask, + LoggerFactory loggerFactory + ) { + this.log = loggerFactory.create(getClass()); +// this.parseTask = parseTask; + this.getStrategoRuntimeProviderTask = getStrategoRuntimeProviderTask; + } + + @Override public String getId() { + return this.getClass().getName(); + } + + @Override public Result exec(ExecContext context, Input input) throws Exception { + final StrategoRuntime strategoRuntime = context.require(getStrategoRuntimeProviderTask, None.instance).getValue().get(); + + final Result parsedAstResult = context.require(input.astSupplier); + if (parsedAstResult.isErr()) return parsedAstResult.ignoreValueIfErr(); + final IStrategoTerm parsedAst = parsedAstResult.unwrapUnchecked(); + + final IStrategoTerm newAst = new Execution(context, strategoRuntime).insertPlaceholdersNearSelection( + parsedAst, input.primarySelection + ); + // Transform any placeholder constructors into real placeholders. + // TODO ? + + // Insert placeholders around the caret position. + // TODO + + // Gather all the placeholders around the caret position + // TODO + + // FIXME: Return the newAst and the placeholders instead + return Result.ofOk(new Output(newAst, Collections.emptyList())); +// return Result.ofOk(new Output(parsedAst, Collections.emptyList())); + } + + + /** + * Contains the state necessary for executing this task. + */ + private final class Execution { + /** The execution context. */ + private final ExecContext context; + /** The Stratego runtime. */ + private final StrategoRuntime strategoRuntime; + /** The term factory. */ + private final ITermFactory termFactory; + + /** + * Initializes a new instance of the {@link Execution} class. + * + * @param context the execution context + * @param strategoRuntime the Stratego runtime + */ + public Execution( + ExecContext context, + StrategoRuntime strategoRuntime + ) { + this.context = context; + this.strategoRuntime = strategoRuntime; + this.termFactory = strategoRuntime.getTermFactory(); + } + + private IStrategoTerm insertPlaceholdersNearSelection(IStrategoTerm term, Region selection) { + // Recursively visit each term that contains the selection. + // Depending on the type of term, we insert placeholders + final TermTransformation transformation = new TermTransformation(termFactory) { + @Override protected boolean traverse(IStrategoTerm fragmentTerm) { + final @Nullable StrategoTermFragment fragment = StrategoTermFragment.fromTerm(fragmentTerm); + if (fragment == null) { + log.error("Could not determine the region of the fragment: {}", fragmentTerm); + return false; + } + fragment.setLeftRecursive(CodeCompletionUtils.isLeftRecursive(fragment, selection, strategoRuntime)); + fragment.setRightRecursive(CodeCompletionUtils.isRightRecursive(fragment, selection, strategoRuntime)); + return isInSelectionAdjacentRegion(fragment, selection); + } + + @Override protected IStrategoTerm transform(IStrategoTerm fragmentTerm) { + final @Nullable StrategoTermFragment fragment = StrategoTermFragment.fromTerm(fragmentTerm); + if (fragment == null) { + log.error("Could not determine the region of the fragment: {}", fragmentTerm); + return fragmentTerm; + } + if (fragment.isError()) { + // Replace the term with a placeholder + return createPlaceholderForFragment(fragment); + } else if (fragmentTerm.getType() == TermType.LIST) { + // Insert a placeholder near/in the selection in the list + // FIXME: This kinda works, but we don't know the Sort and that fails. + // Actually, an additional problem is that we don't have a placeholder to represent lists. +// final IStrategoTerm[] newSubterms = new IStrategoTerm[fragmentTerm.getSubtermCount() + 1]; +// // FIXME: We only insert at the end now, we should insert near the selection +// int insertAt = fragmentTerm.getSubtermCount(); +// for (int i = 0; i < insertAt; i++) { +// newSubterms[i] = fragmentTerm.getSubterm(i); +// } +// // FIXME: Sort is wrong. Also, no position imploder attachments. +// newSubterms[insertAt] = createPlaceholderForSort("X"); +// for (int i = insertAt + 1; i < newSubterms.length; i++) { +// newSubterms[i] = fragmentTerm.getSubterm(i - 1); +// } +// return withSubterms(fragmentTerm, newSubterms); + } + return fragmentTerm; + } + }; + return transformation.transformRecursive(term); + } + + /** + * Creates a placeholder for the specified fragment. + * + * @param sort the sort + * @return the placeholder constructor application term + */ + private IStrategoAppl createPlaceholderForSort(String sort) { + return termFactory.makeAppl(sort + "-Plhdr"); + } + + /** + * Creates a placeholder for the specified fragment. + * + * @param fragment the fragment + * @return the placeholder constructor application term + */ + private IStrategoAppl createPlaceholderForFragment(StrategoTermFragment fragment) { + final IStrategoAppl term = termFactory.makeAppl(fragment.getSort() + "-Plhdr"); + termFactory.copyAttachments(fragment.getTerm(), term); + return term; + } + + /** + * Determines whether the selection intersects with the region of the specified term fragment. + * + * @param fragment the fragment + * @param selection the primary selection + * @return {@code true} when the term intersects with the selection region; + * otherwise, {@code false} + */ + private boolean isInSelectionAdjacentRegion(Fragment fragment, Region selection) { + final @Nullable Region region = CodeCompletionUtils.getAdjacentRegionOf(fragment); + return region != null && region.intersectsWith(selection); + } + } + + + /** + * Input arguments for the {@link AstWithPlaceholdersTaskDef} task. + */ + public static final class Input implements Serializable { + /** Supplies the AST to transform. */ + public final Supplier> astSupplier; + /** The primary selection at which to complete. */ + public final Region primarySelection; + + /** + * Initializes a new instance of the {@link Input} class. + * + * @param primarySelection the primary selection at which to complete + * @param astSupplier supplies the AST to transform + */ + public Input( + Region primarySelection, + Supplier> astSupplier + ) { + this.primarySelection = primarySelection; + this.astSupplier = astSupplier; + } + + @Override public boolean equals(@Nullable Object o) { + if(this == o) return true; + if(o == null || getClass() != o.getClass()) return false; + return innerEquals((Input)o); + } + + /** + * Determines whether this object is equal to the specified object. + * + * Note: this method does not check whether the type of the argument is exactly the same. + * + * @param that the object to compare to + * @return {@code true} when this object is equal to the specified object; + * otherwise, {@code false} + */ + protected boolean innerEquals(Input that) { + // @formatter:off + return this.primarySelection.equals(that.primarySelection) + && this.astSupplier.equals(that.astSupplier); + // @formatter:on + } + + @Override public int hashCode() { + return Objects.hash( + this.primarySelection, + this.astSupplier + ); + } + + @Override public String toString() { + return "PlaceholderAstTaskDef$Input{" + + "primarySelection=" + primarySelection + ", " + + "astSupplier=" + astSupplier + + "}"; + } + } + + + /** + * Output values for the {@link AstWithPlaceholdersTaskDef} task. + */ + public static final class Output implements Serializable { + /** The AST, with placeholders inserted around the caret location. */ + public final IStrategoTerm ast; + /** The placeholders around the caret location. */ + public final List placeholders; + + /** + * Initializes a new instance of the {@link Output} class. + * + * @param ast the AST, with placeholders inserted around the caret location + * @param placeholders the placeholders around the caret location + */ + public Output( + IStrategoTerm ast, + List placeholders + ) { + this.ast = ast; + this.placeholders = placeholders; + } + + @Override public boolean equals(@Nullable Object o) { + if(this == o) return true; + if(o == null || getClass() != o.getClass()) return false; + return innerEquals((Output)o); + } + + /** + * Determines whether this object is equal to the specified object. + * + * Note: this method does not check whether the type of the argument is exactly the same. + * + * @param that the object to compare to + * @return {@code true} when this object is equal to the specified object; + * otherwise, {@code false} + */ + protected boolean innerEquals(Output that) { + // @formatter:off + return this.ast.equals(that.ast) + && this.placeholders.equals(that.placeholders); + // @formatter:on + } + + @Override public int hashCode() { + return Objects.hash( + ast, + placeholders + ); + } + + @Override public String toString() { + return "PlaceholderAstTaskDef$Output{" + + "ast=" + ast + ", " + + "placeholders=" + placeholders + + '}'; + } + } +} diff --git a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java index d63985961..5c4b097dd 100644 --- a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java +++ b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java @@ -66,6 +66,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import static mb.statix.codecompletion.pie.CodeCompletionUtils.findAllPlaceholdersIn; import static mb.statix.codecompletion.pie.CodeCompletionUtils.findPlaceholderAt; import static mb.statix.codecompletion.pie.CodeCompletionUtils.getRegion; import static mb.statix.codecompletion.pie.CodeCompletionUtils.iterableToListView; @@ -134,10 +135,12 @@ public Input(Region primarySelection, ResourceKey file, @Nullable ResourcePath r * otherwise, {@code false} */ protected boolean innerEquals(Input that) { + // @formatter:off return this.primarySelection.equals(that.primarySelection) && this.file.equals(that.file) && Objects.equals(this.rootDirectoryHint, that.rootDirectoryHint) && this.completeDeterministic == that.completeDeterministic; + // @formatter:on } @Override public int hashCode() { @@ -150,7 +153,7 @@ protected boolean innerEquals(Input that) { } @Override public String toString() { - return "CodeCompletionTaskDef.Input{" + + return "CodeCompletionTaskDef$Input{" + "primarySelection=" + primarySelection + ", " + "rootDirectoryHint=" + rootDirectoryHint + ", " + "file=" + file + ", " + @@ -162,6 +165,7 @@ protected boolean innerEquals(Input that) { private final Logger log; private final JsglrParseTaskDef parseTask; private final ConstraintAnalyzeFile analyzeFileTask; + private final AstWithPlaceholdersTaskDef astWithPlaceholdersTaskDef; private final GetStrategoRuntimeProvider getStrategoRuntimeProviderTask; private final TegoRuntime tegoRuntime; private final StatixSpecTaskDef statixSpec; @@ -183,6 +187,7 @@ protected boolean innerEquals(Input that) { * * @param parseTask the parser task * @param analyzeFileTask the analysis task + * @param astWithPlaceholdersTaskDef the task that inserts placeholders near the caret location in the AST * @param getStrategoRuntimeProviderTask the Stratego runtime provider task * @param statixSpec the Statix spec task * @param strategoTerms the Stratego to NaBL terms utility class @@ -202,6 +207,7 @@ protected boolean innerEquals(Input that) { public CodeCompletionTaskDef( JsglrParseTaskDef parseTask, ConstraintAnalyzeFile analyzeFileTask, + AstWithPlaceholdersTaskDef astWithPlaceholdersTaskDef, GetStrategoRuntimeProvider getStrategoRuntimeProviderTask, TegoRuntime tegoRuntime, StatixSpecTaskDef statixSpec, @@ -221,6 +227,7 @@ public CodeCompletionTaskDef( ) { this.parseTask = parseTask; this.analyzeFileTask = analyzeFileTask; + this.astWithPlaceholdersTaskDef = astWithPlaceholdersTaskDef; this.getStrategoRuntimeProviderTask = getStrategoRuntimeProviderTask; this.tegoRuntime = tegoRuntime; this.statixSpec = statixSpec; @@ -269,8 +276,11 @@ public void withEventHandlerProvider(Supplier<@Nullable CodeCompletionEventHandl * Keeps objects used by the code completion algorithm in a more accessible place. */ private final class Execution { + /** The execution context. */ private final ExecContext context; + /** The Stratego runtime. */ private final StrategoRuntime strategoRuntime; + /** The term factory. */ private final ITermFactory termFactory; /** The root directory of the project; or {@code null} when not specified. */ public final @Nullable ResourcePath rootDirectoryHint; @@ -336,7 +346,10 @@ public Execution( final Result upgradedAstResult = upgradePlaceholders(statixAst, placeholderVarMap); if (upgradedAstResult.isErr()) return upgradedAstResult.ignoreValueIfErr(); final ITerm upgradedAst = upgradedAstResult.unwrap(); - final ITermVar placeholder = getCompletionPlaceholder(upgradedAst, primarySelection); + // TODO: We can perform our own placeholder inference here, since in the upgraded AST placeholders + // for lists _are_ possible. The only remaining problem is that we don't have a way to represent + // placeholders for lists in code completion proposals. + final ITermVar placeholder = getCompletionPlaceholders(upgradedAst, primarySelection).get(0); // FIXME: Use all placeholders instead of just the first final SolverState initialState = createInitialSolverState(upgradedAst, statixSecName, statixRootPredicateName, placeholderVarMap); if (eventHandler != null) eventHandler.endPreparation(); @@ -365,9 +378,12 @@ public Execution( log.info("Completion returned no completion proposals."); } else { log.trace("Completion returned the following proposals:\n - " + finalProposals.stream() - .map(i -> i.getLabel()).collect(Collectors.joining("\n - "))); + .map(CodeCompletionItem::getLabel).collect(Collectors.joining("\n - "))); } + // TODO: We should track to which placeholder each completion belongs, + // and insert the completion at that placeholder with properly parenthesized. + if (eventHandler != null) eventHandler.end(); return Result.ofOk(new TermCodeCompletionResult( placeholder, @@ -384,13 +400,15 @@ public Execution( * @return the AST of the file */ private Result parse(Region selection) { - return context.require(parseTask.inputBuilder() + final mb.pie.api.Supplier> astSupplier = parseTask.inputBuilder() .withFile(file) .rootDirectoryHint(Optional.ofNullable(rootDirectoryHint)) .codeCompletionMode(true) .cursorOffset(selection.getStartOffset() /* TODO: Support the whole selection? */) - .buildRecoverableAstSupplier() - ); + .buildRecoverableAstSupplier(); + + return context.require(astWithPlaceholdersTaskDef, new AstWithPlaceholdersTaskDef.Input(selection, astSupplier)) + .map(o -> o.ast); } /** @@ -507,6 +525,7 @@ private ITerm toStatix(IStrategoTerm ast) { * @param selection code selection * @return the term variable of the placeholder being completed */ + @Deprecated private ITermVar getCompletionPlaceholder(ITerm ast, Region selection) { @Nullable ITermVar placeholderVar = findPlaceholderAt(ast, selection.getStartOffset() /* TODO: Support the whole selection? */); if (placeholderVar == null) { @@ -515,6 +534,21 @@ private ITermVar getCompletionPlaceholder(ITerm ast, Region selection) { return placeholderVar; } + /** + * Determines all placeholders near or intersecting the selection. + * + * @param ast the AST to inspect + * @param selection the selection to complete at + * @return the list of term variables of the placeholders being completed + */ + private List getCompletionPlaceholders(ITerm ast, Region selection) { + List placeholderVars = findAllPlaceholdersIn(ast, selection); + if (placeholderVars.isEmpty()) { + throw new IllegalStateException("Completion failed: we don't know the placeholder."); + } + return placeholderVars; + } + /** * Creates the initial solver state for the code completion algorithm. * diff --git a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionUtils.java b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionUtils.java index f9d357303..9b3285c79 100644 --- a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionUtils.java +++ b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionUtils.java @@ -8,8 +8,13 @@ import mb.nabl2.terms.Terms; import mb.nabl2.terms.stratego.TermOrigin; import mb.nabl2.terms.stratego.TermPlaceholder; +import mb.stratego.common.StrategoException; +import mb.stratego.common.StrategoRuntime; import org.checkerframework.checker.nullness.qual.Nullable; +import org.spoofax.interpreter.terms.IStrategoTerm; +import org.spoofax.jsglr.client.imploder.IToken; import org.spoofax.jsglr.client.imploder.ImploderAttachment; +import org.spoofax.terms.util.TermUtils; import java.util.ArrayList; import java.util.Collection; @@ -17,6 +22,7 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; /** * Utility methods used by the code completion algorithm. @@ -78,6 +84,40 @@ public static String makeQualifiedName(String specName, String ruleName) { )); } + /** + * Finds all placeholders near the selection in the specified term. + * + * This method assumes all terms in the term are uniquely identifiable, + * for example through a term index or unique tree path. + * + * @param term the term (an AST with placeholders) + * @param selection the selection + * @return the found placeholders; or an empty list if none where found + */ + public static List findAllPlaceholdersIn(ITerm term, Region selection) { + if (!termContainsSelection(term, selection)) return Collections.emptyList(); + + // Recurse into the term + return term.match(Terms.cases( + (appl) -> appl.getArgs().stream().flatMap(a -> findAllPlaceholdersIn(a, selection).stream()) + .collect(Collectors.toList()), + (list) -> list.match(ListTerms.cases( + (cons) -> { + final ArrayList foundPlaceholders = new ArrayList<>(); + foundPlaceholders.addAll(findAllPlaceholdersIn(cons.getHead(), selection)); + foundPlaceholders.addAll(findAllPlaceholdersIn(cons.getTail(), selection)); + return foundPlaceholders; + }, + (nil) -> null, + (var) -> null + )), + (string) -> null, + (integer) -> null, + (blob) -> null, + (var) -> isPlaceholder(var) ? Collections.singletonList(var) : null + )); + } + /** * Determines whether the specified term contains the specified caret offset. * @@ -110,6 +150,38 @@ public static boolean termContainsCaret(ITerm term, int caretOffset) { return region.contains(caretOffset); } + /** + * Determines whether the specified term intersects with the specified selection. + * + * @param term the term + * @param selection the selection to find + * @return {@code true} when the term intersects with the selection; + * otherwise, {@code false}. + */ + public static boolean termContainsSelection(ITerm term, Region selection) { + @Nullable Region region = getAdjacentRegionOf(term); + if (region == null) { + // One of the children must contain the caret + return term.match(Terms.cases( + (appl) -> appl.getArgs().stream().anyMatch(a -> termContainsSelection(a, selection)), + (list) -> list.match(ListTerms.cases( + (cons) -> { + final boolean headContains = termContainsSelection(cons.getHead(), selection); + if (headContains) return true; + return termContainsSelection(cons.getTail(), selection); + }, + (nil) -> false, + (var) -> false + )), + (string) -> false, + (integer) -> false, + (blob) -> false, + (var) -> false + )); + } + return region.intersectsWith(selection); + } + /** * Attempts to get the region occupied by the specified term. * @@ -124,8 +196,8 @@ public static boolean termContainsCaret(ITerm term, int caretOffset) { int startOffset = imploderAttachment.getLeftToken().getStartOffset(); // We get the zero-based offset of the character following the token, which is why we have to add 1 int endOffset = imploderAttachment.getRightToken().getEndOffset() + 1; - // If the token is empty or malformed, we skip it. (An empty token cannot contain a caret anyway.) - if (endOffset <= startOffset) return null; + // If the token is malformed, we skip it. Empty regions are allowed, as they are used for the inserted placeholders. + if (endOffset < startOffset) return null; return Region.fromOffsets( startOffset, @@ -203,4 +275,215 @@ public static ListView iterableToListView(Iterable iterable) { return new ListView(iterableToList(iterable)); } } + + + /** + * Determines the adjacent region of the specified term fragment, including surrounding layout if necessary. + * + * The adjacent region of a term is the region of a term optionally expanded to include + * layout/error tokens to its left and/or right. + * + * @param fragmentTerm the fragment term + * @return the region of the term; or {@code null} when the region could not be determined + */ + public static @Nullable Region getAdjacentRegionOf(IStrategoTerm fragmentTerm) { + final @Nullable Fragment fragment = StrategoTermFragment.fromTerm(fragmentTerm); + if(fragment == null) return null; + return getAdjacentRegionOf(fragment); + } + + /** + * Determines the adjacent region of the specified term fragment, including surrounding layout if necessary. + * + * The adjacent region of a term is the region of a term optionally expanded to include + * layout/error tokens to its left and/or right. + * + * @param fragmentTerm the fragment term + * @return the region of the term; or {@code null} when the region could not be determined + */ + public static @Nullable Region getAdjacentRegionOf(ITerm fragmentTerm) { + final @Nullable Fragment fragment = NablTermFragment.fromTerm(fragmentTerm); + if(fragment == null) return null; + return getAdjacentRegionOf(fragment); + } + + /** + * Determines the adjacent region of the specified term fragment, including surrounding layout if necessary. + * + * The adjacent region of a term is the region of a term optionally expanded to include + * layout/error tokens to its left and/or right. + * + * @param fragment the fragment + * @return the region of the term; or {@code null} when the region could not be determined + * @see org.metaborg.spoofax.core.completion.JSGLRCompletionService#fromTokens + */ + public static @Nullable Region getAdjacentRegionOf(Fragment fragment) { + // FIXME: Not sure why the JSGLRCompletionService checks whether the term's sort + // is left- or right-recursive. Especially since this causes the layout on the other side to be included? + boolean isLeftRecursive = false;//fragment.isLeftRecursive(); + boolean isRightRecursive = false;//fragment.isRightRecursive(); + + // We will change these tokens to expand or contract the selection + IToken leftToken = fragment.getLeftToken(); + IToken rightToken = fragment.getRightToken(); + + final boolean includeLeftLayout = isRightRecursive || fragment.isList() || fragment.isOptional(); + if (includeLeftLayout) { + leftToken = includeLeftLayout(leftToken); + } else { + leftToken = excludeLeftLayout(leftToken, rightToken); + } + + final boolean includeRightLayout = isLeftRecursive || fragment.isList() || fragment.isOptional(); + if (includeRightLayout) { + rightToken = includeRightLayout(rightToken); + } else { + rightToken = excludeRightLayout(leftToken, rightToken); + } + + // FIXME: Do we need to fix the difference between the offset and the cursor position? + // Taken from JSGLRSourceRegionFactory.fromTokensLayout() + final int leftOffset = leftToken.getStartOffset(); // FIXME: + (leftToken.getKind() != IToken.Kind.TK_LAYOUT && !fragment.isNullable() ? 1 : 0); + final int rightOffset = rightToken.getEndOffset(); // FIXME: + (fragment.isNullable() ? 1 : 0); + + return Region.fromOffsets( + leftOffset, rightOffset + 1, + leftToken.getLine(), rightToken.getLine() + ); + } + + /** + * Includes the layout/error tokens to the left of the selected tokens. + * + * @param leftToken the left-most selected token + * @return the new left-most selected token, that includes layout (or errors) + */ + private static IToken includeLeftLayout(IToken leftToken) { + @Nullable IToken currentToken = leftToken.getTokenBefore(); + IToken newLeftToken = leftToken; + while (currentToken != null) { + if (!isLayout(currentToken)) break; + newLeftToken = currentToken; + currentToken = currentToken.getTokenBefore(); + } + return newLeftToken; + } + + /** + * Includes the layout/error tokens to the right of the selected tokens. + * + * @param rightToken the right-most selected token + * @return the new right-most selected token, that includes layout (or errors or EOF) + */ + private static IToken includeRightLayout(IToken rightToken) { + @Nullable IToken currentToken = rightToken.getTokenAfter(); + IToken newRightToken = rightToken; + while (currentToken != null) { + if (!isLayout(currentToken)) break; + newRightToken = currentToken; + currentToken = currentToken.getTokenAfter(); + } + return newRightToken; + } + + /** + * Excludes the layout/error tokens to the left of the selected tokens. + * + * @param leftToken the left-most selected token + * @param rightToken the right-most selected token + * @return the new left-most selected token, that does not include layout (or errors or EOF) + */ + private static IToken excludeLeftLayout(IToken leftToken, IToken rightToken) { + @Nullable IToken currentToken = leftToken; + IToken newLeftToken = leftToken; + while (currentToken != null && currentToken != rightToken) { + if (!isLayout(currentToken)) break; + newLeftToken = currentToken; + currentToken = currentToken.getTokenAfter(); + } + return newLeftToken; + } + + /** + * Excludes the layout/error tokens to the right of the selected tokens. + * + * @param leftToken the left-most selected token + * @param rightToken the right-most selected token + * @return the new right-most selected token, that does not include layout (or errors) + */ + private static IToken excludeRightLayout(IToken leftToken, IToken rightToken) { + @Nullable IToken currentToken = rightToken; + IToken newRightToken = rightToken; + while (currentToken != null && currentToken != leftToken) { + if (!isLayout(currentToken)) break; + newRightToken = currentToken; + currentToken = currentToken.getTokenBefore(); + } + return newRightToken; + } + + /** + * Gets whether the token is a layout/error token. + * + * @return {@code true} when the token denotes layout or an error; + * otherwise, {@code false} + */ + private static boolean isLayout(IToken token) { + switch (token.getKind()) { + case TK_LAYOUT: + case TK_EOF: + case TK_ERROR: + case TK_ERROR_LAYOUT: + return true; + default: + return false; + } + } + + /** + * Determines if the selection is wholly contained within the fragment, + * and whether the sort of the fragment is left-recursive. + * + * @param fragment the fragment whose sort to check + * @param selection the selection + * @return {@code true} when the fragment's sort is left-recursive; + * otherwise, {@code false}; or {@code false} when it could not be determined + */ + public static boolean isLeftRecursive(StrategoTermFragment fragment, Region selection, StrategoRuntime strategoRuntime) { + if(TermUtils.isAppl(fragment.getTerm()) && selection.getEndOffset() >= fragment.getRightOffset()) { + try { + @Nullable final IStrategoTerm output = strategoRuntime.invokeOrNull("is-left-recursive", strategoRuntime.getTermFactory().makeString(fragment.getSort())); + return output != null; + } catch(StrategoException ex) { + // Failed to check whether term is left-recursive. + return false; + } + } + return false; + } + + /** + * Determines if the selection is wholly contained within the fragment, + * and whether the sort of the fragment is right-recursive. + * + * @param fragment the fragment whose sort to check + * @param selection the selection + * @return {@code true} when the fragment's sort is right-recursive; + * otherwise, {@code false}; or {@code false} when it could not be determined + */ + public static boolean isRightRecursive(StrategoTermFragment fragment, Region selection, StrategoRuntime strategoRuntime) { + // FIXME: Not sure why the JSGLRCompletionService checks whether the term + // is near the selection and right-recursive. + // Especially since this causes the layout on the _left_ side to be included. + if(TermUtils.isAppl(fragment.getTerm()) && selection.getStartOffset() <= fragment.getLeftOffset()) { + try { + @Nullable final IStrategoTerm output = strategoRuntime.invokeOrNull("is-right-recursive", strategoRuntime.getTermFactory().makeString(fragment.getSort())); + return output != null; + } catch(StrategoException ex) { + // Failed to check whether term is right-recursive. + return false; + } + } + return false; + } } diff --git a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/Fragment.java b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/Fragment.java new file mode 100644 index 000000000..bc0531eff --- /dev/null +++ b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/Fragment.java @@ -0,0 +1,129 @@ +package mb.statix.codecompletion.pie; + + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spoofax.interpreter.terms.IStrategoTerm; +import org.spoofax.jsglr.client.imploder.IToken; +import org.spoofax.jsglr.client.imploder.ITokenizer; +import org.spoofax.terms.util.TermUtils; + +/** + * A fragment. + */ +public abstract class Fragment { + + /** The sort of the fragment. */ + private final String sort; + /** The left-most token of the fragment. */ + private final IToken leftToken; + /** The right-most token of the fragment. */ + private final IToken rightToken; + /** The tokenizer of the fragment. */ + private final ITokenizer tokenizer; + /** Whether the fragment's sort is left-recursive. */ + private @Nullable Boolean isLeftRecursive = null; + /** Whether the fragment's sort is right-recursive. */ + private @Nullable Boolean isRightRecursive = null; + + /** + * Initializes a new instance of the {@link Fragment} class. + * + * @param sort the sort of the fragment + * @param leftToken the left-most token of the fragment + * @param rightToken the right-most token of the fragment + * @param tokenizer the tokenizer of the fragment + */ + protected Fragment( + String sort, + IToken leftToken, + IToken rightToken, + ITokenizer tokenizer + ) { + this.sort = sort; + this.leftToken = leftToken; + this.rightToken = rightToken; + this.tokenizer = tokenizer; + } + + /** The sort of the fragment. */ + public String getSort() { + return this.sort; + } + + /** The left-most token of the fragment. */ + public IToken getLeftToken() { + return this.leftToken; + } + + /** The right-most token of the fragment. */ + public IToken getRightToken() { + return this.rightToken; + } + + /** The tokenizer of the fragment. */ + public ITokenizer getTokenizer() { + return this.tokenizer; + } + + /** The left offset of the left-most token. */ + public int getLeftOffset() { + return leftToken.getStartOffset(); + } + + /** The right offset of the right-most token. */ + public int getRightOffset() { + return rightToken.getEndOffset(); + } + + /** Whether the fragment contains no actual source characters. */ + public boolean isEmpty() { + return getRightOffset() < getLeftOffset(); + } + + /** Whether the fragment is a list term. */ + public abstract boolean isList(); + + /** Whether the fragment is an optional term. */ + public boolean isOptional() { + return !isList() && isEmpty() && getLeftToken() == getRightToken(); + } + + /** Whether the fragment is nullable. */ + public boolean isNullable() { + return isList() || isOptional() || isLeftRecursive() || isRightRecursive(); + } + + /** Whether the fragment indicates an error. */ + public boolean isError() { return leftToken.getKind() == IToken.Kind.TK_ERROR && rightToken.getKind() == IToken.Kind.TK_ERROR; } + + /** Gets whether the fragment's sort is left-recursive. */ + public boolean isLeftRecursive() { + assert(isLeftRecursive != null); + return isLeftRecursive; + } + + /** Gets whether the fragment's sort is right-recursive. */ + public boolean isRightRecursive() { + assert(isRightRecursive != null); + return isRightRecursive; + } + + /** + * Sets whether the fragment's sort is left-recursive. + * + * @param value the new value + */ + public void setLeftRecursive(boolean value) { + isLeftRecursive = value; + } + + /** + * Sets whether the fragment's sort is right-recursive. + * + * @param value the new value + */ + public void setRightRecursive(boolean value) { + isRightRecursive = value; + } + +} diff --git a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/NablTermFragment.java b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/NablTermFragment.java new file mode 100644 index 000000000..da34994fe --- /dev/null +++ b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/NablTermFragment.java @@ -0,0 +1,99 @@ +package mb.statix.codecompletion.pie; + + +import mb.nabl2.terms.IListTerm; +import mb.nabl2.terms.ITerm; +import mb.nabl2.terms.stratego.TermOrigin; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spoofax.jsglr.client.imploder.IToken; +import org.spoofax.jsglr.client.imploder.ITokenizer; +import org.spoofax.jsglr.client.imploder.ImploderAttachment; + +/** + * A NaBL term fragment. + */ +public final class NablTermFragment extends Fragment { + + /** + * Creates a {@link NablTermFragment} from a Stratego term. + * + * @param term the Stratego term + * @return the {@link NablTermFragment} instance + */ + public static @Nullable NablTermFragment fromTerm(ITerm term) { + @Nullable final TermOrigin origin = TermOrigin.get(term).orElse(null); + if (origin == null) return null; + + final ImploderAttachment imploderAttachment = origin.getImploderAttachment(); + if (imploderAttachment == null) return null; + + final @Nullable String sort = imploderAttachment.getSort(); + if (sort == null) return null; + + final @Nullable IToken leftToken = imploderAttachment.getLeftToken(); + if (leftToken == null) return null; + + final @Nullable IToken rightToken = imploderAttachment.getRightToken(); + if (rightToken == null) return null; + + final @Nullable ITokenizer tokenizer = (ITokenizer)leftToken.getTokenizer(); + if (tokenizer == null) return null; + + return new NablTermFragment( + term, + sort, + leftToken, + rightToken, + tokenizer + ); + } + + /** The term of the fragment. */ + public final ITerm term; + + /** + * Initializes a new instance of the {@link NablTermFragment} class. + * + * @param term the term of the fragment + * @param sort the sort of the fragment + * @param leftToken the left-most token of the fragment + * @param rightToken the right-most token of the fragment + * @param tokenizer the tokenizer of the fragment + */ + private NablTermFragment( + ITerm term, + String sort, + IToken leftToken, + IToken rightToken, + ITokenizer tokenizer + ) { + super(sort, leftToken, rightToken, tokenizer); + this.term = term; + } + + /** The term of the fragment. */ + public ITerm getTerm() { + return this.term; + } + + /** Whether the fragment is a list term. */ + @Override public boolean isList() { + return term instanceof IListTerm; + } + + @Override public boolean equals(@Nullable Object o) { + if(this == o) return true; + if(o == null || getClass() != o.getClass()) return false; + final NablTermFragment that = (NablTermFragment)o; + return this.term.equals(that.term); + } + + @Override + public int hashCode() { + return term.hashCode(); + } + + @Override public String toString() { + return term.toString(); + } +} diff --git a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/StrategoTermFragment.java b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/StrategoTermFragment.java new file mode 100644 index 000000000..be2fe9c35 --- /dev/null +++ b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/StrategoTermFragment.java @@ -0,0 +1,92 @@ +package mb.statix.codecompletion.pie; + + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spoofax.interpreter.terms.IStrategoTerm; +import org.spoofax.jsglr.client.imploder.IToken; +import org.spoofax.jsglr.client.imploder.ITokenizer; +import org.spoofax.jsglr.client.imploder.ImploderAttachment; +import org.spoofax.terms.util.TermUtils; + +/** + * A Stratego term fragment. + */ +public final class StrategoTermFragment extends Fragment { + + /** + * Creates a {@link StrategoTermFragment} from a Stratego term. + * + * @param term the Stratego term + * @return the {@link StrategoTermFragment} instance + */ + public static @Nullable StrategoTermFragment fromTerm(IStrategoTerm term) { + final @Nullable String sort = ImploderAttachment.getSort(term); + if (sort == null) return null; + + final @Nullable IToken leftToken = ImploderAttachment.getLeftToken(term); + if (leftToken == null) return null; + + final @Nullable IToken rightToken = ImploderAttachment.getRightToken(term); + if (rightToken == null) return null; + + final @Nullable ITokenizer tokenizer = (ITokenizer)ImploderAttachment.getTokenizer(term); + if (tokenizer == null) return null; + + return new StrategoTermFragment( + term, + sort, + leftToken, + rightToken, + tokenizer + ); + } + + /** The term of the fragment. */ + public final IStrategoTerm term; + + /** + * Initializes a new instance of the {@link StrategoTermFragment} class. + * + * @param term the term of the fragment + * @param sort the sort of the fragment + * @param leftToken the left-most token of the fragment + * @param rightToken the right-most token of the fragment + * @param tokenizer the tokenizer of the fragment + */ + private StrategoTermFragment( + IStrategoTerm term, + String sort, + IToken leftToken, + IToken rightToken, + ITokenizer tokenizer + ) { + super(sort, leftToken, rightToken, tokenizer); + this.term = term; + } + + /** The term of the fragment. */ + public IStrategoTerm getTerm() { + return this.term; + } + + /** Whether the fragment is a list term. */ + @Override public boolean isList() { + return TermUtils.isList(term); + } + + @Override public boolean equals(@Nullable Object o) { + if(this == o) return true; + if(o == null || getClass() != o.getClass()) return false; + final StrategoTermFragment that = (StrategoTermFragment)o; + return this.term.equals(that.term); + } + + @Override + public int hashCode() { + return term.hashCode(); + } + + @Override public String toString() { + return term.toString(); + } +} diff --git a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/TermTransformation.java b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/TermTransformation.java new file mode 100644 index 000000000..5c6aa6fed --- /dev/null +++ b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/TermTransformation.java @@ -0,0 +1,113 @@ +package mb.statix.codecompletion.pie; + +import org.spoofax.interpreter.terms.IStrategoAppl; +import org.spoofax.interpreter.terms.IStrategoList; +import org.spoofax.interpreter.terms.IStrategoPlaceholder; +import org.spoofax.interpreter.terms.IStrategoTerm; +import org.spoofax.interpreter.terms.IStrategoTuple; +import org.spoofax.interpreter.terms.ITermFactory; + +/** + * A term transformation. + */ +public abstract class TermTransformation { + private final ITermFactory factory; + + /** + * Initializes a new instance of the {@link TermTransformation} class. + * + * @param factory the term factory + */ + protected TermTransformation(ITermFactory factory) { + this.factory = factory; + } + + /** + * Performs a recursive bottom-up transformation of the specified term. + * + * The {@link #traverse} function determines whether the term is transformed and its subterms are traversed. + * The {@link #transform} function transforms the term with the transformed subterms. + * + * @param term the term to transform + * @return the recursively transformed term + */ + public IStrategoTerm transformRecursive(IStrategoTerm term) { + // TODO: Make this iterative instead of recursive. + final boolean traverse = traverse(term); + if (!traverse) return term; + + boolean changed = false; + final IStrategoTerm[] newSubterms = new IStrategoTerm[term.getSubtermCount()]; + for(int i = 0; i < newSubterms.length; i++) { + final IStrategoTerm oldSubterm = term.getSubterm(i); + final IStrategoTerm newSubterm = transformRecursive(oldSubterm); + changed |= newSubterm != oldSubterm; // Reference equality. + newSubterms[i] = newSubterm; + } + final IStrategoTerm newTerm = changed ? withSubterms(term, newSubterms) : term; + return transform(newTerm); + } + + /** + * Returns whether the transformation should traverse the subterms of the given fragment. + * + * @param fragmentTerm the term to check + * @return {@code true} to traverse the subterms of the given fragment; + * otherwise, {@code false} + */ + protected abstract boolean traverse(IStrategoTerm fragmentTerm); + + /** + * Transforms a term. + * + * The subterms of this term have already been transformed. + * This method is only called on terms for which {@link #traverse} returned {@code true}. + * + * @param fragmentTerm the term to transform + * @return the transformed term; or the original term if nothing was transformed + */ + protected abstract IStrategoTerm transform(IStrategoTerm fragmentTerm); + + /** Gets the term factory. */ + protected ITermFactory getTermFactory() { + return this.factory; + } + + /** + * Creates a copy of the specified term, with its subterms replaced with the given array of subterms. + * + * @param term the term to copy + * @param subterms the new subterms of the term + * @return the modified copy of the term + */ + protected IStrategoTerm withSubterms(IStrategoTerm term, IStrategoTerm[] subterms) { + switch(term.getType()) { + case APPL: + final IStrategoAppl appl = (IStrategoAppl)term; + if (subterms.length != appl.getSubtermCount()) + throw new IllegalArgumentException("Expected " + appl.getSubtermCount() + " subterms, got " + subterms.length + "."); + return factory.makeAppl(appl.getConstructor(), subterms, appl.getAnnotations()); + case TUPLE: + final IStrategoTuple tuple = (IStrategoTuple)term; + if (subterms.length != tuple.getSubtermCount()) + throw new IllegalArgumentException("Expected " + tuple.getSubtermCount() + " subterms, got " + subterms.length + "."); + return factory.makeTuple(subterms, tuple.getAnnotations()); + case LIST: + final IStrategoList list = (IStrategoList)term; + return factory.makeList(subterms, list.getAnnotations()); + case PLACEHOLDER: + final IStrategoPlaceholder placeholder = (IStrategoPlaceholder)term; + return factory.annotateTerm(factory.makePlaceholder(subterms[0]), placeholder.getAnnotations()); + case INT: + case REAL: + case STRING: + case CTOR: + case BLOB: + if (subterms.length > 0) + throw new IllegalArgumentException("Expected 0 subterms, got " + subterms.length + "."); + return term; + default: + throw new IllegalArgumentException("Unsupported type of term: " + term.getType()); + } + } +} diff --git a/lwb/metalang/sdf3_ext_statix/sdf3_ext_statix/src/main/java/mb/sdf3_ext_statix/task/Sdf3ExtStatixGenerateStratego.java b/lwb/metalang/sdf3_ext_statix/sdf3_ext_statix/src/main/java/mb/sdf3_ext_statix/task/Sdf3ExtStatixGenerateStratego.java index 0a53444de..b1e57bcda 100644 --- a/lwb/metalang/sdf3_ext_statix/sdf3_ext_statix/src/main/java/mb/sdf3_ext_statix/task/Sdf3ExtStatixGenerateStratego.java +++ b/lwb/metalang/sdf3_ext_statix/sdf3_ext_statix/src/main/java/mb/sdf3_ext_statix/task/Sdf3ExtStatixGenerateStratego.java @@ -41,7 +41,7 @@ public Input(Supplier> astSupplier, String st } @Override public String toString() { - return "Sdf3ExtStatixGenerateStratego$Input{" + + return "Sdf3ExtStatixGenerateStratego$InpGut{" + "astSupplier=" + astSupplier + ", strategoQualifier='" + strategyAffix + '\'' + '}'; diff --git a/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/stratego/SpoofaxStrategoConfigure.java b/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/stratego/SpoofaxStrategoConfigure.java index abce82f75..c00200a80 100644 --- a/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/stratego/SpoofaxStrategoConfigure.java +++ b/lwb/spoofax.lwb.compiler/src/main/java/mb/spoofax/lwb/compiler/stratego/SpoofaxStrategoConfigure.java @@ -241,14 +241,13 @@ public void generateFromAst(ExecContext context, STask> } catch(Exception e) { throw SpoofaxStrategoConfigureException.sdf3PrettyPrinterGenerateFail(e); } - // HACK: for now disabled completion runtime generation, as it is not used in Spoofax 3 (yet?) -// try { -// sdf3ToCompletionRuntime(context, generatedSourcesDirectory, astSupplier); -// } catch(RuntimeException | InterruptedException e) { -// throw e; // Do not wrap runtime and interrupted exceptions, rethrow them. -// } catch(Exception e) { -// throw StrategoConfigureException.sdf3CompletionRuntimeGenerateFail(e); -// } + try { + sdf3ToCompletionRuntime(context, generatedSourcesDirectory, astSupplier); + } catch(RuntimeException | InterruptedException e) { + throw e; // Do not wrap runtime and interrupted exceptions, rethrow them. + } catch(Exception e) { + throw SpoofaxStrategoConfigureException.sdf3CompletionRuntimeGenerateFail(e); + } if(cfgStrategoConfig.enableSdf3StatixExplicationGen()) { try { sdf3ToStatixGenInj(context, strategyAffix, generatedSourcesDirectory, astSupplier); From fb1818f626218deb4012557254c55040888a9676 Mon Sep 17 00:00:00 2001 From: "Daniel A. A. Pelsmaeker" <647530+Virtlink@users.noreply.github.com> Date: Mon, 10 Jan 2022 20:03:01 +0100 Subject: [PATCH 7/8] Allow semantic errors in program --- .../pie/CodeCompletionTaskDef.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java index 5c4b097dd..cf5a3ea38 100644 --- a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java +++ b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java @@ -37,6 +37,7 @@ import mb.statix.codecompletion.strategies.runtime.InferStrategy; import mb.statix.constraints.CUser; import mb.statix.constraints.messages.IMessage; +import mb.statix.constraints.messages.MessageKind; import mb.statix.solver.IConstraint; import mb.statix.solver.persistent.State; import mb.statix.spec.Spec; @@ -53,6 +54,7 @@ import org.spoofax.terms.util.TermUtils; import java.io.Serializable; +import java.util.AbstractMap; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; @@ -356,11 +358,19 @@ public Execution( // Analyze the AST if (eventHandler != null) eventHandler.beginAnalysis(); final SolverState analyzedState = analyze(initialState); + final ArrayList> allowedMessages = new ArrayList<>(); + if (analyzedState.hasErrors()) { + analyzedState.getMessages().forEach((c, m) -> { + if(m.kind() == MessageKind.ERROR) { + allowedMessages.add(new AbstractMap.SimpleEntry<>(c, m)); + } + }); + } if (eventHandler != null) eventHandler.endAnalysis(); // Execute the code completion Tego strategy if (eventHandler != null) eventHandler.beginCodeCompletion(); - final Seq completionProposals = complete(analyzedState, placeholder, Collections.emptyList() /* TODO: Get the set of analysis errors */); + final Seq completionProposals = complete(analyzedState, placeholder, allowedMessages /*Collections.emptyList() */); final Seq filteredProposals = filterProposals(completionProposals); final List instantiatedProposals = filteredProposals.toList(); // NOTE: This is where we actually coerce the lazy list find the completions. if (eventHandler != null) eventHandler.endCodeCompletion(); @@ -583,9 +593,9 @@ private SolverState analyze(SolverState initialState) { final @Nullable SolverState analyzedState = tegoRuntime.eval(InferStrategy.getInstance(), initialState); if (analyzedState == null) { throw new IllegalStateException("Completion failed: got no result from Tego strategy."); - } else if(analyzedState.hasErrors()) { - // TODO: We can add these errors to the set of allowed errors - throw new IllegalStateException("Completion failed: input program validation failed:\n" + analyzedState.messagesToString()); +// } else if(analyzedState.hasErrors()) { +// // TODO: We can add these errors to the set of allowed errors +// throw new IllegalStateException("Completion failed: input program validation failed:\n" + analyzedState.messagesToString()); } else if(analyzedState.getConstraints().isEmpty()) { throw new IllegalStateException("Completion failed: no constraints left, nothing to complete.\n" + analyzedState); } From d4187389fb8af0e2d9113bbfec0af403543eea2c Mon Sep 17 00:00:00 2001 From: "Daniel A. A. Pelsmaeker" <647530+Virtlink@users.noreply.github.com> Date: Tue, 8 Mar 2022 14:05:30 +0100 Subject: [PATCH 8/8] Add construct-textual-change --- .../statix/codecompletion/pie/CodeCompletionTaskDef.java | 8 ++++++++ .../mb/spoofax/lwb/compiler/stratego/pp.str2.mustache | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java index cf5a3ea38..a4fd807bd 100644 --- a/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java +++ b/core/statix.codecompletion.pie/src/main/java/mb/statix/codecompletion/pie/CodeCompletionTaskDef.java @@ -421,6 +421,14 @@ public Execution( .map(o -> o.ast); } + private Result parenthesize(IStrategoTerm term) { + try { + return Result.ofOk(strategoRuntime.invoke("parenthesize-completion-term", term)); + } catch (StrategoException ex) { + return Result.ofErr(ex); + } + } + /** * Pretty-prints the given term. * diff --git a/lwb/spoofax.lwb.compiler/src/main/resources/mb/spoofax/lwb/compiler/stratego/pp.str2.mustache b/lwb/spoofax.lwb.compiler/src/main/resources/mb/spoofax/lwb/compiler/stratego/pp.str2.mustache index a60c7a93a..1db6c67fb 100644 --- a/lwb/spoofax.lwb.compiler/src/main/resources/mb/spoofax/lwb/compiler/stratego/pp.str2.mustache +++ b/lwb/spoofax.lwb.compiler/src/main/resources/mb/spoofax/lwb/compiler/stratego/pp.str2.mustache @@ -4,6 +4,7 @@ imports strategolib gpp + libspoofax/editor/refactoring/- libspoofax/sdf/pp pp/{{name}}-parenthesize pp/{{sdf3MainModule}}-pp @@ -34,3 +35,6 @@ rules result := ast <+ ast ; result := "" + + construct-textual-change-{{name}} = + construct-textual-change(pp-partial-{{name}}-string, parenthesize-{{name}}, id, id)