Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

experimental HiFi tree diff algorithm for use with quick-fixes and refactoring commands in the IDE #2031

Draft
wants to merge 29 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7b6f519
experimental HiFi tree diff algorithm for use with quick-fixes and re…
jurgenvinju Sep 12, 2024
374a8a2
developing the list diff algorithms with inspiration from the diff tool
jurgenvinju Oct 1, 2024
da7f5a1
Merge branch 'main' into hifi-tree-diff
jurgenvinju Oct 5, 2024
c623d2b
made some progress with the list algorithm
jurgenvinju Oct 7, 2024
1525e73
minor improvements. this is not finished yet
jurgenvinju Oct 7, 2024
3196433
slow progress
jurgenvinju Oct 10, 2024
3f05df4
added demo
jurgenvinju Oct 10, 2024
2547a1a
Merge branch 'main' into hifi-tree-diff
jurgenvinju Oct 13, 2024
ed091f7
exposed IString.indent to String library module to allow users to reu…
jurgenvinju Oct 13, 2024
2462eeb
slow progress on the diff algorithm
jurgenvinju Oct 13, 2024
8abdbd6
more complex example, and debug prints
jurgenvinju Oct 13, 2024
4a55110
finetunes stuff in indentation learner
jurgenvinju Oct 13, 2024
97eb352
testing
jurgenvinju Oct 15, 2024
b051673
Merge branch 'main' into hifi-tree-diff
jurgenvinju Jan 7, 2025
27e8536
Merge branch 'main' into hifi-tree-diff
jurgenvinju Jan 9, 2025
fd6ccbb
fixed nasty bug in Type.intersection w.r.t. parameter types
jurgenvinju Jan 9, 2025
1c0a81d
started on testing HiFiTreeDiff
jurgenvinju Jan 9, 2025
9c64458
minor improvements
jurgenvinju Jan 9, 2025
71a1c00
fixed bug in list diff
jurgenvinju Jan 9, 2025
2677795
oops
jurgenvinju Jan 9, 2025
091b0b9
simplified and repaired equal sublist detection
jurgenvinju Jan 9, 2025
ed1ad03
finding more nested similarity under list elements
jurgenvinju Jan 10, 2025
cf798ec
Finishes HiFiTreeDiff algorithm
jurgenvinju Jan 11, 2025
c84a9e2
fixed omision in ResultFactory for ComposedFunctions
jurgenvinju Jan 11, 2025
af081f5
added more tests, fixed some issues
jurgenvinju Jan 11, 2025
94c9adc
added failing test
jurgenvinju Jan 11, 2025
9856f1c
debugging
jurgenvinju Jan 13, 2025
cbfaa68
more debugging
jurgenvinju Jan 13, 2025
ec718d1
one more test
jurgenvinju Jan 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/org/rascalmpl/interpreter/result/ResultFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ else if (value instanceof OverloadedFunction) {
return (OverloadedFunction) value;
}
}
else if (value instanceof ComposedFunctionResult) {
return (Result<?>) value;
}
else {
// otherwise this is an abstract ICalleableValue
// for which no further operations are defined?
Expand Down
4 changes: 4 additions & 0 deletions src/org/rascalmpl/library/Prelude.java
Original file line number Diff line number Diff line change
Expand Up @@ -3047,6 +3047,10 @@ public IValue stringChars(IList lst){

return values.string(chars);
}

public IString indent(IString indentation, IString content, IBool indentFirstLine) {
return content.indent(indentation, indentFirstLine.getValue());
}

public IValue charAt(IString s, IInteger i) throws IndexOutOfBoundsException
//@doc{charAt -- return the character at position i in string s.}
Expand Down
16 changes: 16 additions & 0 deletions src/org/rascalmpl/library/String.rsc
Original file line number Diff line number Diff line change
Expand Up @@ -663,3 +663,19 @@ str substitute(str src, map[loc,str] s) {
order = sort([ k | k <- s ], bool(loc a, loc b) { return a.offset < b.offset; });
return ( src | subst1(it, x, s[x]) | x <- order );
}

@synopsis{Indent a block of text}
@description{
Every line in `content` will be indented using the characters
of `indentation`.
}
@benefits{
* This operation executes in constant time, independent of the size of the content
or the indentation.
* Indent is the identity function if `indentation == ""`
}
@pitfalls{
* This function works fine if `indentation` is not spaces or tabs; but it does not make much sense.
}
@javaClass{org.rascalmpl.library.Prelude}
java str indent(str indentation, str content, bool indentFirstLine=false);
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,21 @@ void executeDocumentEdit(renamed(loc from, loc to)) {
}

void executeDocumentEdit(changed(loc file, list[TextEdit] edits)) {
str content = readFile(file);

content = executeTextEdits(content, edits);

writeFile(file.top, content);
}

str executeTextEdits(str content, list[TextEdit] edits) {
assert isSorted(edits, less=bool (TextEdit e1, TextEdit e2) {
return e1.range.offset < e2.range.offset;
});

str content = readFile(file);

for (replace(loc range, str repl) <- reverse(edits)) {
assert range.top == file.top;
content = "<content[..range.offset]><repl><content[range.offset+range.length..]>";
}

writeFile(file.top, content);
return content;
}
413 changes: 413 additions & 0 deletions src/org/rascalmpl/library/analysis/diff/edits/HiFiTreeDiff.rsc

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions src/org/rascalmpl/library/lang/pico/HiFiDemo.rsc
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@synopsis{Demonstrates HiFi source-to-source transformations through concrete syntax rewrites and text edits.}
module lang::pico::HiFiDemo

import lang::pico::\syntax::Main;
import IO;
import ParseTree;
import analysis::diff::edits::HiFiTreeDiff;
import analysis::diff::edits::ExecuteTextEdits;

@synopsis{Blindly swaps the branches of all the conditionals in a program}
@description{
This rule is syntactically correct and has a clear semantics. The
layout of the resulting if-then-else-fi statement is also clear.
}
start[Program] flipConditionals(start[Program] program) = visit(program) {
case (Statement) `if <Expression e> then
' <{Statement ";"}* ifBranch>
'else
' <{Statement ";"}* elseBranch>
'fi` =>
(Statement) `if <Expression e> then
' <{Statement ";"}* elseBranch>
'else
' <{Statement ";"}* ifBranch>
'fi`
};

void main() {
t = parse(#start[Program], |project://rascal/src/org/rascalmpl/library/lang/pico/examples/flip.pico|);
println("The original:
'<t>");

u = flipConditionals(t);
println("Branches swapped, comments and indentation lost:
'<u>");

edits = treeDiff(t, u);
println("Smaller text edits:");
iprintln(edits);

newContent = executeTextEdits("<t>", edits);
println("Better output after executeTextEdits:
'<newContent>");

newU = parse(#start[Program], newContent);

assert u := newU : "the rewritten tree matches the newly parsed";
}
21 changes: 21 additions & 0 deletions src/org/rascalmpl/library/lang/pico/examples/flip.pico
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
begin
declare
a : natural,
b : natural;
a := 0;
b := 1;
if a then
% comment 1 %
b := a;
z := z
else
% comment 2 %
a := b;
if b then
z := a
else
z := b
fi;
z := z
fi
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
module lang::rascal::tests::library::analysis::diff::edits::HiFiTreeDiffTests

extend analysis::diff::edits::ExecuteTextEdits;
extend analysis::diff::edits::HiFiTreeDiff;
extend lang::pico::\syntax::Main;

import IO;
import ParseTree;
import String;

public str simpleExample
= "begin
' declare
' a : natural,
' b : natural;
' a := a + b;
' b := a - b;
' a := a - b
'end
'";

@synopsis{Specification of what it means for `treeDiff` to be syntactically correct}
@description{
TreeDiff is syntactically correct if:
* The tree after rewriting _matches_ the tree after applying the edits tot the source text and parsing that.
* Note that _matching_ ignores case-insensitive literals and layout, indentation and comments
}
bool editsAreSyntacticallyCorrect(type[&T<:Tree] grammar, str example, (&T<:Tree)(&T<:Tree) transform) {
orig = parse(grammar, example);
transformed = transform(orig);
edits = treeDiff(orig, transformed);
edited = executeTextEdits(example, edits);
println("<transform> leads to:");
iprintln(edits);

try {
if (transformed := parse(grammar, edited)) {
return true;
}
else {
println("The edited result is not the same:");
println(edited);
println("As the transformed:");
println(transformed);
}
}
catch ParseError(loc l): {
println("<transform> caused a parse error <l> in:");
println(edited);
return false;
}
}

@synopsis{Extract the leading spaces of each line of code}
list[str] indentationLevels(str example)
= [ i | /^<i:[\ ]*>[^\ ]*/ <- split("\n", example)];

@synopsis{In many cases, but not always, treeDiff maintains the indentation levels}
@description{
Typically when a rewrite does not change the lines of code count,
and when the structure of the statements remains comparable, treeDiff
can guarantee that the indentation of a file remains unchanged, even if
significant changes to the code have been made.
}
@pitfalls{
* This specification is not true for any transformation. Only apply it to
a test case if you can expect indentation-preservation for _the entire file_.
}
bool editsMaintainIndentationLevels(type[&T<:Tree] grammar, str example, (&T<:Tree)(&T<:Tree) transform) {
orig = parse(grammar, example);
transformed = transform(orig);
edits = treeDiff(orig, transformed);
edited = executeTextEdits(example, edits);

return indentationLevels(example) == indentationLevels(edited);
}

(&X<:Tree) identity(&X<:Tree x) = x;

start[Program] swapAB(start[Program] p) = visit(p) {
case (Id) `a` => (Id) `b`
case (Id) `b` => (Id) `a`
};

start[Program] naturalToString(start[Program] p) = visit(p) {
case (Type) `natural` => (Type) `string`
};

start[Program] addDeclarationToEnd(start[Program] p) = visit(p) {
case (Program) `begin declare <{IdType ","}* decls>; <{Statement ";"}* body> end`
=> (Program) `begin
' declare
' <{IdType ","}* decls>,
' c : natural;
' <{Statement ";"}* body>
'end`
};

start[Program] addDeclarationToStart(start[Program] p) = visit(p) {
case (Program) `begin declare <{IdType ","}* decls>; <{Statement ";"}* body> end`
=> (Program) `begin
' declare
' c : natural,
' <{IdType ","}* decls>;
' <{Statement ";"}* body>
'end`
};

test bool nulTestWithId()
= editsAreSyntacticallyCorrect(#start[Program], simpleExample, identity);

test bool simpleSwapper()
= editsAreSyntacticallyCorrect(#start[Program], simpleExample, swapAB)
&& editsMaintainIndentationLevels(#start[Program], simpleExample, swapAB);

test bool addDeclarationToEndTest()
= editsAreSyntacticallyCorrect(#start[Program], simpleExample, addDeclarationToEnd);

test bool addDeclarationToStartTest()
= editsAreSyntacticallyCorrect(#start[Program], simpleExample, addDeclarationToStart);

test bool addDeclarationToStartAndEndTest()
= editsAreSyntacticallyCorrect(#start[Program], simpleExample, addDeclarationToStart o addDeclarationToEnd);

test bool addDeclarationToEndAndSwapABTest()
= editsAreSyntacticallyCorrect(#start[Program], simpleExample, addDeclarationToEnd o swapAB);

test bool addDeclarationToStartAndSwapABTest()
= editsAreSyntacticallyCorrect(#start[Program], simpleExample, addDeclarationToStart o swapAB);

test bool addDeclarationToStartAndEndAndSwapABTest()
= editsAreSyntacticallyCorrect(#start[Program], simpleExample, addDeclarationToStart o addDeclarationToEnd o swapAB);

test bool naturalToStringTest()
= editsAreSyntacticallyCorrect(#start[Program], simpleExample, naturalToString);

test bool naturalToStringAndAtoBTest()
= editsAreSyntacticallyCorrect(#start[Program], simpleExample, naturalToString o swapAB);
3 changes: 3 additions & 0 deletions src/org/rascalmpl/types/NonTerminalType.java
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,9 @@ public boolean intersects(Type other) {
if (other == RascalValueFactory.Tree) {
return true;
}
else if (other.isParameter()) {
return other.intersects(this);
}
else if (other instanceof NonTerminalType) {
return ((NonTerminalType) other).intersectsWithNonTerminal(this);
}
Expand Down
Loading