diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000000..2ce24899f27
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.dot diff=-astextplain
diff --git a/.vscode/launch.json b/.vscode/launch.json
index aa506d012ad..833fb54b4da 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -38,7 +38,7 @@
"request": "launch",
"mainClass": "org.rascalmpl.shell.RascalShell",
"projectName": "rascal",
- "cwd" : "${workspaceFolder}/../rascal-tutor",
+ "cwd": "${workspaceFolder}/../rascal-tutor",
"vmArgs": "-Xss80m -Xmx2g -ea"
},
{
diff --git a/pom.xml b/pom.xml
index c27b3e181cd..e8c4d581d87 100644
--- a/pom.xml
+++ b/pom.xml
@@ -169,6 +169,7 @@
package
+
diff --git a/src/org/rascalmpl/interpreter/Evaluator.java b/src/org/rascalmpl/interpreter/Evaluator.java
index 5183ce362df..1c412acdbcb 100755
--- a/src/org/rascalmpl/interpreter/Evaluator.java
+++ b/src/org/rascalmpl/interpreter/Evaluator.java
@@ -989,7 +989,7 @@ private IFunction parserForCurrentModule(RascalFunctionValueFactory vf, ModuleEn
IMap syntaxDefinition = curMod.getSyntaxDefinition();
IMap grammar = (IMap) getParserGenerator().getGrammarFromModules(getMonitor(), curMod.getName(), syntaxDefinition).get("rules");
IConstructor reifiedType = vf.reifiedType(dummy, grammar);
- return vf.parsers(reifiedType, vf.bool(false), vf.bool(false), vf.bool(false), vf.set());
+ return vf.parsers(reifiedType, vf.bool(false), vf.bool(false), vf.bool(false), vf.bool(false), vf.set());
}
private Result evalMore(String command, ISourceLocation location)
diff --git a/src/org/rascalmpl/library/ParseTree.rsc b/src/org/rascalmpl/library/ParseTree.rsc
index 3289f88fd2c..952d5d7306f 100644
--- a/src/org/rascalmpl/library/ParseTree.rsc
+++ b/src/org/rascalmpl/library/ParseTree.rsc
@@ -143,7 +143,6 @@ extend Type;
extend Message;
extend List;
-
@synopsis{The Tree data type as produced by the parser.}
@description{
A `Tree` defines the trees normally found after parsing; additional constructors exist for execptional cases:
@@ -177,6 +176,8 @@ construct ordered and un-ordered compositions, and associativity groups.
<4> `assoc` means all alternatives are acceptable, but nested on the declared side;
<5> `reference` means a reference to another production rule which should be substituted there,
for extending priority chains and such.
+<6> `error` means a node produced by error recovery.
+<7> `skipped` means characters skipped during error recovery, always the last child of an `appl` with a `error` production.
}
data Production
= prod(Symbol def, list[Symbol] symbols, set[Attr] attributes) // <1>
@@ -189,6 +190,9 @@ data Production
| \reference(Symbol def, str cons) // <5>
;
+data Production
+ = \error(Symbol def, Production prod, int dot)
+ | \skipped(Symbol def);
@synopsis{Attributes in productions.}
@description{
@@ -236,7 +240,7 @@ e.g., `int`, `list`, and `rel`. Here we extend it with the symbols that may occu
<4> Layout symbols
<5> Terminal symbols that are keywords
<6> Parameterized context-free non-terminal
-<7> Parameterized lexical non-terminal
+<7> Parameterized lexical non-terminal
<8> Terminal.
<9> Case-insensitive terminal.
<10> Character class
@@ -348,6 +352,15 @@ The latter option terminates much faster, i.e. always in cubic time, and always
while constructing ambiguous parse forests may grow to O(n^p+1), where p is the length of the longest production rule and n
is the length of the input.
+The `allowRecovery` can be set to `true` to enable error recovery. This is an experimental feature.
+When error recovery is enabled, the parser will attempt to recover from parse errors and continue parsing.
+If successful, a parse tree with error and skipped productions is returned (see the definition of `Production` above).
+The `util::ErrorRecovery` module contains a number of functions to analyze trees with errors, for example `hasErrors`, `getSkipped`, and `getErrorText`.
+Note that the resulting parse forest can contain a lot of ambiguities. Any code that processes error trees must be aware of this,
+for instance a simple traversal of all subtrees will be too expensive in most cases. `disambiguateErrors` can be used to
+efficiently prune the forest and leave a tree with a single (or even zero) errors based on simple heuristics, but these heuristics
+are somewhat arbitrary so the usability of this function is limited.
+
The `filters` set contains functions which may be called optionally after the parse algorithm has finished and just before
the Tree representation is built. The set of functions contain alternative functions, only on of them is successfully applied
to each node in a tree. If such a function fails to apply, the other ones are tried. There is no fixed-point computation, so
@@ -392,14 +405,15 @@ catch ParseError(loc l): {
}
```
}
-&T<:Tree parse(type[&T<:Tree] begin, str input, bool allowAmbiguity=false, bool hasSideEffects=false, set[Tree(Tree)] filters={})
- = parser(begin, allowAmbiguity=allowAmbiguity, hasSideEffects=hasSideEffects, filters=filters)(input, |unknown:///|);
-&T<:Tree parse(type[&T<:Tree] begin, str input, loc origin, bool allowAmbiguity=false, bool hasSideEffects=false, set[Tree(Tree)] filters={})
- = parser(begin, allowAmbiguity=allowAmbiguity, hasSideEffects=hasSideEffects, filters=filters)(input, origin);
+&T<:Tree parse(type[&T<:Tree] begin, str input, bool allowAmbiguity=false, bool allowRecovery=false, bool hasSideEffects=false, set[Tree(Tree)] filters={})
+ = parser(begin, allowAmbiguity=allowAmbiguity, allowRecovery=allowRecovery, hasSideEffects=hasSideEffects, filters=filters)(input, |unknown:///|);
+
+&T<:Tree parse(type[&T<:Tree] begin, str input, loc origin, bool allowAmbiguity=false, bool allowRecovery=false, bool hasSideEffects=false, set[Tree(Tree)] filters={})
+ = parser(begin, allowAmbiguity=allowAmbiguity, allowRecovery=allowRecovery, hasSideEffects=hasSideEffects, filters=filters)(input, origin);
-&T<:Tree parse(type[&T<:Tree] begin, loc input, bool allowAmbiguity=false, bool hasSideEffects=false, set[Tree(Tree)] filters={})
- = parser(begin, allowAmbiguity=allowAmbiguity, hasSideEffects=hasSideEffects, filters=filters)(input, input);
+&T<:Tree parse(type[&T<:Tree] begin, loc input, bool allowAmbiguity=false, bool allowRecovery=false, bool hasSideEffects=false, set[Tree(Tree)] filters={})
+ = parser(begin, allowAmbiguity=allowAmbiguity, allowRecovery=allowRecovery, hasSideEffects=hasSideEffects, filters=filters)(input, input);
@synopsis{Generates a parser from an input grammar.}
@@ -415,15 +429,18 @@ So the parse function reads either directly from a str or via the contents of a
which leads to the prefix of the `src` fields of the resulting tree.
The parse function behaves differently depending of the given keyword parameters:
- * `allowAmbiguity`: if true then no exception is thrown in case of ambiguity and a parse forest is returned. if false,
+ * `allowAmbiguity`: if true then no exception is thrown in case of ambiguity and a parse forest is returned. if false,
the parser throws an exception during tree building and produces only the first ambiguous subtree in its message.
if set to `false`, the parse constructs trees in linear time. if set to `true` the parser constructs trees in polynomial time.
- *
+ * 'allowRecovery`: ***experimental*** if true, the parser tries to recover when it encounters a parse error. if a parse error is encountered that can be recovered from,
+ special `error` and `skipped` productions are included in the resulting parse tree. More documentation will be added here when this feature matures.
+ Note that if `allowRecovery` is set to true, the resulting tree can still contain ambiguity nodes related to recovered parse errors, even if `allowAmbiguity`
+ is set to false. When a 'regular` (non-error) ambiguity is found an exception is still thrown in this case.
* `hasSideEffects`: if false then the parser is a lot faster when constructing trees, since it does not execute the parse _actions_ in an
interpreted environment to make side effects (like a symbol table) and it can share more intermediate results as a result.
}
@javaClass{org.rascalmpl.library.Prelude}
-java &T (value input, loc origin) parser(type[&T] grammar, bool allowAmbiguity=false, bool hasSideEffects=false, set[Tree(Tree)] filters={});
+java &T (value input, loc origin) parser(type[&T] grammar, bool allowAmbiguity=false, bool allowRecovery=false, bool hasSideEffects=false, set[Tree(Tree)] filters={});
@javaClass{org.rascalmpl.library.Prelude}
@synopsis{Generates a parser function that can be used to find the left-most deepest ambiguous sub-sentence.}
@@ -436,7 +453,7 @@ the tree that exhibits ambiguity. This can be done very quickly, while the whole
* The returned sub-tree usually has a different type than the parameter of the type[] symbol that was passed in.
The reason is that sub-trees typically have a different non-terminal than the start non-terminal of a grammar.
}
-java Tree (value input, loc origin) firstAmbiguityFinder(type[Tree] grammar, bool hasSideEffects=false, set[Tree(Tree)] filters={});
+java Tree (value input, loc origin) firstAmbiguityFinder(type[Tree] grammar, bool allowRecovery=false, bool hasSideEffects=false, set[Tree(Tree)] filters={});
@synopsis{Generates parsers from a grammar (reified type), where all non-terminals in the grammar can be used as start-symbol.}
@description{
@@ -444,7 +461,7 @@ This parser generator behaves the same as the `parser` function, but it produces
nonterminal parameter. This can be used to select a specific non-terminal from the grammar to use as start-symbol for parsing.
}
@javaClass{org.rascalmpl.library.Prelude}
-java &U (type[&U] nonterminal, value input, loc origin) parsers(type[&T] grammar, bool allowAmbiguity=false, bool hasSideEffects=false, set[Tree(Tree)] filters={});
+java &U (type[&U] nonterminal, value input, loc origin) parsers(type[&T] grammar, bool allowAmbiguity=false, bool allowRecovery=false, bool hasSideEffects=false, set[Tree(Tree)] filters={});
@javaClass{org.rascalmpl.library.Prelude}
@synopsis{Generates a parser function that can be used to find the left-most deepest ambiguous sub-sentence.}
@@ -457,7 +474,7 @@ the tree that exhibits ambiguity. This can be done very quickly, while the whole
* The returned sub-tree usually has a different type than the parameter of the type[] symbol that was passed in.
The reason is that sub-trees typically have a different non-terminal than the start non-terminal of a grammar.
}
-java Tree (type[Tree] nonterminal, value input, loc origin) firstAmbiguityFinders(type[Tree] grammar, bool hasSideEffects=false, set[Tree(Tree)] filters={});
+java Tree (type[Tree] nonterminal, value input, loc origin) firstAmbiguityFinders(type[Tree] grammar, bool allowRecovery=false, bool hasSideEffects=false, set[Tree(Tree)] filters={});
@synopsis{Parse the input but instead of returning the entire tree, return the trees for the first ambiguous substring.}
@description{
@@ -535,7 +552,7 @@ p(type(sort("E"), ()), "e+e", |src:///|);
* reifiying types (use of `#`) will trigger the loading of a parser generator anyway. You have to use
this notation for types to avoid that: `type(\start(sort("MySort")), ())` to avoid the computation for `#start[A]`
}
-java &U (type[&U] nonterminal, value input, loc origin) loadParsers(loc savedParsers, bool allowAmbiguity=false, bool hasSideEffects=false, set[Tree(Tree)] filters={});
+java &U (type[&U] nonterminal, value input, loc origin) loadParsers(loc savedParsers, bool allowAmbiguity=false, bool allowRecovery=false, bool hasSideEffects=false, set[Tree(Tree)] filters={});
@synopsis{Load a previously serialized parser, for a specific non-terminal, from disk for usage}
@description{
@@ -543,7 +560,7 @@ This loader behaves just like ((loadParsers)), except that the resulting parser
bound to a specific non-terminal.
}
@javaClass{org.rascalmpl.library.Prelude}
-java &U (value input, loc origin) loadParser(type[&U] nonterminal, loc savedParsers, bool allowAmbiguity=false, bool hasSideEffects=false, set[Tree(Tree)] filters={});
+java &U (value input, loc origin) loadParser(type[&U] nonterminal, loc savedParsers, bool allowAmbiguity=false, bool allowRecovery=false, bool hasSideEffects=false, set[Tree(Tree)] filters={});
@synopsis{Yield the string of characters that form the leafs of the given parse tree.}
@description{
diff --git a/src/org/rascalmpl/library/Prelude.java b/src/org/rascalmpl/library/Prelude.java
index d873d1e01f3..bd34b9375b4 100644
--- a/src/org/rascalmpl/library/Prelude.java
+++ b/src/org/rascalmpl/library/Prelude.java
@@ -2377,20 +2377,24 @@ public INode arbNode() {
protected final TypeReifier tr;
+ public IFunction parser(IValue start, IBool allowAmbiguity, IBool allowRecovery, IBool hasSideEffects, ISet filters) {
+ return rascalValues.parser(start, allowAmbiguity, allowRecovery, hasSideEffects, values.bool(false), filters);
+ }
+
public IFunction parser(IValue start, IBool allowAmbiguity, IBool hasSideEffects, ISet filters) {
- return rascalValues.parser(start, allowAmbiguity, hasSideEffects, values.bool(false), filters);
+ return rascalValues.parser(start, allowAmbiguity, values.bool(false), hasSideEffects, values.bool(false), filters);
}
- public IFunction firstAmbiguityFinder(IValue start, IBool hasSideEffects, ISet filters) {
- return rascalValues.parser(start, values.bool(true), hasSideEffects, values.bool(true), filters);
+ public IFunction firstAmbiguityFinder(IValue start, IBool allowRecovery, IBool hasSideEffects, ISet filters) {
+ return rascalValues.parser(start, values.bool(true), allowRecovery, hasSideEffects, values.bool(true), filters);
}
- public IFunction parsers(IValue start, IBool allowAmbiguity, IBool hasSideEffects, ISet filters) {
- return rascalValues.parsers(start, allowAmbiguity, hasSideEffects, values.bool(false), filters);
+ public IFunction parsers(IValue start, IBool allowAmbiguity, IBool allowRecovery, IBool hasSideEffects, ISet filters) {
+ return rascalValues.parsers(start, allowAmbiguity, allowRecovery, hasSideEffects, values.bool(false), filters);
}
- public IFunction firstAmbiguityFinders(IValue start, IBool hasSideEffects, ISet filters) {
- return rascalValues.parsers(start, values.bool(true), hasSideEffects, values.bool(true), filters);
+ public IFunction firstAmbiguityFinders(IValue start, IBool allowRecovery, IBool hasSideEffects, ISet filters) {
+ return rascalValues.parsers(start, values.bool(true), allowRecovery, hasSideEffects, values.bool(true), filters);
}
public void storeParsers(IValue start, ISourceLocation saveLocation) {
@@ -2405,18 +2409,18 @@ public void storeParsers(IValue start, ISourceLocation saveLocation) {
}
}
- public IFunction loadParsers(ISourceLocation savedLocation, IBool allowAmbiguity, IBool hasSideEffects, ISet filters) {
+ public IFunction loadParsers(ISourceLocation savedLocation, IBool allowAmbiguity, IBool allowRecovery, IBool hasSideEffects, ISet filters) {
try {
- return rascalValues.loadParsers(savedLocation, allowAmbiguity, hasSideEffects, values.bool(false), filters);
+ return rascalValues.loadParsers(savedLocation, allowAmbiguity, allowRecovery, hasSideEffects, values.bool(false), filters);
}
catch (IOException | ClassNotFoundException e) {
throw RuntimeExceptionFactory.io(e.getMessage());
}
}
- public IFunction loadParser(IValue grammar, ISourceLocation savedLocation, IBool allowAmbiguity, IBool hasSideEffects, ISet filters) {
+ public IFunction loadParser(IValue grammar, ISourceLocation savedLocation, IBool allowRecovery, IBool allowAmbiguity, IBool hasSideEffects, ISet filters) {
try {
- return rascalValues.loadParser(grammar, savedLocation, allowAmbiguity, hasSideEffects, values.bool(false), filters);
+ return rascalValues.loadParser(grammar, savedLocation, allowAmbiguity, allowRecovery, hasSideEffects, values.bool(false), filters);
}
catch (IOException | ClassNotFoundException e) {
throw RuntimeExceptionFactory.io(e.getMessage());
diff --git a/src/org/rascalmpl/library/lang/c90/examples/hello-world.c b/src/org/rascalmpl/library/lang/c90/examples/hello-world.c
new file mode 100644
index 00000000000..acabc95d8c8
--- /dev/null
+++ b/src/org/rascalmpl/library/lang/c90/examples/hello-world.c
@@ -0,0 +1,19 @@
+
+int print(const char *text);
+
+void printHello(char *name) {
+ print("Hello ");
+ print(name);
+ print("!");
+}
+
+int main(int argc, char *argv[]) {
+ char *name;
+ if (argc > 1) {
+ name = argv[1];
+ } else {
+ name = "World";
+ }
+
+ printHello(name);
+}
diff --git a/src/org/rascalmpl/library/lang/diff/unified/UnifiedDiff.rsc b/src/org/rascalmpl/library/lang/diff/unified/UnifiedDiff.rsc
index d4e84750c73..4e2d114f003 100644
--- a/src/org/rascalmpl/library/lang/diff/unified/UnifiedDiff.rsc
+++ b/src/org/rascalmpl/library/lang/diff/unified/UnifiedDiff.rsc
@@ -3,6 +3,8 @@
@contributor{Tijs van der Storm - storm@cwi.nl (CWI)}
module lang::diff::unified::UnifiedDiff
+start syntax DiffFile = Diff;
+
syntax Diff
= Header old Header new Chunk* chunks
;
diff --git a/src/org/rascalmpl/library/lang/diff/unified/examples/example.diff b/src/org/rascalmpl/library/lang/diff/unified/examples/example.diff
new file mode 100644
index 00000000000..ea75dfad7a5
--- /dev/null
+++ b/src/org/rascalmpl/library/lang/diff/unified/examples/example.diff
@@ -0,0 +1,39 @@
+--- a/src/org/rascalmpl/parser/uptr/UPTRNodeFactory.java
++++ b/src/org/rascalmpl/parser/uptr/UPTRNodeFactory.java
+@@ -1,6 +1,7 @@
+ package org.rascalmpl.parser.uptr;
+
+ import java.net.URI;
++import java.util.Arrays;
+ import java.util.IdentityHashMap;
+ import java.util.Map;
+
+@@ -21,7 +22,9 @@ import org.rascalmpl.values.parsetrees.ProductionAdapter;
+ import org.rascalmpl.values.parsetrees.TreeAdapter;
+
+ public class UPTRNodeFactory implements INodeConstructorFactory{
+- private final static RascalValueFactory VF = (RascalValueFactory) ValueFactoryFactory.getValueFactory();
++ private static final RascalValueFactory VF = (RascalValueFactory) ValueFactoryFactory.getValueFactory();
++ private static final IConstructor SKIPPED = VF.constructor(RascalValueFactory.Production_Skipped, VF.constructor(RascalValueFactory.Symbol_IterStar, VF.constructor(RascalValueFactory.Symbol_CharClass, VF.list(VF.constructor(RascalValueFactory.CharRange_Range, VF.integer(1), VF.integer(Character.MAX_CODE_POINT))))));
++
+ private boolean allowAmb;
+
+ public UPTRNodeFactory(boolean allowAmbiguity){
+@@ -141,7 +144,14 @@ public class UPTRNodeFactory implements INodeConstructorFactory children, Object production) {
++ IConstructor prod = (IConstructor) production;
++ IConstructor errorProd = VF.constructor(RascalValueFactory.Production_Error, prod.get(0), prod, VF.integer(children.size()-1));
++ return buildAppl(children, errorProd);
++ }
++
+ }
diff --git a/src/org/rascalmpl/library/lang/dot/examples/parser-state.dot b/src/org/rascalmpl/library/lang/dot/examples/parser-state.dot
new file mode 100644
index 00000000000..183d2b050aa
--- /dev/null
+++ b/src/org/rascalmpl/library/lang/dot/examples/parser-state.dot
@@ -0,0 +1,65 @@
+digraph Parser {
+"Parser"["label"="Parser\nInput: \"void f(){if(1){}}\"\nLocation: 0 ('v')\nStep 5: Reducing terminals"];
+"todo-1"["label"="<0> 0", "shape"="record"];
+"-2"["label"="Epsilon: \n.0@0 ,matchable,end\n?\nin: 'lex(\"LAYOUT\") -> regular(\iter-star(lex(\"LAYOUT\")))'"];
+"7226"["label"="List: 7226\n.0@0 ,expandable,end\n7226\nin: 'LAYOUTLIST -> \iter-star(lex(\"LAYOUT\"))'"];
+"12860"["label"="NonTerminal: LAYOUTLIST\n.1@0 \nlayouts_LAYOUTLIST\nin: Tags Visibility Signature '=' Expression 'when' 12878 ';'"];
+"-1"["label"="NonTerminal: FunctionDeclaration\n.0@-1 \nFunctionDeclaration"];
+"12860" -> "-1";
+"7226" -> "12860";
+"-2" -> "7226";
+"todo-1":"0":sw -> "-2"["label"="Stack"];
+"46484886"["shape"="octagon", "label"="Epsilon"];
+"todo-1":"0":se -> "46484886"["label"="Node"];
+"todoLists":"1" -> "todo-1";
+"todoLists"["label"="<0> 0 | <1> 1 | <2> 2 | <3> 3 | <4> 4 | <5> 5 | <6> 6 | <7> 7 | <8> 8 | <9> 9 | <10> 10 | <11> 11 | <12> 12 | <13> 13 | <14> 14 | <15> 15", "shape"="record"];
+"Parser" -> "todoLists"["label"="todo lists"];
+"stacksToExpand"["label"="", "shape"="record"];
+"Parser" -> "stacksToExpand"["label"="stacks to expand"];
+"terminalsToReduce"["label"="<0> 0", "shape"="record", "color"="red"];
+"terminalsToReduce":"0":sw -> "-2"["label"="Stack"];
+"terminalsToReduce":"0":se -> "46484886"["label"="Node"];
+"Parser" -> "terminalsToReduce"["label"="terminals to reduce"];
+"nonTerminalsToReduce"["label"="", "shape"="record"];
+"Parser" -> "nonTerminalsToReduce"["label"="non-terminals to reduce"];
+"122"["label"="NonTerminal: Tag\n.0@0 ,end\nTag\nin: 'sort(\"Tag\") -> regular(\iter-star-seps(sort(\"Tag\"),[layouts(\"LAYOUTLIST\")]))'"];
+"124"["label"="SeparatedList: 124\n.0@0 ,expandable,end\n124\nin: 'default -> tags'"];
+"12858"["label"="NonTerminal: Tags\n.0@0 \nTags\nin: Tags Visibility Signature '=' Expression 'when' 12878 ';'"];
+"12858" -> "-1";
+"124" -> "12858";
+"122" -> "124";
+"unexpandableNodes":"0" -> "122";
+"13120"["label"="NonTerminal: Comment\n.0@0 ,end\nComment\nin: 'LAYOUT -> Comment'"];
+"7221"["label"="NonTerminal: LAYOUT\n.0@0 ,end\nLAYOUT\nin: 'lex(\"LAYOUT\") -> regular(\iter-star(lex(\"LAYOUT\")))'"];
+"7221" -> "7226";
+"13120" -> "7221";
+"unexpandableNodes":"1" -> "13120";
+"unexpandableNodes"["label"="<0> 0 | <1> 1", "shape"="record"];
+"12824"["label"="Char: \n.0@-1 ,matchable\n0\nin: 0 'sort(\"FunctionDeclaration\")' ':' 12828 0"];
+"unmatchableLeafNodes":"0" -> "12824";
+"128"["label"="Char: \n.0@-1 ,matchable\n0\nin: 0 'sort(\"Tags\")' ':' 132 0"];
+"unmatchableLeafNodes":"1" -> "128";
+"2043"["label"="Literal: \n.0@-1 ,matchable\n'@'\nin: '@' Name '=' Expression"];
+"unmatchableLeafNodes":"2" -> "2043";
+"2065"["label"="Char: \n.0@-1 ,matchable\n0\nin: 0 '\iter-star(sort(\"Tag\"))' ':' 2069 0"];
+"unmatchableLeafNodes":"3" -> "2065";
+"13122"["label"="Char: \n.0@-1 ,matchable,end\n9-13,32,133,160,5760,6158,8192-8202,8232-8233,8239,8287,12288\nin: 'LAYOUT -> [range(9,13),range(32,32),range(133,133),range(160,160),range(5760,5760),range(6158,6158),range(8192,8202),range(8232,8233),range(8239,8239),range(8287,8287),range(12288,12288)]'"];
+"unmatchableLeafNodes":"4" -> "13122";
+"13125"["label"="Char: \n.0@-1 ,matchable\n0\nin: 0 '\iter-star(sort(\"LAYOUT\"))' ':' 13129 0"];
+"unmatchableLeafNodes":"5" -> "13125";
+"7373"["label"="Literal: \n.0@-1 ,matchable\n'/*'\nin: '/*' 7379 '*/'"];
+"unmatchableLeafNodes":"6" -> "7373";
+"7382"["label"="Literal: \n.0@-1 ,matchable\n'//'\nin: '//' 7386"];
+"unmatchableLeafNodes":"7" -> "7382";
+"7389"["label"="Char: \n.0@-1 ,matchable\n0\nin: 0 'sort(\"Comment\")' ':' 7393 0"];
+"unmatchableLeafNodes":"8" -> "7389";
+"unmatchableLeafNodes"["label"="<0> 0 | <1> 1 | <2> 2 | <3> 3 | <4> 4 | <5> 5 | <6> 6 | <7> 7 | <8> 8", "shape"="record"];
+"unmatchableMidProductionNodes"["shape"="record", "label"=""];
+"filteredNodes"["label"="", "shape"="record"];
+"error"["label"="Errors"];
+"Parser" -> "error"["label"="error tracking"];
+"error" -> "unexpandableNodes"["label"="unexpandable"];
+"error" -> "unmatchableLeafNodes"["label"="unmatchable leafs"];
+"error" -> "unmatchableMidProductionNodes"["label"="unmatchable mid-prod"];
+"error" -> "filteredNodes"["label"="filtered"];
+}
diff --git a/src/org/rascalmpl/library/lang/dot/syntax/Dot.rsc b/src/org/rascalmpl/library/lang/dot/syntax/Dot.rsc
index 7fcf9b98ca7..d50f8f66a52 100644
--- a/src/org/rascalmpl/library/lang/dot/syntax/Dot.rsc
+++ b/src/org/rascalmpl/library/lang/dot/syntax/Dot.rsc
@@ -68,7 +68,7 @@ syntax NodeId
| Id Port
;
-syntax Port = ":" Id Id?
+syntax Port = ":" Id (":" Id)?
// | ":" Id
// | ":" CompassPt
;
diff --git a/src/org/rascalmpl/library/lang/pico/examples/fac.pico b/src/org/rascalmpl/library/lang/pico/examples/fac.pico
new file mode 100644
index 00000000000..95a05f3e2e5
--- /dev/null
+++ b/src/org/rascalmpl/library/lang/pico/examples/fac.pico
@@ -0,0 +1,18 @@
+begin declare input : natural,
+ output : natural,
+ repnr : natural,
+ rep : natural,
+ s1 : string,
+ s2 : string;
+ input := 14;
+ output := 1;
+ while input - 1 do
+ rep := output;
+ repnr := input;
+ while repnr - 1 do
+ output := output + rep;
+ repnr := repnr - 1
+ od;
+ input := input - 1
+ od
+end
\ No newline at end of file
diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/Syntax1.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/Syntax1.rsc
index 636eb06ebef..c69ca2d3c42 100644
--- a/src/org/rascalmpl/library/lang/rascal/tests/concrete/Syntax1.rsc
+++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/Syntax1.rsc
@@ -87,4 +87,4 @@ test bool cntES1() = cntES(((ES) `e`).args) == 1;
test bool cntES2() = cntES(((ES) `e,e`).args) == 2;
test bool cntES3() = cntES(((ES) `e ,e`).args) == 2;
test bool cntES4() = cntES(((ES) `e, e`).args) == 2;
-test bool cntES5() = cntES(((ES) `e , e`).args) == 2;
\ No newline at end of file
+test bool cntES5() = cntES(((ES) `e , e`).args) == 2;
diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/BasicRecoveryTests.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/BasicRecoveryTests.rsc
new file mode 100644
index 00000000000..f3cc13aacdd
--- /dev/null
+++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/BasicRecoveryTests.rsc
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2024, NWO-I Centrum Wiskunde & Informatica (CWI)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ *
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ **/
+
+module lang::rascal::tests::concrete::recovery::BasicRecoveryTests
+
+import ParseTree;
+import util::ErrorRecovery;
+
+import lang::rascal::tests::concrete::recovery::RecoveryTestSupport;
+
+layout Layout = [\ ]* !>> [\ ];
+
+syntax S = T;
+
+syntax T = ABC End;
+syntax ABC = 'a' 'b' 'c';
+syntax End = "$";
+
+test bool basicOk() = checkRecovery(#S, "a b c $", []);
+
+test bool abx() = checkRecovery(#S, "a b x $", ["x "]);
+
+test bool axc() = checkRecovery(#S, "a x c $", ["x c"]);
diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/ErrorRecoveryBenchmark.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/ErrorRecoveryBenchmark.rsc
new file mode 100644
index 00000000000..459c3efc293
--- /dev/null
+++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/ErrorRecoveryBenchmark.rsc
@@ -0,0 +1,88 @@
+/**
+* Copyright (c) 2024, NWO-I Centrum Wiskunde & Informatica (CWI)
+* All rights reserved.
+*
+* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+*
+* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+*
+* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+module lang::rascal::tests::concrete::recovery::ErrorRecoveryBenchmark
+
+import lang::rascal::tests::concrete::recovery::RecoveryTestSupport;
+
+import IO;
+import ValueIO;
+import util::Benchmark;
+import util::SystemAPI;
+import String;
+import List;
+
+void runTestC() { testRecoveryC(); }
+void runTestDiff() { testRecoveryDiff(); }
+void runTestDot() { testRecoveryDot(); }
+void runTestJava() { testRecoveryJava(); }
+void runTestJson() { testRecoveryJson(); }
+void runTestPico() { testRecoveryPico(); }
+void runTestRascal() { testRecoveryRascal(); }
+
+FileStats testRecoveryC() = testErrorRecovery(|std:///lang/c90/syntax/C.rsc|, "TranslationUnit", |std:///lang/c90/examples/hello-world.c|);
+FileStats testRecoveryDiff() = testErrorRecovery(|std:///lang/diff/unified/UnifiedDiff.rsc|, "DiffFile", |std:///lang/diff/unified/examples/example.diff|);
+FileStats testRecoveryDot() = testErrorRecovery(|std:///lang/dot/syntax/Dot.rsc|, "DOT", |std:///lang/dot/examples/parser-state.dot|);
+FileStats testRecoveryJava() = testErrorRecovery(|std:///lang/java/syntax/Java15.rsc|, "CompilationUnit", zippedFile("m3/snakes-and-ladders-project-source.zip", "src/snakes/LastSquare.java"));
+FileStats testRecoveryJson() = testErrorRecovery(|std:///lang/json/syntax/JSON.rsc|, "JSONText", |std:///lang/json/examples/ex01.json|);
+FileStats testRecoveryPico() = testErrorRecovery(|std:///lang/pico/syntax/Main.rsc|, "Program", |std:///lang/pico/examples/fac.pico|);
+FileStats testRecoveryRascal() = testErrorRecovery(|std:///lang/rascal/syntax/Rascal.rsc|, "Module", |std:///lang/rascal/vis/ImportGraph.rsc|);
+
+void runLanguageTests() {
+ testRecoveryC();
+ testRecoveryDiff();
+ testRecoveryDot();
+ testRecoveryJava();
+ testRecoveryJson();
+ testRecoveryPico();
+ testRecoveryRascal();
+}
+
+void runRascalBatchTest(int maxFiles=1000, int minFileSize=0, int maxFileSize=4000, int fromFile=0) {
+ int startTime = realTime();
+
+ map[str,str] env = getSystemEnvironment();
+ loc statFile = "STATFILE" in env ? readTextValueString(#loc, env["STATFILE"]) : |unknown:///|;
+
+ println("Writing stats to ");
+
+ TestStats stats = batchRecoveryTest(|std:///lang/rascal/syntax/Rascal.rsc|, "Module", |std:///|, ".rsc", maxFiles, minFileSize, maxFileSize, fromFile, statFile);
+ int duration = realTime() - startTime;
+ println();
+ println("================================================================");
+ println("Rascal batch test done in seconds, total result:");
+ printStats(stats);
+}
+
+// Usage: ErrorRecoveryBenchmark [\ [\ [\ [\]]]]
+int main(list[str] args) {
+ int maxFiles = 1000;
+ int maxFileSize = 1000000;
+ int minFileSize = 0;
+ int fromFile = 0;
+ if (size(args) > 0) {
+ maxFiles = toInt(args[0]);
+ }
+ if (size(args) > 1) {
+ minFileSize = toInt(args[1]);
+ }
+ if (size(args) > 2) {
+ maxFileSize = toInt(args[2]);
+ }
+ if (size(args) > 3) {
+ fromFile = toInt(args[3]);
+ }
+
+ runRascalBatchTest(maxFiles=maxFiles, minFileSize=minFileSize, maxFileSize=maxFileSize, fromFile=fromFile);
+
+ return 0;
+}
\ No newline at end of file
diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/ListRecoveryTests.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/ListRecoveryTests.rsc
new file mode 100644
index 00000000000..a91ee40cb2c
--- /dev/null
+++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/ListRecoveryTests.rsc
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2024, NWO-I Centrum Wiskunde & Informatica (CWI)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ *
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ **/
+
+module lang::rascal::tests::concrete::recovery::ListRecoveryTests
+
+import ParseTree;
+import util::ErrorRecovery;
+
+import lang::rascal::tests::concrete::recovery::RecoveryTestSupport;
+
+layout Layout = [\ ]* !>> [\ ];
+
+syntax S = T End;
+
+syntax T = { AB "," }*;
+syntax AB = "a" "b";
+syntax End = "$";
+
+Tree parseList(str s, bool visualize=false) {
+ return parser(#S, allowRecovery=true, allowAmbiguity=true)(s, |unknown:///?visualize=<"">|);
+}
+
+test bool listOk() = checkRecovery(#S, "a b , a b , a b $", []);
+
+test bool listTypo() = checkRecovery(#S, "a b, a x, ab $", ["x"]);
+
+test bool listTypoWs() = checkRecovery(#S, "a b , a x , a b $", ["x "]);
diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/NOTES.md b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/NOTES.md
new file mode 100644
index 00000000000..95e7d6314eb
--- /dev/null
+++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/NOTES.md
@@ -0,0 +1,23 @@
+
+# Error recovery benchmark
+
+Next to some basic integration tests, this directory contains a simple error recovery benchmark (see `ErrorRecoveryBenchmark.rsc`).
+This benchmark is meant to provide feedback on the quality and speed of error recovery and to be able to track progress in these areas.
+
+The benchmark tests error recovery for a number of languages by performing modifications on a valid input file and attempting to parse the result.
+Currently only two types of modifications are implemented:
+
+- Deletion of single characters
+- Deletion until the next end-of-line character
+
+We anticipate more types of modifications later, possibly based on the parse tree instead of the raw source file.
+
+Note that a benchmark test is documented by a string of characters including:
+
+- `.`: After modification the input still parses without errors
+- '+': After modification error recovery is able to recover by skipping a reasonable amount of input
+- '-': After modification error recovery is able to recover by skipping an excessive amount of input (>25% of the contents of the file)
+- '?': After modification parsing results in an unrecoverable parse error
+- A newline is printed after each line of the input file
+
+Besides the ouptut, files with benchmark measurements can also be generated in CSV format with one measurement per line. To generate this file, specify its location as a `loc` in the environment variable `STATFILE`.
diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/NestedRecoveryTests.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/NestedRecoveryTests.rsc
new file mode 100644
index 00000000000..13cc49373cf
--- /dev/null
+++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/NestedRecoveryTests.rsc
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2024, NWO-I Centrum Wiskunde & Informatica (CWI)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ *
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ **/
+
+module lang::rascal::tests::concrete::recovery::NestedRecoveryTests
+
+import lang::rascal::tests::concrete::recovery::RecoveryTestSupport;
+
+layout Layout = [\ ]* !>> [\ ];
+
+syntax S = T;
+
+syntax T = A B C;
+
+syntax A = "a";
+syntax B = "b" "b";
+syntax C = "c";
+
+test bool nestedOk() = checkRecovery(#S, "a b b c", []);
+
+test bool nestedTypo() = checkRecovery(#S, "a b x c", ["x "]);
diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/PicoRecoveryTests.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/PicoRecoveryTests.rsc
new file mode 100644
index 00000000000..231b4513dc7
--- /dev/null
+++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/PicoRecoveryTests.rsc
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2024, NWO-I Centrum Wiskunde & Informatica (CWI)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ *
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ **/
+
+module lang::rascal::tests::concrete::recovery::PicoRecoveryTests
+
+import lang::pico::\syntax::Main;
+
+import ParseTree;
+import util::ErrorRecovery;
+import lang::rascal::tests::concrete::recovery::RecoveryTestSupport;
+
+Tree parsePico(str input, bool visualize=false)
+ = parser(#Program, allowRecovery=true, allowAmbiguity=true)(input, |unknown:///?visualize=<"">|);
+
+test bool picoOk() = checkRecovery(#Program, "begin declare input : natural,
+ output : natural,
+ repnr : natural,
+ rep : natural;
+ input := 14;
+ output := 0;
+ while input - 1 do
+ rep := output;
+ repnr := input;
+ while repnr - 1 do
+ output := output + rep;
+ repnr := repnr - 1
+ od;
+ input := input - 1
+ od
+end", []);
+
+test bool picoTypo() = checkRecovery(#Program, "begin declare input : natural,
+ output : natural,
+ repnr : natural,
+ rep : natural;
+ input := 14;
+ output := 0;
+ while input - 1 do
+ rep := output;
+ repnr := input;
+ while repnr - 1 do
+ output := output x rep;
+ repnr := repnr - 1
+ od;
+ input := input - 1
+ od
+end", ["output x rep"]);
+
+test bool picoMissingSemi() = checkRecovery(#Program, "begin declare input : natural,
+ output : natural,
+ repnr : natural,
+ rep : natural;
+ input := 14;
+ output := 0;
+ while input - 1 do
+ rep := output;
+ repnr := input;
+ while repnr - 1 do
+ output := output + rep;
+ repnr := repnr - 1
+ od
+ input := input - 1
+ od
+end", ["od
+end", "input := input - 1
+"]);
+
+test bool picoTypoSmall() = checkRecovery(#Program, "begin declare;
+ while input do
+ input x= 14;
+ output := 0
+ od
+end", ["x= 14"]);
+
+test bool picoMissingSemiSmall() = checkRecovery(#Program, "begin declare;
+ while input do
+ input := 14
+ output := 0
+ od
+end", ["output := 0
+ od"]);
+
+test bool picoEof() = checkRecovery(#Program, "begin declare; input := 0;", ["input := 0;"]);
+
+test bool picoEofError() = checkRecovery(#Program, "begin declare x y; input := 0;", ["x y;", "input := 0;"]);
diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/RascalRecoveryTests.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/RascalRecoveryTests.rsc
new file mode 100644
index 00000000000..2b51fb9a1f3
--- /dev/null
+++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/RascalRecoveryTests.rsc
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2024, NWO-I Centrum Wiskunde & Informatica (CWI)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ *
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ **/
+
+module lang::rascal::tests::concrete::recovery::RascalRecoveryTests
+
+import lang::rascal::\syntax::Rascal;
+
+import lang::rascal::tests::concrete::recovery::RecoveryTestSupport;
+
+bool debugging = false;
+
+test bool rascalOk() = checkRecovery(#start[Module], "
+ module A
+
+ int inc(int i) {
+ return i+1;
+ }
+ ", []);
+
+test bool rascalFunctionDeclarationOk() = checkRecovery(#FunctionDeclaration, "void f(){}", []);
+
+test bool rascalModuleFollowedBySemi() = checkRecovery(#start[Module], "
+ module A
+ ;
+ ", [";"]);
+
+test bool rascalOperatorTypo() = checkRecovery(#start[Module], "
+ module A
+
+ int f() = 1 x 1;
+ ", ["x 1;"]);
+
+test bool rascalIllegalStatement() = checkRecovery(#start[Module], "module A void f(){a}", ["a}"]);
+
+test bool rascalMissingCloseParen() = checkRecovery(#start[Module], "module A void f({} void g(){}", ["("]);
+
+test bool rascalFunctionDeclarationMissingCloseParen() = checkRecovery(#FunctionDeclaration, "void f({} void g() {}", ["("]);
+
+test bool rascalIfMissingExpr() = checkRecovery(#FunctionDeclaration, "void f(){if(){1;}}", [")"]);
+
+test bool rascalIfBodyEmpty() = checkRecovery(#start[Module], "module A void f(){1;} void g(){if(1){}} void h(){1;}", ["{", "} "]);
+
+test bool rascalMissingOpeningParen() = checkRecovery(#start[Module],
+"module A
+void f) {
+ }
+ void g() {
+ }", ["}
+",") {"]);
+
+test bool rascalFunFunMissingCloseParen() = checkRecovery(#start[Module], "module A void f(){void g({}} void h(){}", ["void g({}", "} "]);
+
+test bool rascalIfMissingOpeningParen() = checkRecovery(#start[Module],
+"module A void f() {
+ if 1) {
+ 1;
+ }}", ["1) "]);
+
+test bool rascalIfMissingCloseParen() = checkRecovery(#start[Module],
+"module A
+void f() {
+ if (1 {
+ 1;
+ }}", ["1 "]);
+
+test bool rascalIfMissingSemi() = checkRecovery(#start[Module],
+"module A
+void f() {
+ if (true) {
+ a
+ }
+}", ["{", "}
+"]);
diff --git a/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/RecoveryTestSupport.rsc b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/RecoveryTestSupport.rsc
new file mode 100644
index 00000000000..9ee14b38de6
--- /dev/null
+++ b/src/org/rascalmpl/library/lang/rascal/tests/concrete/recovery/RecoveryTestSupport.rsc
@@ -0,0 +1,514 @@
+/**
+ * Copyright (c) 2024, NWO-I Centrum Wiskunde & Informatica (CWI)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+ *
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ **/
+
+module lang::rascal::tests::concrete::recovery::RecoveryTestSupport
+
+import lang::rascal::\syntax::Rascal;
+import ParseTree;
+import util::ErrorRecovery;
+import String;
+import IO;
+import util::Benchmark;
+import Grammar;
+import analysis::statistics::Descriptive;
+import util::Math;
+import Set;
+import List;
+
+import lang::rascal::grammar::definition::Modules;
+
+alias FrequencyTable = map[int val, int count];
+
+public data TestMeasurement(loc source=|unknown:///|, int duration=0) = successfulParse() | recovered(int errorCount=0, int errorSize=0) | parseError() | successfulDisambiguation();
+public data FileStats = fileStats(
+ int totalParses = 0,
+ int successfulParses=0,
+ int successfulRecoveries=0,
+ int successfulDisambiguations=0,
+ int failedRecoveries=0,
+ int parseErrors=0,
+ int slowParses=0,
+ FrequencyTable parseTimeRatios=(),
+ FrequencyTable errorCounts=(),
+ FrequencyTable errorSizes=());
+
+public data TestStats = testStats(
+ int filesTested=0,
+ int testCount=0,
+ FrequencyTable successfulParses=(),
+ FrequencyTable successfulRecoveries=(),
+ FrequencyTable successfulDisambiguations=(),
+ FrequencyTable failedRecoveries=(),
+ FrequencyTable parseErrors=(),
+ FrequencyTable slowParses=(),
+ FrequencyTable parseTimeRatios=(),
+ FrequencyTable errorCounts=(),
+ FrequencyTable errorSizes=());
+
+private TestMeasurement testRecovery(&T (value input, loc origin) standardParser, &T (value input, loc origin) recoveryParser, str input, loc source, loc statFile, int referenceParseTime) {
+ int startTime = 0;
+ int duration = 0;
+ int disambDuration = -1;
+ int errorCount = 0;
+ int errorSize=0;
+ str result = "?";
+ TestMeasurement measurement = successfulParse();
+ try {
+ startTime = realTime();
+ Tree t = standardParser(input, source);
+ duration = realTime() - startTime;
+ measurement = successfulParse(source=source, duration=duration);
+ result = "success";
+ } catch ParseError(_): {
+ startTime = realTime();
+ try {
+ Tree t = recoveryParser(input, source);
+ int parseEndTime = realTime();
+ duration = parseEndTime - startTime;
+ list[Tree] errors = findBestErrors(t);
+ errorCount = size(errors);
+ disambDuration = realTime() - parseEndTime;
+ result = "recovery";
+ if (errors == []) {
+ measurement = successfulDisambiguation(source=source, duration=duration);
+ } else {
+ errorSize = (0 | it + size(getErrorText(err)) | err <- errors);
+ measurement = recovered(source=source, duration=duration, errorCount=errorCount, errorSize=errorSize);
+ }
+ } catch ParseError(_): {
+ result = "error";
+ duration = realTime() - startTime;
+ measurement = parseError(source=source, duration=duration);
+ }
+ }
+
+ if (statFile != |unknown:///|) {
+ int ratio = percent(duration, referenceParseTime);
+ appendToFile(statFile, "