Skip to content

Commit 0c70917

Browse files
committed
Add refactor command
1 parent 1bd9ef2 commit 0c70917

File tree

10 files changed

+304
-10
lines changed

10 files changed

+304
-10
lines changed

lkql_jit/cli/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@
122122
<artifactId>launcher-common</artifactId>
123123
<version>${graalvm.version}</version>
124124
</dependency>
125+
<dependency>
126+
<groupId>com.adacore</groupId>
127+
<artifactId>liblkqllang</artifactId>
128+
<version>0.1</version>
129+
</dependency>
125130
</dependencies>
126131

127132
</project>

lkql_jit/cli/src/main/java/com/adacore/lkql_jit/LKQLMain.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
LKQLLauncher.LKQLRun.class,
1818
LKQLChecker.Args.class,
1919
GNATCheckWorker.Args.class,
20-
LKQLDoc.class
20+
LKQLDoc.class,
21+
LKQLRefactor.class
2122
},
2223
description =
2324
"Unified driver for LKQL (Langkit query language). Allows you to run LKQL "
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
//
2+
// Copyright (C) 2005-2024, AdaCore
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
//
5+
6+
package com.adacore.lkql_jit;
7+
8+
import static com.adacore.liblkqllang.Liblkqllang.*;
9+
10+
import java.io.PrintWriter;
11+
import java.nio.charset.StandardCharsets;
12+
import java.util.ArrayList;
13+
import java.util.HashMap;
14+
import java.util.List;
15+
import java.util.concurrent.Callable;
16+
import java.util.function.Consumer;
17+
import java.util.function.Predicate;
18+
import picocli.CommandLine;
19+
20+
/**
21+
* Refactor command for LKQL. Allows to run automatic migrations, allowing the LKQL team to create
22+
* migrators to adapt to syntactic or semantic changes in LKQL.
23+
*
24+
* <p>Migrators work on the stream of tokens directly. Migrator implementer will attach actions
25+
* (append/prepend/replace) on tokens, which allows to modify the output stream without working on
26+
* text directly, which simplifies the expression of refactorings.
27+
*
28+
* <p>A migrator implementer will typically:
29+
*
30+
* <p>1. Find the nodes he wants to refactor in the LKQL tree 2. From those nodes, find the tokens
31+
* inside the nodes that need to be altered 3. Attach actions to those tokens
32+
*
33+
* <p>Then the rewriter will emit a new file for every LKQL unit, either inplace or on stdout.
34+
*
35+
* <p>To add a refactoring, you need to extend the 'refactoringKind' enum to add a new refactoring
36+
* id, and then extend the 'getRefactoring' method to return an anonymous function that will add
37+
* actions to the list of actions.
38+
*/
39+
@CommandLine.Command(
40+
name = "refactor",
41+
description = "Automatically run a refactoring on LKQL code")
42+
public class LKQLRefactor implements Callable<Integer> {
43+
44+
@CommandLine.Spec public picocli.CommandLine.Model.CommandSpec spec;
45+
46+
@CommandLine.Parameters(description = "LKQL files to refactor")
47+
public List<String> files = new ArrayList<>();
48+
49+
@CommandLine.Option(
50+
names = {"-i", "--inplace"},
51+
description = "Rewrite the files in place")
52+
public boolean inPlace;
53+
54+
private enum refactoringKind {
55+
IS_TO_COLON
56+
}
57+
58+
@CommandLine.Option(
59+
names = {"-r", "--refactoring"},
60+
description = "Refactoring to run. Valid values: ${COMPLETION-CANDIDATES}",
61+
required = true)
62+
private refactoringKind refactoring;
63+
64+
/** Kind of actions that can be attached to a token. */
65+
private enum actionKind {
66+
APPEND,
67+
PREPEND,
68+
REPLACE
69+
}
70+
71+
/**
72+
* Action record, encapsulating an action that can be attached to a token. When applied, the
73+
* action will either append the given text, prepend the given text, or replace the token text
74+
* with the given text, depending on the kind.
75+
*
76+
* <p>To remove a token, simply use a REPLACE action with an empty text.
77+
*/
78+
private record Action(actionKind kind, String text) {}
79+
80+
/**
81+
* Global map of actions that will be applied on tokens. To append an action, use the addAction
82+
* method.
83+
*/
84+
private final HashMap<String, List<Action>> actions = new HashMap<>();
85+
86+
/**
87+
* Helper to create a unique Id for a token. TODO: This method exists only because the
88+
* 'hashCode' method on Tokens is wrong. This should be fixed at the Langkit level. See
89+
* eng/libadalang/langkit#857.
90+
*
91+
* @return the id as a string
92+
*/
93+
private String getTokenId(Token token) {
94+
return token.unit.getFileName()
95+
+ (token.isTrivia() ? "trivia " + token.triviaIndex : token.tokenIndex)
96+
+ token.kind;
97+
}
98+
99+
/** Add an action to the list of actions to apply */
100+
public void addAction(Token token, Action action) {
101+
List<Action> actionList;
102+
var tokenId = getTokenId(token);
103+
if (actions.containsKey(tokenId)) {
104+
actionList = actions.get(tokenId);
105+
} else {
106+
actionList = new ArrayList<>();
107+
actions.put(tokenId, actionList);
108+
}
109+
110+
actionList.add(action);
111+
}
112+
113+
/**
114+
* Helper for refactor writers: Returns the first token that satisfies the predicate 'pred',
115+
* iterating on tokens from 'fromTok' (including 'fromTok').
116+
*
117+
* If no token is found, return the null token.
118+
*/
119+
public static Token firstWithPred(Token fromTok, Predicate<Token> pred) {
120+
var curTok = fromTok;
121+
while (!curTok.isNone() && !pred.test(curTok)) {
122+
curTok = curTok.next();
123+
}
124+
return curTok;
125+
}
126+
127+
/**
128+
* Internal method: rewrite a unit by printing all the tokens via the 'write' callback. Apply
129+
* the actions at the same time.
130+
*/
131+
private void printAllTokens(AnalysisUnit unit, Consumer<String> write) {
132+
for (var tok = unit.getFirstToken(); !tok.isNone(); tok = tok.next()) {
133+
var tokActions = actions.get(getTokenId(tok));
134+
if (tokActions != null) {
135+
var replaceActions =
136+
tokActions.stream().filter(c -> c.kind == actionKind.REPLACE).toList();
137+
assert replaceActions.size() <= 1 : "Only one replace action per token";
138+
139+
var prependActions =
140+
tokActions.stream().filter(c -> c.kind == actionKind.PREPEND).toList();
141+
142+
var appendActions =
143+
tokActions.stream().filter(c -> c.kind == actionKind.APPEND).toList();
144+
145+
for (var action : prependActions) {
146+
write.accept(action.text);
147+
}
148+
149+
if (!replaceActions.isEmpty()) {
150+
write.accept(replaceActions.get(0).text);
151+
} else {
152+
write.accept(tok.getText());
153+
}
154+
155+
for (var action : appendActions) {
156+
write.accept(action.text);
157+
}
158+
} else {
159+
write.accept(tok.getText());
160+
}
161+
}
162+
}
163+
164+
/**
165+
* Internal method: rewrite a unit by printing all the tokens, either to stdout, or to the
166+
* original file, depending on the value of the '-i' command line flag.
167+
*/
168+
private void printAllTokens(AnalysisUnit unit) {
169+
try {
170+
if (this.inPlace) {
171+
var writer = new PrintWriter(unit.getFileName(), StandardCharsets.UTF_8);
172+
printAllTokens(unit, writer::print);
173+
writer.close();
174+
} else {
175+
printAllTokens(unit, System.out::print);
176+
}
177+
} catch (Exception e) {
178+
throw new RuntimeException(e);
179+
}
180+
}
181+
182+
/**
183+
* Helper for findAll. Visit all children of 'node', calling 'cons' on each of them. TODO: Hoist
184+
* in Java bindings (see eng/libadalang/langkit#859)
185+
*/
186+
private static void visitChildren(LkqlNode node, Consumer<LkqlNode> cons) {
187+
if (node == null || node.isNone()) {
188+
return;
189+
}
190+
191+
for (var c : node.children()) {
192+
if (c != null && !c.isNone()) {
193+
cons.accept(c);
194+
visitChildren(c, cons);
195+
}
196+
}
197+
}
198+
;
199+
200+
/**
201+
* Helper for refactor writers: Find all nodes that are children of root and which satisfies the
202+
* predicate 'pred'. TODO: Hoist in Java bindings (see eng/libadalang/langkit#859)
203+
*/
204+
public static List<LkqlNode> findAll(LkqlNode root, Predicate<LkqlNode> pred) {
205+
var result = new ArrayList<LkqlNode>();
206+
visitChildren(
207+
root,
208+
(c) -> {
209+
if (pred.test(c)) {
210+
result.add(c);
211+
}
212+
});
213+
return result;
214+
}
215+
216+
/**
217+
* Return the refactoring corresponding to enum value passed on command line. This is where
218+
* concrete refactorings are implemented.
219+
*/
220+
public Consumer<AnalysisUnit> getRefactoring() {
221+
switch (refactoring) {
222+
case IS_TO_COLON:
223+
return unit -> {
224+
for (var det : findAll(unit.getRoot(), (n) -> n instanceof NodePatternDetail)) {
225+
var tokIs =
226+
firstWithPred(det.tokenStart(), (t) -> t.kind == TokenKind.LKQL_IS);
227+
228+
if (!tokIs.isNone()) {
229+
// Replace "is" -> ":"
230+
addAction(tokIs, new Action(actionKind.REPLACE, ":"));
231+
232+
// Get rid of previous token if it is a whitespace
233+
var prev = tokIs.previous();
234+
if (prev.kind == TokenKind.LKQL_WHITESPACE) {
235+
addAction(tokIs.previous(), new Action(actionKind.REPLACE, ""));
236+
}
237+
}
238+
}
239+
};
240+
}
241+
return null;
242+
}
243+
244+
@Override
245+
public Integer call() {
246+
var refactoring = getRefactoring();
247+
var ctx = AnalysisContext.create();
248+
for (var file : files) {
249+
var unit = ctx.getUnitFromFile(file);
250+
refactoring.accept(unit);
251+
printAllTokens(unit);
252+
actions.clear();
253+
}
254+
return 0;
255+
}
256+
}

testsuite/drivers/base_driver.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -451,13 +451,12 @@ def _define_lkql_executables(self) -> None:
451451
# If the mode is JIT
452452
if self.env.options.mode == "jit":
453453
python_wrapper = P.join(self.env.support_dir, "lkql_jit.py")
454-
command_base = [sys.executable, python_wrapper]
455-
self.lkql_exe = [*command_base, "run"]
456-
self.lkql_checker_exe = [*command_base, "check"]
457-
self.gnatcheck_worker_exe = [*command_base, "gnatcheck_worker"]
454+
self.command_base = [sys.executable, python_wrapper]
458455

459456
# If the mode is native JIT
460457
elif self.env.options.mode == "native_jit":
461-
self.lkql_exe = ["lkql", "run"]
462-
self.lkql_checker_exe = ["lkql", "check"]
463-
self.gnatcheck_worker_exe = ["lkql", "gnatcheck_worker"]
458+
self.command_base = ["lkql"]
459+
460+
self.lkql_exe = [*self.command_base, "run"]
461+
self.lkql_checker_exe = [*self.command_base, "check"]
462+
self.gnatcheck_worker_exe = [*self.command_base, "gnatcheck_worker"]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from drivers.base_driver import BaseDriver
2+
3+
4+
class RefactorDriver(BaseDriver):
5+
"""
6+
This driver runs an 'lkql refactor' command on a given lkql file, and
7+
compares the resulting file to the output file.
8+
9+
The LKQL script to refactor must be placed in a file called 'input.lkql'
10+
11+
The expected output must be written in a file called `test.out`
12+
13+
Test arguments:
14+
- refactoring_name: The name of the refactoring to run
15+
"""
16+
17+
perf_supported = True
18+
19+
def run(self) -> None:
20+
self.check_run([
21+
*self.command_base, 'refactor',
22+
'--refactoring', self.test_env['refactoring'], 'input.lkql'
23+
])

testsuite/python_support/lkql_jit.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,6 @@
3838
'-cp', class_path,
3939
f'-Djava.library.path={java_library_path}',
4040
f'-Dtruffle.class.path.append={P.join(lkql_jit_home, "lkql_jit.jar")}',
41+
'--add-opens=org.graalvm.truffle/com.oracle.truffle.api.strings=ALL-UNNAMED',
4142
f'com.adacore.lkql_jit.LKQLMain'
4243
] + sys.argv[1:])
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@check()
2+
fun foo(node) =
3+
node is AspectAssoc(f_id is "Hello")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@check()
2+
fun foo(node) =
3+
node is AspectAssoc(f_id: "Hello")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
driver: refactor
2+
refactoring: IS_TO_COLON

testsuite/testsuite.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from drivers import (
1919
checker_driver, gnatcheck_driver, interpreter_driver, parser_driver, java_driver,
20-
benchmarks_driver,
20+
benchmarks_driver, refactor_driver
2121
)
2222

2323
class PerfTestFinder(YAMLTestFinder):
@@ -86,7 +86,8 @@ class LKQLTestsuite(Testsuite):
8686
'java': java_driver.JavaDriver,
8787
'checker': checker_driver.CheckerDriver,
8888
'gnatcheck': gnatcheck_driver.GnatcheckDriver,
89-
'benchmarks': benchmarks_driver.BenchmarksDriver,}
89+
'benchmarks': benchmarks_driver.BenchmarksDriver,
90+
'refactor': refactor_driver.RefactorDriver,}
9091

9192
def add_options(self, parser: ArgumentParser) -> None:
9293
parser.add_argument(

0 commit comments

Comments
 (0)